Guide

Trailing Stops in Pine Script: How to Code Them Right

trail_points and trail_offset are measured in ticks, and they do two different jobs. Getting either wrong produces a trailing stop that never activates or never moves.

12 min read

A trailing stop is the exit most people describe correctly in words and implement incorrectly in code. The reason is that strategy.exit() splits the idea into two separate parameters that both happen to be measured in ticks: one decides when the trail starts following price, the other decides how far behind it follows. Confuse them, or pass a price where ticks belong, and you get an exit that looks plausible and behaves nothing like a trailing stop.

Short answer

In strategy.exit(), trail_points sets how much profit in ticks is required before the trailing stop activates, and trail_offset sets how far in ticks behind the best price the stop then follows. trail_price is an alternative to trail_points that takes an absolute price instead of a tick distance. Both trail_points and trail_offset are tick counts, not prices, so a price distance has to be divided by syminfo.mintick first. This article covers the mechanics; it does not suggest what any trail distance should be.

Key facts

  • trail_points is an activation threshold: the profit in ticks required before the trailing stop begins following price.
  • trail_offset is the trailing distance: how far in ticks behind the most favourable price reached the stop sits.
  • trail_price is an alternative to trail_points that specifies the activation level as an absolute price rather than a tick distance.
  • Both trail_points and trail_offset are measured in ticks, where one tick is syminfo.mintick for the current symbol.
  • trail_offset on its own activates immediately, because with no activation threshold there is nothing to wait for.
  • A trailing stop only ever moves in the favourable direction. It does not loosen when price retraces, which is what distinguishes it from a recalculated stop.
  • The trail is evaluated from bar data, so within a single bar the platform cannot know whether the high or the stop level was reached first.
  • strategy.exit() can combine a trailing stop with a fixed stop and a target in the same call, and whichever triggers first ends the trade.
ParameterUnitJobIf you omit it
trail_pointsTicksProfit required before the trail activatesThe trail activates immediately
trail_pricePriceActivation level as an absolute priceUse trail_points instead, not both
trail_offsetTicksDistance behind the best priceNo trailing stop is placed at all
stopPriceA fixed stop, independent of the trailOnly the trail protects the position
limitPriceA fixed target, independent of the trailThe trade only ends on a stop

What the two parameters actually do

Think of a trailing stop as having two phases. In the first phase nothing is trailing: the trade is open, price is moving, and the stop is either absent or fixed. The trail is dormant. The transition into the second phase happens when the trade has moved far enough in your favour, and trail_points is what defines "far enough" as a number of ticks of profit. Until that threshold is reached, the trail does not exist.

In the second phase the stop follows. It sits trail_offset ticks behind the most favourable price the trade has seen, and it updates whenever a new best price occurs. Crucially it only ever moves in the favourable direction. If price retraces, the stop stays where it was rather than backing off, and that ratchet behaviour is the entire point. A stop that loosened on a retracement would not be a trailing stop, it would be a stop that never fills.

Once the two phases are clear, the common failures explain themselves. Setting trail_offset without trail_points means there is no dormant phase, so the trail activates the moment the trade opens, which is legitimate but is a different rule from what most people describe. Setting trail_points without trail_offset means you have specified when to start trailing but not how far behind to trail, so no trailing stop is placed at all. And setting a trail_points value that price never reaches means the trail never wakes up, so the trade behaves exactly as though you had not written it.

pine
//@version=6
strategy("Trailing stop, both parameters", overlay = true)

// Both of these are TICK counts. Naming them so the unit is in the name
// is the cheapest defence against the classic mistake.
activateTicks = input.int(100, "Activate after (ticks of profit)", minval = 1)
trailTicks    = input.int(40,  "Trail behind by (ticks)",          minval = 1)

longSignal = ta.crossover(ta.sma(close, 10), ta.sma(close, 30))

if longSignal and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    strategy.exit("Long exit", from_entry = "Long",
         trail_points = activateTicks,
         trail_offset = trailTicks)

The variable names carry the unit, which is not stylistic fussiness. A reader who sees trailTicks knows immediately that 40 is forty ticks rather than forty dollars, and the person most likely to need that reminder is you, six months later, wondering why the stop is nowhere near where you thought.

Converting a price distance into ticks

Most trailing rules are conceived in price or in volatility terms, not in ticks. "Trail two ATR behind the high" is a natural way to describe a rule and a direct route to the wrong code, because ATR is a price distance and trail_offset is a tick count. The conversion is a single division by syminfo.mintick, which holds the tick size for the current symbol.

Doing the conversion explicitly, in a named variable, on its own line, is worth the three extra characters. It gives you something to print with log.info() when the stop is not where you expect, and it makes the units auditable rather than buried inside an argument list. Rounding matters too: a tick count has to be a whole number, so wrap the division in math.round() or math.floor() deliberately rather than letting the platform decide.

There is one trap in this conversion that catches people who have otherwise done everything right. syminfo.mintick varies by instrument, so a strategy that hardcodes a tick count works on the symbol it was developed on and silently means something different on any other. A hardcoded 40 ticks might be forty cents on one instrument and forty points on another. If a strategy is meant to be portable, derive the tick count from a price or volatility measure rather than typing a number.

pine
//@version=6
strategy("Trailing stop from ATR", overlay = true)

atrLength     = input.int(14,   "ATR length",                minval = 1)
activateMult  = input.float(2.0, "Activate after (x ATR)",   minval = 0.1)
trailMult     = input.float(1.5, "Trail behind by (x ATR)",  minval = 0.1)

atrValue = ta.atr(atrLength)

// One explicit conversion from price distance to ticks, per side.
// syminfo.mintick is the tick size of whatever symbol is on the chart,
// which is what makes this portable rather than hardcoded.
activateTicks = math.round(atrValue * activateMult / syminfo.mintick)
trailTicks    = math.round(atrValue * trailMult    / syminfo.mintick)

longSignal = ta.crossover(ta.sma(close, 10), ta.sma(close, 30))

if longSignal and strategy.position_size == 0
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0 and trailTicks > 0
    strategy.exit("Long exit", from_entry = "Long",
         // A fixed stop as a floor, in case price never reaches the
         // activation threshold and the trail never wakes up.
         stop         = strategy.position_avg_price - atrValue * activateMult,
         trail_points = activateTicks,
         trail_offset = trailTicks)

The fixed stop in that call is not redundant. A pure trailing stop offers no protection during the dormant phase, so a trade that goes against you immediately has nothing beneath it. Combining a fixed stop with a trail gives you a floor before activation and a ratchet after it, and whichever triggers first ends the trade.

trail_price versus trail_points

These are two ways to express the same activation threshold, and you use one or the other rather than both. trail_points says "activate after this many ticks of profit", which is relative to the entry price. trail_price says "activate when price reaches this level", which is absolute. Neither is better; they suit different ways of describing a rule.

trail_price is the natural choice when the activation level comes from something on the chart rather than from a distance. If you want the trail to begin once price clears a prior swing high, that swing high is a price, and converting it into a tick distance from entry is an unnecessary round trip that introduces a chance to get the arithmetic wrong. Pass the level directly.

trail_points is the natural choice when the rule genuinely is about distance travelled, and it has the advantage of being independent of the entry price, so it behaves consistently across trades. The thing to avoid is specifying both in the same call, which is ambiguous, and which is precisely the kind of double specification whose resolution changed between v5 and v6.

Why the backtest may flatter the trail

A trailing stop is unusually sensitive to the limits of bar data, and it is worth understanding why before drawing conclusions from a backtest. The trail follows the most favourable price reached, which within a bar means the high for a long position. But the stop level derived from that high, and the question of whether price then fell to that stop, both happen inside the same bar, and a bar’s four numbers cannot establish the sequence.

The consequence is that trailing results depend more on fill assumptions than most other exit types. On a bar with a large range, the trail may be credited with following price to the high and then stopping out at a level price technically visited, in an order that may or may not have occurred. This is not a Pine Script problem and no coding change fixes it. It is the nature of testing an intrabar mechanism against aggregated bars.

Two things reduce the distortion. Testing on a lower timeframe makes each bar smaller relative to the trail distance, so less of the mechanism happens inside a single bar. TradingView’s Bar Magnifier, on higher account tiers, uses lower-timeframe data to resolve intrabar sequence during a backtest. Neither eliminates the issue, and the honest reading of a trailing-stop backtest is that its exit prices carry more uncertainty than a fixed stop’s would.

When to write the trail yourself instead

The built-in trailing parameters cover a specific shape of rule: activate after a threshold, then follow at a fixed distance. Plenty of rules do not fit that shape. Trailing behind a moving average, behind the low of the last N bars, or at a distance that changes as the trade progresses are all reasonable ideas that trail_offset cannot express, because the offset is a constant tick count.

For those, maintain the level yourself in a var variable and pass it to the stop parameter, updating it on each bar with a math.max() so it can only ratchet upward for a long position. This is more code and it is entirely transparent: you can plot the level, print it, and reason about exactly when it moved. The one thing you must not forget is the ratchet. A stop recomputed each bar without math.max() will loosen whenever the underlying reference falls, which turns a trailing stop into a stop that follows price down.

That distinction is worth stating clearly because it is the most common bug in hand-rolled trailing stops. A stop set to the twenty-bar low, recomputed each bar, is not a trailing stop. It moves both ways. Wrapping it in math.max() against its own previous value is the single line that makes it one.

pine
//@version=6
strategy("Hand-rolled ratcheting trail", overlay = true)

trailMult = input.float(2.0, "Trail distance (x ATR)", minval = 0.1)
atrValue  = ta.atr(14)

var float trailLevel = na

longSignal = ta.crossover(ta.sma(close, 10), ta.sma(close, 30))

if longSignal and strategy.position_size == 0
    trailLevel := close - atrValue * trailMult
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    candidate = close - atrValue * trailMult
    // math.max is the ratchet. Without it the stop follows price down
    // as well as up, which is a different and much worse rule.
    trailLevel := math.max(trailLevel, candidate)
    strategy.exit("Long exit", from_entry = "Long", stop = trailLevel)

if strategy.position_size == 0
    trailLevel := na

plot(trailLevel, "Trailing stop", color = color.red, style = plot.style_linebr)

Resetting the level to na when flat matters as much as the ratchet. Without the reset, the next trade inherits the previous trade’s stop, which is either far below the new entry and therefore useless or above it and therefore triggers immediately. Plotting the level is what makes both mistakes obvious within seconds of loading the script.

Verifying a trailing stop

Plot the level and watch a few trades. A trailing stop has a distinctive visual signature: a line that steps upward and never downward while a long position is open, then disappears. If the plotted line moves down at any point, the ratchet is missing. If it never moves, the activation threshold was never reached or trail_offset was omitted. If it does not appear at all, the trail was never placed. Three different bugs, each identifiable at a glance.

Then check the exit prices in the List of Trades against the plotted line. The exit should sit at or very near the level the line had reached, and a systematic gap between them usually means a unit error: the trail is a different distance from what you specified because ticks and prices were mixed somewhere in the chain.

Print the tick conversion with log.info() at entry. The three numbers worth printing are syminfo.mintick, the computed activation tick count, and the computed trail tick count. Seeing them written out is the fastest way to catch an order-of-magnitude error, because a trail of 4 ticks and a trail of 400 look equally reasonable in source code and completely different in a log line.

Where a Pine-focused workflow helps

Trailing stops are a case where the failure mode of a general chat assistant is specific and predictable. Asked for a trailing stop, models routinely produce a strategy.exit() call that passes a price distance into trail_offset, because the parameter name says offset and nothing in the name says ticks. The code compiles, the backtest runs, and the result describes a rule nobody wrote. Because the output looks correct, this is one of the easier mistakes to ship.

PineScripter is the product we build, and the relevant difference is that it retrieves the Pine Script manual as context, so the unit of a parameter is something it can reference rather than infer from the name. Its edits are line-level and arrive as a diff, which matters here because the fix is usually one division and a rounding call rather than a new strategy.

The verification boundary is unchanged though, and for trailing stops it is worth repeating. No tool can tell you whether a trail distance is appropriate, and none can resolve the intrabar ambiguity that makes trailing backtests less reliable than fixed-stop ones. Plot the level, read a few trades, and treat the equity curve as the least informative output on the screen.

A unit fix is one line, which is why a line-level diff beats a regenerated script

Frequently asked questions

How do I add a trailing stop in Pine Script?

Call strategy.exit() with trail_points set to the profit in ticks required before the trail activates and trail_offset set to how far in ticks behind the best price the stop should follow. Both are tick counts. If you omit trail_offset no trailing stop is placed at all, and if you omit trail_points the trail activates immediately.

What is the difference between trail_points and trail_offset?

trail_points is an activation threshold, the amount of profit in ticks needed before trailing begins. trail_offset is the trailing distance, how far in ticks behind the most favourable price the stop sits once it is active. They do different jobs and most trailing stops need both.

Are trail_points and trail_offset in price or in ticks?

Ticks. One tick is syminfo.mintick for the current symbol, so a price distance must be divided by syminfo.mintick and rounded to a whole number before it is passed. Passing a price directly compiles and runs but produces a trail at a completely different distance than intended.

Why is my trailing stop not moving?

Either the activation threshold was never reached, so the trail is still dormant, or trail_offset was omitted, so no trailing order exists. Plot the stop level to tell the two apart: a line that never appears means no order was placed, and a line that appears but never steps up means the offset is far larger than you intended.

Why does my trailing stop move down as well as up?

That is a hand-rolled stop missing its ratchet, not a trailing stop. If you compute the level yourself each bar and pass it to the stop parameter, wrap it in math.max() against its own previous value for a long position so it can only ever move in the favourable direction. Also reset it to na when the position closes.

Can I use a trailing stop with a fixed stop and target together?

Yes. A single strategy.exit() call accepts stop, limit, and the trailing parameters at once, and whichever triggers first ends the trade. This is a common combination, because a pure trailing stop provides no protection before its activation threshold is reached.

The practical takeaway

A trailing stop is two decisions, not one: when it starts and how far behind it follows. Keep both in ticks, do the conversion from price on its own named line, add a fixed stop for the dormant phase, and plot the level so a missing ratchet or a wrong order of magnitude is visible immediately rather than after a hundred trades.

Because the fix for a broken trail is usually one division rather than a new strategy, PineScripter is our product and edits the lines in place so you can see exactly what changed. It cannot tell you what trail distance is appropriate, and the plotted stop level on your chart remains the real check.

Sources

Related reading: stop loss and take profit in Pine Script, position sizing in Pine Script, persistent state with var and varip, how TradingView backtesting works, the ATR stop loss calculator.

Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.