This is the reference for how orders execute in ml4t-backtest. Every behavioral detail described here corresponds to a named config parameter, so you can tune or override it.
Orders submitted during on_data() are queued and filled at the next bar's open price.
Bar N: Strategy sees close=$100, submits buy order
Bar N+1: Order fills at open=$101
This is the realistic model -- your strategy decides based on today's information and the trade executes at tomorrow's opening auction.
from ml4t.backtest.config import BacktestConfig
from ml4t.backtest.types import ExecutionMode
config = BacktestConfig(execution_mode=ExecutionMode.NEXT_BAR) # defaultOrderType.MOC is the exception. In NEXT_BAR mode, MOC orders submitted during
on_data() still fill on the current bar, at the close, after strategy logic runs.
Orders fill at the current bar's close price, in the same bar they are submitted.
Bar N: Strategy sees close=$100, submits buy order, fills at close=$100
This mode is useful for comparing against vectorized frameworks (VectorBT) where signals and fills happen simultaneously. It carries look-ahead risk for production strategies because the strategy can "see" the close before deciding to trade at the close.
config = BacktestConfig(execution_mode=ExecutionMode.SAME_BAR)The execution_price parameter controls which price source is used for market order fills:
| Value | Fill Price | Typical Use |
|---|---|---|
OPEN |
Next bar's open | Default, realistic |
CLOSE |
Current bar's close | VectorBT comparison |
VWAP |
Volume-weighted average | Requires volume data |
MID |
(high + low) / 2 | Simple approximation |
PRICE |
FeedSpec.price_col / bar["price"] |
Custom reference price, derived bars |
BID |
Best bid quote | Passive or conservative sell-side marking |
ASK |
Best ask quote | Aggressive buy-side fills |
QUOTE_MID |
Quote midpoint | Microstructure-aware marking |
QUOTE_SIDE |
Ask for buys, bid for sells | Side-aware market execution |
PRICE is the default mark source and follows your feed schema. If you map price_col="mid_price", then both bar["price"] and ExecutionPrice.PRICE use that midpoint.
OrderType.MOC does not use execution_price; it always fills at the current bar's
close.
Open positions are marked independently of how market orders fill. mark_price uses the same ExecutionPrice enum as execution_price.
This is useful when you want to:
- fill orders at
QUOTE_SIDEbut mark inventory atQUOTE_MID - trade from a synthetic
price_colwhile keeping fills atOPENorCLOSE - mark long inventory conservatively on the bid and short inventory on the ask via
QUOTE_SIDE
from ml4t.backtest.config import ExecutionPrice
config = BacktestConfig(
execution_price=ExecutionPrice.QUOTE_SIDE,
mark_price=ExecutionPrice.QUOTE_MID,
)If the requested quote field is unavailable, the broker falls back to the feed reference price and then to OHLC where applicable.
When you provide bid/ask data and enable quote-aware execution or marking, the backtest is quote-aware, not just OHLCV-aware.
That affects both execution and reporting:
- fills preserve the quote source and nullable quote context used for execution
- trades preserve entry and exit quote summaries
- portfolio state reflects the configured
mark_price
This makes quote-side behavior auditable after the run instead of burying it in aggregate PnL only.
When multiple orders are pending on the same bar, the engine must decide the processing sequence. This matters because fills affect cash, which affects whether subsequent orders can be accepted.
All exits process first (freeing cash), then all entries:
1. Process all exit orders → cash freed
2. Mark-to-market remaining positions
3. Process all entry orders → gatekeeper checks cash
This is the most capital-efficient ordering. Matches VectorBT's call_seq='auto'.
Orders process in submission order. Each order's gatekeeper check sees cash from all prior fills:
1. First submitted order fills → cash updated
2. Second submitted order fills → cash updated
3. ...
Matches Backtrader's submission-order processing.
Orders process in submission order (typically alphabetical by asset) without exit/entry separation. Unlike EXIT_FIRST, exits do not pre-free cash for later entries.
Matches LEAN's per-order sequential buying-power model.
from ml4t.backtest.config import FillOrdering
config = BacktestConfig(fill_ordering=FillOrdering.EXIT_FIRST) # default
config = BacktestConfig(fill_ordering=FillOrdering.FIFO)
config = BacktestConfig(fill_ordering=FillOrdering.SEQUENTIAL)When using EXIT_FIRST, entries are processed after exits. The entry_order_priority controls the sequence of entry orders:
| Value | Behavior |
|---|---|
SUBMISSION |
Keep strategy submission order (default) |
NOTIONAL_DESC |
Larger dollar entries first |
NOTIONAL_ASC |
Smaller dollar entries first |
Position rules (StopLoss, TakeProfit, TrailingStop) are evaluated on every bar using OHLC data. Quote-aware execution changes market fills and position marking, but stop triggers still evaluate against bar data. The key question is: at what price does a triggered stop fill?
The stop_fill_mode parameter controls stop/take-profit fill prices:
| Mode | Fill Price | Use Case |
|---|---|---|
STOP_PRICE |
Exact stop/target level | Default, standard model |
CLOSE_PRICE |
Bar's close price | VectorBT with close-only data |
BAR_EXTREME |
Bar's low (stop) or high (TP) | Conservative/optimistic model |
NEXT_BAR_OPEN |
Next bar's open price | Zipline-style deferred exits |
Gap handling. If the bar opens beyond the stop level (a gap through the stop), the fill price is the bar's open, not the stop price. This accurately models gap risk:
Stop set at $95.00
Bar opens at $93.00 (gap down)
Fill price = $93.00 (open), not $95.00 (stop)
The stop_level_basis controls what price the stop percentage is calculated from:
| Value | Base Price | Use Case |
|---|---|---|
FILL_PRICE |
Actual fill price including slippage | Default, most frameworks |
SIGNAL_PRICE |
Close price when signal was generated | Backtrader behavior |
Example: You set StopLoss(pct=0.05). With FILL_PRICE, the stop is 5% below where you actually got filled. With SIGNAL_PRICE, it's 5% below the close of the bar where you submitted the order.
Trailing stops track a "water mark" -- the highest price since entry (for longs) or lowest price since entry (for shorts) -- and exit when price retraces by a percentage from that mark.
trail_hwm_source controls which price updates the water mark:
| Value | Update Price | Framework |
|---|---|---|
CLOSE |
Bar's close | Default, most frameworks |
BAR_EXTREME |
Bar's high (longs) / low (shorts) | VectorBT Pro with OHLC |
initial_hwm_source controls the water mark on the entry bar:
| Value | Initial HWM | Framework |
|---|---|---|
FILL_PRICE |
Actual fill price | Default, event-driven frameworks |
BAR_CLOSE |
Entry bar's close | |
BAR_HIGH |
Entry bar's high | VectorBT Pro with OHLC |
trail_stop_timing controls when water marks update relative to the stop check. This is the subtlest parameter and the one that causes the most divergence between frameworks.
LAGGED (default): Check stop using the previous bar's water mark, then update water mark at end of current bar. This creates a 1-bar lag -- the stop can't trigger based on a new high set in the current bar.
INTRABAR: Update water mark before checking. The stop can trigger based on the current bar's extreme. More aggressive than LAGGED.
VBT_PRO: Two-pass algorithm matching VectorBT Pro exactly:
- Pass 1: Check stop using the previous bar's water mark against the current bar's HIGH (long) or LOW (short)
- If pass 1 doesn't trigger, update the water mark from the current bar's extreme
- Pass 2: Check stop using the updated water mark against CLOSE only (not HIGH/LOW)
This precisely reproduces VBT Pro's can_use_ohlc=False behavior in the second pass.
from ml4t.backtest.config import TrailStopTiming, WaterMarkSource, InitialHwmSource
# VectorBT Pro compatible trailing stops
config = BacktestConfig(
trail_stop_timing=TrailStopTiming.VBT_PRO,
trail_hwm_source=WaterMarkSource.BAR_EXTREME,
initial_hwm_source=InitialHwmSource.BAR_HIGH,
)Every fill records the price source that was used along with nullable quote context:
price_sourcereference_pricequote_mid_pricebid_priceask_pricespreadbid_sizeask_sizeavailable_size
That data is available both in memory and in result.to_fills_dataframe() / fills.parquet, which makes it possible to audit quote-side behavior after the run.
Trade summaries preserve the same context at entry and exit, and
result.to_portfolio_state_dataframe() reflects the configured mark source for
each end-of-bar snapshot.
| Type | Calculation | Config |
|---|---|---|
NONE |
No commission | commission_rate=0 |
PERCENTAGE |
% of trade value | commission_rate=0.001 (0.1%) |
PER_SHARE |
Fixed $ per share | commission_per_share=0.005 |
PER_TRADE |
Fixed $ per trade | commission_per_trade=5.0 |
TIERED |
Volume-based tiers | Custom model |
from ml4t.backtest.config import CommissionType
# Interactive Brokers style
config = BacktestConfig(
commission_type=CommissionType.PER_SHARE,
commission_per_share=0.005,
commission_minimum=1.0,
)| Type | Calculation | Config |
|---|---|---|
NONE |
No slippage | |
PERCENTAGE |
% of price | slippage_rate=0.001 (0.1%) |
FIXED |
Fixed $ per share | slippage_fixed=0.01 |
VOLUME_BASED |
Size vs volume | slippage_rate=0.1 (10% volume limit) |
Slippage models remain separate from quote-side execution:
QUOTE_SIDEcrosses the observed spread using bid/ask quotes- slippage adds an extra synthetic execution penalty on top of the chosen source
This lets you model spread and market impact separately.
Stop orders can have additional slippage via stop_slippage_rate:
config = BacktestConfig(
slippage_rate=0.001, # 0.1% for market orders
stop_slippage_rate=0.001, # Additional 0.1% for stop exits
)The settlement_delay parameter simulates T+N settlement:
config = BacktestConfig(
settlement_delay=2, # T+2 (US equities standard)
settlement_reduces_buying_power=True, # Unsettled cash not spendable
)With T+2 settlement, cash from selling shares on Monday isn't available for buying until Wednesday.
| Parameter | Default | Purpose |
|---|---|---|
initial_cash |
100,000 | Starting cash |
cash_buffer_pct |
0.0 | Reserve this % of cash (never invest it) |
reject_on_insufficient_cash |
True | Reject orders that exceed buying power |
skip_cash_validation |
False | Bypass gatekeeper entirely (Zipline-style) |
buying_power_reservation |
False | Reserve cash at submission time (LEAN-style) |
from ml4t.backtest.config import ShareType
# Allow fractional shares (crypto, some brokers)
config = BacktestConfig(share_type=ShareType.FRACTIONAL)
# Round down to whole shares (most equity brokers)
config = BacktestConfig(share_type=ShareType.INTEGER)The Machine Learning for Trading book demonstrates execution semantics across chapters:
- Ch16 / NB11 (
engine_divergence_anatomy) — detailed analysis of how SAME_BAR vs NEXT_BAR and fill ordering affect backtest results - Ch18 (
portfolio_construction) — LinearImpact and SquareRootImpact market impact models with VolumeParticipationLimit - Ch16 case studies — each case study uses setup.yaml to configure commission_rate, slippage_rate, and execution_mode
- Book Guide -- chapter and case-study map for execution workflows
- Configuration -- complete reference for all 40+ parameters
- Profiles -- pre-configured settings for each framework
- Risk Management -- position rules and portfolio limits