Runbook / Investigation: regime-replay corrected backfill stuck at 2026-06-12
Date: 2026-07-15 · Status: RESOLVED 2026-07-15 — root cause confirmed on prod + run history; durable fix shipped (nightly schedule, pinned computed_at, coverage gate — §10) · Type: data-freshness incident + diagnose/fix runbook
Repo: sequency-data · Read alongside: docs/runbooks/regime-vnext-rd.md, docs/superpowers/plans/2026-06-14-regime-sp1-canonical-grid.md, .github/workflows/regime-replay-backfill.yml
Every code claim below cites afile:lineread fromorigin/main(worktree.worktrees/data-runs-detail, at/after merge commit734ce62f). Names that could NOT be confirmed from code are marked# TODO confirm on prod. HISTORICAL SNAPSHOT — READ §10 FOR THE CURRENT STATE. §§1–9 describe the incident AS FOUND, and the fix PR that added §10 rewroteregime-replay-backfill.yml(nightly cron, optional inputs, pinned monotoniccomputed_at, freshness gate) and touchedmain.gocomments andregime-parity-check.sh. Workflow line-number citations and present-tense claims in §§4–7 (“no schedule trigger”, “defaultto:is 2026-06-12”, “no SLA row”, input defaults, “final step”) refer to the PRE-FIX file at734ce62fand are intentionally left as the incident record — do not “correct” them, and do not operate from §7’s original dispatch recipe; the current procedure is §10.
1. Symptom
Running a strategy backtest in the v2 rich Strategy-Lab Backtest view (/strategy-lab/backtest, cross-sectional engine) with date_to at/near today
fails with:
Backtest failed — regime labels stale: cover through 2026-06-12 but backtest date_to=2026-07-16; re-run the regime-replay corrected backfill
- Affected surface: every v2 / cross-sectional backtest whose window ends after 2026-06-12. Windows ending on/before 2026-06-12 pass.
- Blocks: the newly-shipped Builder → rich-backtest flow (app#424) and any recent-window backtest.
- Nature: a backend data-freshness problem — the regime-label coverage has not advanced past 2026-06-12. Independent of frontend routing.
date_to > 2026-06-12 (any strategy /
universe). The guard fires at run start, before any simulation
(executor.py:200). No prod mutation required to reproduce.
2. What the coverage check does (the error source)
File:api/app/services/backtester/regime_tagger.py
Function: assert_regime_labels_cover(ts_client, date_to) — lines 35–74.
- Reads the label coverage with a single query (line 54–55):
→ table
daily_regime_labels, columndate. - Empty/epoch guard (line 64–68): if
max_date is Noneor<= 1970-01-02raisesStaleRegimeLabelsError("… daily_regime_labels is empty …"). - The failing comparison (line 70–74):
Hard-fail, no tolerance / grace window. Any
date_tostrictly greater thanmax(date)aborts the run. - Caller:
api/app/services/backtester/executor.py:198–200invokes it with the split-narrowed end date (effective_end) at run start. Rationale in the docstring (regime_tagger.py:24–32): without the guard, trades past the last label are silently tagged"UNKNOWN"bytag_trades_with_regime(regime_tagger.py:167–173), masking a coverage gap as a benign regime.
max(date) currently in daily_regime_labels.
3. What feeds daily_regime_labels (the dependency chain)
variant = 'corrected'
(053:60-61). The live daily emit writes variant = 'emitted'
(regime_archive_writer.go:278, scheduler.go:753) and therefore does NOT
advance daily_regime_labels. Only the corrected variant does, and the only
producer of corrected rows is the regime-replay command (main.go:142).
The Python regime backfill referenced in 041’s header comment is retired
(regime_tagger.py:8-9, 053 header) — do not resurrect it.
4. What the regime-replay corrected backfill is + how it’s triggered
Producer binary: services/pattern-detector/cmd/regime-replay/main.go
- Rebuilds a clean regime buffer from epoch and folds
collectors.Step()oldest→newest over[from, to], writing onecorrectedrow per session (main.go:120-149). Pure/deterministic fold (main.go:6-16). - Idempotent:
regime_archiveisReplacingMergeTree(computed_at); a given(--from, --to, --run-id)is byte-reproducible (main.go:12-16,resolveComputedAtmain.go:163-172). Re-running the SAME run-id dedups to itself; a NEW run-id supersedes. - Requires
--from,--to,--run-id(main.go:59-64); readsCLICKHOUSE_URL(SPY bars +regime_archive) andRELATIONAL_DB_URL(market_breadthVIX) (main.go:88-115).
.github/workflows/regime-replay-backfill.yml
on: workflow_dispatchONLY — lines 16–17. There is NOschedule:/ cron trigger (confirmed:grep schedule|cronreturns onlyworkflow_dispatch). Nothing runs this automatically.- Default
to:input is'2026-06-12'— line 26. Defaultfrom:is'2021-04-22'(line 22); defaultrun_id'sp1-corrected-backfill'(line 30). - It cross-compiles
regime-replay,scps it to compute, and runs it via SSH reading/opt/sequency/.env(lines 69–107). This is the canonical, NOT-manual-SSH path (workflow header lines 6–9; CLAUDE.md deploy-safety). deploy-worker.yml:616-617only builds theregime-replaybinary on deploy — it does not run the backfill.
The defaultThere was no Data-Freshness SLA row for regime labels into: '2026-06-12'(regime-replay-backfill.yml:26) matches the stale coverage date in the error exactly. That is the smoking gun.
docs/DATA_FRESHNESS_SLAS.md (it listed options/bars/confluence/VP but not
regime) — reinforcing that no scheduled contract kept this data fresh. (Row
added by the fix — §10.)
5. Grounded hypotheses for why it’s stuck at 2026-06-12 (ranked)
-
(Most likely) The backfill is manual-only and was never re-run past its
initial window. It has no cron (
regime-replay-backfill.yml:16-17); the last dispatch used the default--to 2026-06-12(line 26). The liveemittedvariant keeps advancing daily but is filtered out of the projection (053:60-61), sodaily_regime_labelsfroze at the last manual backfill. Evidence: workflow isworkflow_dispatch-only; defaultto== the stale date; onlymain.go:142writescorrected. -
A re-run was dispatched but left
toat the2026-06-12default (or an earlier date), re-writing the same window with no forward progress. Same symptom, same evidence line (:26). Distinguish from #1 only via Actions run history. -
A re-run was dispatched with a later
--tobut failed on compute.BuildRegimeInputshard-errors if inputs are missing over the window (main.go:121-124); a failed job leaves coverage unchanged. Most plausible underlying cause is #4. Rule out via the Actions run log + the job’s own post-runregime_archivesummary (workflow lines 103-106). -
(Upstream) An input needed by the fold is itself stale past 2026-06-12,
so even a correctly-dispatched re-run can’t extend coverage:
market_breadthVIX in Postgres (regime_input_builder.go:163-165) — if VIX rows stop at 2026-06-12, the fold can’t produce later sessions.- SPY daily bars from
market_data_1min(regime_input_builder.go:41-46) — if SPY 1-min ingestion lapsed. Less likely (SPY bars are core), but check. If either is stale, the root cause is upstream and the backfill re-run is blocked until it’s refreshed.
-
(Least likely) The
daily_regime_labels_mvstopped propagating newcorrectedinserts. The MV only fires on inserts toregime_archive(053:44-61); if nocorrectedinsert has happened since 2026-06-12 (hypotheses 1–4), there is simply nothing for it to project — this is a consequence, not an independent cause. Only suspect a broken MV ifregime_archivehascorrectedrows past 2026-06-12 butdaily_regime_labelsdoes not.
6. RUNBOOK — diagnose on prod (READ-ONLY)
Run from the compute server (has vRack access to ClickHouse10.0.0.2 and
Postgres 10.0.0.2). None of these mutate data.
A. Current label coverage (the exact value in the error):
corrected vs emitted coverage in the substrate — proves where the gap is:
--to? (needs GitHub, not prod)
7. Fix path (pipeline, NOT manual SSH) — ⚠️ SUPERSEDED: use the §10 procedure
Per CLAUDE.md deploy-safety and the workflow header (lines 6–9), the corrected series is (re)advanced only by re-running the GitHub Actions workflow — never by SSHing to runregime-replay by hand.
- Preconditions: confirm §6.E shows
market_breadthVIX and SPY bars are fresh through your targetto. If not, resolve that upstream first. - Dispatch the backfill: GitHub → Actions → “Regime Replay Backfill” →
Run workflow, with:
from=2021-04-22(default; full byte-reproducible history — safe,ReplacingMergeTreededups).Or an incremental— the rewritten workflow rejects non-dry partial-history runs (the percentile warm-up of a short window diverges from the full fold; §10).2026-06-01to extendto= today (or the latest completed trading session) — this is the value the pre-fix default2026-06-12failed to advance.run_id= a NEW unique id, e.g.sp1-corrected-backfill-2026-07-15.A new id gets a fresh— WRONG, this was part of the bug (§10.3): the hash-derived stamp is deterministic but NOT monotonic, so a new run id was never guaranteed to supersede. The rewritten workflow always pins a strictly-increasingcomputed_atand supersedes prior corrections--computed-at, making the LATEST run supersede regardless of run_id.- Optionally set
dry_run: truefirst to print the fold summary with no writes (main.go:131-153). Keeplogic_versionunchanged (sp1-v1) — a NEW logic_version makes prior backtests non-comparable (workflow lines 11–14;053KNOWN LIMITATION 16-23).
- The workflow self-verifies: the run step prints per-variant
count/min/maxofregime_archive(pre-fix lines 103–106); post-fix the real verification is the hard freshness/MV-integrity gate step after it. - Confirm the fix (READ-ONLY):
SELECT max(date) FROM daily_regime_labels≈ today (§6.A). The MV (053:49-61) propagates the newcorrectedrows automatically.- Re-run a v2 backtest with a recent
date_to— it should passassert_regime_labels_coverand complete.
schedule: on regime-replay-backfill.yml is the structural cause.
Options: (a) add a nightly cron to that workflow that folds [2021-04-22, today]
with a per-day run_id (mirrors regime-parity-nightly.yml:17-20, which already
runs “after the daily regime emit”); or (b) a design change to let the projection
read emitted as a fallback where corrected is absent (SP2 territory — the
053 single-logic_version limitation, 16-23, must be handled first). Option (a)
— plus a monotonic pinned stamp, hard freshness/MV gates, and a parity-side
staleness backstop — shipped with this runbook update; see §10.
8. Open questions (need prod / GitHub access to answer)
- When was the backfill last dispatched, with what
from/to/run_id, and did it succeed? (§6.Cgh run list.) Distinguishes hypotheses 1 vs 2 vs 3. - Is
market_breadthVIX fresh past 2026-06-12? (§6.E.) If not, the root cause is upstream VIX ingestion, not the backfill itself.# TODO confirm on prod - What writes
market_breadthVIX, and on what schedule? Not resolved from the pattern-detector code read; likely the daily-daemon or a VIX backfill (cmd/backfill-vixis built indeploy-worker.yml:618-619).# TODO confirm on prod - Is the live
pattern-detectoremit still advancingemitted? (§6.B/D.) Confirms the SPY input pipeline is healthy so a re-run will have data. - Did a second
correctedlogic_versionever get written? If so the053single-version assumption (16-23) is violated and the projection could carry the wrong variant on a date.# TODO confirm on prod
9. Single most-likely root cause (one line)
Thecorrected regime substrate that feeds daily_regime_labels is advanced
only by the operator-triggered regime-replay-backfill.yml, which has no
scheduled trigger (workflow_dispatch-only, lines 16–17) and whose default
to is 2026-06-12 (line 26) — so it was run once through 2026-06-12 and
never re-run, freezing coverage while the live emitted variant (filtered out of
the projection at 053:60-61) kept advancing unused.
10. Resolution (2026-07-15)
Root cause confirmed = §5 hypothesis 1. Evidence gathered on prod + GitHub:- Run history (§6.C): the workflow has run exactly twice ever — both on
2026-06-16, both
workflow_dispatch, both success. Never dispatched again; no failed runs exist. The backfill never failed — it was never re-run. (Answers §8 Q1; rules out hypotheses 2/3.) - Prod substrate (§6.A/B):
daily_regime_labelsmax(date) = 2026-06-12.regime_archive: corrected = 1,243 rows, 2021-07-01→2026-06-12, all with a single hash-derivedcomputed_atof 2021-08-16 (run idsp1-corrected-backfill); emitted advancing through 2026-07-15. Substrate gap, not MV propagation — hypothesis 5 ruled out. - Upstream healthy (§6.E; rules out hypothesis 4):
market_breadthVIX max(time) = 2026-07-15 20:14 UTC; SPYmarket_data_1minmax day = 2026-07-15. (Answers §8 Q2/Q4.) - Single corrected
logic_version(sp1-v1) / single run id — the053single-version assumption holds (§8 Q5).
- Nightly
schedule:cron, 06:00 UTC Tue–Sat — the structural fix (regime-replay-backfill.yml). One run per completed trading session: after the prior session’s after-hours bars complete (the SPY daily aggregation is deliberately full-day, parity-locked to the live path —regime_input_builder.goheader) and before the 07:00 UTCregime-parity-nightlygate, which then validates the fresh corrected rows. - Inputs are now optional, resolved and validated in one step — a schedule
run or an all-empty dispatch resolves to
from=2021-04-22,to=<current ET date>,run_id=sp1-corrected-nightly-<to>. The hardcodedto: '2026-06-12'default footgun is gone. All values (includingdry_runandrun_number) are charset-validated before the firstGITHUB_ENVwrite — raw dispatch inputs can carry newlines, which would otherwise smuggle unvalidated variable assignments into later steps’ ssh command lines. Non-dry runs must keepfrom=2021-04-22: a shorter window’s percentile warm-up (~207 trading days of pad) diverges from the full fold near thresholds, and mixing the two provenances in one table is silent inconsistency. --computed-atpinned to<UTC run date>T06:00:00Z + github.run_numberseconds —resolveComputedAt(main.go) derives the ReplacingMergeTree version stamp fromhash(run_id), which is deterministic but not monotonic: a newer run id was never guaranteed to supersede an older one (that false claim in main.go’s comments is corrected in this PR). The pinned stamp is strictly increasing across workflow runs, so the LATEST run always supersedes — nightlies, catch-ups, and corrective re-runs after upstream data fixes all behave identically, with no same-day-collision or earlier-totraps. Every pinned stamp also exceeds the original backfill’s 2021-08-16 hash stamp. Re-folds of unchanged history are byte-identical, so overlap supersedes are content-neutral.- Hard post-run gate, session-based, on BOTH tables — the job fails when
corrected
max(date)is more than 1 SPY trading session behind (so holiday clusters cannot false-alarm), or whendaily_regime_labelsmax(date)!= correctedmax(date)(a broken053projection MV — §5 hypothesis 5 — now fails loudly instead of freezing silently).BuildRegimeInputsonly WARNs on stale inputs (regimeStalenessTolDays), so this workflow gate is the loud failure that was missing. Failures firePROBE_ALERT_WEBHOOK, mirroringregime-parity-nightly.yml. - Independent staleness backstop in the parity gate —
scripts/regime-parity-check.shgains FATAL check[4c]: more thanSTALE_TOL_SESSIONS(default 3) SPY sessions beyond correctedmax(date)fails the 07:00 UTC parity job. This monitors the producer from OUTSIDE it: if the backfill cron silently stops firing (GitHub drops/disables schedules), its in-job gate never runs — the parity job still pages. docs/DATA_FRESHNESS_SLAS.mdgains the previously-missing regime-labels row.
daily_regime_labels max(date) reaches the
last completed session, and that a recent-window v2 backtest passes
assert_regime_labels_cover. If a same-day row ever folds partial after-hours
data (intraday dispatch), any later run supersedes it — stamps are strictly
increasing per run.