Backtesting¶
This guide covers how backtests are constructed, how the engine consumes data, and how results are produced.
Backtest Pipeline¶
- Load config and build data source, execution, risk, and regime modules.
- Create iterators over bars, ticks, and order books.
- Load data into the engine.
- Initialize strategy with
StrategyContext. - Run the event loop and produce fills and portfolio updates.
- Emit results including equity curve and performance metrics.
Backtest Engine Lifecycle¶
engine::EngineFactory::createbuilds aBacktestEnginefrom a config object.BacktestEngine::configure_executionwires slippage, commission, market impact, and latency.BacktestEngine::configure_risksets limits and stop-loss rules.BacktestEngine::configure_regimeconstructs the detector from config.BacktestRunner::runbinds a strategy, loads data iterators, and executes the loop.
Execution Realism¶
Backtests now support resting-order simulation inside the execution pipeline:
- limit, stop, and stop-limit orders can remain active after submission
- these orders are re-evaluated on later bar, tick, quote, and order-book events
- fills are generated only when later market data crosses the order conditions
- marketable orders still fill immediately when the current market supports execution
- bar-only backtests can now choose
close_only,open_only, or syntheticintrabar_ohlcreplay
This is now a materially stronger exchange-style replay foundation. The simulator can vary execution timing and executable prices by venue, react to dynamic halt/resume events, and enforce simple calendar closures. It is still not a full exchange matching engine, but execution outcomes are no longer only post-trade analytics layered onto a single venue-neutral path.
Bar Replay Modes¶
When the dataset is primarily OHLC bars, execution.simulation.bar_mode controls how a bar is exposed to the execution pipeline:
close_only(default): only the bar close is used. This matches the previous behavior.open_only: only the bar open is used. This is useful for strategies that trade on the next bar open.intrabar_ohlc: the bar is replayed as a deterministic synthetic path. Bullish and flat bars useopen -> low -> high -> close; bearish bars useopen -> high -> low -> close.execution.simulation.tick_modechooses whether execution follows synthetic bar replay (synthetic_ticks) or prefers real tick/quote/book events (real_ticks).execution.simulation.synthetic_tick_profilechooses the synthetic execution-tick path when bars are driving execution:bar_close,bar_open, orohlc_4tick.
ohlc_4tick is still an approximation, but it lets resting stop and limit orders react to the full bar range instead of only the close. In real_ticks mode, once a symbol has seen real tick-like market data, later bars stop driving execution for that symbol and are used only for analytics and mark-to-market.
Fill Policies And Requotes¶
Backtests can now apply broker-style execution policy controls through execution.policy:
fillsets the default behavior for Day/GTC orders when the order itself does not already requestIOCorFOK.max_deviation_bpssets the allowed adverse move between the requested price and the eventual executable price.price_drift_actionchooses whether an out-of-band move is ignored, rejected, or treated as a requote.
This is especially useful for stop-triggered orders, where a bar or tick can cross the stop and then gap through the expected entry price.
Latency is now modeled as an actual activation window for execution:
- an order submitted at
t0withexecution.latency.ms > 0does not become executable untilt0 + latency - price-drift rules compare the requested price at submission with the executable price when the activation window opens
This makes requotes and rejects reflect delayed execution instead of only changing the emitted fill timestamp.
For resting limit orders, execution.queue.* can model queue ahead at the touch. That lets the simulator distinguish between:
- maker fills after the order has waited through visible queue
- taker fills when price moves through the limit before the queue clears
execution.queue.depth_mode also lets the queue model use only the top visible level, the exact resting price level, or the full visible same-side depth ahead of the order.
The queue model now also supports deeper persistent behavior:
execution.queue.aging_fractionreduces queue ahead when the market moves away from the order, which approximates cancellations ahead while your order keeps restingexecution.queue.replenishment_fractionadds queue back ahead when visible same-price liquidity builds after the order is already resting
This makes the queue state path-dependent instead of only decreasing monotonically on touch events.
Session-Aware Execution¶
The execution simulator can now enforce a configurable session window:
execution.session.enabledexecution.session.start_hhmmexecution.session.end_hhmmexecution.session.open_auction_minutesexecution.session.close_auction_minutesexecution.session.weekdaysexecution.session.closed_datesexecution.session.haltexecution.session.halted_symbols
When session gating is enabled:
- fills are deferred until the configured session is open
MarketOnOpenorders only fill during the opening auction windowMarketOnCloseorders only fill during the closing auction window- configured holidays and disallowed weekdays block execution while preserving resting orders
- halted symbols remain non-executable until the halt is removed from config or a runtime resume event arrives
Market data, strategy logic, and valuation still continue to run. The gate affects execution eligibility, not data replay.
Venue routing can also push venue-level execution state into the simulator:
execution.routing.venues[].price_adjustment_bpsshifts the executable price per venueexecution.routing.venues[].latency_msoverrides the global latency for routed child orders on that venue
This means split child orders can now differ by venue in both timing and execution price, not only in post-fill attribution.
Account Margin Scaffolding¶
The backtest portfolio now derives account-level margin state from execution.account.margin.* (or execution.margin.* when you are already passing the execution block directly):
initial_margin_ratiomaintenance_margin_ratiostop_out_margin_level
These settings do not force liquidation yet. They currently provide deterministic account state for:
Portfolio::margin_snapshot()PortfolioSnapshothistoryengine.portfolio.equity_curve()in Python, which now includes margin columns such asbuying_powerandmargin_excess
Margin Enforcement¶
The backtest engine now supports configurable enforcement under execution.account.enforcement.*:
enabledmargin_call_action:ignore,cancel_open_orders, orhalt_tradingstop_out_action:none,liquidate_all, orliquidate_worst_firsthalt_after_stop_out
When maintenance margin is breached, the engine can cancel resting orders or halt new order entry. When the stop-out threshold is breached, the engine can:
- flatten every open position immediately, or
- liquidate the worst unrealized PnL positions first and stop once the account is back above the stop-out threshold
This liquidation is deterministic and uses the portfolio’s latest mark price. It is designed for backtest realism and auditability, not exchange-level liquidation-book simulation.
Financing And Borrow¶
Daily carry is now applied on trading-day transitions under execution.account.financing.*:
enabledlong_rate_bps_per_dayshort_borrow_bps_per_day
At the first event of a new trading day, the engine debits or credits cash based on the prior day’s held positions and their latest marked prices. Long financing rates can be positive (cost) or negative (credit). Short borrow rates are typically positive costs.
Venue Diagnostics¶
Backtest results now include venue-aware execution diagnostics derived from fill metadata:
- per-venue fill counts
- per-venue split-parent attribution
- commission and transaction-cost decomposition
- maker-fill ratio
- weighted average slippage in basis points
In Python, use:
results.venue_fill_summary()results.account_curve()results.account_state()
This makes multi-venue routing analysis available directly from the result object instead of requiring custom post-processing.
For a single runnable example that combines these controls in one place, see examples/python_execution_realism/.
For a compiled C++ example focused on calendar closures and runtime halt/resume control, run ./build/bin/regimeflow_backtest_controls_demo and see examples/backtest_controls_demo/README.md.
Configuration Modes¶
RegimeFlow supports two config formats:
- C++ Engine Config: nested
engine,data,strategy,risk,execution,regime,plugins. - Python BacktestConfig: flat keys used by the Python bindings and CLI.
For both, see reference/configuration.md.
Selecting The Strategy¶
- Built-in strategies are registered in
strategy::StrategyFactory. - Plugins can provide additional strategies.
- Python backtests can use a
module:Classstrategy viaregimeflow.cli.
Example (built-in):
.venv/bin/python -m regimeflow.cli \
--config quickstart.yaml \
--strategy moving_average_cross
Example (custom Python):
.venv/bin/python -m regimeflow.cli \
--config quickstart.yaml \
--strategy my_strategies:MyStrategy
Backtest Results¶
BacktestResults exposes:
- Equity curve and portfolio snapshots.
- Trades and fills.
- Performance metrics and report exports.
Performance And Regime Metrics¶
The report pipeline now uses a consistent metric definition across top-level and regime-aware summaries:
- portfolio Sharpe, Sortino, Calmar, max drawdown, win rate, and profit factor follow the same definitions in both
performanceandperformance_summary - intraday annualization is inferred from snapshot timestamps instead of assuming daily bars
- regime return is compounded inside each regime segment
- regime time share is duration-weighted
- transition return is compounded across each transition window before averaging by transition pair
This avoids the older mismatch where intraday runs could annualize differently across report sections, and it prevents regime exposure from being distorted by uneven event spacing.
In Python, you can export these directly:
results = engine.run("moving_average_cross")
results.report_json()
results.report_csv()
results.equity_curve()
results.trades()
For native C++ tester surfaces, BacktestEngine now also exposes:
dashboard_snapshot()dashboard_snapshot_json()dashboard_terminal()
These power terminal dashboards and direct snapshot export without waiting for final BacktestResults.
The regimeflow_strategy_tester example tool layers on top of these APIs and supports:
- live terminal refresh,
- a split-pane TUI layout,
- tab-specific rendering,
- non-blocking keyboard-driven tab switching,
- file-based JSON snapshot export for external pollers.
The interactive browser dashboard is provided by the Python visualization package. The C++ toolchain provides terminal dashboards, tester-style TUI output, and snapshot export, but not the web dashboard itself.
Parallel Backtests¶
BacktestEngine::run_parallel can run multiple parameter sets. Use this for grid searches or random sweeps, and combine with walk-forward optimization for robust selection.
See guide/walkforward.md for the optimizer flow.
The Python strategy tester dashboard now detects walk-forward results and adds an Optimization view automatically. That view includes:
- window-by-window in-sample vs out-of-sample fitness,
- parameter evolution across windows,
- parameter stability scores,
- overfitting diagnostics from the optimizer.