trader.simulation.step module¶
The shadow-portfolio step (issue #33, Slice 3e Part 4).
A forward simulator that consumes the decision already written to `decisions` by the real pipeline, so every configured strategy — shadow or trading — becomes measurable at a cost of zero extra model calls. Full rationale: docs/superpowers/specs/2026-08-11-shadow-portfolio-amended-design.md.
The isolation boundary, structural rather than conventional:
This module never places an order. Nothing here imports brokers/ or execution/. It takes bars, decisions, and a repository — no broker, no OrderExecutor.
Simulated money never shares a table with real money. No flag on trades or round_trips marks a row as simulated; the five sim_* tables are the only place simulated state exists, so a query over real money is incapable of counting a simulated fill by construction.
A simulator failure must never stop the real cycle — enforced by the caller (pipeline/run_once.py), which wraps advance() in its own try/except, the same belt-and-braces discipline discover_symbols() gets from cli/main.py.
Always-be-Buying (AbB), issue #42. Every configured strategy also gets a second, separately identified portfolio ({strategy_id}::abb, ABB_PORTFOLIO_SUFFIX in persistence/simulation.py) that reads the exact same recorded decisions but with disable_sell_signals=True: a sell decision is ignored outright, so only the trailing stop can ever close a position. It answers “how much of this strategy’s return is the sell side costing” in isolation from whether its buy/entry side is any good — see SimulationRunner.run(), which steps both portfolios per strategy, each in its own try/except.
- trader.simulation.step.advance(*, strategy_id, now, bar_repository, decision_repository, simulation_repository, settings, pipeline_config, discovery_settings, fixed_symbols, portfolio_key=None, disable_sell_signals=False)[source]¶
Step strategy_id’s simulated portfolio through every completed, not-yet-applied bar of settings.sim_interval.
portfolio_key names the SimPortfolio row this call reads and writes; it defaults to strategy_id when left unset, which is every caller before issue #42. Decisions are always read using the real strategy_id — only the portfolio/account identity changes — which is how the Always-be-Buying (AbB) variant gets its own isolated ledger from the same recorded decisions a strategy’s normal portfolio already reads.
disable_sell_signals (issue #42) makes a recorded signal_action == “sell” a no-op for this call, as if it were unreadable — no round trip, no cash or position change — so only the trailing-stop test can ever close a position. It does not touch the stop itself.
Idempotent: a bar already recorded in SimPortfolio.last_bar_ts is never reapplied, and SimEquitySnapshot’s own (portfolio_id, bar_ts) unique constraint is the second, independent guard against the same thing. Never raises past this function for anything that can be isolated to one symbol or one bar — the caller’s own try/except is the last resort, not the first.
- Parameters:
strategy_id (str)
now (datetime)
bar_repository (BarRepository)
decision_repository (DecisionRepository)
simulation_repository (SimulationRepository)
settings (SimulationSettings)
pipeline_config (PipelineConfig)
discovery_settings (DiscoverySettings)
fixed_symbols (Collection[str])
portfolio_key (str | None)
disable_sell_signals (bool)
- Return type:
None