Market Impact & Execution Costs¶
The costs and funding tutorial reconciles executed examples across all four cash-flow sources.
Realistic backtesting requires modeling the costs of executing trades. ml4t-backtest provides three layers of cost modeling: commission, slippage, and market impact.
Individual cost-model snippets assume a configured backtest and, where shown, an existing result. The linked costs tutorial runs complete gross-to-net comparisons.
Cost Layers¶
| Layer | What It Models | Config |
|---|---|---|
| Commission | Broker fees | commission_type, commission_rate |
| Slippage | Spread crossing or synthetic execution drag | slippage_type, slippage_rate |
| Market impact | Price movement from your order | market_impact_model= kwarg |
Commission and slippage are configured via BacktestConfig. Market impact is an optional model passed to the Engine.
Commission Models¶
Percentage (Default)¶
from ml4t.backtest import BacktestConfig
config = BacktestConfig(
commission_rate=0.001, # 10 bps per trade
)
Per-Share¶
from ml4t.backtest import BacktestConfig, CommissionType
config = BacktestConfig(
commission_type=CommissionType.PER_SHARE,
commission_per_share=0.005, # $0.005 per share
commission_minimum=1.0, # $1 minimum per trade
)
Per-Contract (Futures)¶
config = BacktestConfig(
commission_type=CommissionType.PER_CONTRACT,
commission_per_share=2.50, # $2.50 per contract
)
PER_CONTRACT is an alias for PER_SHARE - same math, clearer intent for futures.
Custom Models¶
For volume-tiered or combined commission structures, use model objects:
from ml4t.backtest.models import TieredCommission, CombinedCommission
# Volume-tiered (Interactive Brokers style)
tiered = TieredCommission(tiers=[
(300, 0.0035), # First 300 shares: $0.0035/share
(3000, 0.0020), # 301-3000: $0.0020/share
(float('inf'), 0.0015), # 3001+: $0.0015/share
])
# Combined (base + percentage)
combined = CombinedCommission(
fixed=1.0, # $1 base
percentage=0.0005, # Plus 5 bps of notional
)
A custom commission model implements calculate(asset, quantity, price) and
returns the fee for that quantity. The engine may call it before execution to
estimate cash or margin requirements, then call it at the actual fill price.
Only the fill-time value is charged. Estimates use a deep copy of the model,
so a model that advances an internal volume tier on an executed fill does not
advance it for a rejected or unfilled estimate. Custom models must support
deepcopy, and calculate must not have external effects such as writing to a
database or shared counter. A single fill that closes a position and opens
its opposite is charged once for its full quantity. The fee is allocated between the closing
and opening trade records in proportion to their quantities. Partial fills are
charged separately when they execute.
Slippage Models¶
Slippage adjusts the configured execution price in the adverse direction. Use the spread model for a bar-only estimate of bid-ask crossing, or a percentage or fixed amount for other execution drag.
Percentage (Default)¶
config = BacktestConfig(
slippage_rate=0.001, # 10 bps for market orders
stop_slippage_rate=0.001, # Additional 10 bps for stop exits
)
Stop exits can have additional slippage because stops trigger during fast markets.
Fixed¶
from ml4t.backtest.config import SlippageType
config = BacktestConfig(
slippage_type=SlippageType.FIXED,
slippage_fixed=0.01, # $0.01 per share
)
Spread¶
Use this when you only have bars and want to approximate bid-ask crossing in currency units.
from ml4t.backtest.config import SlippageType, SpreadConvention
config = BacktestConfig(
slippage_type=SlippageType.SPREAD,
slippage_spread=0.02, # $0.02 quoted spread
slippage_spread_convention=SpreadConvention.FULL_SPREAD,
)
FULL_SPREAD means the configured value is the quoted bid-ask spread, so the
engine applies half-spread per side. HALF_SPREAD means the configured value is
already the per-side crossing cost.
For asset-specific assumptions:
config = BacktestConfig(
slippage_type=SlippageType.SPREAD,
slippage_spread=0.02,
slippage_spread_by_asset={"AAPL": 0.01, "MSFT": 0.015},
slippage_spread_convention=SpreadConvention.HALF_SPREAD,
)
If you already have bid/ask quotes, prefer execution_price=QUOTE_SIDE and keep
synthetic spread slippage disabled unless you explicitly want additional impact.
Market Impact Models¶
Market impact models add an adverse price adjustment that depends on order size relative to reported volume. Calibrate the model parameters for the market and bar frequency you simulate.
Import from ml4t.backtest.execution:
No Impact (Default)¶
Linear Impact¶
Price impact proportional to order size relative to bar volume:
Here \(P\) is the reference price, \(Q\) is order quantity, \(V\) is the bar volume, and \(\eta\) is the configured coefficient. \(\Delta P\) is added for buys and subtracted for sells.
from ml4t.backtest.execution import LinearImpact
engine = Engine(
feed, strategy, config,
market_impact_model=LinearImpact(coefficient=0.1),
)
An order that is 10% of bar volume with coefficient=0.1 moves the fill price by 1%.
Impact from every model here is entirely temporary: calculate is handed one order and holds no
reference to earlier slices of the same parent, so there is nothing for a permanent component to
persist into. LinearImpact accepted a permanent_fraction argument through 0.1.6 and never read
it, so a caller asking for a mostly permanent model got a fully temporary one and no warning. The
argument is removed rather than defaulted, so the request now raises TypeError instead of being
answered wrongly. Model persistence outside the engine if you need it.
Square-Root Impact¶
This model scales the price adjustment with the square root of estimated daily-volume participation:
Here \(\sigma\) is the model's configured daily volatility and \(a\) is
adv_factor, the configured multiplier that converts bar volume into an
estimated average daily volume. The defaults are \(\sigma=0.02\) and \(a=1.0\).
The model does not estimate either value from the feed.
from ml4t.backtest.execution import SquareRootImpact
engine = Engine(
feed, strategy, config,
market_impact_model=SquareRootImpact(coefficient=0.5),
)
Both impact models return zero adjustment when bar volume is missing or zero. Calibrate their coefficients against observed execution costs before using them for performance estimates.
Volume Participation Limits¶
Prevent orders from consuming too much bar volume:
from ml4t.backtest.execution import VolumeParticipationLimit
engine = Engine(
feed, strategy, config,
execution_limits=VolumeParticipationLimit(max_participation=0.10),
)
Orders exceeding 10% of bar volume are partially filled (the remainder stays pending).
Perpetual Futures Funding¶
Pass a Polars frame to Engine(..., funding_df=funding). Each row names a feed
timestamp and asset, with either rate or amount_per_unit:
from datetime import datetime
import polars as pl
funding = pl.DataFrame({
"timestamp": [datetime(2024, 1, 2, 8)],
"asset": ["BTC-PERP"],
"rate": [0.0001],
})
result = Engine(feed=feed, strategy=strategy, config=config,
funding_df=funding).run()
payments = result.to_funding_dataframe()
print(payments.select("timestamp", "asset", "cash_delta"))
print(result.metrics["total_funding"])
A positive rate debits a long and credits a short. For a held position, the
cash transfer is -quantity * latest_price * contract_multiplier * rate.
Alternatively, amount_per_unit gives an account-currency amount per unit of
underlying, multiplied by position quantity and contract multiplier. Negative
values reverse the direction. Each row must provide exactly one of the two.
Funding is applied after the bar's reference price becomes available and before orders eligible at that timestamp or the strategy callback run. A position opened at that timestamp does not pay that event. If the asset has no bar at the event, the latest earlier positive reference price is used. Events must match feed timestamps and known assets; duplicate, missing, or nonfinite values raise before the run. A rate event for a held position without a causal price raises before any payment at that timestamp changes cash.
Funding is a separate cash flow, not a fill or trading fee. The result includes
funding.parquet, to_funding_dataframe(), total_funding, and
num_funding_events. Trading P&L
and costs retain their existing definitions; terminal equity includes funding
in addition to trading P&L. A scheduled event for a flat asset records zero
cash transfer.
Cost Impact Analysis¶
To measure cost impact, run the same strategy with and without costs:
# Full costs
config_real = BacktestConfig(
commission_rate=0.002,
slippage_rate=0.002,
)
# Zero costs
config_zero = BacktestConfig(
commission_rate=0.0,
slippage_rate=0.0,
)
result_real = Engine(feed, strategy, config_real).run()
result_zero = Engine(feed2, strategy2, config_zero).run()
cost_drag = result_zero.metrics['total_return_pct'] - result_real.metrics['total_return_pct']
print(f"Cost drag: {cost_drag:.2f}%")
In the book¶
Chapter 18, Section 18.4, Market impact calibration examines how execution size changes impact. Gross versus net performance shows the portfolio effect of those costs.
Next Steps¶
- Book Guide -- where cost realism and quote-aware execution appear in the book
- Execution Semantics - fill timing, ordering, and stop modes
- Configuration - all commission and slippage parameters
- Rebalancing - how costs interact with weight-based rebalancing