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.
//@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 * atris 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 usemath.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. SettinglongStopand callingstrategy.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 invarand 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_pointsandtrail_offsetare measured in ticks.trail_offset = 3 * atrtrails by 3 × ATR ticks, which is a tiny distance; divide bysyminfo.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.
Related pages
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.