Files
momentum-system/tests/unit/test_retention_and_telemetry.py
T
momentum devandClaude Fable 5 12f7fefec0 feat: nightly event retention + external telemetry push
- runtime/retention.py: prune 8 high-volume debug event types older than
  RETENTION_DAYS (default 14) once/day at RETENTION_HOUR (default 03:00).
  Audit events (orders, fills, closes, risk rules) are never pruned. The
  events table hit 6 GB in 3 weeks at ~340k rows/day; this caps it.
- runtime/telemetry_push.py: POST a JSON snapshot (equity, day realized
  P&L, trades/wins, open positions, last-event age) to TELEMETRY_PUSH_URL
  every TELEMETRY_PUSH_INTERVAL_SECONDS (default 300). Unset URL = step
  disabled. Push failures are reported in the step result, never raised —
  an external outage can't touch the loop. Works as healthchecks.io
  dead-man ping + payload log in one.
- wired both as scheduler steps in run_live_paper (retention gated by
  RETENTION_ENABLED, telemetry by TELEMETRY_PUSH_URL presence)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:25:09 -05:00

80 lines
2.8 KiB
Python

"""Retention prune + telemetry push snapshot."""
from datetime import datetime, timedelta
from storage.event_store import EventStore
from storage.event_schema import (
EventMode,
ModuleTickEvent,
PositionClosedEvent,
)
from runtime.retention import prune_events, maybe_prune
from runtime.telemetry_push import build_payload, push_snapshot
def _tick(ts):
return ModuleTickEvent(
timestamp=ts, mode=EventMode.PAPER, correlation_id="t",
message="tick", module="triggers", stage="completed",
duration_ms=1.0, input_count=0, output_count=0)
def _close(ts, pnl):
return PositionClosedEvent(
timestamp=ts, mode=EventMode.PAPER, correlation_id="t",
message="closed", position_id="p1", symbol="AAA",
exit_reason="stop_loss", entry_price=1.0,
exit_price=1.0 + pnl, realized_pnl=pnl,
payload={"symbol": "AAA", "realized_pnl": pnl})
def test_prune_deletes_old_spam_keeps_audit_and_recent():
store = EventStore(":memory:")
now = datetime.now()
old = now - timedelta(days=30)
store.emit(_tick(old)) # old spam -> pruned
store.emit(_tick(now)) # recent spam -> kept
store.emit(_close(old, 5.0)) # old AUDIT event -> kept forever
r = prune_events(store, days=14, now=now)
assert r["deleted"] == 1
assert len(store.query_events(event_type="module_tick", limit=10)) == 1
assert len(store.query_events(event_type="position_closed", limit=10)) == 1
def test_maybe_prune_only_in_window_and_once_per_day():
import runtime.retention as retention
retention._last_run = None
store = EventStore(":memory:")
at_2am = datetime.now().replace(hour=2)
at_3am = datetime.now().replace(hour=3)
assert maybe_prune(store, now=at_2am) == "idle"
assert maybe_prune(store, now=at_3am).startswith("pruned")
assert maybe_prune(store, now=at_3am) == "done-today"
def test_build_payload_day_stats():
store = EventStore(":memory:")
now = datetime.now()
store.emit(_close(now, 100.0))
store.emit(_close(now, -40.0))
store.emit(_close(now - timedelta(days=2), 999.0)) # not today -> excluded
p = build_payload(store, broker="tradier", now=now)
assert p["broker"] == "tradier"
assert p["day_trades"] == 2
assert p["day_wins"] == 1
assert p["day_realized"] == 60.0
assert p["last_event_age_s"] is not None
def test_push_snapshot_disabled_without_url(monkeypatch):
monkeypatch.delenv("TELEMETRY_PUSH_URL", raising=False)
store = EventStore(":memory:")
assert push_snapshot(store) == "disabled"
def test_push_snapshot_survives_unreachable_url(monkeypatch):
monkeypatch.setenv("TELEMETRY_PUSH_URL", "http://127.0.0.1:1/nope")
store = EventStore(":memory:")
out = push_snapshot(store)
assert out.startswith("push failed")