How to Code an ATR Trailing Stop Strategy in Pine v6
By HorizonAI Team · 13 min read · Intermediate
How to Code an ATR Trailing Stop Strategy in Pine Script v6
A fixed stop answers one question: how much room does this trade get at entry? An ATR trailing stop keeps asking a better one: after price has moved in your favor, where is the trade objectively wrong now?
Short answer: In Pine Script v6, you can trail an exit with strategy.exit() using trail_points and trail_offset, both measured in ticks. For an ATR-based trail that never moves away from price, calculate the highest high since a long entry or lowest low since a short entry, subtract or add an ATR multiple, then ratchet the saved stop with math.max() or math.min().
The built-in trail is compact and useful when a fixed tick distance is enough. A custom ATR trail is the better build when you want the distance to adapt to volatility, want an initial protective stop, and need to plot the exact stop series you are testing. The strategy below includes both approaches behind one input.
What an ATR trailing stop actually does
Average True Range (ATR) measures recent price movement in price units. On a market with a 2.00 ATR, a 2 ATR long trail sits roughly 4.00 below the relevant high. As volatility expands, the candidate stop gets wider; as price advances, the ratchet can rise.
A correct long trailing stop has one non-negotiable rule: it can stay still or move up, never down. A short trailing stop can stay still or move down, never up. That is why a naive formula such as high - atr * multiplier is not a trailing stop by itself. ATR can rise on a volatile bar and make that raw value fall.
This guide uses a 9/21 EMA crossover only to create repeatable entries. The entry signal is not the point. Replace it later with a break-and-retest setup, a VWAP entry, or your own market-structure rule while leaving the exit engine intact.
Know the Pine trailing-stop units before writing code
Pine’s built-in strategy.exit() trailing arguments do not take prices or ATR values directly. trail_points is the favorable move required to activate the trail, and trail_offset is the distance from the best price after activation. Both are numbers of minimum ticks, so convert an ATR distance with distance / syminfo.mintick.
| Term | Meaning | Example on a market with 0.25 minimum tick |
|---|---|---|
syminfo.mintick | Smallest valid price increment | 0.25 |
trail_points | Profit move needed before trailing activates | 8 points = 2.00 price units |
trail_offset | Distance behind best price after activation | 12 points = 3.00 price units |
trail_price | Explicit activation price alternative | Long entry price + 2.00 |
stop | Absolute protective exit price | 98.00 |
When you supply trail_points, activation is measured from entry. When you supply trail_price, activation occurs at that stated price. Pine requires trail_offset with either activation method. TradingView documents those parameters and the activation behavior in its strategy concepts guide and strategy.exit() reference.
For an ATR trail, freeze the ATR-derived tick values at entry if you use the built-in route. Recomputing trail_offset from the latest ATR each bar makes the configuration harder to reason about. For the custom route, deliberately recalculating the candidate from current ATR is fine because the ratchet prevents loosening.
Build the complete long and short ATR strategy
Paste this code into a new TradingView strategy. It has four jobs:
- It enters on a 9 EMA crossing a 21 EMA.
- It sets an initial stop at
ATR × stop multiplierfrom the entry. - It activates the custom ratchet only after price has moved
ATR × activation multiplierin favor. - It lets you compare the custom stop with TradingView’s built-in tick-based trail using the Exit engine input.
//@version=6
strategy("ATR Trailing Stop Strategy v6", overlay = true, pyramiding = 0, process_orders_on_close = true, calc_on_order_fills = true)
// Entry inputs
fastLength = input.int(9, "Fast EMA", minval = 1)
slowLength = input.int(21, "Slow EMA", minval = 2)
// Exit inputs
exitEngine = input.string("Custom ATR ratchet", "Exit engine", options = ["Custom ATR ratchet", "Built-in tick trail"])
atrLength = input.int(14, "ATR length", minval = 1)
stopAtrMult = input.float(2.0, "Initial stop ATR multiple", minval = 0.1, step = 0.1)
activationAtrMult = input.float(1.0, "Trail activation ATR multiple", minval = 0.1, step = 0.1)
trailAtrMult = input.float(2.5, "Trailing ATR multiple", minval = 0.1, step = 0.1)
showBuiltInGuide = input.bool(true, "Plot built-in trail guide")
// Repeatable example entry logic
fastEma = ta.ema(close, fastLength)
slowEma = ta.ema(close, slowLength)
atrValue = ta.atr(atrLength)
longSignal = ta.crossover(fastEma, slowEma)
shortSignal = ta.crossunder(fastEma, slowEma)
if longSignal and strategy.position_size <= 0
strategy.entry("Long", strategy.long)
if shortSignal and strategy.position_size >= 0
strategy.entry("Short", strategy.short)
// Persistent state, reset for each position.
var float entryAtr = na
var float longPeak = na
var float shortTrough = na
var float longStop = na
var float shortStop = na
var int activationTicksAtEntry = na
var int offsetTicksAtEntry = na
newLong = strategy.position_size > 0 and strategy.position_size[1] <= 0
newShort = strategy.position_size < 0 and strategy.position_size[1] >= 0
flat = strategy.position_size == 0
if newLong or newShort
entryAtr := atrValue
activationTicksAtEntry := int(math.max(1, math.round(entryAtr * activationAtrMult / syminfo.mintick)))
offsetTicksAtEntry := int(math.max(1, math.round(entryAtr * trailAtrMult / syminfo.mintick)))
if newLong
longPeak := high
shortTrough := na
longStop := strategy.position_avg_price - entryAtr * stopAtrMult
shortStop := na
if newShort
shortTrough := low
longPeak := na
shortStop := strategy.position_avg_price + entryAtr * stopAtrMult
longStop := na
// Custom long trail: the stop cannot move down.
if strategy.position_size > 0
longPeak := math.max(nz(longPeak, high), high)
longActivationPrice = strategy.position_avg_price + entryAtr * activationAtrMult
longCandidate = longPeak - atrValue * trailAtrMult
if longPeak >= longActivationPrice
longStop := math.max(nz(longStop, longCandidate), longCandidate)
// Custom short trail: the stop cannot move up.
if strategy.position_size < 0
shortTrough := math.min(nz(shortTrough, low), low)
shortActivationPrice = strategy.position_avg_price - entryAtr * activationAtrMult
shortCandidate = shortTrough + atrValue * trailAtrMult
if shortTrough <= shortActivationPrice
shortStop := math.min(nz(shortStop, shortCandidate), shortCandidate)
if flat
entryAtr := na
longPeak := na
shortTrough := na
longStop := na
shortStop := na
activationTicksAtEntry := na
offsetTicksAtEntry := na
// One exit engine at a time prevents competing orders from masking results.
if exitEngine == "Custom ATR ratchet"
strategy.exit("Long custom exit", from_entry = "Long", stop = longStop)
strategy.exit("Short custom exit", from_entry = "Short", stop = shortStop)
else
strategy.exit("Long built-in exit", from_entry = "Long", trail_points = activationTicksAtEntry, trail_offset = offsetTicksAtEntry)
strategy.exit("Short built-in exit", from_entry = "Short", trail_points = activationTicksAtEntry, trail_offset = offsetTicksAtEntry)
// Visuals. The built-in guide is a model of its tick trail, not a broker-side fill report.
float builtInLongGuide = strategy.position_size > 0 and not na(longPeak) and longPeak >= strategy.position_avg_price + activationTicksAtEntry * syminfo.mintick ? longPeak - offsetTicksAtEntry * syminfo.mintick : na
float builtInShortGuide = strategy.position_size < 0 and not na(shortTrough) and shortTrough <= strategy.position_avg_price - activationTicksAtEntry * syminfo.mintick ? shortTrough + offsetTicksAtEntry * syminfo.mintick : na
plot(fastEma, "Fast EMA", color = color.teal)
plot(slowEma, "Slow EMA", color = color.orange)
plot(exitEngine == "Custom ATR ratchet" ? longStop : na, "Custom long stop", color = color.red, style = plot.style_linebr, linewidth = 2)
plot(exitEngine == "Custom ATR ratchet" ? shortStop : na, "Custom short stop", color = color.red, style = plot.style_linebr, linewidth = 2)
plot(showBuiltInGuide and exitEngine == "Built-in tick trail" ? builtInLongGuide : na, "Built-in long guide", color = color.fuchsia, style = plot.style_linebr, linewidth = 2)
plot(showBuiltInGuide and exitEngine == "Built-in tick trail" ? builtInShortGuide : na, "Built-in short guide", color = color.fuchsia, style = plot.style_linebr, linewidth = 2)
bgcolor(strategy.position_size > 0 ? color.new(color.green, 92) : strategy.position_size < 0 ? color.new(color.red, 92) : na)
The custom mode starts with a real stop from the entry price. It does not tighten until the trade reaches the activation threshold. Once active, the math.max() call preserves the higher of the old stop and the new ATR candidate for longs. The short branch mirrors that logic with math.min().
A value of ATR 14, initial stop 2.0, activation 1.0, and trail 2.5 is a sensible first test on liquid instruments. It is not a universal setting. A 5-minute index future and a daily crypto chart can have very different noise profiles even when both use ATR 14.
Use the built-in trail when fixed ticks are enough
Select Built-in tick trail to send TradingView a native trailing instruction through strategy.exit(). At entry, the script converts the activation and offset ATR distances into whole ticks and stores them. That gives a clean apples-to-apples test against the custom approach.
The built-in implementation is concise, but it has trade-offs:
Built-in strategy.exit() trail | Custom ATR ratchet |
|---|---|
| Uses tick counts | Uses price-series calculations |
| Trail distance is fixed from entry in this script | Candidate distance can adapt to current ATR |
| Activation and offset are engine parameters | Initial stop, activation, and ratchet are explicit |
| Actual active stop is not exposed as a series | Exact tested stop is plotted |
| Best for simple, fixed-distance exits | Best for inspectable ATR logic |
The magenta line is a visual guide to the built-in tick trail. It is not an independent exit order. The red line in custom mode is the actual stop price supplied to strategy.exit() on that bar.
If you only need “activate after 40 ticks, trail 25 ticks,” keep the built-in exit. If you need “start at 2 ATR risk, then trail 2.5 current ATR after a 1 ATR move,” use custom state. That distinction matters more than the number of lines in the script.
Read the plots without fooling yourself
A stop plot shows the price level the script calculated at bar close, not necessarily every price update that occurred inside the candle. A bar whose high activates a long trail and whose low crosses it can contain both events, but the chart bar does not tell Pine which happened first without lower-timeframe information.
The strategy sets process_orders_on_close = true, so entries are processed on the signal bar’s close. That makes the example easier to inspect: the position, frozen entry ATR, and stop state begin from a defined point. It does not turn bar-close logic into tick-by-tick simulation.
For a serious test, open Strategy Properties and model commission and slippage. Then test the same settings with Bar Magnifier where it is available. TradingView’s documentation explains why historical bars and realtime updates can behave differently through its bar-state model. If your stop seems to move differently live, inspect execution timing before calling it repainting; our guide on why Pine scripts repaint separates changing realtime-bar values from lookahead errors.
Test parameters in a deliberate order
Do not optimize all four ATR inputs at once. That produces a good-looking parameter cluster you cannot explain.
Start with the entry logic fixed at 9/21 EMA and test these ranges on one market and one timeframe:
- ATR length: 10, 14, 20. Shorter ATR reacts faster; longer ATR smooths sudden expansion.
- Initial stop multiple: 1.5, 2.0, 2.5. This defines the trade’s pre-activation room.
- Activation multiple: 0.5, 1.0, 1.5. Higher activation leaves the initial stop in place longer.
- Trail multiple: 2.0, 2.5, 3.0. Smaller values protect open profit earlier but will exit trend pullbacks more often.
Record net profit, maximum drawdown, average trade, profit factor, and trade count, not only the top line. A setting that wins because it took six trades is a hypothesis, not a deployable rule. Use the same discipline described in backtesting metrics and the common backtesting mistakes.
Keep an out-of-sample segment untouched while choosing the values. For example, develop on January through September, then evaluate once on October through December. Do not keep changing inputs after seeing that final segment.
Common ATR trailing-stop mistakes
❌ Mistake: treating ATR as ticks. Passing 2.5 to trail_offset does not mean 2.5 ATR. It means roughly two or three minimum ticks, depending on rounding.
✅ Do this: Convert price distance to ticks with math.round(atrValue * trailAtrMult / syminfo.mintick). Store the integer at entry when testing a fixed built-in trail.
❌ Mistake: recalculating longStop = longPeak - ATR × multiplier without a ratchet. A volatility spike can make the stop lower even though price has not made a new high.
✅ Do this: For longs, use math.max(previousStop, candidateStop). For shorts, use math.min(previousStop, candidateStop). Those two lines enforce the exit rule.
❌ Mistake: activating the custom trail at entry by accident. If activation is zero, the stop may jump to the ATR candidate as soon as the position exists.
✅ Do this: Start with an activation multiple of 1.0 and an initial stop multiple of 2.0. Plot the line, then verify it remains at the initial price until the trade earns that movement.
❌ Mistake: judging a trailing exit from one chart resolution. A 1-hour bar can hide the sequence of activation, a new high, and a stop touch.
✅ Do this: Compare the result on the trading timeframe and a lower timeframe, use Bar Magnifier for the final test, and add realistic fill costs. For a broader testing workflow, see how to backtest a trading strategy.
Pro tips for making the exit match the market
Match ATR length to holding period. A 14-bar ATR on a 5-minute chart reflects roughly 70 minutes of recent movement. If your average trade lasts several sessions, test ATR 14 and 20 on the hourly or daily decision chart instead of forcing a very short intraday volatility measure into the exit.
Separate entry risk from profit protection. The initial 2 ATR stop and 2.5 ATR trail do different jobs. One limits the loss before a move works. The other decides how much pullback a profitable trend gets. Give them separate inputs, as the script does.
Use an activation threshold to avoid instant churn. A 1 ATR activation means price must first demonstrate movement in your direction. On a noisy 1-minute chart, test 1.5 ATR before reducing the trailing distance.
Save a visual audit chart. Mark ten long and ten short trades, then inspect whether each red stop moved only in the permitted direction. This catches logic errors that a summary metric cannot expose.
Generating this without writing the code yourself
You can ask HorizonAI to create or revise this exact TradingView artifact in plain English. Describe the mechanics, not just “make an ATR trail,” so the generated Pine Script v6 has the state variables, activation rule, and plots you intend.
Create a Pine Script v6 TradingView strategy with 9/21 EMA crossover entries. Use ATR 14. On each entry, set an initial stop at 2.0 ATR. After price moves 1.0 ATR in favor, trail a custom stop at 2.5 current ATR from the highest high since a long entry or lowest low since a short entry. The long stop may only rise and the short stop may only fall. Include
strategy.entry,strategy.exit, plots for both EMAs and the active stop, and inputs for all parameters. Use bar-close processing and no pyramiding.
Or ask for the comparison version:
Add an input that switches between the custom ATR ratchet and built-in
strategy.exit()trailing logic. Convert ATR activation and trailing distances to ticks usingsyminfo.mintick, freeze those tick values at entry, and plot a guide for the built-in trail.
HorizonAI returns Pine Script code you can edit in chat, with compiler validation to catch and explain errors. It writes the strategy code; you run it yourself in TradingView’s Pine Editor and Strategy Tester. Try it free →
FAQs
Is trail_offset in dollars, points, or ticks in Pine Script?
trail_offset is measured in minimum ticks, not dollars and not ATR units. Convert a price distance by dividing it by syminfo.mintick, then round to a whole number of ticks.
Why does my ATR trailing stop move farther away after volatility rises?
Your script is probably assigning the newest ATR-based candidate directly to the stop. Preserve the prior long stop with math.max() or the prior short stop with math.min() so volatility cannot loosen it.
Can I use both a fixed stop and a Pine built-in trailing stop on one exit?
You can configure several exit conditions, but competing exit orders can make a backtest difficult to interpret. This build uses a fixed initial stop in custom mode and a standalone built-in trail in comparison mode so each result has one clear exit engine.
Does a plotted trailing stop guarantee that price filled there?
No. A plot is the calculated stop level, while a historical bar may not reveal the exact intrabar order of high and low. Check fill assumptions, costs, and lower-timeframe behavior before trusting a close backtest result.
Final thoughts
A usable ATR trailing stop is not “ATR below price.” It is an initial risk rule, an activation rule, and a one-way ratchet. Build those pieces separately, plot the resulting stop, and compare it against Pine’s tick-based exit rather than assuming the two behave the same.
One practical pro tip: start with the custom 2.0 ATR initial stop, 1.0 ATR activation, and 2.5 ATR trail, then change only one parameter family per test. You’ll learn why the exit changed instead of merely finding a lucky combination.
Related articles
- ATR Indicator Explained — Understand the volatility measure that drives the stop distance.
- Pine Script Tutorial for Beginners — Build and run a first TradingView strategy from scratch.
- How to Use Bar Magnifier in a Pine Script v6 Strategy — Improve the realism of intrabar strategy testing.
- How to Add Commission and Slippage to a Pine Script Strategy — Model trading friction in a backtest.
- Code a Heikin Ashi Strategy in Pine v6 Without Fake Fills — Avoid synthetic-price execution traps.
- Automate a Supertrend Strategy with Alerts in Pine Script v6 — Add alert-ready logic to another trend-following build.
- Simple Moving Average Crossover Strategy — Compare a basic moving-average entry framework.
- Understanding Backtesting Metrics — Read profit factor, drawdown, and trade-count results correctly.
Questions about ATR trailing stops? Join our Discord to discuss with other traders!
