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.
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.
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:
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.
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).
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).
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:
fees/slippage, and straightforward in backtrader by re-running the broker configuration.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.
Educational only — not financial advice. Trading involves substantial risk of loss.