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:
chemavx
2026-07-02 14:05:25 +00:00
co-authored by Claude Fable 5
parent eb4f67414a
commit 0ac48ba7f8
5 changed files with 919 additions and 4 deletions
+18 -4
View File
@@ -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 ──────────────────────────────────