Edgoro
Strategy · Strategies

Pine Script ATR trailing stop strategy (v6 example)

An ATR trailing stop in a Pine Script v6 strategy: a stop that only moves in the trade's favor, set before the fill and passed to strategy.exit() on every bar.

Short answer

Compute the stop yourself on every bar as close - multiplier * ta.atr(length), keep the highest value reached since entry with math.max() in a var variable, and pass it to strategy.exit() as stop. Set the first value on the signal bar so the trade is protected from the bar it fills on.

The script

A breakout strategy, long and short, whose only exit is an ATR stop that moves with the trade and never back. Paste it into a new strategy in the Pine Editor, then save and add it to the chart.

Pine Script v6
//@version=6
strategy("ATR trailing stop", overlay = true, initial_capital = 10000, default_qty_type = strategy.percent_of_equity, default_qty_value = 10, commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 2)

atrLen   = input.int(14, "ATR length", minval = 1)
trailMul = input.float(3.0, "Trail distance (x ATR)", minval = 0.5, step = 0.25)
entryLen = input.int(55, "Breakout lookback (bars)", minval = 2)

atr     = ta.atr(atrLen)
hiPrev  = ta.highest(high, entryLen)[1]
loPrev  = ta.lowest(low, entryLen)[1]
longIn  = ta.crossover(close, hiPrev)
shortIn = ta.crossunder(close, loPrev)

// Where a fresh stop would go on this bar
float longCand  = close - trailMul * atr
float shortCand = close + trailMul * atr

var float longStop  = na
var float shortStop = na

// Ratchet: a long stop can only rise, a short stop can only fall
if strategy.position_size > 0
    longStop := math.max(nz(longStop, longCand), longCand)
if strategy.position_size < 0
    shortStop := math.min(nz(shortStop, shortCand), shortCand)

// Flat: forget the old stops, and set the first one on the signal bar
if strategy.position_size == 0
    longStop  := na
    shortStop := na
    if longIn
        strategy.entry("Long", strategy.long)
        longStop := longCand
    else if shortIn
        strategy.entry("Short", strategy.short)
        shortStop := shortCand

// The same exit ID each bar replaces the order with the new level
if not na(longStop)
    strategy.exit("Long trail", "Long", stop = longStop)
if not na(shortStop)
    strategy.exit("Short trail", "Short", stop = shortStop)

plot(longStop, "Long stop", color.teal, 2, plot.style_linebr)
plot(shortStop, "Short stop", color.red, 2, plot.style_linebr)
plot(hiPrev, "Breakout high", color.new(color.gray, 50))
plot(loPrev, "Breakout low", color.new(color.gray, 50))

How it works

  • The candidate. longCand = close - trailMul * atr is where a stop would be if the trade started now. On its own it moves down whenever price falls or ATR widens, which a trailing stop must not do.
  • The ratchet. math.max(longStop, longCand) keeps the better of the stored stop and the new candidate, so the long stop only rises. Shorts use math.min(). nz() covers the case where no stop is stored yet.
  • The first stop on the signal bar. strategy.entry() fills at the next bar's open. Setting longStop and calling strategy.exit() on the signal bar means the stop order is waiting for the fill: an exit order can be placed before its entry fills, and it activates with it.
  • One exit ID, updated. Calling strategy.exit("Long trail", "Long", stop = longStop) on every bar with the same ID replaces the previous order with the new level instead of adding orders.
  • Timing. The stop computed at a bar's close applies from the next bar. The line drawn on a bar is therefore the stop for the bar that follows.
  • Costs in the declaration (commission and slippage) keep the Strategy Tester's numbers closer to what trading costs; set your own. The Strategy Tester settings guide covers the rest.

Pitfalls

  • A stop that moves both ways. strategy.exit(..., stop = close - 3 * atr) with no memory lowers the stop when ATR widens or price drops, so it no longer trails. Keep the level in var and only accept a better value.
  • No stop on the first bar. If the stop is only set once strategy.position_size > 0, the bar on which the entry fills has no protection: a gap or a large bar can run far past where the stop should have been.
  • Ticks vs price. strategy.exit's built-in trail_points and trail_offset are measured in ticks. trail_offset = 3 * atr trails by 3 × ATR ticks, which is a tiny distance; divide by syminfo.mintick. The details are in the strategy.exit() guide.
  • stop and loss together. Passing a price (stop) and a tick distance (loss) to the same exit makes v6 use whichever is hit first, which is rarely the intent. Pass one of them.
  • What happens inside a bar. On history the Strategy Tester only knows each bar's open, high, low and close, so it assumes a path through the bar to decide whether the stop was hit. Results can differ from live trading on bars where price touched the stop and came back. Bar Magnifier (use_bar_magnifier = true, where your plan offers it) uses lower-timeframe data to decide.

Variations

Trail from the highest high, not the close. A chandelier-style stop hangs below the highest high since entry. Add this before the ratchet and use hiSince - trailMul * atr as the long candidate:

// Highest high since the long entry, reset when flat
var float hiSince = na
hiSince := strategy.position_size > 0 ? math.max(nz(hiSince, high), high) : na
plot(hiSince - trailMul * atr, "Chandelier level", color.new(color.teal, 60), style = plot.style_linebr)

The built-in trailing stop, in ticks. strategy.exit() can trail on its own once the ATR distance is converted to ticks. The distance is then fixed at entry and the order trails on every tick, not only at the close:

//@version=6
strategy("Built-in trailing stop", overlay = true, initial_capital = 10000, default_qty_type = strategy.percent_of_equity, default_qty_value = 10, commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 2)
atr = ta.atr(14)
hi55 = ta.highest(high, 55)[1]
breakout = ta.crossover(close, hi55)
if breakout and strategy.position_size == 0
    strategy.entry("Long", strategy.long)
    // 3 x ATR converted from price to ticks; trail_points = 0 starts trailing at once
    strategy.exit("Trail", "Long", stop = close - 3 * atr, trail_points = 0, trail_offset = 3 * atr / syminfo.mintick)

The Chandelier Exit guide and the Supertrend guide show the same idea as indicators.

Questions

How do I make a trailing stop that only moves up in Pine Script?

Store the stop in a var variable and update it with stop := math.max(stop, newLevel) while the long trade is open, then pass it to strategy.exit("Exit", "Long", stop = stop) on every bar. Use math.min() for shorts.

Should I use trail_offset or compute the ATR stop myself?

trail_offset trails on every price update by a fixed distance in ticks set when you call it. Computing the stop yourself lets the distance follow ATR bar by bar and lets you trail from closes or highs, but it only updates at each bar's close.

Can the stop trigger on the close instead of intrabar?

Yes: instead of a stop order, check if close < longStop and call strategy.close("Long"). The trade then exits at the next bar's open after a close beyond the stop, which ignores wicks but can exit further away.

Every script on this page was checked with Edgoro Pine Check (Pine Script v6 rules). Pine Check is a static checker, not TradingView's compiler: TradingView compiles the script when you save it in the Pine Editor. The scripts are examples to learn from, not trading systems. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.

Need a different version of this script?

Describe the change in plain English and Edgoro writes the Pine Script v6 for you, checked with Pine Check. For example:

“Write a strategy that buys a close above the 55-bar high and sells a close below the 55-bar low, with a 3 ATR trailing stop that only moves in the trade's favor, 0.05% commission and 2 ticks of slippage.”