3. Real costs: commissions, slippage and position sizing
Level 12: Backtesting and Real Risk Metrics
In Level 5 you built a deliberately naive backtest: no commissions, no slippage, entering and exiting at the exact closing price. We warned you at the time that it was not a result, it was a sketch. This module settles that debt.
Commissions
In Backtrader they are configured in a single line:
cerebro.broker.setcommission(commission=0.001) # 0.1% of notionalThat 0.1% looks harmless. Do the arithmetic: it applies to the buy and to the sell, so every complete round trip costs 0.2% of the notional traded. A strategy that does 250 round trips a year on the full capital pays 50% a year in commissions. No reasonable statistical edge survives that. Turnover is a first-order design variable, not an implementation detail.
For US equities a per-share scheme with a minimum is usually more realistic:
class PerShareCommission(bt.CommInfoBase):
params = (
('commission', 0.005), # dollars per share
('stocklike', True),
('commtype', bt.CommInfoBase.COMM_FIXED),
)
def _getcommission(self, size, price, pseudoexec):
return max(abs(size) * self.p.commission, 1.0) # minimum of one dollar
cerebro.broker.addcommissioninfo(PerShareCommission())Slippage
Commission is the visible cost; slippage is the invisible one, and almost always the larger. It is the difference between the price your backtest assumes and the price at which your order actually crosses: the spread you pay, the impact your own volume causes, and the price movement between deciding and executing.
cerebro.broker.set_slippage_perc(perc=0.0005) # 0.05% per trade
# or as an absolute amount:
# cerebro.broker.set_slippage_fixed(fixed=0.02)There is a cruel asymmetry worth internalizing. Mean reversion strategies buy when the price falls and sell when it rises; they are providing liquidity, and the market tends to charge them for that service precisely when it is moving most. Momentum strategies chase the price, that is, they consume liquidity and pay the full spread. In both cases real slippage tends to be worse than the historical average exactly on the most volatile days, which are the days on which the strategy generates the most signals.
A practical rule: if your strategy's average return per trade is of the same order of magnitude as the asset's average spread, the strategy does not exist. It is noise decorated with an equity curve.
Position sizing
cerebro.addsizer(bt.sizers.FixedSize, stake=1000)This sizer always buys 1000 shares. With starting capital of 100,000 and a price of 180 dollars per share, each order tries to move 180,000 dollars: more than the entire capital. The simulated broker will reject the order for insufficient margin and your backtest will do far fewer trades than you think, without warning you. The opposite problem also exists: if the asset trades at 8 dollars, each trade moves 8% of the capital and the strategy ends up under-invested.
More sensible alternatives:
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)Or a custom sizer that sizes by risk: decide how much you are willing to lose per trade (say, 1% of capital) and divide that amount by the distance to the stop in currency per share. That way the size adapts to the asset's volatility instead of to the accident of its nominal price.
A quick and very revealing check: run the same backtest with zero costs and with realistic costs. If the difference between the two results is larger than the profit of the second one, what you had was not a strategy.