- 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>
80 lines
2.8 KiB
Python
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")
|