trader.replay.run module

Drives the pre-registered sample through the real LlmStrategy, and scores it.

Instantiates LlmStrategy directly — never through registry.build_strategy, which exposes no clock — with a fresh ArchiveNewsAt and a fresh instance per row: both are bound to that row’s decision instant, and reusing one LlmStrategy across rows would leave last_inputs from the previous row readable if a call raised. decision_memory=None and max_reuse_age_minutes=0 together switch the reuse gate off completely — either alone would too, but a carried-forward HOLD from a neighbouring replayed row must never be possible, so both are set so it cannot be half-configured.

Reads data/trader.db and Alpaca’s news history through the production repositories, for reads only. Writes no decisions, bars, or news_archive row, and never constructs a broker — this module cannot place an order.

class trader.replay.run.ReplayRunner(*, news_repository, bar_repository, market_data, llm_provider_factory, window_hours=72.0, lookback_bars=30, seed=7)[source]

Bases: object

Replays a pre-registered ReplayRow sample through the real LlmStrategy.

Parameters:
  • news_repository (_NewsRepo)

  • bar_repository (_BarRepo)

  • market_data (_MarketDataProvider)

  • llm_provider_factory (Callable[[], LlmProvider])

  • window_hours (float)

  • lookback_bars (int)

  • seed (int)

run(rows, out_dir, *, resume=False, dry_run=False, shortfalls=())[source]

Replay every row not already present, append as it goes, then score.

–resume reads the CSV back and skips any (symbol, decision_at) already present — a run that loses everything to a crash at row 500 of 540 is a design defect, not bad luck. Every row is flushed to disk the moment it completes.

Parameters:
Return type:

RunReport

class trader.replay.run.RunReport(seed, generated_at, fetch_dates, n_rows, n_scored, csv_path, json_path)[source]

Bases: object

What a run produced, and the provenance needed to reproduce it.

Parameters:
  • seed (int)

  • generated_at (str)

  • fetch_dates (dict[str, str])

  • n_rows (int)

  • n_scored (int)

  • csv_path (str)

  • json_path (str)

seed: int
generated_at: str
fetch_dates: dict[str, str]
n_rows: int
n_scored: int
csv_path: str
json_path: str
trader.replay.run.rescore_from_csv(csv_path, out_dir, *, seed=7, fetch_dates=None, shortfalls=(), dry_run=False, spy_prices=None)[source]

Re-run _summarize over an already-replayed CSV — no model, no fetch.

Every replayed decision (action, confidence, and every horizon’s forward return) is already on disk in csv_path; a change to the scoring formula (issue #29’s edge_A -> edge_B headline swap is the first case) should not cost another 1.7-1.9 hours of Ollama calls to re-judge. This is the capability run()’s own –resume path already leans on (_read_scored_rows reads exactly this CSV shape back) turned into a standalone entry point, because a metric amendment is not a one-off.

spy_prices is optional and, when omitted, edge_grid_market_adjusted is not recomputed — SPY’s forward-price grid needed to market-adjust an arbitrary other symbol’s entry day cannot be reconstructed from the CSV alone (each symbol’s sampled entry days are drawn independently, so the CSV’s own SPY rows do not cover every day another symbol needs), and fetching it fresh means a market-data call this function deliberately does not make on your behalf. Every other section — the headline, the non-market-adjusted edge_grid, hit rate, abstention, calibration, counts — comes entirely from the CSV and is fully reprocessed. Pass a freshly fetched ForwardPrices for “SPY” to get the market-adjusted grid recomputed too.

Parameters:
  • csv_path (Path)

  • out_dir (Path)

  • seed (int)

  • fetch_dates (dict[str, str] | None)

  • shortfalls (Sequence[SampleShortfall])

  • dry_run (bool)

  • spy_prices (ForwardPrices | None)

Return type:

RunReport