Commit Graph
5 Commits
Author SHA1 Message Date
dennisthiessenandClaude Opus 5 7fdcac3b55 docs: carry the risk-monitor wording through docs, comments and logs
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m12s
Deploy / deploy (push) Successful in 37s
Follows 5ea0785, which renamed the user-visible labels. This finishes the pass
so code, docs and operator output use one vocabulary: README (pipeline list,
route table, FRED row), the methodology doc title, .env.example and config
comments, the snapshot model / event-study / service / test docstrings, the
scheduler section headers and morning-pipeline docstring, the TopBar status
text ("bullish regime" -> "bullish trend"), and the four "Regime monitor:" log
prefixes.

Deliberately NOT changed, because "market regime" is also a standard finance
term and most occurrences are not this job: the backtest caveat "~6 months is
roughly one market regime" in backtest_service, README, BacktestPanel and every
generated reports/*.json; "a regime shift" in TrackRecordPanel; and the
capacity-bracket findings doc. Renaming those would have made the text wrong.

Also unchanged, being persisted or externally linked rather than wording: the
regime_monitor / market_regime job ids, the regime_quadrant_enabled setting key,
the /regime route, METHODOLOGY and the snapshot fields, the service/test module
filenames, and docs/research/regime-monitor-v3.md's path (referenced from commit
messages). The doc now carries a one-line note recording the old name and why
those identifiers still use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:51:49 +02:00
dennisthiessenandClaude Opus 5 3483797e75 fix(regime): reseed stored history on a sensor change, and stop faking an observation
Two review findings on 46ace50.

[P1] Raising HY_OAS_WINDOW_DAYS to 700 only reached newly computed rows. A
routine run recomputes the latest trading date alone, and `rebuilding` was keyed
on "no v3 snapshot exists at all", which is false once the cutover has run --
so every row already written kept the credit gap the wider window exists to
close, indefinitely.

Adds SENSOR_REVISION: stamped into each snapshot, absent on pre-marker rows
(read as 1), and a stored revision below the current one triggers exactly one
reseed. Deliberately not METHODOLOGY, which would partition the history API and
discard the cached event study -- neither warranted, since the study recomputes
its Warning series from source rather than reading snapshots and so cannot be
staled by a reseed.

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. Replaying by
session count would have left the oldest stored rows unrepaired -- the exact
rows the fix targets. At 672 days the replay covers ~464 sessions, W3's oldest
requirement lands on the first fetched OAS day, and the ~400-session series the
cutover wrote is fully covered. A test asserts that relationship so the two
constants cannot drift back into recreating the gap.

[P2] With nothing ever collected, current_observation returned available=true
and the default placeholders -- "unknown" for every hyperscaler, "mixed" for the
reaction -- so the card announced a reading that never happened. Those are the
absence of an observation, not an observation of absence. Gated on `observed`
(non-null fetched_at, the one field every path writing real content stamps),
which blanks the content and drives a proper empty state naming where an admin
collects one. This was a regression from 46ace50; fundamental_overlay never had
it, since no observation means no effective date means pending.

Also renames the leftover v2 identifiers in the touched paths
(rewrite_existing_v2, latest_v2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:22:25 +02:00
dennisthiessenandClaude Opus 5 46ace501a2 refactor(regime): collapse the monitor page, fix the OAS rebuild window
The page had twelve stacked blocks, several of them different views of the
same numbers. The quadrant plot and the score-history chart drew the same two
series from the same query key, which read as two datasets; they are now one
card with a Time | Path toggle. The two pillar disclosures become one grouped
table, and three prose blocks (data quality, basket, coverage) become one
provenance chip strip. Page text is now limited to what changes how the reader
interprets today's number; the rest moved to the methodology doc.

Removes three stale-threshold bugs of one class. The quadrant fell back to v2's
60/60 dividers when quadrant_config was absent -- the real values are 50/40 and
they feed alert_service, so the chart could disagree with what actually fires.
The gauge fell back to v2's 30/60/80 band ticks, and drew a divider line that
always landed on its own "elevated" tick. The time series' reference lines were
at 30/60/80, which correspond to nothing in v3; they are now per-axis dashed
lines read from the same quadrant_config. Rendering also surfaced a live
clipping bug inherited from the old chart: margin.left -18 against YAxis
width 28 left ~10px for a 3-digit label, so every Y tick was cut off.

HY_OAS_WINDOW_DAYS was 400 *calendar* days while a rebuild replays
REBUILD_SESSIONS = 400 *trading* sessions (~579 calendar days), so the oldest
~180 days of any rebuild got no OAS at all and both credit sensors returned
None. State then lands at 80% coverage and Warning at exactly MIN_COVERAGE, so
both still publish bands -- a series that looks homogeneous while its oldest
rows were scored without credit. Widened to 700. This needs no methodology
bump: C1 reads [-1] and W3 reads [-21], both from the end, so widening only
prepends and every live score is bit-identical. Sequenced deliberately, since
acting on the open findings below bumps METHODOLOGY and fires the rebuild.

A just-collected fundamental observation was hidden until its effective date --
one day, three over a weekend -- because the live reading called the
point-in-time function, so refreshing appeared to do nothing. That was the
opposite of what the doc claimed. fundamental_overlay stays the gated record
(it runs for every replayed date during a rebuild); current_observation is the
live reading and reports the effective date instead of blanking the content.
Nothing in the overlay is scored, so showing it early cannot reach a published
number.

Documents four calculation findings. Three are not implemented, since each
changes a published score and so requires a v4 cut: State's top band is a
credit-event band (credit returns 0.0 rather than None below the 3.5 anchor, so
it is pinned at zero at weight 20 -- with everything else pegged State computes
to exactly 80.0, the breaking threshold); V1 saturates at VIX 30; and the
deliberate max(P1,P2,P3) defeats P3's anchoring because P1 is binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:53:45 +02:00
dennisthiessenandClaude Opus 5 83c0555e52 Event study: report its own statistical limits
Deploy / lint (push) Failing after 8s
Deploy / test (push) Skipped
Deploy / deploy (push) Skipped
The v3 cutover run scored 2/4 corrections warned against v2's 3/4, which reads
like a regression and is not one. Only 4 of the 11 detected corrections fall in
the holdout, so recall is one event from a different headline -- and the event
that flips is decided by threshold placement, not by what the score saw. "v3
without the credit sensor" catches 2025-02-21 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 fire outside the horizon then never reset below.

Two caveats are now computed and surfaced rather than left for the reader to
infer:

- Holdout event count against MIN_EVENTS_FOR_CONFIDENCE. The summary sentence
  states how many of the detected corrections actually fall in the test period.
- Warning-sensor coverage across the split. The score renormalises over what is
  available, so a training window predating a sensor's history freezes the
  threshold on a different construct than the holdout is measured against. At
  the cutover that is 39% of training sessions with all three sensors versus
  100% of the test period, credit history beginning 2023-07-25.

Restricting the threshold to sensor-matched training sessions was tested and
rejected: those sessions are a calm recent stretch, so the threshold falls from
32.3 to 22.5 and false alarms rise from 3.3 to 8.6/yr. It swaps a coverage bias
for a regime-selection bias. The report states its limits instead.

_warning_series now returns per-session sensor counts alongside the scores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:12:53 +02:00
dennisthiessenandClaude Opus 5 019ca1342a Rewrite Regime Monitor as v3: fundamentals off the score, desaturate P3
Deploy / lint (push) Successful in 11s
Deploy / test (push) Successful in 1m43s
Deploy / deploy (push) Successful in 38s
The LLM-sourced capex/earnings observations carried 12+8 of 100 Warning points,
so both pegged at 100 produced a Warning of 20.0 -- below the event study's 25.3
alarm threshold and still inside the "stable" band. The reading was
arithmetically incapable of changing anything on screen, which is why refreshing
it appeared to do nothing. They are now a qualitative overlay reported beside
the scores rather than diluted into them.

Calibrated against the 408 v2 sessions to 2026-07-24, reproduced offline from
Alpaca + FRED; the harness matched the stored prod distribution exactly before
any parameter was changed.

State:
- P3 used dd_pct * 5, reaching 100 at a 20% drawdown -- the 90th percentile of
  the observed distribution -- so 39/408 sessions sat at exactly 100 with no
  resolution left during the part of a selloff that matters most. Replaced with
  anchored breakpoints keeping headroom past the observed 36% maximum, blended
  2:1 like P1/P2 instead of max(). P3's realized share of State falls from 65%
  to 40%, matching its nominal weight.
- Credit level is now anchors-only. ICE capped FRED's BAMLH0A0HYM2 at a rolling
  3-year window in April 2026, silently turning the 10-year percentile leg into
  a 3-year one that scored 20 points of stress at an OAS of 3.5 -- the level its
  own anchors call "mild". The anchors already encode the long-run distribution.

Warning:
- Added HY OAS 20-session widening (25%). The level is pinned at zero below the
  3.5 anchor; its rate of change is not.
- Divergence tapers to a 0.35 floor instead of a hard price_ret >= 0 gate, which
  zeroed the sensor through every decline: on 2026-07-24 the basket shed 10
  points of participation in 20 sessions and Warning printed exactly 0.
- The event study and the live monitor now share one sensor definition, so they
  cannot silently drift apart.

Bands are per axis (State 20/50/80, Warning 20/40/60) with quadrant dividers at
50/40; v2 Warning never exceeded 64.9 against a shared 60, leaving that half of
the quadrant unreachable. Realized shares: State 73/15/8/3%, Warning 69/20/8/3%.

Snapshots now record credit_history_days and vix_history_days -- the percentile
defect went unnoticed for months because nothing asserted the window the code
claimed.

Cutover: the first run rebuilds 400 sessions automatically; the Event Study job
must be re-run, as its cached report self-invalidates on the methodology check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:36:57 +02:00