fix(dashboard): a CPR level must not run across a day it has no band for - #180
Merged
Merged
Conversation
On the 5m chart one set of levels ran flat from 2026-09-16 all the way to 2026-09-21 instead of each day carrying its own. Two faults met. The days had no band. The ladder is built ONCE from the history CSV, whose last row was 09-16, while the candles beside it come from the runner's store: INTRADAY_LOOKBACK_DAYS seeds it with several days of REST history and DASHBOARD_CHART_BARS=2200 keeps ~5.9 sessions. Nothing server-side ever extended the ladder from that store. And the renderer ignored the span. A band covers its own session -- the server guarantees it and a test there says so -- but a stepped line runs flat from one point to the next, so a band with no successor stretches over every day in between. Both are fixed. The live payload now carries `cpr_days`: bands for the completed sessions in the store, built by `dashboard_history.day_segments`, which is the same shift-and-aggregate the CSV ladder uses rather than a second copy of it. A stored band wins on a date both hold, because the CSV's sessions are complete downloads while the store's oldest is only whatever its window reaches back to; a store session that does not start at 09:15 is dropped rather than believed, since nothing marks a high as partial once it is a number. Memoized on the frame's session set, so it rebuilds about once a day. `cprRuns` cuts the levels into runs of adjacent periods, one series each, ending on the last bar the run's final band owns. A break is by PERIOD -- the session on minutes, the MONTH on Daily -- never by any bar in between: the store's copy of a session can hold a bar the CSV's copy does not, and the two ladders meet exactly there. Days no band can be computed for now show no CPR, which is the honest answer. Only the newest run keeps its axis labels, and BOTH `lastValueVisible` and `title` have to go: the library gates its second axis view on `"" !== title || seriesLastValueMode === 0`, with `lastValueVisible` nowhere in that test, so an older run still printed "R1 (chart)" with no number. Verified on a scratch server in the operator's real shape -- CSV ending 09-16, store covering 09-17 through today. Six per-day blocks, each ~233 columns wide with 0.5px of vertical travel (the broken build had one 458-column track drifting 43-66px), nine axis labels for nine enabled levels, and with the store trimmed so 09-17 has no computable band that day draws no CPR at all. Daily still shows monthly bands against a matching caption. `test_a_cpr_level_never_runs_across_a_day_that_has_no_band` lifts the pure geometry out of the asset and runs it under node -- the first test here that executes the page's logic rather than reading it, because source assertions have now let two rendering bugs through. It skips where node is absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
One set of CPR levels ran flat from 2026-09-16 all the way to 2026-09-21 on the 1m and 5m charts, instead of each day carrying its own. Two independent faults met.
Those days had no band. The ladder is built once, at startup, from the history CSV — whose last row was
2026-09-16 15:29. The candles beside it come from the runner's store instead:INTRADAY_LOOKBACK_DAYS=7seeds it over REST andDASHBOARD_CHART_BARS=2200keeps ~5.9 sessions. Nothing server-side ever extended the ladder from that store;cpr_segmentshad exactly one call site andpublish_historyis called once.And the renderer ignored the span. A band covers its own session and nothing else —
dashboard_historyguarantees it andtest_a_band_spans_only_its_own_sessionsays so in as many words. A stepped line cannot express that on its own: it runs flat from one point to the next, so a band with no successor stretches over every day in between.Fault two is the one that misleads — it draws the 16th's pivot over the 21st's session as if it were that session's level. Fault one is why there was a gap to bleed across. Both are fixed.
Fill what the data supports
The live payload now carries
cpr_days: bands for the completed sessions the store still holds, built bydashboard_history.day_segments— the same shift-and-aggregate the CSV ladder uses, extracted so there is still exactly one CPR implementation rather than two that agree today.Two exclusions, both deliberate:
cprblock — the one its caption describes.A stored band wins on a date both hold. Memoized on the frame's session set, so it rebuilds about once a day, and it runs where
_refresh_chart_cpralready has the frame — no extra store copy, and the "never reaches the broker or a mutating gate" assertion still holds.Make the remainder honest
cprRunscuts the levels into runs of adjacent periods, one series each, each ending on the last bar its final band owns. Consecutive days stay in one series so the vertical step between them is drawn as before; a run break is where the line simply stops.A break is by period — the session on the minute timeframes, the month on Daily — and never by "any bar in between". The store's copy of a session can hold a bar the CSV's copy does not (a 15:30 print; here it was exactly one bar), and the two ladders meet precisely there. Splitting on that put a second set of price labels down the axis for a seam nobody can see.
Only the newest run keeps its axis labels, and both
lastValueVisibleandtitlehave to go. The library draws two axis views per series and gates the second on"" !== title || seriesLastValueMode === 0—lastValueVisibleis nowhere in that test — so an older run with its title kept still printedR1 (chart)on the axis with no number beside it.Rejected: one series per band (hundreds rebuilt on every scroll); computing the missing levels in JS (a second copy of the CPR algebra, the cost ADR-0017 exists to bound).
Verification
Scratch server on port 8799 in the operator's real shape — CSV ending 09-16, store covering 09-17 through today, each stand-in session a different day so every band's levels differ and a bleed would be visible rather than accidentally flat.
cpr_daysin the payloaddashboard_history.cpr_segmentson the same frameGates: 611 + 28 unittest, 1,582 pytest, coverage policy + 74.2 % locally, ruff, mypy ×2, compileall, bandit as CI runs it, pre-commit,
node --check.Worth knowing
There is now a test that EXECUTES the page's logic.
test_a_cpr_level_never_runs_across_a_day_that_has_no_bandliftsfirstBarAtOrAfter,dayKeyandcprRunsout of the asset by brace-matching and runs them undernode -e, asserting that a gap day splits the runs, contiguous days do not, a bandless band neither draws nor splits, and the CSV/live seam does not split. Source assertions have let two rendering bugs through in a row — this is the first check that would have caught either. Itpytest.skips where node is absent.The practical remedy alone would not have been enough.
python algo.py fetch-databrings the CSV up to date and the symptom goes away — until tomorrow. The chart has to be right with a stale CSV, because by the end of any week it will be.🤖 Generated with Claude Code