Why backtests without commission and slippage are fiction, and why parameters optimised on historical data don't survive forward testing — with practical fixes for both.
Two assumptions silently destroy backtest credibility: that trades execute at exactly the signal price, and that the parameters found by optimisation will continue to work on unseen data.
The first assumption is broken by commission and slippage. Every real trade incurs execution costs — the bid/ask spread, brokerage commission, and market impact. On a strategy that takes many trades, these costs compound into a substantial drag. A strategy with a profit factor of 1.4 in a zero-cost backtest may have a profit factor of 0.9 after realistic costs — a profitable-looking system that actually loses money.
The second assumption is broken by curve-fitting. When you test 50 parameter combinations and keep the best one, you have essentially found the settings that happened to fit the historical data best. Those settings often fail on new data because they captured noise rather than genuine market structure. Walk-forward testing addresses this by separating the data used to find parameters from the data used to evaluate them.
Commission applies to every trade — twice. Once when you enter and once when you exit. A 0.1% commission per side is 0.2% round-trip. On 100 trades per year, that is 20% of the traded capital consumed by commissions, regardless of the strategy's gross performance.
The `commission_type` parameter in Pine Script's `strategy()` declaration accepts several formats:
Setting `commission_value = 0.1` with `strategy.commission.percent` means 0.1% of the trade's notional value is deducted on each fill. A $10,000 trade incurs $10 commission per fill, $20 round trip.
The practical test: run your strategy first with no commission, then with realistic commission. If the profit factor drops significantly (say, from 1.8 to 1.1), the strategy's raw edge is too thin to survive real-world execution costs and needs refinement.
Slippage is the difference between the price your script signals an entry and the price the order actually fills at. In liquid markets during normal hours, slippage is modest — often just 1–2 ticks. In thin markets, at market open, or during news events, slippage can be substantial.
A realistic starting value is 1–2 ticks for liquid instruments. For less liquid instruments, 3–5 ticks or more may be appropriate.
Walk-forward testing is a methodology for testing whether optimised strategy parameters generalise to unseen data. The process:
If the out-of-sample results are reasonably close to the in-sample results, the strategy has some robustness. If the out-of-sample results are dramatically worse, the in-sample optimisation was largely curve-fitting.
In TradingView, you cannot automate walk-forward testing from Pine Script alone, but you can perform it manually:
A strategy that is genuinely robust tends to show these characteristics:
A strategy that fails these checks is not necessarily worthless, but it warrants more investigation before trading it live.
//@version=6
strategy("Realistic Backtest — Commission & Slippage",
overlay=true,
commission_type = strategy.commission.percent,
commission_value = 0.1, // 0.1% per side = 0.2% round trip
slippage = 2, // 2 ticks of slippage per fill
default_qty_type = strategy.percent_of_equity,
default_qty_value = 10) // risk 10% of equity per trade
fastEMA = ta.ema(close, 9)
slowEMA = ta.ema(close, 21)
atrVal = ta.atr(14)
longEntry = ta.crossover(fastEMA, slowEMA) and barstate.isconfirmed
shortEntry = ta.crossunder(fastEMA, slowEMA) and barstate.isconfirmed
if longEntry
strategy.entry("Long", strategy.long)
strategy.exit("Long Exit", "Long",
stop=close - atrVal * 2, limit=close + atrVal * 4)
if shortEntry
strategy.entry("Short", strategy.short)
strategy.exit("Short Exit", "Short",
stop=close + atrVal * 2, limit=close - atrVal * 4)
plot(fastEMA, "Fast EMA", color=color.blue)
plot(slowEMA, "Slow EMA", color=color.orange)
plotshape(longEntry, style=shape.triangleup, location=location.belowbar, color=color.green, size=size.small)
plotshape(shortEntry, style=shape.triangledown, location=location.abovebar, color=color.red, size=size.small)
// ── Performance summary table ─────────────────────────────────────────
var table t = table.new(position.top_right, 2, 6,
bgcolor=color.new(color.black, 75), border_width=1)
if barstate.islast
winRate = strategy.wintrades / math.max(strategy.closedtrades, 1) * 100
pf = strategy.grossprofit / math.max(strategy.grossloss, 1)
table.cell(t, 0, 0, "Net Profit", text_color=color.white, text_size=size.small)
table.cell(t, 1, 0, str.tostring(strategy.netprofit, "#.##"),
text_color=strategy.netprofit > 0 ? color.green : color.red, text_size=size.small)
table.cell(t, 0, 1, "Win Rate", text_color=color.white, text_size=size.small)
table.cell(t, 1, 1, str.tostring(winRate, "#.#") + "%", text_color=color.white, text_size=size.small)
table.cell(t, 0, 2, "Profit Factor", text_color=color.white, text_size=size.small)
table.cell(t, 1, 2, str.tostring(pf, "#.##"),
text_color=pf >= 1.5 ? color.green : pf >= 1.0 ? color.yellow : color.red, text_size=size.small)
table.cell(t, 0, 3, "Max Drawdown", text_color=color.white, text_size=size.small)
table.cell(t, 1, 3, str.tostring(strategy.max_drawdown, "#.##"), text_color=color.red, text_size=size.small)
table.cell(t, 0, 4, "Total Trades", text_color=color.white, text_size=size.small)
table.cell(t, 1, 4, str.tostring(strategy.closedtrades), text_color=color.white, text_size=size.small)
table.cell(t, 0, 5, "Commission", text_color=color.white, text_size=size.small)
table.cell(t, 1, 5, "0.1%/side", text_color=color.gray, text_size=size.small)