Plan 003 step 5 (completes plan 003). research/eod_reconcile.py diffs broker truth (open positions, today's live orders) against event-store truth (today's submits/fills/closes) and emits a loud eod_reconcile event + Telegram alert on any drift — the duplicate-resting-order-after-restart class (2026-07-06) caught automatically instead of by hand. Read-only (reports, never auto-fixes). Wired into both scheduler blocks after premarket_eval. Pure diff core is unit-tested. Suite: 373 passed (+22 plan-003 tests), 15 fail = WSL-loopback dashboard only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
"""Nightly cage-count reconcile (plan 003 step 5).
|
|
|
|
The pure diff (compute_reconcile) is the money logic; run() is thin I/O over a
|
|
stubbed broker + an in-memory store.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from research.eod_reconcile import compute_reconcile, run
|
|
from storage.event_store import EventStore
|
|
from storage.event_schema import EventMode, OrderSubmittedEvent
|
|
|
|
|
|
def _sub(sym):
|
|
return {"payload": {"symbol": sym}}
|
|
|
|
|
|
def test_reconcile_clean_when_broker_matches_store():
|
|
events = {"order_submitted": [_sub("AAA")], "order_filled": [], "position_closed": []}
|
|
r = compute_reconcile(broker_positions=[{"symbol": "AAA"}], broker_orders=[], events=events)
|
|
assert r["ok"] and r["mismatches"] == []
|
|
|
|
|
|
def test_reconcile_flags_untracked_position():
|
|
# broker holds BBB but no submit event today -> mismatch
|
|
events = {"order_submitted": [_sub("AAA")], "order_filled": [], "position_closed": []}
|
|
r = compute_reconcile([{"symbol": "AAA"}, {"symbol": "BBB"}], [], events)
|
|
assert not r["ok"]
|
|
assert any("BBB" in m for m in r["mismatches"])
|
|
|
|
|
|
def test_reconcile_flags_untracked_live_order():
|
|
# a live resting order with no submit event = the duplicate-after-restart case
|
|
events = {"order_submitted": [], "order_filled": [], "position_closed": []}
|
|
orders = [{"symbol": "CCC", "status": "accepted"}]
|
|
r = compute_reconcile([], orders, events)
|
|
assert not r["ok"]
|
|
assert any("CCC" in m for m in r["mismatches"])
|
|
|
|
|
|
def test_reconcile_ignores_terminal_orders():
|
|
# a filled/canceled broker order is expected to differ — not a mismatch
|
|
events = {"order_submitted": [], "order_filled": [], "position_closed": []}
|
|
orders = [{"symbol": "DDD", "status": "filled"}, {"symbol": "EEE", "status": "canceled"}]
|
|
r = compute_reconcile([], orders, events)
|
|
assert r["ok"]
|
|
|
|
|
|
def test_reconcile_flags_closed_but_still_held():
|
|
events = {"order_submitted": [_sub("AAA")], "order_filled": [],
|
|
"position_closed": [_sub("AAA")]}
|
|
r = compute_reconcile([{"symbol": "AAA"}], [], events)
|
|
assert not r["ok"]
|
|
assert any("closed" in m for m in r["mismatches"])
|
|
|
|
|
|
class _StubClient:
|
|
def __init__(self, positions, orders):
|
|
self._p, self._o = positions, orders
|
|
def get_positions(self):
|
|
return self._p
|
|
def get_orders(self, status="all", after=None):
|
|
return self._o
|
|
|
|
|
|
def test_run_emits_event_and_alerts_on_mismatch():
|
|
store = EventStore(":memory:")
|
|
store.emit(OrderSubmittedEvent(
|
|
timestamp=datetime.now(), mode=EventMode.PAPER, correlation_id="t",
|
|
message="sub", symbol="AAA", order_id="o1", side="buy", quantity=1, price=1.0,
|
|
payload={"symbol": "AAA"}))
|
|
# broker also holds an UNTRACKED name -> mismatch
|
|
client = _StubClient([{"symbol": "AAA"}, {"symbol": "ZZZ"}], [])
|
|
alerts = []
|
|
now = datetime(2026, 7, 13, 16, 30)
|
|
r = run(store, client, now_et=now, notify=lambda t: alerts.append(t))
|
|
assert not r["ok"]
|
|
assert len(alerts) == 1 and "ZZZ" in alerts[0]
|
|
assert store.query_events(event_type="risk_rule_triggered")
|
|
|
|
|
|
def test_run_clean_no_alert():
|
|
store = EventStore(":memory:")
|
|
store.emit(OrderSubmittedEvent(
|
|
timestamp=datetime.now(), mode=EventMode.PAPER, correlation_id="t",
|
|
message="sub", symbol="AAA", order_id="o1", side="buy", quantity=1, price=1.0,
|
|
payload={"symbol": "AAA"}))
|
|
client = _StubClient([{"symbol": "AAA"}], [])
|
|
alerts = []
|
|
r = run(store, client, now_et=datetime(2026, 7, 13, 16, 30), notify=lambda t: alerts.append(t))
|
|
assert r["ok"] and alerts == []
|