From 81c6f5fe2fc50f64eba6b6c5cc0a12235eb5a976 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Wed, 15 Jul 2026 09:13:42 +0200 Subject: [PATCH] fix: refresh latest regime trading session --- .env.example | 2 +- app/services/regime_monitor_service.py | 2 +- docs/research/regime-monitor-v2.md | 5 +++ frontend/src/pages/RegimePage.tsx | 6 +++ tests/unit/test_regime_monitor.py | 53 ++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 2362898..35f8b80 100644 --- a/.env.example +++ b/.env.example @@ -28,7 +28,7 @@ FINNHUB_API_KEY= ALPHA_VANTAGE_API_KEY= # Regime Monitor — FRED (VIX + HY credit spreads). Free key: https://fred.stlouisfed.org/docs/api/api_key.html -# Optional: without it the VIX (P5) and credit-spread (F2) signals show as n/a. +# Optional: without it the volatility (V1) and credit (C1) pillars show as n/a. FRED_API_KEY= # Scheduled Jobs diff --git a/app/services/regime_monitor_service.py b/app/services/regime_monitor_service.py index f1df46f..1f4f5f4 100644 --- a/app/services/regime_monitor_service.py +++ b/app/services/regime_monitor_service.py @@ -772,7 +772,7 @@ async def update_regime_monitor(db: AsyncSession, rebuild_sessions: int = REBUIL written, latest_result = await _upsert_snapshot( db, computed, - rewrite_existing_v2=rebuilding or snapshot_date == date.today(), + rewrite_existing_v2=rebuilding or snapshot_date == latest_date, ) snapshots_written += int(written) await db.commit() diff --git a/docs/research/regime-monitor-v2.md b/docs/research/regime-monitor-v2.md index 85832c1..e99edcb 100644 --- a/docs/research/regime-monitor-v2.md +++ b/docs/research/regime-monitor-v2.md @@ -48,6 +48,11 @@ Each snapshot stores the fixed basket symbols, hash, and freeze date. Reconstruc history before that freeze date is retrospective/exploratory; readings after it form the forward record. +The automatic 400-session rebuild is intentionally one-shot: it runs only when +no v2 snapshot exists. If an initial seed used partial data or the wrong basket, +the operational reseed procedure is to remove the v2 snapshot rows and run the +Regime Monitor job again. There is no routine force-rebuild flag. + ## Warning study The study calls the outcome a **10% correction**, not a regime break. The first diff --git a/frontend/src/pages/RegimePage.tsx b/frontend/src/pages/RegimePage.tsx index 053d764..5a50c48 100644 --- a/frontend/src/pages/RegimePage.tsx +++ b/frontend/src/pages/RegimePage.tsx @@ -328,6 +328,12 @@ export default function RegimePage() { footnote={<>Breadth divergence, SMH/SPY rollover, and point-in-time fundamental observations. Unknown or stale fundamentals reduce coverage; they never default to 50.} /> +

+ Data quality · oldest market input:{' '} + {data.data_quality?.oldest_market_input_age_days == null + ? 'unavailable' + : `${data.data_quality.oldest_market_input_age_days}d`} +

}> }> diff --git a/tests/unit/test_regime_monitor.py b/tests/unit/test_regime_monitor.py index a4c794e..5e3fa9e 100644 --- a/tests/unit/test_regime_monitor.py +++ b/tests/unit/test_regime_monitor.py @@ -169,6 +169,59 @@ async def test_prior_v2_snapshot_is_immutable_without_explicit_rebuild(db_sessio assert row.total_score == 10.0 +@pytest.mark.asyncio +async def test_routine_can_refresh_latest_trading_session_after_civil_day_rolls( + monkeypatch, +): + latest_date = date(2020, 1, 3) + config = copy.deepcopy(DEFAULT_CONFIG) + prices = { + "SMH": [(latest_date, 100.0)], + "QQQ": [(latest_date, 100.0)], + "SPY": [(latest_date, 100.0)], + } + rewrites: list[bool] = [] + + async def fake_config(_db): + return config + + async def fake_overrides(_db): + return {"locked": True, "fetched_at": None, "effective_date": None} + + async def fake_prices(_config, _start, _end): + return prices + + async def fake_fred(_series_id, _start, _end): + return None + + async def fake_breadth(_db, _symbols, window, min_tickers): + return {}, {} + + async def fake_latest(_db): + return object(), {"methodology": "v2"} + + async def fake_upsert(_db, result, *, rewrite_existing_v2): + rewrites.append(rewrite_existing_v2) + return True, result + + class FakeDB: + async def commit(self): + return None + + monkeypatch.setattr(rms, "get_regime_config", fake_config) + monkeypatch.setattr(rms, "get_fundamental_overrides", fake_overrides) + monkeypatch.setattr(rms, "_fetch_prices", fake_prices) + monkeypatch.setattr(rms, "_fetch_fred_series", fake_fred) + monkeypatch.setattr(rms.breadth_service, "compute_breadth_details", fake_breadth) + monkeypatch.setattr(rms, "_latest_v2_row", fake_latest) + monkeypatch.setattr(rms, "_upsert_snapshot", fake_upsert) + + result = await rms.update_regime_monitor(FakeDB()) + + assert result["date"] == latest_date.isoformat() + assert rewrites == [True] + + def test_compute_index_uses_one_max_price_vote_and_has_no_combined_score(): end = date(2026, 6, 26) rising = [100.0 + index * 0.2 for index in range(700)]