fix(backtest): roll back the session after a swallowed DB failure
Every DB call in run_backtest is best-effort so one unreadable ticker cannot abort the whole replay, but the handlers swallowed the exception without clearing the transaction. asyncpg then reports "current transaction is aborted" for every later statement, and the first unguarded one — the report write — surfaced it as the job error, long after the real cause. Add _rollback_quietly at the three swallowing sites (benchmark load, parallel fetch, sequential replay), matching the guard price_service already uses. Load plain symbols instead of Ticker instances: a rollback expires ORM objects held across it, and touching an expired attribute afterwards triggers sync lazy-loading, which raises on an AsyncSession. rr_scanner_service hit this same trap. Only .symbol was ever used. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1620,3 +1620,39 @@ async def test_run_backtest_smoke(session):
|
||||
sweep = sorted(report["sweep"], key=lambda r: r["min_momentum_percentile"], reverse=True)
|
||||
counts = [r["total"] for r in sweep]
|
||||
assert counts == sorted(counts) # ascending as threshold descends
|
||||
|
||||
|
||||
async def test_run_backtest_rolls_back_a_failed_ticker_fetch(session, monkeypatch):
|
||||
"""A failed per-ticker read must not leave the session mid-failed-transaction.
|
||||
|
||||
Every DB call in the replay loop is best-effort, but swallowing the error
|
||||
without a rollback leaves asyncpg in "current transaction is aborted": every
|
||||
later statement fails the same way until the first unguarded one — the report
|
||||
write — surfaces it as the job error, long after the real cause.
|
||||
"""
|
||||
await _seed_oscillating_ticker(session, "AAA")
|
||||
await _seed_oscillating_ticker(session, "OSC")
|
||||
|
||||
real_fetch = bt._fetch_columns
|
||||
rolled_back: list[str] = []
|
||||
|
||||
async def failing_fetch(db, symbol):
|
||||
if symbol == "AAA":
|
||||
raise RuntimeError("simulated OHLCV read failure")
|
||||
return await real_fetch(db, symbol)
|
||||
|
||||
real_rollback = session.rollback
|
||||
|
||||
async def tracking_rollback():
|
||||
rolled_back.append("x")
|
||||
await real_rollback()
|
||||
|
||||
monkeypatch.setattr(bt, "_fetch_columns", failing_fetch)
|
||||
monkeypatch.setattr(session, "rollback", tracking_rollback)
|
||||
|
||||
report = await bt.run_backtest(session)
|
||||
|
||||
assert rolled_back, "a failed ticker fetch left the session un-rolled-back"
|
||||
# the surviving ticker is still replayed after the rollback
|
||||
assert report["tickers"] == 2
|
||||
assert report["candidates"] >= 1
|
||||
|
||||
Reference in New Issue
Block a user