CHART_FUTURES emits a bar only once its minute is over, so the chart stepped
once a minute and sat still in between, which reads as a dead feed.
LEVEL_ONE_FUTURES carries real trades on the same socket and the same login — no
extra REST call, no extra rate limit — and reports delayed: False on this
account. It was verified back in M6 and never subscribed to. It is now, building
a forming bar for the current minute that the authoritative CHART_FUTURES bar
then supersedes.
Three constraints shaped it, each a real bug avoided:
- Tick bars never reach the aggregator. It accumulates with current.v +=
incoming.v, so re-sending the same forming minute would add its volume into
every higher timeframe again on every update. Runtime.on_bar returns early for
an unclosed bar: store, set price, broadcast, stop.
- Emissions are throttled, SCHWAB_TICK_SECONDS default 1.0, because /ES trades
many times a second and each emission is a store write plus a broadcast to
every open socket. Negative drops the Level 1 subscription entirely.
- A tick for a minute CHART_FUTURES has already closed is dropped, or a late
trade would overwrite a settled exchange bar with a partial one.
Bid-only updates are skipped rather than carried forward: a bid is not a trade
and must not extend a candle's high or low. Alerts stay on closed bars — a level
is judged on a settled bar, not a price that may not last the minute — which
needed no change, since on_bar already gated on closed.
Verified against the live socket: 15 forming bars and 2 closed bars in 100
seconds, the closed bar superseding each forming minute. Verified in a browser:
the last candle's high and low visibly extend within the minute, no console
errors. 85 tests pass, four of them new.
The plan gains the cold-restart options asked for: make seeding non-quadratic
first, then persist cooldowns, then persist bars.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chart sat about ten hours behind a perfectly healthy feed. The header price
updated live while the last candle stayed put, which reads as a dead stream.
Every layer checked out in isolation, because every layer was correct: /api/bars
served bars to the current minute from schwab, store.put keeps them strictly
ascending, the WebSocket snapshot delivered 1000 ascending bars ending at the
live edge, and the browser received all of it plus a bar event every minute.
Interrogating the page's own chart object is what separated "the data is
missing" from "the data is off-screen":
seriesLen 1000 seriesLast 10:48 (7786.25) data complete
visible 08-07T20:41 -> 08-10T00:35 viewport 617 bars too far left
617 is exactly the daily bar count. setBars derived a visible *logical* range
from the candle array length, then syncVisibleLevels attached the daily MA
series, whose 617 daily points pre-date the 1m window. A logical index addresses
the chart's shared time scale — the union of every series' time points — so
prepending those points renumbered every index and slid the view off the live
edge one tick after it had been set correctly. A time range names the instant
instead, and later series cannot move it.
Worth keeping: screenshots alone were actively misleading. The stale time axis
showed a Friday-to-Sunday gap that read as an ordinary session break, so the
view looked plausible while being ten hours wrong.
The plan gains a dated session log (§16) for this and for two things that cost a
detour today — the image needing a rebuild for schwab-py, which the bind mount
hides, and the ~82 second startup during which the port refuses connections and
an open tab logs a wall of ERR_CONNECTION_REFUSED. That slow seed is recorded,
not fixed: it replays every bar through on_bar and rebuilds all five MA levels
per daily bar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified against a live account before and after writing it. CHART_FUTURES
delivers one true-OHLCV minute bar per symbol per minute, LEVEL_ONE_FUTURES
reports delayed: false, and consecutive bars arrived sixty seconds apart through
the production code path.
Yahoo stays. Schwab serves no futures history whatever, so seed_source resolves
to Yahoo even when SEED_SOURCE=schwab is asked for — the pairing is the intended
configuration rather than a fallback. The symbols differ, ES=F against /ES, so
Settings.live_symbol picks the live one while seeding always uses Yahoo's.
Three findings worth keeping, each of which cost a round trip:
- get_quote() singular returns the wrong instrument entirely. It puts the symbol
in the URL path, where the leading slash is normalised away, so /ES resolves to
Eversource Energy at $72 and returns HTTP 200 with a populated body. Only
get_quotes() plural, which passes symbols as a query parameter, returns the
future. A 200 is not evidence; assetMainType is.
- Streaming requires the Accounts and Trading product. StreamClient.login() reads
/trader/v1/userPreference for its socket URL, and that path does not exist in
Market Data Production.
- /ES resolves to the active contract on Schwab's side, so the contract roll
handling the plan left open needs no code.
The stream drops the oldest queued message rather than stalling the socket, and
surfaces a dead pump task instead of waiting forever on a queue nothing fills.
schwab-py moves into requirements.txt, imported only when LIVE_SOURCE=schwab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was never in the enabled timeframes, so it held zero bars and produced no
levels, but it still carried weight 8 in the scoring table and forced
bucket_start to special-case a wall-clock ET anchor whose entire purpose was
surviving DST transitions. That was the most intricate logic in session.py,
maintained for a timeframe nobody used.
Daily is now the only session-anchored bucket, which is a much easier rule to
state and to keep correct. The DST parametrised tests go with it; the Sunday
open and daily boundary cases remain.
Manual-line tests move to 1h, so the weight assertions drop from 8 to 4.
The plan document keeps its 4h examples — rewriting a dozen illustrative
sentences would churn more than it clarifies — but the timeframe-roles section
now records the removal so nothing reads as a spec to build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Planning-only commit: no application code yet.
The plan specifies a realtime /ES chart that derives moving averages and
trendlines across multiple timeframes, projects them onto one chart in a
shared (time, price) plane, and alerts when levels from different
timeframes converge.
Key findings that shaped it, all verified against source rather than
assumed:
- Schwab streams realtime futures fine (CHART_FUTURES, LEVEL_ONE_FUTURES)
but provides no futures price *history* at all. An account does not
change this; it is an API-surface limit.
- Yahoo's chart endpoint needs no key and has exactly what Schwab lacks:
~730d of hourly ES=F (~750 sessions), enough to warm a 200DMA from
startup. So it serves as both the no-keys dev source and the history
seeder, behind one MarketDataSource protocol.
- Yahoo anchors daily bars to midnight ET while the CME session runs
18:00-17:00 ET, so daily bars are built from hourly using our own
session rules instead.
- Lightweight Charts v5 replaced addCandlestickSeries() with
addSeries(CandlestickSeries, ...); most tutorials online are v4.
Build order defers judgment-heavy work: moving averages first (fully
deterministic), then confluence scoring, then hand-drawn trendlines.
Automatic trendline detection comes last, tuned against the hand-drawn
lines as ground truth.
Includes a real trimmed Yahoo response as a test fixture; it contains a
null in the OHLC arrays, which is the parsing case that needs handling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>