Strategy Advanced#
The previous chapter covered basic entry and exit commands. This chapter dives deeper into settings that affect backtest accuracy: commissions, slippage, position sizing, and how to avoid common backtesting pitfalls.
Commission Settings#
Commission is an unavoidable cost in backtesting. Set it in the strategy() function:
//@version=6
strategy("Strategy with Commission",
overlay=true,
initial_capital=100000,
commission_type=strategy.commission.percent, // percentage commission
commission_value=0.1) // 0.1% per tradecommission_type options:
| Constant | Description |
|---|---|
strategy.commission.percent | Charged as a percentage of trade value |
strategy.commission.cash_per_contract | Fixed amount per contract |
strategy.commission.cash_per_order | Fixed amount per order |
Slippage Settings#
Slippage simulates the difference between the actual execution price and the signal price:
strategy("Strategy with Slippage",
overlay=true,
slippage=2) // Slip 2 minimum tick units per executionPosition Sizing#
Fixed Quantity#
strategy("Fixed Size",
default_qty_type=strategy.fixed,
default_qty_value=1) // Trade 1 contract/share each timeFixed Cash Amount#
strategy("Fixed Cash",
default_qty_type=strategy.cash,
default_qty_value=10000) // Invest $10,000 each timePercentage of Equity#
strategy("Percent of Equity",
default_qty_type=strategy.percent_of_equity,
default_qty_value=10) // Invest 10% of total equity each timeDynamic Quantity Calculation#
Specify qty directly in strategy.entry to dynamically calculate size based on current equity:
//@version=6
strategy("Dynamic Position Size", overlay=true, initial_capital=100000)
riskPct = input.float(2.0, "Risk per Trade (%)") / 100
atrMult = input.float(2.0, "Stop-Loss ATR Multiplier")
atrValue = ta.atr(14)
stopAmount = atrValue * atrMult // Stop distance per unit
riskAmount = strategy.equity * riskPct // Max acceptable loss per trade
qty = math.floor(riskAmount / stopAmount) // Calculate position size
ma20 = ta.sma(close, 20)
if ta.crossover(close, ma20) and strategy.position_size == 0
strategy.entry("Long", strategy.long, qty=qty)
strategy.exit("Exit", "Long", stop=close - stopAmount)pyramiding — Adding to a Position#
The pyramiding parameter sets the maximum number of open orders in the same direction:
strategy("Pyramiding Strategy", overlay=true, pyramiding=3) // Up to 3 long orders at once
// Add to the position each time price pulls back to the MA
ma = ta.sma(close, 20)
if close > ma and close < ma * 1.02
strategy.entry("Long", strategy.long, qty=1)Avoiding Backtesting Pitfalls#
1. Lookahead Bias#
Pine Script’s strategy.entry defaults to filling on the open of the next bar after the signal bar closes, which is the correct behavior.
However, some approaches can introduce “peeking into the future”:
// ❌ Wrong: uses the current bar's high as the entry price
// (that price existed before the signal was generated)
if close > ma
strategy.entry("Long", strategy.long, limit=high)
// ✅ Correct: fills on the next bar's open (default behavior)
if close > ma
strategy.entry("Long", strategy.long)2. Overfitting#
Avoid repeatedly tuning parameters on the same historical dataset. Recommendations:
- Reserve an “out-of-sample” test period (e.g., the most recent 20% of data)
- Use fewer parameters; simpler strategies are more robust
3. barmerge.lookahead Setting#
When fetching data from other timeframes using request.security (see CH15), always use barmerge.lookahead_off to avoid peeking at the close of a future bar.
Common strategy.* Query Variables#
| Variable | Description |
|---|---|
strategy.equity | Current total equity (including unrealized P&L) |
strategy.netprofit | Realized net profit/loss |
strategy.position_size | Current position size (positive = long, negative = short) |
strategy.position_avg_price | Average entry price of the position |
strategy.opentrades | Number of currently open trades |
strategy.closedtrades | Total number of closed trades |
strategy.wintrades | Number of winning trades |
strategy.losstrades | Number of losing trades |
Practical Example: RSI Strategy with Full Risk Management#
//@version=6
strategy("RSI Strategy", overlay=false,
initial_capital=100000,
default_qty_type=strategy.percent_of_equity,
default_qty_value=10,
commission_type=strategy.commission.percent,
commission_value=0.1)
// Parameters
rsiLen = input.int(14, "RSI Period")
oversold = input.int(30, "Oversold Threshold")
overbought = input.int(70, "Overbought Threshold")
stopPct = input.float(3.0, "Stop Loss (%)", step=0.5) / 100
targetPct = input.float(6.0, "Take Profit (%)", step=0.5) / 100
rsiValue = ta.rsi(close, rsiLen)
// Entry: RSI crosses back up from oversold
buySignal = ta.crossover(rsiValue, oversold)
sellSignal = ta.crossunder(rsiValue, overbought)
if buySignal
strategy.entry("Long", strategy.long)
if strategy.position_size > 0
entryPx = strategy.position_avg_price
strategy.exit("Exit Long", "Long",
stop=entryPx * (1 - stopPct),
limit=entryPx * (1 + targetPct))
// Plotting
plot(rsiValue, "RSI", color=color.purple)
hline(overbought, color=color.red, linestyle=hline.style_dashed)
hline(oversold, color=color.green, linestyle=hline.style_dashed)
hline(50, color=color.gray, linestyle=hline.style_dotted)