feat(risk-monitor): measure the rule that fires, and give fundamentals their own channel
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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Point-in-time history for the sourced fundamental observation
|
||||
|
||||
Revision ID: 033
|
||||
Revises: 032
|
||||
Create Date: 2026-08-12 00:00:00.000000
|
||||
|
||||
The hyperscaler capex / "good news, stock down" read lived in a single
|
||||
``SystemSetting`` slot, so each refresh overwrote the last and no history
|
||||
existed. The read is now a categorical channel reported alongside State and
|
||||
Warning (never a term in either), and a channel with no history cannot be
|
||||
replayed: a snapshot rebuild would record every historical session as if nothing
|
||||
had ever been observed, and the event study could not measure the channel at all.
|
||||
|
||||
Keyed on ``effective_date`` (the session the observation becomes usable on,
|
||||
normally the next weekday) rather than ``fetched_at``, because that is the gate
|
||||
that stops a rebuild stamping today's reading onto historical rows.
|
||||
|
||||
The table starts empty. ``update_regime_monitor`` records the currently stored
|
||||
observation on its next run, so a deployment does not lose the live reading —
|
||||
but genuine history does not exist and cannot be invented here. Backfilling it
|
||||
from the SEC capex line and earnings-date reactions is separate work; until then
|
||||
every historical session reads ``unknown``, which is the honest value rather than
|
||||
a guessed one.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "033"
|
||||
down_revision: Union[str, None] = "032"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"regime_fundamental_observations",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("effective_date", sa.Date(), nullable=False),
|
||||
sa.Column("f1_score", sa.Float(), nullable=True),
|
||||
sa.Column("f3_score", sa.Float(), nullable=True),
|
||||
sa.Column("capex_json", sa.Text(), nullable=False),
|
||||
sa.Column("good_news_stock_down", sa.String(length=10), nullable=False),
|
||||
sa.Column("reasoning", sa.Text(), nullable=True),
|
||||
sa.Column("source", sa.String(length=30), nullable=False),
|
||||
sa.Column("fetched_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"effective_date", name="uq_regime_fundamental_observations_effective_date"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_regime_fundamental_observations_effective_date",
|
||||
"regime_fundamental_observations",
|
||||
["effective_date"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(
|
||||
"ix_regime_fundamental_observations_effective_date",
|
||||
table_name="regime_fundamental_observations",
|
||||
)
|
||||
op.drop_table("regime_fundamental_observations")
|
||||
Reference in New Issue
Block a user