""" Tests for bug #7 — /api/summary must agree with the executor's cash model. Regression: /api/summary computed total_trades as len() over a LIMIT-500 query (capped once history grows) and reimplemented cash as bankroll - sum(net_cost of open trades) from that same capped query. Fix: counts come from COUNT(*) (compute_metrics_from_db) and cash comes from cash_available() — the same helper PaperExecutor.initialize() uses — fed by the same source (get_open_position_data). This test runs both consumers against one fake DB state and asserts they report identical cash. """ import asyncio import pytest import api.main as api_main from bot.executor.paper import PaperExecutor, cash_available BANKROLL = 10_000.0 # PAPER_BANKROLL default used by both bot and API class FakeDB: """One DB state served to both the API endpoint and the executor.""" def __init__(self, positions: dict[str, float], total_net_cost: float, total_trades: int, open_count: int): self._positions = positions self._total_net_cost = total_net_cost self._total = total_trades self._open = open_count # Shared source: executor.initialize() and /api/summary both call this. async def get_open_position_data(self): return dict(self._positions), self._total_net_cost # /api/summary only: async def get_metrics_history(self, days=1): return [] async def compute_metrics_from_db(self): return { "total_trades": self._total, "open_count": self._open, "closed_count": self._total - self._open, } async def get_recently_closed_inverted(self, hours=24): return set() async def get_legacy_incomplete_count(self): return 0 def _run(db: FakeDB, monkeypatch) -> tuple[dict, PaperExecutor]: monkeypatch.setattr(api_main, "db", db) monkeypatch.delenv("PAPER_BANKROLL", raising=False) async def run(): summary = await api_main.get_summary() ex = PaperExecutor(db=db, bankroll=BANKROLL) await ex.initialize() return summary, ex return asyncio.run(run()) def test_api_and_executor_report_same_cash(monkeypatch): db = FakeDB( positions={"m1": 100.0, "m2": 80.0}, total_net_cost=183.60, # 180 + fees total_trades=12, open_count=2, ) summary, ex = _run(db, monkeypatch) assert summary["cash_available"] == pytest.approx(ex.get_portfolio().cash) assert summary["cash_available"] == pytest.approx( cash_available(BANKROLL, 183.60) ) assert summary["total_deployed"] == pytest.approx(183.60) def test_total_trades_not_capped_by_query_limit(monkeypatch): """700 trades in DB: the old len(LIMIT 500) reported 500.""" db = FakeDB( positions={"m1": 100.0}, total_net_cost=102.0, total_trades=700, open_count=1, ) summary, _ = _run(db, monkeypatch) assert summary["total_trades"] == 700 assert summary["open_trades_count"] == 1 assert summary["closed_trades_count"] == 699 def test_cash_consistency_with_no_open_positions(monkeypatch): db = FakeDB(positions={}, total_net_cost=0.0, total_trades=0, open_count=0) summary, ex = _run(db, monkeypatch) assert summary["cash_available"] == pytest.approx(BANKROLL) assert ex.get_portfolio().cash == pytest.approx(BANKROLL)