◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / Modeling Commission and Slippage in Python Backtests
Python

Modeling Commission and Slippage in Python Backtests

A backtest run with zero trading costs almost always overstates its results, because commissions and slippage quietly eat into every fill. This article shows exactly how to set commission and slippage in backtrader (setcommission, set_slippage_perc) and vectorbt (fees, slippage), and why getting the units right matters. It is educational material about backtest realism, not financial advice or any claim about profitability.

What does modeling commission and slippage actually mean?

Modeling commission and slippage means subtracting realistic trading costs from every simulated fill so the backtest resembles live execution instead of an idealized world. Commission is the fee a broker or exchange charges per trade (often a percentage of notional, sometimes a fixed amount). Slippage is the gap between the price your signal saw and the price you actually get filled at, caused by spread, latency, and market impact. A zero-cost backtest silently assumes both are zero, which is never true.

The reason this matters so much is that costs compound with turnover. A strategy that trades once a month barely notices a few basis points; a strategy that trades several times a day pays that cost hundreds of times. Two backtests with identical entry and exit logic can look completely different once realistic costs are applied — and the difference is largest for exactly the high-frequency, tight-margin strategies that look most attractive on paper. This is one of the most common reasons a backtest fails to survive contact with a live account, a theme covered in [why most backtests fail](/learn/why-most-backtests-fail).

The rest of this article is concrete: how to set these numbers in the two most common Python backtesting libraries, backtrader and vectorbt, and the unit traps that make people accidentally model 100x too much or too little cost.

Why a zero-cost backtest overstates results

The edge in many strategies lives inside the spread. Consider a mean-reversion system that buys when price dips and sells when it recovers. If the average round-trip move it captures is 20 basis points, and the effective spread plus commission is 8 basis points per round trip, then 40% of the gross edge is gone before you account for anything else. Model costs as zero and that 8 bps vanishes from the report — inflating every performance metric that depends on it.

The distortion is not uniform. It falls hardest on:

  • High-turnover strategies, where costs are paid many times.
  • Strategies whose signals cluster in volatile or illiquid conditions, where real slippage is worst.
  • Strategies optimized on a cost-free objective, which tend to select for lots of small, frequent trades — precisely the pattern costs punish.

That last point connects cost modeling to overfitting. If you tune parameters against a zero-cost backtest, the optimizer is free to discover a high-churn configuration that would be unprofitable the moment costs are added. Curve-fitting to a frictionless market is still curve-fitting — see [overfitting and curve-fitting explained](/learn/overfitting-curve-fitting-explained). Adding realistic costs before you optimize is one of the cheapest defenses against selecting a strategy that only works when trading is free. None of this implies any particular strategy is or is not profitable; the point is only that omitting costs biases the estimate upward.

How do you set commission and slippage in backtrader?

backtrader is event-driven: the broker charges commission and applies slippage as each order fills. Commission is set on the broker with setcommission, and here the classic footgun is the percabs argument. When commtype is percentage-based, percabs=True means you are passing 0.001 to mean 0.1%; percabs=False means you are passing the number as a literal percentage (0.1 means 0.1%). Mixing these up produces a 100x error in either direction.

import backtrader as bt

cerebro = bt.Cerebro()

# 0.1% commission per side. percabs=True => 0.001 is read as 0.1%,
# NOT as 0.001%. Be explicit about commtype and percabs.
cerebro.broker.setcommission(
    commission=0.001,
    commtype=bt.CommInfoBase.COMM_PERC,
    percabs=True,
)

# 0.05% slippage applied to every fill price (buys slip up, sells slip down).
cerebro.broker.set_slippage_perc(perc=0.0005)

set_slippage_perc(perc=0.0005) moves each fill 0.05% against you — buys fill higher, sells fill lower — which is the realistic direction. If you prefer a fixed tick amount instead, set_slippage_fixed(fixed=...) is available, but note that when slip_perc is non-zero it takes precedence. Because backtrader fills on the next bar after your signal by default, it does not by itself create look-ahead; the cost model layers cleanly on top of correct bar timing. For a deeper treatment of that timing, see [backtrader line indexing explained](/learn/backtrader-indexing-explained).

How do you set fees and slippage in vectorbt?

vectorbt is vectorized: instead of an event loop, you pass fees and slippage directly to Portfolio.from_signals (and the other from_* constructors), and they apply to the whole simulated array at once. Both are fractions, so 0.001 means 0.1%. There is no percabs-style ambiguity here, but there is a different, more dangerous trap: signal timing.

In vectorbt, from_signals fills at the price of the bar where the signal is True. If you compute an entry condition on a bar's close and pass that boolean as the entry on the same bar, you are assuming you could act on information from the bar you are still inside — a look-ahead bug. The fix is to shift entries and exits forward by one bar so you act on the next bar's price.

import vectorbt as vbt

price = vbt.YFData.download('BTC-USD').get('Close')

fast = price.rolling(10).mean()
slow = price.rolling(30).mean()

raw_entries = fast > slow
raw_exits = fast < slow

# Shift by 1: a signal computed from this bar's close is acted on
# NEXT bar, not this one. Skips the look-ahead footgun.
entries = raw_entries.shift(1, fill_value=False)
exits = raw_exits.shift(1, fill_value=False)

pf = vbt.Portfolio.from_signals(
    close=price,
    entries=entries,
    exits=exits,
    fees=0.001,       # 0.1% per trade
    slippage=0.0005,  # 0.05% adverse fill
    init_cash=10_000,
)

The slippage argument worsens each fill in the adverse direction automatically, so you do not shift prices yourself. Cost realism and correct timing are separate concerns — getting the fees right does nothing to fix a look-ahead entry, and vice versa. You can validate the timing of code like this with the free [Python Backtest Validator](/python).

How much should you assume? Sizing costs honestly

There is no universal correct number; costs depend on the instrument, venue, order type, and size. But a few principles keep the estimate honest rather than flattering:

  • Model at least half the quoted spread as slippage for marketable orders, plus commission on top. For a liquid instrument that might be a few basis points; for a thin one it can be far more.
  • When unsure, round costs up, not down. An overstated cost gives a conservative backtest; an understated one gives a mirage. If a strategy only survives at implausibly low costs, that is a finding, not a nuisance.
  • Stress-test the assumption. Re-run the backtest at 1x, 2x, and 3x your baseline cost. A robust result degrades gracefully; a fragile one collapses. This is cheap to do in vectorbt by passing arrays of fees/slippage, and straightforward in backtrader by re-running the broker configuration.
  • Remember that costs interact with the validation split. A strategy tuned on in-sample data with optimistic costs can look strong in-sample and fall apart out-of-sample once real costs bite — see [in-sample vs out-of-sample](/learn/in-sample-vs-out-of-sample) and [walk-forward analysis](/learn/walk-forward-analysis).

Realistic cost modeling does not make a strategy good or bad; it makes the backtest a more faithful estimate. Treat every number in this article as an illustration of the mechanics, not a recommendation. Nothing here is financial advice, and none of it implies any strategy is profitable.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro