Data flow
Both modes share the same conceptual pipeline; only the last stage differs (simulate vs. execute).
Backtest
SymbolScanner.scan(candidates) # marketdata -> scan signals
│ universe
▼
MarketDataClient.get_bars(universe, tf, start, end) # {symbol: OHLCV}
│
▼ per symbol
Strategy.process_data(bars) # + indicator columns
Strategy.generate_signals(processed) # {timestamp: BUY/SELL/HOLD/...}
│
▼
BacktestEngine._simulate_symbol(...) # bar-by-bar fills, stop/take/exit
│ trades
▼
analytics.performance.compute_backtest_metrics(...) # -> metrics dict
analytics.reporting.log_backtest_report(...) # -> text
The engine carries realised cash across symbols so a run can't spend the same dollar twice, and force-closes any open position at the final bar.
Live
LiveEngine.start(universe)
├── _warm_up: get_bars(lookback) -> process_data -> strategy.warm_up(buffer)
└── MarketDataClient.stream(universe, on_bar) # auto-reconnecting bar stream
│ BarEvent (full OHLCV) per bar
▼
Strategy.process_bar(symbol, {o,h,l,c,v}, ts) # -> signal
│ actionable signal
▼
LiveTrader.handle_signal(symbol, signal, price)
├── entry: PositionSizer.size(...) -> submit_bracket_order
└── exit: Broker.close_position
│
▼
AlpacaBroker -> Alpaca SDK
The stream feeds the strategy the full streamed OHLCV bar (not just the close),
and the PositionSizer is pluggable — RiskBasedSizer by default, or
PortfolioWeightSizer when running live --portfolio. See
Portfolio manager.
Why the pipeline is shared
process_data and generate_signals are identical in both modes — the strategy
doesn't know whether it's being backtested or traded live. The difference is only
what consumes the signal: a simulator (engine) or a broker (execution). This is
the payoff of separating concerns.