Sizing, commission, slippage and what a backtest cannot know
Lesson 18 · about 12 min
A strategy with the default settings trades at zero cost, in unlimited size, at exactly the printed price. The tester reports that world faithfully. Making the simulation resemble your account takes three things: a sizing rule that matches how you actually risk money, commission and slippage that match your broker, and an understanding of what the emulator structurally cannot model.
Sizing by percent of equity
default_qty_type=strategy.percent_of_equity, default_qty_value=10 puts 10% of current equity into each trade. That is a position size, not a risk size: the amount you lose if the stop is hit depends on how far away the stop is. Two trades at 10% of equity with stops at 1% and 5% away risk 0.1% and 0.5% of the account. Percent of equity is fine for comparing signal logic; it is wrong for a strategy whose stops vary with volatility.
Sizing by risk
The risk-management course's rule, position = dollars at risk divided by stop distance, translates directly:
//@version=6
strategy("Risk-sized entries", overlay=true, initial_capital=10000,
default_qty_type=strategy.fixed, default_qty_value=1,
commission_type=strategy.commission.percent, commission_value=0.05,
slippage=2, pyramiding=0)
riskPct = input.float(1.0, "Risk per trade %", minval=0.1, step=0.1)
atrMult = input.float(2.0, "ATR stop multiple", minval=0.5, step=0.25)
atr = ta.atr(14)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
signal = ta.crossover(fast, slow)
if signal and strategy.position_size == 0
stopDist = atr * atrMult
riskDollars = strategy.equity * riskPct / 100
qty = math.floor(riskDollars / stopDist)
if qty > 0
strategy.entry("Long", strategy.long, qty=qty)
strategy.exit("Exit", "Long", stop=math.round_to_mintick(close - stopDist),
limit=math.round_to_mintick(close + stopDist * 2))
plot(fast, "Fast", color=color.orange)
plot(slow, "Slow", color=color.blue)
With $10,000 equity, 1% risk and a $2.50 stop distance: risk $100, quantity floor(100 / 2.5) = 40 units. strategy.equity includes open profit, so sizing compounds as the account grows and shrinks as it draws down, which is the behaviour a fixed-fractional plan wants.
Adjustments by market:
- Stocks: shares are whole numbers;
math.floorhandles it. - Futures: contracts are whole numbers and each point is worth
syminfo.pointvalue, soqty = math.floor(riskDollars / (stopDist * syminfo.pointvalue)). - Forex and crypto: fractional quantities are normal; drop the
math.flooror round to the broker's lot step. Note thatstrategy.equityis in the strategy's currency, so a stop distance in a quote currency other than the account currency needs conversion.
Commission
commission_type takes strategy.commission.percent (percent of trade value per fill), strategy.commission.cash_per_contract (currency per unit per fill) or strategy.commission.cash_per_order (currency per fill). commission_value is the number. Typical starting points: 0.05% to 0.1% for stocks or crypto, a few currency units per contract for futures, a spread-equivalent for forex. Every fill pays, so a round trip pays twice.
Slippage
slippage=2 moves every fill two ticks against you: market buys fill two ticks above the bar's open, stops fill two ticks worse than the stop price. It is a blunt tool that does not scale with volatility or size, but it is far better than zero. For liquid instruments one to two ticks is a reasonable default; for thin ones, more. If a strategy's edge disappears with two ticks of slippage, you have learned that the edge was smaller than the spread.
Commission and slippage can also be set in the Strategy Tester's Properties tab without changing code. The code values are the defaults; document them in the script description so users know what the published numbers assume.
What the emulator cannot know
- Liquidity. Every order fills in full at one price. A backtest that trades 10,000 shares of a stock that prints 50,000 a day is fiction.
- Intrabar path. Bars are OHLC; where price went in between is a guess (module 7's bar magnifier helps).
- Gaps. A stop inside a gap fills at the next open, which the emulator does model, but a limit in a gap fills at the open too, better than reality often allows.
- Funding, borrow and margin. Crypto perpetual funding rates, stock borrow fees for shorts, and margin calls are not modelled.
margin_longandmargin_shortin the declaration let the tester close positions when equity is insufficient, but only approximately. - Data quality. Backtests run on the chart's data, which for some symbols is back-adjusted continuous futures, split-adjusted stocks or thinly traded early history.
- History depth. The tester only sees the bars loaded on the chart, which depends on timeframe and plan.
- You. The emulator never hesitates, never doubles size after a loss, and never misses a signal at 3 a.m.
Key idea: Size by risk (
qty = floor(equity × risk% ÷ stop distance)), set a commission and a slippage that match your broker, and remember the emulator fills everything in full at one price on data that may not be what you will trade.
Try it: Run the risk-sized strategy with
commission_value=0andslippage=0, note the net profit, then set 0.1% and 2 ticks. Compute what fraction of the original profit was costs. Then changeriskPctto 3 and look at the maximum drawdown in the tester; it should move roughly threefold.
Recap
- Percent of equity sizes the position, not the risk; risk-based sizing divides dollars at risk by stop distance and uses
strategy.equity. - Whole units for stocks and futures (
math.floor), point value for futures, fractional for forex and crypto. commission_typeandcommission_valuecharge every fill;slippagein ticks moves every fill against you.- Zero-cost defaults make any strategy look better; state the assumptions in the description.
- The emulator ignores liquidity, funding, borrow costs and human error, and only sees the bars loaded on the chart.
See it drawn
Original diagrams for the ideas on this page. Illustrative, not real market data.