chore: cleanup duplicate trade save, misleading cycle counters, and /api/summary inconsistencies
CI/CD / build-and-push (push) Successful in 7s

Bug #5: metrics.record_trade() only delegated to save_trade(), which
executor.execute() already calls — every trade was written twice (deduped
only by ON CONFLICT DO NOTHING). Remove the redundant call and the now-dead
method. RealExecutor.execute() raises NotImplementedError, so real mode is
unaffected.

Bug #6 (CYCLE SUMMARY): manifold accepted/rejected counters only increment
on the active-signal path, so with MANIFOLD_SIGNAL_ENABLED=false they always
printed 0/0 — print 'manifold_signal: disabled' instead.
family_conflicts_prevented duplicated blocked_by_family (same counter
printed twice); removed. gnews_cap was a dead variable with a misleading
comment; removed.

Bug #7 (/api/summary): total_trades was len() over a LIMIT-500 query —
capped once history grows; counts now come from COUNT(*) via
compute_metrics_from_db. cash_available was reimplemented in the API;
extract cash_available() in paper.py (same formula, unchanged) and feed it
from get_open_position_data() — the exact source/helper
PaperExecutor.initialize() uses. Test asserts API and executor report
identical cash for the same DB state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-06-11 17:21:32 +00:00
co-authored by Claude Fable 5
parent 02cbfc0b94
commit 7ebb87aede
5 changed files with 147 additions and 25 deletions
+102
View File
@@ -0,0 +1,102 @@
"""
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)