fix(backtest): roll back the portfolio-sim DB failures too

The first pass guarded the replay loop but not the portfolio-simulation block,
which re-fetches price columns and loads the benchmark and the live exit policy
from the same session much later. A failure in any of those swallows the
exception without clearing the transaction — the identical failure mode, with
the identical symptom: the report write is the first unguarded statement and
takes the blame.

The outer handler is the backstop for the price_columns loop, which has no
handler of its own; rolling back a session an inner handler already cleared is
a no-op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 16:10:57 +02:00
co-authored by Claude Opus 5
parent 6ca7f13779
commit fbca38e144
2 changed files with 39 additions and 0 deletions
+32
View File
@@ -1656,3 +1656,35 @@ async def test_run_backtest_rolls_back_a_failed_ticker_fetch(session, monkeypatc
# the surviving ticker is still replayed after the rollback
assert report["tickers"] == 2
assert report["candidates"] >= 1
async def test_run_backtest_rolls_back_a_failed_portfolio_sim_load(session, monkeypatch):
"""The portfolio-sim block loads the benchmark and the live exit policy from
the same session, well after the replay loop. A failure there poisons the
transaction exactly as one in the loop does, and the report write pays for it.
"""
await _seed_oscillating_ticker(session, "OSC")
rolled_back: list[str] = []
called: list[str] = []
async def failing_exit_policy(db):
called.append("x")
raise RuntimeError("simulated exit-policy read failure")
real_rollback = session.rollback
async def tracking_rollback():
rolled_back.append("x")
await real_rollback()
monkeypatch.setattr(
"app.services.paper_trade_service.get_exit_policy", failing_exit_policy
)
monkeypatch.setattr(session, "rollback", tracking_rollback)
report = await bt.run_backtest(session)
assert called, "the portfolio-sim block never ran; test proves nothing"
assert rolled_back, "a failed portfolio-sim load left the session un-rolled-back"
assert report["tickers"] == 1