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 trade

commission_type options:

ConstantDescription
strategy.commission.percentCharged as a percentage of trade value
strategy.commission.cash_per_contractFixed amount per contract
strategy.commission.cash_per_orderFixed 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 execution

Position Sizing#

Fixed Quantity#

strategy("Fixed Size",
         default_qty_type=strategy.fixed,
         default_qty_value=1)  // Trade 1 contract/share each time

Fixed Cash Amount#

strategy("Fixed Cash",
         default_qty_type=strategy.cash,
         default_qty_value=10000)  // Invest $10,000 each time

Percentage of Equity#

strategy("Percent of Equity",
         default_qty_type=strategy.percent_of_equity,
         default_qty_value=10)  // Invest 10% of total equity each time

Dynamic 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:

ParameterRoleUnit
trail_priceActivation: arm the stop when price reaches this absolute levelprice
trail_pointsActivation: arm the stop once profit from the entry price reaches this distanceticks
trail_offsetTrail distance: how far behind the best price the stop sitsticks

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:

How the three trailing stop parameters relatePrice rises from the entry; once profit reaches trail_points the trailing stop arms. The stop then sits trail_offset below the highest price since entry and only ever moves up. The trade exits when price retraces into the stop.timetrail_pointstrail_offsettrail_offsethighest pricetrailing stopactivation levelentry pricetrail_priceentryarmedexit
Once armed, the stop (red) always sits at the highest price since entry minus 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. Set trail_offset larger than trail_points and 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_price for the activation level (for example strategy.position_avg_price * 1.02) — but trail_offset still 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#

VariableDescription
strategy.equityCurrent total equity (including unrealized P&L)
strategy.netprofitRealized net profit/loss
strategy.position_sizeCurrent position size (positive = long, negative = short)
strategy.position_avg_priceAverage entry price of the position
strategy.opentradesNumber of currently open trades
strategy.closedtradesTotal number of closed trades
strategy.wintradesNumber of winning trades
strategy.losstradesNumber 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)

Reference#