Skip to content

feat(backtest): per-bar decision tape - #627

Merged
FlyM1ss merged 16 commits into
mainfrom
feat/backtest-decision-tape
Oct 9, 2026
Merged

FlyM1ss merged 16 commits into
mainfrom
feat/backtest-decision-tape

Conversation

@FlyM1ss

@FlyM1ss FlyM1ss commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Records every decision bar of a My Agents pipeline backtest into the run's #622 agent trace as a decision_recorded / execution_result pair: raw model orders (intent), pre-decision portfolio (state), post-gate actions, fill plan, fills, rejections, driver (llm / llm_fallback / rule_based) and whether the gate rewrote the model. Read back with traces.service.load_decision_tape(run_id).

intent + state is the replay unit; actions is the same-engine reproduction check. Evidence (test_decision_tape_engine.py):

  • test_recorded_actions_reproduce_the_run_on_the_same_engine
  • test_intent_replay_through_the_gate_reproduces_an_llm_run (+ ..._keeps_null_and_non_finite_fields — intent keeps raw nulls/NaN/containers; orders_for_replay restores them)
  • test_perturbed_engine_diverges_at_the_first_filled_bar

Bars are written in batches of 25 (append_events, one transaction, both twins) and flushed at loop end and before a failed trace closes; a SIGTERM'd run loses at most its unflushed buffer. Per-run decision_tape_summary() reports gate_rewrites, write failures, oversize skips, dropped bars; the recorder suspends after 3 consecutive store failures. No tape/trace exception reaches the bar loop (test_broken_trace_writes_do_not_change_the_run).

Trace close: the child closes it from trace_lifecycle (constructor → both baselines → update_run_baselines) and writes nothing on SystemExit. The parent labels a still-running trace once, after slot/reservation cleanup, on a 10s-bounded thread:

arm label
cancel run_cancelled
timeout run_timed_out
returncode > 0 run_failed
returncode < 0 (signal it did not send, e.g. OOM) run_killed
completed but trace still open trace_close_failed
parent exception run_killed

A completed trace is never relabelled.

Realism is out of scope. The recorded engine is frictionless, latency-free and on unadjusted bars; the conformance suite (#623) scores realism. Follow-ups are listed in the spec (adjusted bars with provenance first; plus a newly found intraday marking rule to pin before comparing engines).

Trace store moves to AGENT_RUNS_DATABASE_URL (was CONTENT_DATABASE_URL, #622): per-bar tape rows are run data and stay off the users/content DB. Traces written since #622 remain in the content DB, unread. Boot line: trace_store backend: postgres (<host>/<db>).

Not touched: portfolio_manager.py, trading/execution.py, step sessions, AI Hedge Fund runtime, leaderboard. Full backend suite: 6575 passed; the only failures are the 7 local-env test_report_pdf ones, same set as main.

🤖 Generated with Claude Code

FlyM1ss and others added 14 commits October 8, 2026 17:21
Intent + pre-decision state is the engine-independent replay unit; recorded
actions are a same-engine reproduction check only. Parent labels trace
outcomes per arm; child writes nothing on SystemExit. Market realism is
scored by the conformance suite, not retrofitted into the engine being
replaced; adjusted US bars with provenance is the one item split out as a
follow-up PR.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
gate_rewrote no longer raises on a non-numeric model position_size
(treated as matching any size). Plan: Task 3 recorder gains
record_bar_from so Task 4's payload builders run inside the swallow layer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Suspend the decision tape recorder after MAX_CONSECUTIVE_STORE_FAILURES (3)
failed bars in a row. Swallowing exceptions only bounds a store that fails
fast; one that hangs before raising (Neon dial, TCP timeout) would cost that
hang on every bar and could push the run into the subprocess timeout. Later
bars are counted in the new summary key suspended_skipped without touching
the store; a successful write resets the streak.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Parent trace label: arms only record it; written once, last in finally,
  after finalize_run and _finalize_slot, on a daemon thread joined with
  TRACE_LABEL_DEADLINE_SECONDS. A hung or raising trace store can no longer
  strand a slot or a reservation.
- Intent is an exact copy of the gate's input: present-null fields kept,
  non-finite numbers and object/array values carry their JSON text under
  raw[field], non-object entries kept whole; orders_for_replay restores them.
  bound_payload marks an order unreplayable rather than silently changing it.
- gate_rewrote is False on llm_fallback bars; gate_rewrites counts LLM-driven
  bars only.
- Perturbed-engine test maps the divergent curve point to its decision bar via
  the fill plan, and adds a script whose first fill is on bar 2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Parent: label the trace in an inner finally so a raising cleanup step
  cannot leave it running; a negative returncode (signal the parent did
  not send) labels run_killed.
- Child: trace_lifecycle treats an exception raised while a SystemExit
  unwinds as the exit and writes nothing.
- Tape: rejection reason/strategy_reason truncated and shed before a bar
  is dropped; load_decision_tape marks each bar complete or not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agentic-trading-lab Ready Ready Preview Oct 9, 2026 4:25am UTC

Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/api/routers/backtests.py Fixed
Comment thread dashboard/backend/api/routers/backtests.py Fixed
Comment thread dashboard/backend/api/routers/backtests.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
- Trace stays open through both baselines and update_run_baselines, so a
  baseline failure no longer leaves a `completed` trace.
- Tape bars are batched (TAPE_FLUSH_BARS) into one store transaction via a
  new append_events on both trace-store twins; flushed at loop end and
  before the trace closes on failure.
- Trace store moves to AGENT_RUNS_DATABASE_URL (run data, off the
  users/content DB).
- fail_trace_if_running raises on store errors; the worker and
  finish_trace_best_effort log ERROR lines instead of failing silently.
- Collapsed rejections carry only the collapse key plus this bar's size,
  not the first rejection's timestamp, size or reasoning.
- gate_rewrote ignores sells widened to the sellable position, buys left
  at size 0, and symbol padding.
- Pre-decision snapshots run inside the recorder's swallow layer and are
  skipped when the recorder is a no-op or suspended; the rejection
  snapshot covers only the events a bar could collapse into.
- Oversized raw model values mark the order unreplayable instead of
  dropping the bar.
- _run_with_deadline is shared by the spend lookup and the trace label.
- CodeQL: comments on the empty excepts; test raises go through a helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
Comment thread dashboard/backend/tests/domain/backtesting/test_decision_tape.py Fixed
CodeQL py/side-effect-in-assert on the capture tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@FlyM1ss
FlyM1ss merged commit 896d635 into main Oct 9, 2026
8 checks passed
@FlyM1ss
FlyM1ss deleted the feat/backtest-decision-tape branch October 9, 2026 04:37

This branch was successfully deployed

1 active deployment
Preview — db8cbf84 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants