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>
This commit is contained in:
@@ -52,7 +52,14 @@ METHODOLOGY = "v3"
|
|||||||
# Snapshots are reseeded on a methodology bump, but fundamental observations are
|
# Snapshots are reseeded on a methodology bump, but fundamental observations are
|
||||||
# collected by hand/LLM and carried across it when the format is compatible.
|
# collected by hand/LLM and carried across it when the format is compatible.
|
||||||
CATEGORICAL_FUNDAMENTAL_METHODOLOGIES = frozenset({"v2", "v3"})
|
CATEGORICAL_FUNDAMENTAL_METHODOLOGIES = frozenset({"v2", "v3"})
|
||||||
REBUILD_SESSIONS = 400
|
|
||||||
|
# Bumped when a fix changes what historical rows *should* contain without
|
||||||
|
# changing the live formula, so stored history needs one reseed. Deliberately
|
||||||
|
# not METHODOLOGY: that partitions the history API and discards the cached event
|
||||||
|
# study, neither of which is warranted here -- the study recomputes its Warning
|
||||||
|
# series from source rather than reading snapshots, so a reseed cannot stale it.
|
||||||
|
# Snapshots written before this marker existed carry no key and read as 1.
|
||||||
|
SENSOR_REVISION = 2
|
||||||
MIN_COVERAGE = 75.0
|
MIN_COVERAGE = 75.0
|
||||||
SOURCE_MAX_LAG_DAYS = 7
|
SOURCE_MAX_LAG_DAYS = 7
|
||||||
|
|
||||||
@@ -91,6 +98,14 @@ HY_OAS_STRESSED = 7.0
|
|||||||
# live scores are unchanged and this needs no methodology bump. Stays under
|
# live scores are unchanged and this needs no methodology bump. Stays under
|
||||||
# ICE's ~3-year cap so FRED still honours the request.
|
# ICE's ~3-year cap so FRED still honours the request.
|
||||||
HY_OAS_WINDOW_DAYS = 700
|
HY_OAS_WINDOW_DAYS = 700
|
||||||
|
|
||||||
|
# A rebuild replays every session inside this window. Bounded by 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 (~28 calendar days)
|
||||||
|
# inside HY_OAS_WINDOW_DAYS, so replaying further back would recreate the exact
|
||||||
|
# credit gap a reseed exists to close. 672 days is ~464 trading sessions, which
|
||||||
|
# comfortably covers the 400-session series the v3 cutover wrote.
|
||||||
|
REBUILD_LOOKBACK_DAYS = HY_OAS_WINDOW_DAYS - 28
|
||||||
W3_OAS_LOOKBACK = 20
|
W3_OAS_LOOKBACK = 20
|
||||||
W3_OAS_FULL_SCALE_PCT = 35.0
|
W3_OAS_FULL_SCALE_PCT = 35.0
|
||||||
|
|
||||||
@@ -536,19 +551,27 @@ def current_observation(overrides: dict, config: dict, as_of: date) -> dict:
|
|||||||
published number; the stored snapshot keeps the gate.
|
published number; the stored snapshot keeps the gate.
|
||||||
"""
|
"""
|
||||||
effective, pending, age, stale = _overlay_timing(overrides, config, as_of)
|
effective, pending, age, stale = _overlay_timing(overrides, config, as_of)
|
||||||
|
# The default override carries "unknown"/"mixed" placeholders for every
|
||||||
|
# hyperscaler. Those are the absence of an observation, not an observation
|
||||||
|
# of absence, and must never be presented as collected. ``fetched_at`` is
|
||||||
|
# the collection timestamp and is the only field written on every path that
|
||||||
|
# produces real content (LLM refresh and manual save both stamp it).
|
||||||
|
observed = bool(overrides.get("fetched_at"))
|
||||||
return {
|
return {
|
||||||
|
"observed": observed,
|
||||||
# Live availability is about usefulness, not effectiveness: a pending
|
# Live availability is about usefulness, not effectiveness: a pending
|
||||||
# observation is the freshest thing we have.
|
# observation is the freshest thing we have -- but nothing collected is
|
||||||
"available": not stale,
|
# never available.
|
||||||
|
"available": observed and not stale,
|
||||||
"pending": pending,
|
"pending": pending,
|
||||||
"stale": stale,
|
"stale": stale,
|
||||||
"effective_date": effective.isoformat() if effective else None,
|
"effective_date": effective.isoformat() if effective else None,
|
||||||
"age_days": age,
|
"age_days": age,
|
||||||
"capex": overrides.get("capex"),
|
"capex": overrides.get("capex") if observed else None,
|
||||||
"good_news_stock_down": overrides.get("good_news_stock_down"),
|
"good_news_stock_down": overrides.get("good_news_stock_down") if observed else None,
|
||||||
"capex_stress": overrides.get("f1_score"),
|
"capex_stress": overrides.get("f1_score") if observed else None,
|
||||||
"earnings_stress": overrides.get("f3_score"),
|
"earnings_stress": overrides.get("f3_score") if observed else None,
|
||||||
"reasoning": overrides.get("reasoning"),
|
"reasoning": overrides.get("reasoning") if observed else None,
|
||||||
"source": overrides.get("source"),
|
"source": overrides.get("source"),
|
||||||
"fetched_at": overrides.get("fetched_at"),
|
"fetched_at": overrides.get("fetched_at"),
|
||||||
}
|
}
|
||||||
@@ -680,6 +703,8 @@ def _compute_index(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"methodology": METHODOLOGY,
|
"methodology": METHODOLOGY,
|
||||||
|
# Not part of the history filter -- only the reseed trigger.
|
||||||
|
"sensor_revision": SENSOR_REVISION,
|
||||||
"date": as_of.isoformat(),
|
"date": as_of.isoformat(),
|
||||||
"state": state,
|
"state": state,
|
||||||
"warning": warning,
|
"warning": warning,
|
||||||
@@ -939,7 +964,7 @@ async def _upsert_snapshot(
|
|||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
result: dict,
|
result: dict,
|
||||||
*,
|
*,
|
||||||
rewrite_existing_v2: bool,
|
rewrite_existing: bool,
|
||||||
) -> tuple[bool, dict]:
|
) -> tuple[bool, dict]:
|
||||||
snapshot_date = date.fromisoformat(result["date"])
|
snapshot_date = date.fromisoformat(result["date"])
|
||||||
existing = await db.execute(select(RegimeSnapshot).where(RegimeSnapshot.date == snapshot_date))
|
existing = await db.execute(select(RegimeSnapshot).where(RegimeSnapshot.date == snapshot_date))
|
||||||
@@ -956,15 +981,23 @@ async def _upsert_snapshot(
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
existing_v2 = _parse_snapshot(row.breakdown_json)
|
existing_parsed = _parse_snapshot(row.breakdown_json)
|
||||||
if existing_v2 is not None and not rewrite_existing_v2:
|
if existing_parsed is not None and not rewrite_existing:
|
||||||
return False, existing_v2
|
return False, existing_parsed
|
||||||
row.total_score = float(state_score or 0.0)
|
row.total_score = float(state_score or 0.0)
|
||||||
row.band = state_band or "unavailable"
|
row.band = state_band or "unavailable"
|
||||||
row.breakdown_json = payload
|
row.breakdown_json = payload
|
||||||
return True, result
|
return True, result
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_revision(snapshot: dict) -> int:
|
||||||
|
"""Sensor revision of a stored snapshot; pre-marker rows read as 1."""
|
||||||
|
try:
|
||||||
|
return int(snapshot.get("sensor_revision") or 1)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def _parse_snapshot(raw: str) -> dict | None:
|
def _parse_snapshot(raw: str) -> dict | None:
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(raw)
|
parsed = json.loads(raw)
|
||||||
@@ -984,7 +1017,9 @@ async def _latest_snapshot_row(db: AsyncSession) -> tuple[RegimeSnapshot, dict]
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def update_regime_monitor(db: AsyncSession, rebuild_sessions: int = REBUILD_SESSIONS) -> dict:
|
async def update_regime_monitor(
|
||||||
|
db: AsyncSession, rebuild_lookback_days: int = REBUILD_LOOKBACK_DAYS
|
||||||
|
) -> dict:
|
||||||
config = await get_regime_config(db)
|
config = await get_regime_config(db)
|
||||||
overrides = await get_fundamental_overrides(db)
|
overrides = await get_fundamental_overrides(db)
|
||||||
if _fundamentals_stale(overrides, config) and not overrides.get("locked"):
|
if _fundamentals_stale(overrides, config) and not overrides.get("locked"):
|
||||||
@@ -1018,10 +1053,18 @@ async def update_regime_monitor(db: AsyncSession, rebuild_sessions: int = REBUIL
|
|||||||
logger.warning("Regime monitor: fixed-basket breadth skipped: %s", exc)
|
logger.warning("Regime monitor: fixed-basket breadth skipped: %s", exc)
|
||||||
breadth, breadth_counts, divergence = {}, {}, {}
|
breadth, breadth_counts, divergence = {}, {}, {}
|
||||||
|
|
||||||
latest_v2 = await _latest_snapshot_row(db)
|
latest_snapshot = await _latest_snapshot_row(db)
|
||||||
rebuilding = latest_v2 is None and bool(leader_series)
|
# A stored series written under an older sensor revision is reseeded once.
|
||||||
|
# Without this, raising HY_OAS_WINDOW_DAYS would only ever reach newly
|
||||||
|
# computed rows: routine runs touch the latest date alone, so every older row
|
||||||
|
# would keep the credit gap indefinitely.
|
||||||
|
rebuilding = bool(leader_series) and (
|
||||||
|
latest_snapshot is None
|
||||||
|
or _snapshot_revision(latest_snapshot[1]) < SENSOR_REVISION
|
||||||
|
)
|
||||||
if rebuilding:
|
if rebuilding:
|
||||||
dates = [d for d, _ in leader_series[-max(1, rebuild_sessions):]]
|
floor = end - timedelta(days=rebuild_lookback_days)
|
||||||
|
dates = [d for d, _ in leader_series if d >= floor] or [latest_date]
|
||||||
else:
|
else:
|
||||||
# Routine PIT rule: only the latest trading date may be inserted/updated.
|
# Routine PIT rule: only the latest trading date may be inserted/updated.
|
||||||
dates = [latest_date]
|
dates = [latest_date]
|
||||||
@@ -1045,7 +1088,9 @@ async def update_regime_monitor(db: AsyncSession, rebuild_sessions: int = REBUIL
|
|||||||
written, latest_result = await _upsert_snapshot(
|
written, latest_result = await _upsert_snapshot(
|
||||||
db,
|
db,
|
||||||
computed,
|
computed,
|
||||||
rewrite_existing_v2=rebuilding or snapshot_date == latest_date,
|
# True for *every* replayed date on a reseed, or it would write one
|
||||||
|
# row and leave the rest at the old revision.
|
||||||
|
rewrite_existing=rebuilding or snapshot_date == latest_date,
|
||||||
)
|
)
|
||||||
snapshots_written += int(written)
|
snapshots_written += int(written)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -154,6 +154,14 @@ and refreshing appeared to do nothing. That was the opposite of what this sectio
|
|||||||
already claimed. Showing it early cannot leak into a published number, because
|
already claimed. Showing it early cannot leak into a published number, because
|
||||||
nothing in the overlay is scored (see "Fundamentals left the score").
|
nothing in the overlay is scored (see "Fundamentals left the score").
|
||||||
|
|
||||||
|
`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 `mixed` 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_overlay` never had this problem: no observation means no
|
||||||
|
effective date, which means `pending`, which already blanks the content.
|
||||||
|
|
||||||
Each snapshot stores the fixed basket symbols, hash, and freeze date.
|
Each snapshot stores the fixed basket symbols, hash, and freeze date.
|
||||||
Reconstructed history before that freeze date is retrospective/exploratory.
|
Reconstructed history before that freeze date is retrospective/exploratory.
|
||||||
|
|
||||||
@@ -276,6 +284,27 @@ 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
|
truncation canary, since a 700-day request returning ~1095 days' worth is now
|
||||||
the visible ceiling.
|
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 bumps
|
The fix was sequenced deliberately: acting on items 1–3 above bumps
|
||||||
`METHODOLOGY`, which fires `rebuilding`, which would have baked the credit-less
|
`METHODOLOGY`, which fires `rebuilding`, which would have baked the credit-less
|
||||||
rows into the fresh series. Fixing the window afterwards would mean reseeding
|
rows into the fresh series. Fixing the window afterwards would mean reseeding
|
||||||
|
|||||||
@@ -500,6 +500,9 @@ export interface RegimeFundamentalOverlay {
|
|||||||
reasoning: string | null;
|
reasoning: string | null;
|
||||||
source: string | null;
|
source: string | null;
|
||||||
fetched_at: string | null;
|
fetched_at: string | null;
|
||||||
|
/** Whether anything was actually collected. Live reading only; the snapshot's
|
||||||
|
* point-in-time overlay omits it. */
|
||||||
|
observed?: boolean;
|
||||||
observed_in_snapshot?: boolean;
|
observed_in_snapshot?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,15 +125,30 @@ const CAPEX_TONE: Record<CapexState, string> = {
|
|||||||
unknown: 'text-gray-500',
|
unknown: 'text-gray-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const OVERLAY_TITLE = 'Fundamental overlay · context, not scored';
|
||||||
|
|
||||||
function FundamentalOverlayCard({ overlay }: { overlay: RegimeFundamentalOverlay }) {
|
function FundamentalOverlayCard({ overlay }: { overlay: RegimeFundamentalOverlay }) {
|
||||||
const capex = overlay.capex ?? {};
|
const capex = overlay.capex ?? {};
|
||||||
const reaction = overlay.good_news_stock_down;
|
const reaction = overlay.good_news_stock_down;
|
||||||
|
|
||||||
|
// Nothing collected: the stored default is "unknown" for every hyperscaler
|
||||||
|
// and "mixed" for the reaction, which are placeholders, not a reading.
|
||||||
|
if (overlay.observed === false) {
|
||||||
|
return (
|
||||||
|
<div className="glass border border-white/[0.06] p-5">
|
||||||
|
<div className="text-[11px] uppercase tracking-wider text-gray-500">{OVERLAY_TITLE}</div>
|
||||||
|
<p className="mt-3 text-xs text-gray-500">
|
||||||
|
No observation collected yet. An admin can collect one under Admin · Monitor settings. It is
|
||||||
|
context only — it never enters State or Warning.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="glass border border-white/[0.06] p-5">
|
<div className="glass border border-white/[0.06] p-5">
|
||||||
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||||
<div className="text-[11px] uppercase tracking-wider text-gray-500">
|
<div className="text-[11px] uppercase tracking-wider text-gray-500">{OVERLAY_TITLE}</div>
|
||||||
Fundamental overlay · context, not scored
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-gray-500">
|
<div className="flex flex-wrap items-center gap-2 text-[11px] text-gray-500">
|
||||||
{overlay.source && <span>{overlay.source}</span>}
|
{overlay.source && <span>{overlay.source}</span>}
|
||||||
{/* When pending, the line below is the single carrier of this date. */}
|
{/* When pending, the line below is the single carrier of this date. */}
|
||||||
|
|||||||
@@ -279,6 +279,37 @@ def test_live_observation_is_visible_before_its_effective_date():
|
|||||||
assert current_observation(overrides, config, date(2026, 8, 22))["available"] is False
|
assert current_observation(overrides, config, date(2026, 8, 22))["available"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_uncollected_observation_is_not_reported_as_collected():
|
||||||
|
"""The default override is placeholders, not a reading.
|
||||||
|
|
||||||
|
``capex`` defaults to "unknown" for every hyperscaler and the reaction to
|
||||||
|
"mixed". Surfacing those as an observation made the card claim a read that
|
||||||
|
never happened.
|
||||||
|
"""
|
||||||
|
names = DEFAULT_CONFIG["tickers"]["hyperscalers"]
|
||||||
|
nothing_collected = {
|
||||||
|
"f1_score": None,
|
||||||
|
"f3_score": None,
|
||||||
|
"capex": {name: "unknown" for name in names},
|
||||||
|
"good_news_stock_down": "mixed",
|
||||||
|
"reasoning": None,
|
||||||
|
"fetched_at": None,
|
||||||
|
"effective_date": None,
|
||||||
|
"source": "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
blank = current_observation(nothing_collected, DEFAULT_CONFIG, date(2026, 8, 7))
|
||||||
|
assert blank["observed"] is False
|
||||||
|
assert blank["available"] is False
|
||||||
|
assert blank["capex"] is None
|
||||||
|
assert blank["good_news_stock_down"] is None
|
||||||
|
assert blank["reasoning"] is None
|
||||||
|
|
||||||
|
# One real observation flips it, placeholders and all.
|
||||||
|
collected = {**nothing_collected, "fetched_at": "2026-08-07T10:00:00+00:00", "source": "gemini"}
|
||||||
|
assert current_observation(collected, DEFAULT_CONFIG, date(2026, 8, 7))["observed"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_fundamentals_do_not_move_the_warning_score():
|
def test_fundamentals_do_not_move_the_warning_score():
|
||||||
"""The v3 complaint: a maxed-out LLM read must not silently do nothing.
|
"""The v3 complaint: a maxed-out LLM read must not silently do nothing.
|
||||||
|
|
||||||
@@ -462,11 +493,11 @@ async def test_prior_snapshot_is_immutable_without_explicit_rebuild(db_session):
|
|||||||
changed["state"] = {"score": 90.0, "band": "breaking"}
|
changed["state"] = {"score": 90.0, "band": "breaking"}
|
||||||
|
|
||||||
written, _ = await rms._upsert_snapshot(
|
written, _ = await rms._upsert_snapshot(
|
||||||
db_session, first, rewrite_existing_v2=True
|
db_session, first, rewrite_existing=True
|
||||||
)
|
)
|
||||||
await db_session.flush()
|
await db_session.flush()
|
||||||
rewritten, persisted = await rms._upsert_snapshot(
|
rewritten, persisted = await rms._upsert_snapshot(
|
||||||
db_session, changed, rewrite_existing_v2=False
|
db_session, changed, rewrite_existing=False
|
||||||
)
|
)
|
||||||
row = (
|
row = (
|
||||||
await db_session.execute(
|
await db_session.execute(
|
||||||
@@ -509,10 +540,10 @@ async def test_routine_can_refresh_latest_trading_session_after_civil_day_rolls(
|
|||||||
return {}, {}
|
return {}, {}
|
||||||
|
|
||||||
async def fake_latest(_db):
|
async def fake_latest(_db):
|
||||||
return object(), {"methodology": "v3"}
|
return object(), {"methodology": "v3", "sensor_revision": rms.SENSOR_REVISION}
|
||||||
|
|
||||||
async def fake_upsert(_db, result, *, rewrite_existing_v2):
|
async def fake_upsert(_db, result, *, rewrite_existing):
|
||||||
rewrites.append(rewrite_existing_v2)
|
rewrites.append(rewrite_existing)
|
||||||
return True, result
|
return True, result
|
||||||
|
|
||||||
class FakeDB:
|
class FakeDB:
|
||||||
@@ -533,6 +564,91 @@ async def test_routine_can_refresh_latest_trading_session_after_civil_day_rolls(
|
|||||||
assert rewrites == [True]
|
assert rewrites == [True]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("stored", "expect_reseed"),
|
||||||
|
[
|
||||||
|
({"methodology": "v3"}, True), # written before the marker existed
|
||||||
|
({"methodology": "v3", "sensor_revision": 1}, True),
|
||||||
|
({"methodology": "v3", "sensor_revision": rms.SENSOR_REVISION}, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_a_stale_sensor_revision_reseeds_stored_history(
|
||||||
|
monkeypatch, stored, expect_reseed
|
||||||
|
):
|
||||||
|
"""Widening the OAS window has to reach rows that are already stored.
|
||||||
|
|
||||||
|
Routine runs recompute only the latest date, so without this trigger every
|
||||||
|
older row would keep the credit gap the wider window exists to close.
|
||||||
|
"""
|
||||||
|
sessions = [date.today() - timedelta(days=offset) for offset in reversed(range(10))]
|
||||||
|
prices = {symbol: [(day, 100.0) for day in sessions] for symbol in ("SMH", "QQQ", "SPY")}
|
||||||
|
written: list[date] = []
|
||||||
|
revisions: list[int] = []
|
||||||
|
|
||||||
|
async def fake_config(_db):
|
||||||
|
return copy.deepcopy(DEFAULT_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(), stored
|
||||||
|
|
||||||
|
async def fake_upsert(_db, result, *, rewrite_existing):
|
||||||
|
written.append(date.fromisoformat(result["date"]))
|
||||||
|
revisions.append(result["sensor_revision"])
|
||||||
|
# Every replayed row must be rewritable, or a reseed writes one row.
|
||||||
|
assert rewrite_existing is True
|
||||||
|
return True, result
|
||||||
|
|
||||||
|
class FakeDB:
|
||||||
|
async def commit(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
for name, value in (
|
||||||
|
("get_regime_config", fake_config),
|
||||||
|
("get_fundamental_overrides", fake_overrides),
|
||||||
|
("_fetch_prices", fake_prices),
|
||||||
|
("_fetch_fred_series", fake_fred),
|
||||||
|
("_latest_snapshot_row", fake_latest),
|
||||||
|
("_upsert_snapshot", fake_upsert),
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(rms, name, value)
|
||||||
|
monkeypatch.setattr(rms.breadth_service, "compute_breadth_details", fake_breadth)
|
||||||
|
|
||||||
|
await rms.update_regime_monitor(FakeDB())
|
||||||
|
|
||||||
|
if expect_reseed:
|
||||||
|
assert written == sessions, "a reseed must replay the whole stored span"
|
||||||
|
else:
|
||||||
|
assert written == [sessions[-1]], "a current revision must not reseed"
|
||||||
|
assert set(revisions) == {rms.SENSOR_REVISION}
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_rebuild_span_stays_inside_the_oas_window():
|
||||||
|
"""The reseed must not replay rows it cannot compute credit for.
|
||||||
|
|
||||||
|
Each replayed row needs W3's lookback inside the fetched OAS window; if the
|
||||||
|
replay reached further back than the fetch, the reseed would recreate the
|
||||||
|
very gap it exists to close.
|
||||||
|
"""
|
||||||
|
replay_calendar_days = rms.REBUILD_LOOKBACK_DAYS
|
||||||
|
w3_lookback_calendar = rms.W3_OAS_LOOKBACK * 7 / 5 # business days -> calendar
|
||||||
|
assert replay_calendar_days + w3_lookback_calendar <= rms.HY_OAS_WINDOW_DAYS
|
||||||
|
# ...and still covers the 400-session series the v3 cutover wrote.
|
||||||
|
assert replay_calendar_days >= 400 * 365 / 252
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_manual_llm_refresh_recomputes_latest_regime_snapshot(monkeypatch):
|
async def test_manual_llm_refresh_recomputes_latest_regime_snapshot(monkeypatch):
|
||||||
calls: list[str] = []
|
calls: list[str] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user