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)Trailing Stops#
The example above uses a fixed stop: once the position is open, the stop price never moves. A fixed stop caps the loss but cannot lock in profit that a favourable move has already handed you. The trailing stop built into strategy.exit moves up automatically as price advances (long) or down (short), and never retreats.
A trailing stop has two parts, and both are required:
| Parameter | Role | Unit |
|---|---|---|
trail_price | Activation: arm the stop when price reaches this absolute level | price |
trail_points | Activation: arm the stop once profit from the entry price reaches this distance | ticks |
trail_offset | Trail distance: how far behind the best price the stop sits | ticks |
Supply trail_offset plus either trail_price or trail_points. If both activation arguments are given, whichever would arm the stop first wins.
The relationship is easiest to see in a picture — a long trade:
trail_offset, until price retraces into the stop and the trade exits.Three things worth noting: the stop does not exist before activation; once armed it rises in step with every new high and holds still while price pulls back; and the exit is always below the peak by exactly trail_offset.
Basic Usage#
//@version=6
strategy("Trailing Stop", overlay=true)
ma20 = ta.sma(close, 20)
if ta.crossover(close, ma20) and strategy.position_size == 0
strategy.entry("Long", strategy.long)
// Arm after 100 ticks of profit, then trail 50 ticks behind the high
strategy.exit("Exit", "Long", trail_points=100, trail_offset=50)Once armed on a long, the stop follows the highest price reached since entry; when price retraces more than trail_offset from that high, the stop order fills. Shorts work in reverse, trailing the lowest price.
Which argument is larger decides whether the exit is a win: at the moment it arms, the stop sits at
entry + trail_points - trail_offset. Above, 100 > 50, so the stop starts above the entry price and any later fill is a profit. Settrail_offsetlarger thantrail_pointsand the stop arms below the entry price instead, making that fill a loss — before commission and slippage are even counted.
Common Mistake: trail_offset on Its Own#
trail_offset only defines how far to trail — not when to start. On its own it produces no trailing stop at all:
// ❌ No activation argument: the trailing stop never arms
strategy.exit("Exit", "Long", trail_offset=50)
// ✅ Add trail_points (or trail_price)
strategy.exit("Exit", "Long", trail_points=100, trail_offset=50)Unit Trap: Ticks, Not Price#
trail_points and trail_offset are both measured in ticks, not in currency. Passing a price distance such as an ATR value straight in gives a stop distance that is off by orders of magnitude — divide by syminfo.mintick first:
//@version=6
strategy("ATR Trailing Stop", overlay=true)
atrValue = ta.atr(14)
trailDist = atrValue * 2 // Price distance
trailTick = trailDist / syminfo.mintick // Converted to ticks
if ta.crossover(close, ta.sma(close, 20)) and strategy.position_size == 0
strategy.entry("Long", strategy.long)
strategy.exit("Exit", "Long",
trail_points = trailTick,
trail_offset = trailTick)To think in prices instead, use
trail_pricefor the activation level (for examplestrategy.position_avg_price * 1.02) — buttrail_offsetstill only accepts ticks.
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)