feat(replay): R1 replay core — clock injection + replay of archived cycles
Re-executes BayesianStrategy.evaluate() over the R0 archive and stores results in replay_runs/replay_decisions, tagged with git sha + a hash of the strategy constants (same hash vs archive = determinism check, different hash = counterfactual run). - bayesian.py: optional as_of param on evaluate()/_days_to_resolution() (clock injection; default None = wall clock, prod behavior unchanged — the only touch to frozen code, purely additive) - bot/replay.py: replay engine + CLI (python -m bot.replay --from --to); ReplayNews feeds archived sentiment back (GNews never called, per-cycle budget bypassed — archived sentiment already encodes it); manifold/db not wired (observational-only in prod); recorded-vs-replayed compare at 1e-9 tolerance - schema.sql: replay_runs + replay_decisions (+ indexes), idempotent - db.py: 6 replay accessors/writers - tests: 19 new round-trip fidelity tests (104 total green) Validated against a real prod cycle (2026-07-02T14:03:15Z, 46 markets, 4 skip paths incl. the Georgia confidence record): 46/46 matched, max float delta 0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
eb4f67414a
commit
0ac48ba7f8
@@ -167,15 +167,22 @@ def _regime_min_edge(category: str, days_to_resolution: int) -> float:
|
||||
return 0.10 # tech, crypto/finance, events, default
|
||||
|
||||
|
||||
def _days_to_resolution(end_date: str) -> int:
|
||||
"""Return calendar days until market resolution, or 30 if unknown."""
|
||||
def _days_to_resolution(end_date: str, as_of: Optional[datetime] = None) -> int:
|
||||
"""Return calendar days until market resolution, or 30 if unknown.
|
||||
|
||||
as_of (Replay R1): reference clock for the computation. None (production)
|
||||
means wall-clock now; a replay run passes the archived cycle_ts so
|
||||
days-to-resolution — and therefore the regime edge threshold — is computed
|
||||
against the moment the decision was originally made.
|
||||
"""
|
||||
if not end_date:
|
||||
return 30 # conservative: treat as medium-term
|
||||
try:
|
||||
dt = datetime.fromisoformat(end_date.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
days = (dt - datetime.now(timezone.utc)).days
|
||||
now = as_of if as_of is not None else datetime.now(timezone.utc)
|
||||
days = (dt - now).days
|
||||
return max(0, days)
|
||||
except (ValueError, TypeError):
|
||||
return 30
|
||||
@@ -457,10 +464,17 @@ class BayesianStrategy:
|
||||
market: Market,
|
||||
ext: ExternalSignals,
|
||||
occupied_families: set[str],
|
||||
as_of: Optional[datetime] = None,
|
||||
) -> Optional[TradingSignal]:
|
||||
"""
|
||||
Evaluate a market and return a TradingSignal if actionable.
|
||||
|
||||
as_of (Replay R1): clock injection — None in production (wall-clock
|
||||
now); a replay passes the archived cycle_ts so the regime threshold
|
||||
matches the original decision moment. Only days-to-resolution
|
||||
depends on the clock; everything else is a pure function of
|
||||
(market, ext, occupied_families) and the news/manifold clients.
|
||||
|
||||
Returns None with a structured log line in all skip cases.
|
||||
Skip reasons (Phase 5 observability):
|
||||
SKIP_UNSUPPORTED — category not supported
|
||||
@@ -558,7 +572,7 @@ class BayesianStrategy:
|
||||
return None
|
||||
|
||||
# ── Phase 4: regime min-edge ─────────────────────────────────────────
|
||||
days = _days_to_resolution(market.end_date)
|
||||
days = _days_to_resolution(market.end_date, as_of)
|
||||
regime_min = _regime_min_edge(category, days)
|
||||
|
||||
# ── Bayesian probability estimation ──────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user