DocumentationUnder the hood

Regime and canary backtest runbook

Updated: 2026-07-25 09:45 CEST

This is the single umbrella for proving and tuning ibkr regime and ibkr canary. Keep the work here. Do not add another experiment plan, tuning plan, or backtest framework unless this runbook says to.

The goal: prove that regime and canary produce useful stress detection without overfitting to named events.

Plain definitions

Artifact map

All backtest artifacts live in one of these places:

Path Purpose
docs/docs/internals/regime-backtest.md This runbook: sequencing, gates, stop rules, and current backlog.
docs/docs/internals/regime-dashboard.md Product contract for live ibkr regime; not a tuning backlog.
scripts/backtest/ Reproducible data-build and comparison scripts only.
build/backtest/backtest_sources*.jsonl Source ledgers: URLs, checksums, gaps, and retrieval status. Built, not checked in.
internal/cli/testdata/regime_pit_panel_sample.jsonl Tiny point-in-time sample consumed by build-regime. Checked in; a unit test reads it.
internal/cli/testdata/regime_backtest_sample.jsonl Tiny compact replay sample consumed by backtest regime. Checked in.
internal/cli/testdata/canary_backtest_sample.jsonl Tiny canary replay sample with account and position overlays. Checked in.
build/backtest/*.jsonl Captured evidence corpora. Built on demand, git-ignored, never checked in.

Do not hand-edit generated compact regime rows when the point-in-time panel is the source of truth. Rebuild them.

Command sequence

Small smoke fixtures:

ibkr backtest regime --input internal/cli/testdata/regime_backtest_sample.jsonl
ibkr backtest canary --input internal/cli/testdata/canary_backtest_sample.jsonl

Capture a fresh tuning and holdout split before tuning anything. The checked-in sourced corpora were deleted in July 2026: they recorded verdicts from regime code that has been corrected several times since, so they measured the old bugs rather than the current engine.

ibkr backtest capture-opportunity --preset top-movers --include-regime --split tuning \
  --append /tmp/regime_tuning.jsonl
ibkr backtest capture-opportunity --preset top-movers --include-regime --split holdout \
  --holdout-plan <plan-id> --append /tmp/regime_holdout.jsonl
ibkr backtest regime --input /tmp/regime_tuning.jsonl
ibkr backtest canary --input /tmp/regime_tuning.jsonl

Keep captures outside the repository. A corpus checked in beside the unit-test fixtures reads as a fixture and stops being regenerated.

Point-in-time regime builder:

ibkr backtest build-regime --input internal/cli/testdata/regime_pit_panel_sample.jsonl \
  > /tmp/regime_backtest_rows.jsonl
ibkr backtest regime --input /tmp/regime_backtest_rows.jsonl

That is two passes only when starting from raw point-in-time market rows. If the input is already compact regime JSONL, run ibkr backtest regime directly.

Tier 1 expanded panel. Build it, then work from the build output:

python3 scripts/backtest/build-tier1-regime-panel.py
ibkr backtest build-regime --input build/backtest/regime_pit_panel_tier1.jsonl \
  > build/backtest/regime_backtest_tier1.jsonl
ibkr backtest regime --input build/backtest/regime_backtest_tier1.jsonl
python3 scripts/backtest/compare-tier1-vol-rules.py

Data tiers

Tier 0: smoke fixtures

Tier 1: expanded volatility/calm/event panel

Tier 2: confirmation proxy panel

Tier 2 input classification

The Tier 2 inputs are not all product indicators. This is the final classification for the current pass:

Series Classification Runtime decision
RSP/SPY Historical proxy for missing breadth/participation. Do not promote. Live ibkr regime already has first-class constituent breadth.
QQQE/QQQ Historical proxy for Nasdaq mega-cap concentration/participation. Do not promote into broad regime. Keep as backtest context unless a separate concentration surface is designed.
IWM/SPY Context-only risk appetite/size rotation. Do not promote; it is not S&P breadth and can fire during benign leadership shifts.
HYG/LQD, HYG/IEF Historical credit-confirmation proxies. Do not promote now. Runtime already exposes HYG/SPY and official HY OAS; add another live credit ETF row only if HY OAS proves persistently unavailable.
LQD/TLT Context-only. Excluded from active confirmation because it mixes credit, duration, and rates effects.
TLT/IEF, SHY/IEF Context-only rates/duration stress proxies. Do not promote; they are not MOVE/rates vol and produced calm/rally false alarms.
FRED rates/curve series Context or source-data support. Do not promote as new regime indicators without a separate product definition and point-in-time source gate.

No Tier 2 ETF proxy is promoted into live ibkr regime in this pass. The production change is narrower: isolated red VVIX is treated as an unconfirmed equity-volatility warning unless severity or already-visible tape confirms it. The red row remains visible in ibkr regime; only the cluster vote is downgraded from stress to watch.

Current findings

From the sourced corpora

Tier 1

Tier 2 proxy pass

Tuning pass sequence

Run this sequence and stop at the first failed gate:

  1. Validate Tier 2 proxy sources and record them in the source ledger.
  2. Build a Tier 2 point-in-time panel by extending Tier 1 with confirmation proxy features.
  3. Split labels into:
    • watch: early warning / elevated risk.
    • stress: observable market damage or strongly confirmed broad stress.
  4. Compare exactly three stress-signal rules:
    • current: any red cluster is stress.
    • confirmation-only: isolated red volatility is not stress.
    • severity split: isolated red volatility is watch unless severity or independent confirmation is strong enough.
  5. Tune only the severity split, and only on the tuning split.
  6. Score holdout once the tuning behavior is stable.

The isolated-VVIX downgrade is live policy; the regime dashboard contract owns its wording and thresholds, along with which rows stay visible under it. One constraint belongs to this runbook rather than to a dashboard row:

Confirmation gates in replay

The live engine applies confirmation eligibility (depth + persistence + cadence-freshness, see the dashboard contract) before any red may confirm. Point-in-time panels are independent daily observations, so the replay evaluates DAY-1 gates: depth and freshness bind, persistence is sessions=1, and streak-gated indicators confirm only through their fast-path depths. Bands-only panel rows (no raw values) therefore cannot satisfy depth-gated fast paths, a known fidelity limit. Expected consequence in the curated sample: 2023-03-13 (SVB Monday) reads early_warning day 0 and confirms with persistence, while crash days (volmageddon, COVID, the 2024 carry unwind) still confirm same-day via fast paths and tape co-signs.

Sequence-aware streak replay is the follow-up: typed regime decision events in the daemon's sole live authority ($XDG_STATE_HOME/ibkr/daemon.db, see the dashboard contract) forward-collect raw values, depths, streaks, eligibility, and governor decisions per snapshot. That event corpus is the calibration source for promoting threshold sets out of pending_backtest, per versioned label, with measured false-alarm and recall rates.

Data gates

Tier 2 data is green only if all are true:

If these fail, do not tune. Fix data or stop.

Tuning gates

A tuning change is allowed only if all are true:

If Tier 2 cannot materially improve precision without destroying recall, stop tuning and revisit the product definition. Do not add more indicators just to force convergence.

Verification gates

Before calling a pass done:

make check
make smoke

After CLI or daemon changes, also install and smoke the actual binary:

make restart-daemon
ibkr status
ibkr backtest build-regime --input internal/cli/testdata/regime_pit_panel_sample.jsonl \
  > /tmp/ibkr-build-regime-smoke.jsonl
ibkr backtest regime --input /tmp/ibkr-build-regime-smoke.jsonl

Not doing