Skip to content
GetProfitable
Search

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.floor handles it.
  • Futures: contracts are whole numbers and each point is worth syminfo.pointvalue, so qty = math.floor(riskDollars / (stopDist * syminfo.pointvalue)).
  • Forex and crypto: fractional quantities are normal; drop the math.floor or round to the broker's lot step. Note that strategy.equity is 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

  1. 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.
  2. Intrabar path. Bars are OHLC; where price went in between is a guess (module 7's bar magnifier helps).
  3. 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.
  4. Funding, borrow and margin. Crypto perpetual funding rates, stock borrow fees for shorts, and margin calls are not modelled. margin_long and margin_short in the declaration let the tester close positions when equity is insufficient, but only approximately.
  5. 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.
  6. History depth. The tester only sees the bars loaded on the chart, which depends on timeframe and plan.
  7. 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=0 and slippage=0, note the net profit, then set 0.1% and 2 ticks. Compute what fraction of the original profit was costs. Then change riskPct to 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_type and commission_value charge every fill; slippage in 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.

Slippage on a market orderA buy order clears four price levels, so the average price paid is worse than the price first quoted.Buy 1,000 shares at marketpricesell orders resting (bar length = size)20.04300 shares20.03200 shares20.01200 shares20.00300 sharesnothing resting at 20.02order sweeps up the bookaverage fill 20.02SLIPPAGE0.02 a share$20.00 in totalintended 20.00Each level fills at its own price; the average is what you really paid.
Slippage on a market order. You click at 20.00, but only 300 shares are resting there, so the rest of the order fills at 20.01, 20.03 and 20.04. The average price paid is 20.02, and that two-cent gap is slippage.
How a position size is worked outAccount size, risk per trade and stop distance feed into one box giving the number of shares.ACCOUNT SIZE$25,000your capitalRISK PER TRADE1%of the accountSTOP DISTANCE$0.50entry to stopPOSITION SIZE500 sharesrisk budget: $25,000 × 1% = $250position size: $250 ÷ $0.50 = 500 shares
Working out a position size. Three numbers decide how big a trade is: the account, the share of it put at risk, and the distance from entry to stop. One percent of $25,000 is a $250 budget, and a $0.50 stop divides into that 500 times.
Bid-ask spread in an order bookSell orders stacked above buy orders with a gap between the best of each.SELLERS (asks)50.0690050.051,40050.0460050.011,10050.002,30049.99800spread = 0.03BUYERS (bids)
The bid-ask spread. Buy orders sit below, sell orders above, and the gap between the best bid (50.01) and best ask (50.04) is the spread you pay to cross. Bar length shows the size resting at each price.

Finished this module? Take the module quiz.