The Warning study measured a fitted percentile crossing that nothing consumes. What reaches Telegram is a quadrant change: fixed 50/40 dividers, hysteresis, two-session confirmation, 3-day cooldown. Those thresholds are constants, not fits, so there is no training set to protect and all 11 detected corrections are evaluable instead of the 4 that fell in a holdout. Replaying it: 1/10 corrections, 0.9 false alarms/year. Random alarms at the same firing rate match or beat that in 65% of draws. The panel now carries ablations (does the quadrant machinery earn its place?), external baselines (does the score earn its complexity?), and that null, because a bare "2 of 4" was unreadable in either direction. Nothing in the alert path was retuned on the strength of it. Fundamentals become a third channel rather than a term in either score. v3 cut them arguing 12+8 of 100 points "could not change any published conclusion" -- true only when every technical sensor reads zero; weighted they moved the bar for the 40 divider from 40 to 25. But no fusion weight is measurable either: with ~10 events and no fundamental history, any weight is a policy preference presented as a measurement. So the read is a categorical state (supportive/neutral/adverse/ unknown) with an evidence grade, derived by fixed rules from stored facts, read by confluence. The LLM extracts and explains; it does not score. Absence stays absence throughout. `unknown` is unreachable by averaging, a stale or empty observation may display but never confirm, extraction failures map to `unknown` rather than `mixed`, and the study rows are coverage-matched and marked not-measurable until enough corrections are covered -- otherwise a fortnight of observations renders as 0/10 and reads as a failed test. Observations become a real time series (migration 033); they lived in a single overwritten settings slot, so no history existed to replay. Pre-rename snapshots are adapted rather than discarded. METHODOLOGY stays v4 -- no score changed -- so no reseed; STUDY_SCHEMA moves to 3 and discards the cached report. Post-deploy: re-run Event Study from Admin -> Jobs. The panel reads "not run yet" until then. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
838 lines
45 KiB
Markdown
838 lines
45 KiB
Markdown
# AI/Tech Risk Monitor v4 methodology
|
||
|
||
Named "Regime Monitor" until 2026-08-07; the filename's `regime` stem, the
|
||
`regime_monitor` job id, the `/regime` route and the `METHODOLOGY`/snapshot
|
||
fields keep the old word, because those are persisted or externally linked.
|
||
|
||
The AI/Tech Risk Monitor is an observational risk thermometer. It does not
|
||
gate entries, exits, position size, ranking, or alerts about individual setups.
|
||
|
||
**v4 supersedes v3** (2026-08-08). Unlike v3, whose calibration was ad-hoc and
|
||
never landed, every number below is reproducible:
|
||
|
||
```
|
||
.venv/Scripts/python.exe scripts/run_regime_monitor_calibration.py --methodology v2_reconstruction,v2_reconstruction_oas400,v3,v4,v4-vix-only,v4-p1-only --cache-dir .calib-cache
|
||
```
|
||
|
||
`v3` and `v4` are mandatory — the row-wise `state_v4 <= state_v3` invariant is
|
||
a hard gate and needs both — and the replayed **start** date is asserted
|
||
against the published window. The session *count* alone proves nothing, since
|
||
the harness slices the tail of the price series to whatever was asked for.
|
||
|
||
The harness replays the 408 sessions ending 2026-07-24 from the live inputs
|
||
(Alpaca for all 33 symbols, FRED for VIX and HY OAS) with no database, and
|
||
reproduces the published v2 and v3 figures before it will emit anything:
|
||
|
||
| figure | published | replayed |
|
||
|---|---|---|
|
||
| v2 State avg | 22.6 | 22.68 |
|
||
| v2 State p80 | 35.1 | **35.1** |
|
||
| v2 State max | 91.2 | **91.2** |
|
||
| v2 P3 pegged | 39 | **39** |
|
||
| v2 W1 live | 108 | **108** |
|
||
| v3 State max | 87.4 | **87.4** |
|
||
| v3 band shares | 73.3 / 15.0 / 8.3 / 3.4 | 73.0 / 15.4 / 8.1 / 3.4 |
|
||
|
||
It refuses to emit a band recommendation, and exits non-zero, unless every hard
|
||
gate passes — 33 symbols fetched with full warm-up, the whole basket on every
|
||
session, the calendar anchors, 100% coverage on every row, and a row-wise
|
||
`state_v4 <= state_v3` invariant. Reading a calibration result out of a run whose
|
||
pipeline did not validate is meant to be structurally impossible.
|
||
|
||
## The fundamental channel (2026-08-12)
|
||
|
||
The monitor has **three channels**, not two scores with a decoration:
|
||
|
||
- **State** — current observable technical stress (price, breadth, credit, volatility).
|
||
- **Warning** — observable deterioration that may precede stress (breadth
|
||
divergence, relative strength, credit impulse).
|
||
- **Fundamental context** — a categorical state (`supportive` / `neutral` /
|
||
`adverse` / `unknown`) with an `evidence_quality` grade.
|
||
|
||
The third is **never a term in the other two**. They are read together by
|
||
confluence:
|
||
|
||
| Warning | Fundamentals | Reading |
|
||
|---|---|---|
|
||
| Calm | Supportive/neutral | Normal |
|
||
| Elevated | Supportive/neutral | Technical warning, not fundamentally confirmed |
|
||
| Calm | Adverse | Fundamental concern; tape has not confirmed |
|
||
| Elevated | Adverse | Confluence — highest attention |
|
||
|
||
`METHODOLOGY` stays **v4**: no score changed, so partitioning the history API and
|
||
discarding the event study cache would be churn. `STUDY_SCHEMA` moved to 3
|
||
instead, and is now the only thing that discards a stale report.
|
||
|
||
### Why the read is a channel and not a weight
|
||
|
||
Two things are true at once, and only this shape honours both.
|
||
|
||
**v3's reason for removing fundamentals from the score was wrong.** Not stale —
|
||
wrong. v3 argued that F1 (capex) and F3 (good-news-stock-down), carrying 12 + 8
|
||
of 100 Warning points, "could not change any published conclusion" because pegged
|
||
they produced a Warning of exactly 20.0, below the alarm threshold. That
|
||
arithmetic holds only when *every* technical sensor reads exactly zero, which is
|
||
the one case that never matters. Warning is a weighted average, so the sensors
|
||
add:
|
||
|
||
| technical Warning | without fundamentals | with them pegged | delta |
|
||
|---|---|---|---|
|
||
| 0 | 0.0 | 20.0 | +20.0 |
|
||
| 20 | 20.0 | 36.0 | +16.0 |
|
||
| 25 | 25.0 | **40.0** | +15.0 |
|
||
| 35 | 35.0 | **48.0** | +13.0 |
|
||
| 50 | 50.0 | 60.0 | +10.0 |
|
||
| 80 | 80.0 | 84.0 | +4.0 |
|
||
|
||
Pegged fundamentals lowered the technical Warning needed to reach the 40 quadrant
|
||
divider from 40 to 25. That is a 15-point shift in where the alert fires, which
|
||
is emphatically a changed conclusion. The v3 section below is kept as written,
|
||
with this correction attached, because its reasoning is cited elsewhere in this
|
||
file and a silent overwrite would hide that the error was ever made.
|
||
|
||
**But no weight is measurable either.** A weighted modifier was built and
|
||
reverted: 0–25 points added onto the technical Warning, sized so a maxed-out read
|
||
carried a calm tape over the 40 divider on its own. Nothing could justify the 25.
|
||
With ~10 correction events and essentially no fundamental history, any fusion
|
||
weight is a policy preference presented as a measurement — and the debate it
|
||
invites ("does the read deserve 10%, 20%, 30%?") has no evidence that can settle
|
||
it. Adding a slow categorical judgement to a fast continuous score also
|
||
manufactures precision by summing unlike things, and it forces a missing
|
||
observation to silently redistribute its weight onto the technical sensors, which
|
||
is the opposite of leaving it unknown.
|
||
|
||
So: the read gets a channel, not a coefficient. Both facts survive — the v3
|
||
removal was badly argued *and* no weight is defensible — because "report it
|
||
separately" is the only design that neither buries the observation nor invents a
|
||
number for it.
|
||
|
||
### Derivation
|
||
|
||
Deterministic, from the stored categorical facts. The LLM is an **extraction and
|
||
explanation layer**: it finds the capex guidance, classifies it, and cites it.
|
||
Fixed rules turn those facts into a state, so the same observation always yields
|
||
the same category.
|
||
|
||
`capex_signal`: any `cutting` → adverse; else any `holding` → neutral; else all
|
||
known `raising` → supportive; nothing known → unknown.
|
||
`reaction_signal`: `yes` → adverse, `mixed` → neutral, `no` → supportive,
|
||
`unknown` → unknown.
|
||
|
||
`mixed` and `unknown` are different reaction states and were merged until
|
||
2026-08-13. A failed LLM parse fell back to `mixed`, so an extraction error
|
||
became *neutral evidence* — an observation of normality manufactured out of a
|
||
bug. `mixed` now means an observed mixed reaction; anything unreadable, missing
|
||
or unattempted is `unknown` and contributes nothing.
|
||
|
||
Combined by precedence, never by averaging: **any adverse read carries**; both
|
||
unknown → unknown; every observed signal supportive → supportive; otherwise
|
||
neutral.
|
||
|
||
`unknown` is deliberately unreachable by combination. Averaging would let two
|
||
`cutting` reads and two `unknown` ones land on "neutral", presenting missing
|
||
evidence as evidence of normality — the same conflation `current_observation`
|
||
already refuses between "no observation" and "an observation of zero". Two cuts
|
||
and two unknowns read **adverse with `evidence_quality: partial`**.
|
||
|
||
`evidence_quality` is ordered by what an operator needs first: `unavailable`
|
||
(nothing collected) → `stale` (past `fundamental_staleness_days`) → `manual`
|
||
(hand override) → `complete` / `partial`.
|
||
|
||
### Presentation and alerts
|
||
|
||
The Path view colours each dot by the fundamental state recorded that day; the
|
||
axes are untouched, because context is confluence information rather than a
|
||
position on either axis. The card leads with the state and evidence grade.
|
||
|
||
Alerts stay **separate**, off one toggle:
|
||
|
||
- quadrant change — the market axes moved (existing);
|
||
- `regime_fundamental` — the context changed, e.g. neutral → adverse;
|
||
- `regime_confluence` — Warning elevated *and* fundamentals adverse.
|
||
|
||
`unknown` never alerts: an absence of evidence is not a change in the evidence,
|
||
and alerting on it would train the reader to ignore the channel. Both new
|
||
triggers seed silently on first run, as the quadrant alert does.
|
||
|
||
### The observation is now a real time series
|
||
|
||
`regime_fundamental_observations` (migration 033), one row per `effective_date`,
|
||
upserted. Before this it lived in a single `SystemSetting` slot that every
|
||
refresh overwrote, so no history existed at all — which made the read impossible
|
||
to replay, impossible to backtest, and meant a rebuild recorded every historical
|
||
session as if nothing had been observed. `update_regime_monitor` carries the
|
||
pre-existing single-slot observation into the series on its next run.
|
||
|
||
### What this does not establish
|
||
|
||
The table starts empty and fills one observation at a time, so the fundamental
|
||
rows are **untested, not failed**. Two things enforce that rather than one:
|
||
|
||
- they are **coverage-matched** — scored only on sessions where the channel had
|
||
usable context and on corrections whose warning horizon fell inside it, with a
|
||
market-only comparator over the identical window so any difference between them
|
||
is the channel and not the window;
|
||
- `measurable` stays false until `MIN_EVENTS_FOR_CONFIDENCE` corrections are
|
||
covered, and the panel prints "insufficient exposure" rather than a ratio.
|
||
|
||
Without the first, one day of coverage would render as 0/10 — recreating, one
|
||
observation later, exactly the tested-versus-unavailable confusion the flag was
|
||
added to prevent. The market rows are unchanged, and the 1/10 shipped-rule figure
|
||
remains a verdict on the technical sensors and the alert machinery alone.
|
||
|
||
The rationale for expecting the read to matter is the operator's: hyperscaler
|
||
capex is the demand side of the entire AI trade, and good earnings being sold is
|
||
a classic late-cycle tell. Both are plausible. Neither is measured here, and this
|
||
file's convention is that published numbers are reproducible.
|
||
|
||
**The path forward is accumulation, then a test — in that order.** Once enough
|
||
point-in-time observations exist, test whether the state improves prediction
|
||
*conditional on* Warning. If it does, a fitted and calibrated model has something
|
||
to fit; until then there is nothing to calibrate against. Backfilling would get
|
||
there faster: capex direction is derivable from the 10-Q/10-K capex line, which
|
||
the SEC fundamentals import already carries, and "good news, stock down" from
|
||
earnings dates plus next-day returns, which the Dolt earnings import already
|
||
carries. That last one is worth computing deterministically rather than asking
|
||
the LLM to judge, for the same reason the state derivation is rule-based.
|
||
|
||
## What changed in v4
|
||
|
||
**V1 stopped saturating at VIX 30.** `(vix - 15) / 15` reached 100 at VIX 30 —
|
||
the same defect v3 had *just* removed from P3, left in place one sensor over. VIX
|
||
30 is a bad week, 50 is a crisis and 82 was March 2020, and all three scored
|
||
identically. In the calibration window this flattened five distinct April-2025
|
||
prints (52.33, 46.98, 45.31, 40.72, 38.57) into a single 100. It pegged on 14 of
|
||
408 sessions; under the anchors below, none.
|
||
|
||
**The trend break is graded by depth, not a yes/no.** `_under_200` returned a
|
||
bare 0/100, so P1 printed 100 the moment SMH and QQQ were both under their
|
||
average — and because the price pillar takes `max(P1, P2, P3)`, that pinned the
|
||
pillar and stopped P3's anchored ladder resolving anything for the whole of a
|
||
selloff. It pegged on 46 of 408 sessions; now none. A 2% break reads ~30 where it
|
||
used to read 100.
|
||
|
||
`max()` was **kept**. The defect was the step function feeding it, not the vote
|
||
itself, and v3's "one capped vote for correlated reads" rationale still holds.
|
||
The `P1_SCORE_CAP` fallback drafted during design was to fire if P1 became the
|
||
sole price argmax on **more than 80% of sessions with State ≥ 40** — i.e. if it
|
||
had quietly become a second drawdown sensor. Measured on that population: 47
|
||
qualifying sessions, P1 sole argmax on **17 of them (36.2%)**, against P2's 16
|
||
and P3's 14. Well under the threshold, so the cap is not shipped.
|
||
|
||
**The top State band moved 80 → 65.** See Calibration; this is the one change
|
||
that is about the band rather than a sensor.
|
||
|
||
**Scope.** All three are State-side. `WARNING_BANDS`, `WARNING_WEIGHTS`,
|
||
`QUADRANT_WARNING_DIVIDER` and the event study's frozen threshold are untouched.
|
||
`QUADRANT_STATE_DIVIDER` stays 50 because only `breaking` moved.
|
||
|
||
## What changed in v3
|
||
|
||
**Fundamentals left the score.** F1 (capex) and F3 (good-news-stock-down)
|
||
carried 12 + 8 of 100 Warning points. Pegged at maximum stress they produced a
|
||
Warning of exactly 20.0 — below the event study's 25.3 alarm threshold, and
|
||
still inside the "stable" band. The sourced observation could not change any
|
||
published conclusion, so refreshing it looked like it did nothing. They are now
|
||
a qualitative overlay reported beside the scores. Capex also stopped scoring
|
||
`raising` and `holding` identically at 0: `holding` is the deceleration case and
|
||
now scores 50, so a boom no longer reads the same as a stall.
|
||
|
||
> **Corrected 2026-08-12.** The claim in this paragraph is false. "Pegged
|
||
> they produced a Warning of exactly 20.0" describes only the case where every
|
||
> technical sensor reads zero; Warning is a weighted average, so in the general
|
||
> case those 20 points added +10 to +20 and moved the technical score needed to
|
||
> reach the 40 quadrant divider from 40 to 25. The observation was removed for
|
||
> being *underweighted*, on reasoning that mistook a corner case for the whole
|
||
> range. See "The fundamental channel" above for what replaced it — a separate
|
||
> categorical channel, not a restored weight. The capex `holding` rescale in the second half
|
||
> of this paragraph stands and is still live.
|
||
|
||
**The drawdown sensor stopped saturating.** v2 used `dd_pct * 5`, reaching 100 at
|
||
a 20% drawdown — the 90th percentile of the observed distribution. 39 of 408
|
||
sessions sat at exactly 100 with no resolution left, and the price pillar showed
|
||
the top band on 13.5% of sessions. v3 uses named anchors with headroom past the
|
||
observed 36% maximum, and blends leader/confirm 2:1 as P1 and P2 already did
|
||
instead of taking `max()`. P3's realized share of State falls from 65% to 40%,
|
||
matching its nominal weight.
|
||
|
||
**Warning gained a sensor with range.** The HY OAS *level* is pinned at zero
|
||
below the 3.5 mild anchor (2.77 at the cutover), so credit contributed nothing
|
||
in a calm tape. Its 20-session rate of change still does, and spread widening is
|
||
a classic lead.
|
||
|
||
**The credit percentile leg was removed.** Its reference window silently shrank
|
||
from 10 years to 3 when ICE restricted the upstream series in April 2026, after
|
||
which it scored 20 points of stress at a spread the same sensor's anchors call
|
||
"mild". See Calibration below.
|
||
|
||
**Breadth loss counts during declines.** v2's divergence gate was
|
||
`price_ret >= 0`, so the sensor zeroed during every selloff. On 2026-07-24 the
|
||
basket shed 10 points of participation in 20 sessions while SMH fell 11.9% and
|
||
Warning printed exactly 0. v3 tapers to a floor instead: deterioration counts
|
||
fully when price masks it (true divergence, the dangerous pre-top case) and at
|
||
35% when price confirms it. Breadth *level* lives in State, but breadth
|
||
*velocity* appears nowhere else, so this is not double counting.
|
||
|
||
**Bands are per axis.** v2 Warning never exceeded 64.9 in 408 sessions while
|
||
State reached 91.2, yet both used 30/60/80 with quadrant dividers at 60. The
|
||
upper half of the Warning axis was unreachable.
|
||
|
||
## Outputs
|
||
|
||
**State** — current structural stress:
|
||
|
||
- Price structure, 40%: `max(P1, P2, P3)`, one capped vote for correlated reads.
|
||
- Fixed-basket breadth level, 25%.
|
||
- HY option-adjusted credit spread level, 20%.
|
||
- VIX level, 15%.
|
||
|
||
**Warning** — deterioration and divergence:
|
||
|
||
- Fixed-basket breadth divergence, 45%.
|
||
- 60-session SMH/SPY relative-strength deterioration, 30%.
|
||
- HY OAS 20-session widening, 25%.
|
||
|
||
**Fundamental context** — a categorical third channel, not a term in either
|
||
score. See "The fundamental channel" above.
|
||
|
||
Combined, RSP/SPY (former F4), and the NVDA canary (former P6) do not enter v3
|
||
or v4.
|
||
|
||
## Calibration
|
||
|
||
### Interpolated sensor tables
|
||
|
||
All three are `(x, stress score)` pairs read by `_interpolate`, flat outside the
|
||
first and last anchor.
|
||
|
||
| sensor | anchors |
|
||
|---|---|
|
||
| P3 drawdown (% below the 52w high) | 0→0, 4→10, 8→25, 16→50, 28→78, 40→100 |
|
||
| **P1 trend break** (% below the 200-DMA) | 0→**20**, 3→35, 8→55, 15→75, 25→100 |
|
||
| **V1 volatility** (VIX level) | 15→0, 20→20, 25→38, 30→55, 40→80, 55→100 |
|
||
|
||
P1's floor of 20 at the crossing is deliberate: the break itself is a genuine
|
||
binary event and deserves a floor; only the depth past it is graded. P1 is
|
||
calibrated to sit alongside P3 rather than swamp it — the 200-DMA lags, so a 20%
|
||
drawdown typically coincides with ~10% below the average, where P1 reads ~61
|
||
against P3's ~59.
|
||
|
||
V1 reaches full scale at 55 rather than at 2020's ~82: anchoring the top at a
|
||
once-in-a-generation print would make VIX 50 — a genuine crisis — read only ~70.
|
||
The anchors encode the long-run distribution as constants, the same argument the
|
||
credit level uses. Unlike P1 and V1, whose slopes ease off monotonically, P3's do
|
||
not (2.5, 3.75, 3.125, 2.33, 1.83) — its gentle onset is intentional and the
|
||
monotone-slope test excludes it.
|
||
|
||
Credit impulse is relative (+35% over 20 sessions = 100) rather than absolute,
|
||
because +0.5pp means something very different at an OAS of 2.7 than at 8.0.
|
||
|
||
### Bands
|
||
|
||
Round, meaning-anchored numbers, **not** percentile fits — those would drift on
|
||
every rebuild and silently rewrite what past snapshots meant.
|
||
|
||
**Why `breaking` moved 80 → 65.** With credit calm, `f2_credit_spreads` returns
|
||
`0.0` (not `None`), so it keeps its full 20 points pinned at zero. Price, breadth
|
||
and volatility at *literal maximum* therefore sum to:
|
||
|
||
(100×40 + 100×25 + 0×20 + 100×15) / 100 = 80.0 exactly
|
||
|
||
`band_for` uses `>=`, so v3's top band was reachable only by touching its floor
|
||
to the decimal, with nothing above it. The band was fit on v2, when credit's
|
||
since-removed percentile leg still contributed regularly; the sensor is not
|
||
wrong — a calm-credit selloff genuinely *is* less stressed than one with credit
|
||
contagion — the threshold was stale.
|
||
|
||
Chosen by scenario arithmetic on unchanged weights (`_scenarios` in the harness
|
||
computes these, so they are machine-checked, not prose):
|
||
|
||
| scenario | price | breadth | C1 | V1 | State |
|
||
|---|---|---|---|---|---|
|
||
| Ordinary tape (3% dd, breadth 65%, VIX 16, OAS 2.8) | 7.5 | 0 | 0 | 4.0 | **3.6** |
|
||
| 10% correction, calm credit (2% below, breadth 35%, VIX 24) | 31.2 | 62.5 | 0 | 34.4 | **33.3** |
|
||
| **2022-style drawdown, calm credit, no death cross** | 90.8 | 100 | 0 | 60.0 | **70.3** |
|
||
| **same, with death cross** (P2 pegged) | 100 | 100 | 0 | 60.0 | **74.0** |
|
||
| Credit event on top (OAS 6.0, VIX 45) | 100 | 100 | 75.0 | 86.7 | **93.0** |
|
||
| March 2020 (everything pegged) | 100 | 100 | 100 | 100 | **100** |
|
||
|
||
Rows 3 and 4 are the case this monitor exists to measure, and they must print
|
||
`breaking`. At 80 they do not. **65** clears them under either P2 assumption,
|
||
which matters because P2 is set by the 50/200-DMA gap and no drawdown figure
|
||
implies it; 70 would have left 0.33 points of headroom in row 3, reproducing the
|
||
defect being fixed.
|
||
|
||
Realized shares, **reported not fitted**, over the 408 sessions to 2026-07-24:
|
||
|
||
| Axis | stable | watch | elevated | breaking | thresholds |
|
||
|------|--------|-------|----------|----------|------------|
|
||
| State (v4) | 78.9% | 13.0% | 4.7% | **3.4%** | 20 / 50 / **65** |
|
||
| Warning | 69.4% | 19.6% | 7.6% | 3.4% | 20 / 40 / 60 |
|
||
|
||
The v4 `breaking` share lands on 3.4% — the same as v3's — having been chosen by
|
||
scenario reasoning rather than aimed at that number. Sensitivity: 60 gives 5.1%,
|
||
70 gives 1.2%.
|
||
|
||
Quadrant dividers sit at each axis's watch/elevated boundary: State 50,
|
||
Warning 40. Only `breaking` moved in v4, so the dividers and every alert
|
||
threshold are unchanged. `test_quadrant_dividers_match_the_band_boundaries` now
|
||
enforces that relationship, which nothing did before.
|
||
|
||
Scores renormalize over available fixed weights, but a band is published only at
|
||
75% or greater coverage. Trend deltas are suppressed when the participating
|
||
pillar set changes. Zero means ordinary/healthy; only stress contributes.
|
||
|
||
Credit level is the named HY OAS anchors alone: 3.5 mild, 5.0 elevated, 7.0
|
||
stressed, linear between, and nothing else. v2 blended those anchors at 70% with
|
||
a 30% upper-tail percentile over a nominally 10-year window.
|
||
|
||
That leg was removed rather than repaired. ICE restricted FRED to a rolling
|
||
3-year window for `BAMLH0A0HYM2` in April 2026 — the series metadata states it
|
||
outright ("Starting in April 2026, this series will only include 3 years of
|
||
observations"), and an unbounded request returns the same 795 observations as a
|
||
30-year one. The v2 percentile therefore ranked the current spread against three
|
||
uniformly tight years (range 2.59–4.61 over the calibration window), which made
|
||
it fire early and saturate absurdly: at an OAS of 3.50 — the level the anchors
|
||
call *mild*, scoring zero stress — the blended sensor read 20.1, and the
|
||
percentile leg pegged at 100 by an OAS of 4.5. Across the 408 sessions it
|
||
roughly tripled the credit sensor's average (2.70 vs 1.00) and more than doubled
|
||
its nonzero days (60 vs 27).
|
||
|
||
The anchors already encode the long-run distribution as constants, so the
|
||
percentile was a second, noisier estimate of the same thing. What it was
|
||
genuinely reaching for — "unusual versus recent history" — is now W3 on the
|
||
Warning axis, computed as a rate of change, which is where deterioration
|
||
belongs. Removing it moved State's average by −0.4 and its maximum by −3.8, left
|
||
Warning bit-identical, and did not shift any band threshold.
|
||
|
||
A long-history alternative (`BAA10Y`, Fed-published, 7,712 observations back to
|
||
1997) was considered and rejected: ranking an HY spread against investment-grade
|
||
history is not a coherent statistic, and it would rescue a leg that is redundant
|
||
anyway.
|
||
|
||
Every snapshot now records `data_quality.credit_history_days` and
|
||
`vix_history_days`. This defect was invisible for roughly three months because
|
||
nothing asserted the window the code claimed; the spans make a future upstream
|
||
truncation show up in the record instead of quietly reshaping a sensor.
|
||
|
||
**Survivorship caveat.** The basket was frozen 2026-07-15 but the calibration
|
||
window reaches back to 2024, so names were partly selected for having done well.
|
||
Every distribution above inherits that bias. It is the same bias v2 carried, so
|
||
the v2/v3 comparison is like-for-like, but the absolute band shares are
|
||
optimistic.
|
||
|
||
**Which OAS window the published v2 figures used.** v2 requested 13 years of HY
|
||
OAS and sliced `HY_OAS_REFERENCE_YEARS = 10.0` per session; ICE serves only ~3
|
||
years (778 observations from 2023-08-08), so the effective window was that. But
|
||
production v2 also fetched only 400 *calendar* days at one point — the bug fixed
|
||
2026-08-07 — and whether the published numbers predate that was not recoverable
|
||
from the text. Settled by replay rather than assumed: the
|
||
`v2_reconstruction_oas400` variant truncates the OAS **source series** to 400
|
||
days (patching the per-session window cannot simulate data that was simply
|
||
absent) and yields avg 26.54, p80 42.52, max **100.00**, against published
|
||
22.6 / 35.1 / 91.2. Full coverage reproduces all three. So the published figures
|
||
correspond to the untruncated fetch.
|
||
|
||
**The top VIX anchors are exercised, not just asserted.** The window contains a
|
||
52.33 close (2025-04-08), so the 40 → 80 → 55 → 100 segment is fed by real data
|
||
rather than justified from long-run history alone.
|
||
|
||
## Point-in-time record
|
||
|
||
The first run under a new `METHODOLOGY` rebuilds every session inside
|
||
`REBUILD_LOOKBACK_DAYS` — 672 calendar days, roughly 464 trading sessions;
|
||
routine runs thereafter insert/update only the latest trading date. The bound is
|
||
in calendar days rather than a session count because the binding constraint is
|
||
the OAS fetch: each replayed row needs W3's lookback inside
|
||
`HY_OAS_WINDOW_DAYS`, so replaying further back would recreate the credit gap a
|
||
reseed exists to close. The history API and main chart show only snapshots matching
|
||
the current methodology, so a bump reseeds the series rather than splicing two
|
||
formulas into one line.
|
||
|
||
The fundamental channel keeps its effective date (normally the next session after
|
||
collection) and is never replayed backward, so a rebuild cannot stamp today's
|
||
observation onto historical snapshots. Since the observations became a real
|
||
series (`regime_fundamental_observations`, migration 033), the effective-date
|
||
lookup *is* the gate: a replayed session gets whichever observation was live on
|
||
it, and sessions before the first one read `unknown`.
|
||
|
||
Two functions, deliberately: `fundamental_context` is the **record** and keeps
|
||
the gate — it runs for every replayed date during a rebuild, so it must never
|
||
grow a bypass flag. `current_observation` is the **live reading** behind
|
||
`fundamental_live`, and *reports* the effective date instead of blanking the
|
||
content.
|
||
|
||
Until 2026-08-07 the live reading called the gated function, so a just-collected
|
||
observation stayed hidden until the next weekday — three days over a weekend —
|
||
and refreshing appeared to do nothing. That was the opposite of what this section
|
||
already claimed. Showing it early cannot leak into a published score, because
|
||
nothing in the channel is scored.
|
||
|
||
`current_observation` gates on `observed` (a non-null `fetched_at`, the one field
|
||
every path writing real content stamps). Without it, the default override —
|
||
`unknown` for every hyperscaler and, since 2026-08-13, `unknown` for the reaction
|
||
— was reported as a live observation with `available: true`, so the card
|
||
presented placeholders as a collected reading. Those are the absence of an
|
||
observation, not an observation of absence. `fundamental_context` never had this
|
||
problem: no observation means no effective date, which means `pending`, which
|
||
already blanks the content.
|
||
|
||
**`usable` is what may confirm; `available` is only what to display.** Three
|
||
distinct things, and collapsing any two of them is a bug:
|
||
|
||
- `state` — the last thing observed. Survives going stale, so the card can show it.
|
||
- `available` — *timing*: there is an effective, non-stale record to display.
|
||
- `usable` — *content*: available **and** the observation actually determined
|
||
something (`state != "unknown"`).
|
||
|
||
The confluence alert and all three coverage-matched study rules gate on `usable`.
|
||
Gating on `available` instead has two failure modes, and both were live at some
|
||
point in this design:
|
||
|
||
1. a reading past `fundamental_staleness_days` would corroborate every Warning
|
||
crossing indefinitely — the strongest claim this channel makes, from the data
|
||
with the least right to make it;
|
||
2. an LLM run that failed to extract anything produces a perfectly fresh
|
||
observation that knows nothing. Counting it as exposure means repeated
|
||
extraction failures slowly accumulate coverage until the fundamental rows flip
|
||
to a *measurable* 0/8 — a failed result published for a channel that never saw
|
||
a thing, which is precisely what coverage-matching exists to prevent.
|
||
|
||
**Pre-rename snapshots are adapted, not discarded.** The channel was stored as
|
||
`fundamental_overlay` until 2026-08-12. The rename shipped without a methodology
|
||
bump — no score changed — so those rows are still served and were never reseeded.
|
||
Reading only the new key would have turned every one of them into `unknown`,
|
||
silently dropping real recorded evidence: historical Path colours, and exposure
|
||
the event study can legitimately count. `_parse_snapshot` derives the channel
|
||
from a legacy overlay's own stored facts (its capex map supplies the basket, so
|
||
the derivation uses the names observed at the time rather than today's config).
|
||
Normalising there rather than at each call site means no reader can receive an
|
||
un-adapted row. Delete only after a reseed has rewritten the whole window.
|
||
|
||
**The blob and the series row are one transaction.** They are the same
|
||
observation seen by the live card and by the point-in-time replay; committing
|
||
them separately leaves a window where a failure publishes one and not the other,
|
||
and the two then disagree permanently with nothing to detect it. Both writers use
|
||
`settings_store.upsert_setting` (which does not commit) plus a single commit;
|
||
`record_fundamental_observation` deliberately takes no commit of its own so
|
||
`update_regime_monitor` keeps its own transaction boundary.
|
||
|
||
Each snapshot stores the fixed basket symbols, hash, and freeze date.
|
||
Reconstructed history before that freeze date is retrospective/exploratory.
|
||
|
||
## Presentation
|
||
|
||
The page is deliberately thin: two gauges, one chart card, one pillar table, the
|
||
overlay, and a provenance strip. Time and Path are two projections of the same
|
||
snapshot series and share one card and one query key — they were previously two
|
||
panels, which read as two datasets. Methodology rationale lives in this document,
|
||
not on the page; page text is limited to what changes how the reader interprets
|
||
today's number. The quadrant dividers rendered in Path view come from
|
||
`quadrant_config` and are the same constants the alert path consumes
|
||
(`alert_service`), so the chart cannot drift from what actually fires.
|
||
|
||
## Warning study
|
||
|
||
The study calls the outcome a **10% correction**, not a regime break. It measures
|
||
two rules against that outcome, plus enough context to tell whether either number
|
||
is any good.
|
||
|
||
A cached report is discarded when its methodology no longer matches *or* when
|
||
`STUDY_SCHEMA` moves, so the panel reverts to "not run yet" rather than showing
|
||
stale numbers or a report missing half its blocks. **Re-run the Event Study job
|
||
after a methodology cutover or a schema bump.**
|
||
|
||
### The headline is the rule that actually fires
|
||
|
||
Until 2026-08-12 the study measured a bare rising-edge crossing of an
|
||
80th-percentile threshold fitted on the first 70% of sessions. **Nothing consumes
|
||
that rule.** What reaches Telegram is `_collect_regime_quadrant`: a quadrant
|
||
change with State ≥ 50 and Warning ≥ 40 as fixed dividers, a ±5 hysteresis
|
||
deadband, a two-session confirmation, a 3-day cooldown, and a 75% coverage gate
|
||
on both axes. The two differ on every one of those axes, including the threshold
|
||
itself (a fitted ~32 against a shipped 40).
|
||
|
||
`replay_quadrant_changes` replays the shipped state machine over the whole
|
||
sample. Three details are reproduced rather than cleaned up, because a state
|
||
machine written from first principles gets each of them wrong:
|
||
|
||
- the prior session is classified against the **current baseline**, not against
|
||
its own predecessor, so confirmation asks "did yesterday already look like this
|
||
change" rather than "did yesterday change too";
|
||
- the baseline advances only when an alert actually fires, so a change blocked by
|
||
confirmation or cooldown is re-evaluated against the old quadrant next session;
|
||
- one cooldown is shared by every quadrant change, so a 3→4 alert can swallow a
|
||
4→2 alert three days later.
|
||
|
||
Two consequences worth stating. The alarm is dated at the **confirmation**, not
|
||
at the first crossing, which costs one session of lead by construction. And the
|
||
rule alerts on changes in both directions, so the replay's exits are recorded but
|
||
filtered out by `entry_alarms` — only entering a Warning-high quadrant is a
|
||
warning about anything.
|
||
|
||
The replay reuses `_compute_index` rather than re-deriving the axes. That is the
|
||
same anti-drift argument that produced `warning_sensor_scores`: the v2 study
|
||
re-derived Warning by hand and would have kept measuring the old construct
|
||
through a scoring change. State has no equivalent shared helper, so the snapshot
|
||
builder itself is the shared definition.
|
||
|
||
**Nothing is fitted, so nothing needs protecting from a training set.** There is
|
||
no split, and every detected correction is evaluable instead of the four that
|
||
happen to land in the last 30%. The `underpowered` and "threshold frozen on a
|
||
different construct" caveats do not apply to this variant.
|
||
|
||
### Reading the result
|
||
|
||
A bare "2 of 4" is unreadable in either direction, so the report scores four more
|
||
rules through the same `evaluate_alarms` harness over the same events and
|
||
sessions, and adds a null. All use fixed thresholds — a threshold fitted on the
|
||
full sample would have lookahead the shipped rule does not, and one fitted on a
|
||
split could only be scored on the holdout events.
|
||
|
||
| kind | rules | the question |
|
||
|---|---|---|
|
||
| ablation | Warning ≥ 40 bare, State ≥ 50 bare | does the quadrant machinery earn its place? |
|
||
| baseline | leader below its 50-DMA, VIX ≥ 20 | does the score earn its complexity? |
|
||
| null | K random alarms at the observed firing rate | is any of this better than chance? |
|
||
|
||
The two kinds must not be read as one list. If a baseline matches the score, the
|
||
composite is not earning its complexity and that is the finding — it does not
|
||
mean the monitor is worthless, since State and Warning exist to be *read*, but it
|
||
caps how much further calibration is justified. If the bare Warning crossing
|
||
beats the shipped rule, the machinery (not the sensor) is what is costing recall.
|
||
|
||
The null draws only from sessions a rule could actually have fired on. Over the
|
||
whole sample it would be diluted by warm-up sessions and would understate what
|
||
chance achieves — which matters, because with ~11 events and a 20-session horizon
|
||
roughly a sixth of the sample already sits inside a hit window. It is seeded, so
|
||
a re-run cannot move the report. Corrections cluster and uniform placement does
|
||
not, so it is the **floor, not the bar**: an alarm process that clustered would
|
||
beat it for reasons unrelated to foresight.
|
||
|
||
### First result (2026-08-12): the shipped rule is not distinguishable from chance
|
||
|
||
Replayed over 2021-07-14 → 2026-08-12. The 200-DMA warm-up means the baseline
|
||
only seeds on 2022-05-26, so 1056 of 1276 sessions are evaluable and 10 of the 11
|
||
detected corrections fall inside them.
|
||
|
||
| rule | kind | warned | FA/yr | median lead |
|
||
|---|---|---|---|---|
|
||
| **Quadrant alert (shipped)** | | **1/10** | **0.9** | 19d |
|
||
| Quadrant alert, both axes high | ablation | 0/10 | 0.9 | — |
|
||
| Warning ≥ 40, bare crossing | ablation | 3/10 | 4.8 | 20d |
|
||
| State ≥ 50, bare crossing | ablation | 0/10 | 0.7 | — |
|
||
| SMH below its 50-DMA | baseline | 7/10 | 6.7 | 8d |
|
||
| VIX ≥ 20 | baseline | 4/10 | 7.2 | 9.5d |
|
||
| Random alarms, same firing rate | null | 0.9 ± 0.8 | — | — |
|
||
|
||
**P(chance ≥ 1/10) = 0.65.** Alarms scattered at random over the same sessions at
|
||
the rule's own firing rate match or beat it two times in three. Whatever the
|
||
score knows, this rule is not transmitting it.
|
||
|
||
Three readings, in order of how much they should change:
|
||
|
||
**The machinery costs more than it protects.** The bare Warning crossing catches
|
||
3 with a 20-session lead; wrapping it in the quadrant rule drops that to 1. The
|
||
State condition is the largest single cost — requiring both axes high catches
|
||
nothing at all, which is what a coincident axis gating a leading one predicts.
|
||
Hysteresis, the two-session confirmation and the shared cooldown between them
|
||
take the rest, and the cooldown is shared across *every* quadrant change, so
|
||
exits consume the budget that entries need. Only 5 of the 15 replayed changes are
|
||
Warning-high entries.
|
||
|
||
**The crude baselines beat everything on recall, at a price.** SMH below its
|
||
50-DMA catches 7 of 10 — but at 6.7 false alarms a year against the shipped
|
||
rule's 0.9. That is a 7× recall improvement for 7× the noise, so it is not a
|
||
clean dominance and this table cannot settle it; the missing axis is what a false
|
||
alarm actually costs, which nothing here measures. What it does settle is that
|
||
the composite is not buying recall the 50-DMA does not already have.
|
||
|
||
**The 0.9 false alarms/year is not the achievement it looks like.** A rule that
|
||
almost never fires has few false alarms by construction. Read the two columns
|
||
together or not at all.
|
||
|
||
Recorded from an offline replay (live Alpaca + FRED, no database, breadth
|
||
computed from the same Alpaca closes rather than the stored universe). The job in
|
||
Admin → Jobs is the canonical path and reads breadth from the DB, so re-run it to
|
||
confirm these figures before treating them as the record.
|
||
|
||
**This is a verdict on the market channels only.** The fundamental and confluence
|
||
rows in the same table are marked `measurable: false` and print "not measurable"
|
||
rather than a ratio: with an empty observation series they never fire, and a 0/10
|
||
sitting in a comparison column would read as tested-and-failed. `false` here means
|
||
the input does not exist yet, not that the rule lost.
|
||
|
||
(The figures above were also produced under a briefly-built weighted modifier and
|
||
came back bit-identical, which is what confirmed the modifier was inert over the
|
||
whole window — the numbers depend on the technical sensors alone either way.)
|
||
|
||
**Not acted on.** Nothing in the alert path was changed on the strength of this.
|
||
The obvious candidates — dropping the State condition from the entry test,
|
||
separating the entry and exit cooldowns, or lowering the Warning divider — are
|
||
threshold changes to a live alerting rule and want their own decision.
|
||
|
||
### The coverage gap relocates, it does not close
|
||
|
||
Dropping the fitted threshold makes the whole sample evaluable, but most of the
|
||
extra events predate 2023-08. W3 does not exist there, so Warning renormalises to
|
||
`(W1×45 + W2×30)/75` and the fixed 40 divider is applied to a different construct
|
||
than it was reasoned about. The report therefore splits shipped-rule metrics at
|
||
the credit sensor's first session and the panel states both, because replacing
|
||
one misleading headline with a differently misleading one would be no gain.
|
||
|
||
Convenient side effect: the pre-credit era *is* the "Warning without W3"
|
||
ablation, measured on real sessions rather than simulated ones, so that ablation
|
||
is not run separately.
|
||
|
||
Alarms and events are assigned to eras by index, so an alarm days before the
|
||
boundary matching an event days after it lands in the earlier era. With the eras
|
||
years long and the events sparse, that costs nothing.
|
||
|
||
### The fitted variant, kept for continuity
|
||
|
||
The 70/30 percentile study is still computed and still reported, collapsed, with
|
||
its `reliability` block intact — it is a genuinely different question, and it is
|
||
what earlier revisions of this document report. Its caveats stand:
|
||
|
||
**The holdout is thin.** The study detects 11 corrections across 5 years but the
|
||
70/30 split leaves only 4 in the test period. Recall is one event away from a
|
||
materially different headline, and in practice the event that flips is decided by
|
||
where the frozen threshold happens to land rather than by whether the score saw
|
||
anything. The v3 cutover run illustrates it: v3 scored 2/4 against v2's 3/4, but
|
||
"v3 without the credit sensor" scores 3/4 at a *higher* threshold (35.5) than
|
||
shipped v3 misses it at (32.3) — because the alarm rule needs a rising edge, and a
|
||
lower threshold can mean the alarm already fired outside the 20-session horizon
|
||
and never reset below. Below `MIN_EVENTS_FOR_CONFIDENCE` holdout events the
|
||
report says so explicitly.
|
||
|
||
Some events carry no information at all for comparison: in that run every
|
||
variant caught 2026-03-06, every variant missed 2026-06-05, and every variant
|
||
"caught" 2025-11-20 with a 1-session lead, which is coincident rather than a
|
||
warning. The headline recall does not currently discount those; a minimum-lead
|
||
rule is the obvious next change and has not been made.
|
||
|
||
**Sensor coverage straddles the split.** The score renormalises over available
|
||
sensors, so a training window predating a sensor's history freezes the threshold
|
||
on a different construct than the holdout is measured against. At the v3 cutover
|
||
only 39% of training sessions had all three Warning sensors versus 100% of the
|
||
test period, because credit history begins 2023-07-25.
|
||
|
||
Restricting the threshold to sensor-matched training sessions was tried and is
|
||
*not* the fix: those sessions are a calm recent stretch, so the threshold drops
|
||
from 32.3 to 22.5 and false alarms rise from 3.3 to 8.6 per year. It trades a
|
||
coverage bias for a regime-selection bias. The honest position is that a fitted
|
||
threshold is hypersensitive to window choice at this sample size — which is the
|
||
strongest argument for making the unfitted shipped rule the headline.
|
||
|
||
### Considered and not done
|
||
|
||
**An ETF credit proxy (HYG/IEF) to extend W3 back over the whole sample.** It
|
||
would trade "two sensors versus three" for "proxy sensor versus real sensor" —
|
||
still a construct straddle, but no longer flagged by the coverage split. This is
|
||
the same objection that rejected `BAA10Y` as a percentile reference. If ever
|
||
revisited, check the impulse correlation on the three years of real-OAS overlap
|
||
first and report it as a sensitivity, never as the headline.
|
||
|
||
**A depth sweep (5%/7%/15% corrections) for more events.** `EVENT_COOLDOWN_DAYS`
|
||
is 40, so at shallower thresholds re-triggers inside a single decline merge or
|
||
drop and the denominator moves for cooldown reasons rather than market ones.
|
||
|
||
## Resolved in v4 (raised 2026-08-07, shipped 2026-08-08)
|
||
|
||
The three questions this section used to hold are now answered. Kept here
|
||
because the reasoning that resolved them is not obvious from the code.
|
||
|
||
**1. `breaking` had zero headroom — resolved by moving the band, not the sensor.**
|
||
`f2_credit_spreads` returns `0.0`, not `None`, below the 3.5 mild anchor, so
|
||
credit stays *available* at weight 20 and is pinned at zero on roughly 93% of
|
||
sessions rather than being renormalized out. Price + breadth + volatility at
|
||
literal maximum therefore summed to exactly 80.0 — v3's threshold, to the
|
||
decimal.
|
||
|
||
The sensor is **deliberately unchanged**. A calm-credit selloff genuinely is less
|
||
stressed than one with credit contagion, so scoring it lower is correct; what was
|
||
stale was `STATE_BANDS`, fit on v2 while credit's since-removed percentile leg
|
||
still contributed. Making credit `None` when calm was considered and rejected: it
|
||
would leave State on 80% coverage, which still publishes, but consumes the whole
|
||
buffer — any *second* missing pillar would then suppress the band, and the 7d/30d
|
||
trend deltas would null out every time OAS crossed 3.5, because `_delta`
|
||
suppresses on a change of participating pillars. See Calibration for the
|
||
scenario arithmetic behind 65.
|
||
|
||
**2. V1 saturated at VIX 30 — resolved with an anchor table.** See "What changed
|
||
in v4".
|
||
|
||
**3. `max(P1, P2, P3)` defeated P3's anchoring — resolved by grading `_under_200`,
|
||
keeping `max()`.** The `max` was deliberate ("one capped vote for correlated
|
||
reads") and survives; the binary step feeding it was the defect.
|
||
|
||
**Its limit, stated precisely.** `_death_cross` is `clamp(-gap_pct * 20)`, so P2
|
||
pegs at a −5% 50/200-DMA gap — routine in a real downtrend. In a *deep* selloff
|
||
the price pillar therefore still reaches 100 via P2 even with P1 graded. What v4
|
||
repairs is the shallow-to-moderate break, which is where resolution was most
|
||
obviously missing: a 10% correction 2% below the average now scores 31 where v3
|
||
scored 100. It would be wrong to claim "the price pillar no longer pegs".
|
||
|
||
P2 did not peg once in the 408-session calibration window, so this is a property
|
||
of the sensor rather than an observed problem. Grading P2 the same way is the
|
||
natural next item if it starts binding; the replay reports a P2-pegged census
|
||
alongside P3 and V1 so the evidence accumulates.
|
||
|
||
## Fixed 2026-08-07: the OAS fetch window did not cover a rebuild
|
||
|
||
`HY_OAS_WINDOW_DAYS` was 400 **calendar** days, but a rebuild replays
|
||
`leader_series[-REBUILD_SESSIONS:]` — 400 **trading** sessions, about 579
|
||
calendar days. The oldest ~180 calendar days of any rebuild therefore got no OAS
|
||
data at all, so `f2_credit_spreads` and `w3_credit_impulse` both returned `None`.
|
||
Verified: State then lands at 80% coverage and Warning at exactly 75.0% —
|
||
`MIN_COVERAGE` — so **both still publish bands**. The rebuilt series would look
|
||
homogeneous while its oldest rows had been scored without credit, the tell being
|
||
a null `data_quality.credit_history_days` on exactly those rows.
|
||
|
||
The window is now 700 days: it must cover the oldest replayed date (~579) plus
|
||
W3's lookback and slack, while staying under ICE's ~3-year cap so FRED still
|
||
honours the request. This required **no methodology bump** — C1 reads
|
||
`oas_values[-1]` and W3 reads `oas_values[-21]`, both indexed from the end, so
|
||
widening only prepends older observations and every live score is bit-identical.
|
||
Confirmed by evaluating both windows against a varying synthetic series: today's
|
||
C1/W3 match exactly, while the oldest rebuild row goes from `None`/`None` to real
|
||
values.
|
||
|
||
Expect `credit_history_days` on new snapshots to rise from ~400 to ~700. That is
|
||
the widened request, not new upstream history — and it makes the chip a better
|
||
truncation canary, since a 700-day request returning ~1095 days' worth is now
|
||
the visible ceiling.
|
||
|
||
**Widening the window alone does not repair stored history.** Routine runs
|
||
recompute only the latest trading date, and `rebuilding` was keyed on "no v3
|
||
snapshot exists at all" — which is false once the cutover has run — so every row
|
||
already written would have kept its credit gap indefinitely. `SENSOR_REVISION`
|
||
fixes that: it is stamped into each snapshot, snapshots predating it read as 1,
|
||
and a stored revision below the current one triggers exactly one reseed.
|
||
|
||
It is deliberately not `METHODOLOGY`. That constant partitions the history API
|
||
and discards the cached event study; neither is warranted here, because the study
|
||
recomputes its Warning series from source (`_warning_series` calls
|
||
`warning_sensor_scores` against freshly fetched prices and OAS) rather than
|
||
reading snapshots, so a reseed cannot stale it.
|
||
|
||
The reseed is bounded by `REBUILD_LOOKBACK_DAYS` in calendar days rather than a
|
||
session count, because the binding constraint is the OAS fetch: each replayed row
|
||
needs W3's 20-business-day lookback inside `HY_OAS_WINDOW_DAYS`. At 672 days the
|
||
replay reaches ~464 sessions, W3's oldest requirement lands exactly on the first
|
||
fetched OAS day, and the ~400-session series the v3 cutover wrote is fully
|
||
covered. A test asserts that relationship so the two constants cannot drift into
|
||
recreating the gap.
|
||
|
||
The fix was sequenced deliberately: acting on items 1–3 above bumped
|
||
`METHODOLOGY`, which fires `rebuilding`, which would have baked the credit-less
|
||
rows into the fresh series. Fixing the window first meant the v4 reseed replayed
|
||
a clean window; doing it the other way round would have meant reseeding twice.
|
||
|
||
## Operator rule
|
||
|
||
Quadrant alerts default off for new/reset configurations. When enabled they
|
||
require fresh inputs, at least 75% coverage on both axes, two consecutive daily
|
||
confirmations, hysteresis, and cooldown. Every alert states: **Risk thermometer —
|
||
not a trade signal.**
|