Edgoro
Guides

Pine Script strategy.exit(): stop loss and take profit that behave as you expect

How strategy.exit() works in Pine Script v6: price or tick-based stops and targets, levels fixed at entry, break-even, trailing stops, partial take-profits.

Most Pine Script strategies get their entries right and their exits subtly wrong. The stop drifts because it's recalculated every bar, the take-profit never triggers because the entry ID is misspelled, or a trailing stop activates at a distance nobody intended because the units were ticks, not price.

strategy.exit() is the function behind almost every stop-loss and take-profit in a TradingView strategy. This guide explains how it works in Pine Script v6, shows a complete example you can paste, and covers break-even moves, trailing stops and partial exits. The strategies here are examples for testing, not recommendations.

How strategy.exit() works

strategy.exit() places exit orders for a position opened by strategy.entry(). The important parameters:

  • id — a name for this exit, shown in the list of trades.
  • from_entry — the ID of the entry it protects. It must match exactly: "Long" and "long" are different.
  • The exit levels, as prices or as distances in ticks (next section).
  • qty_percent — the share of the position this exit covers, for partial exits.

The stop and limit from one call form a bracket: when one fills, the other is cancelled. Calling strategy.exit() again with the same id on a later bar replaces its orders with the new levels, which is how break-even and custom trailing logic work.

Prices or ticks

ParameterUnitMeaning
stopPriceStop-loss at this exact price
limitPriceTake-profit at this exact price
lossTicksStop-loss this many ticks from the entry price
profitTicksTake-profit this many ticks from the entry price
trail_pricePriceTrailing stop activates when price reaches this level
trail_pointsTicksTrailing stop activates after this much profit
trail_offsetTicksDistance the trailing stop keeps behind the best price

A tick is syminfo.mintick, the smallest price increment of the symbol: 0.00001 on many five-digit forex feeds, 0.01 on many stocks, 0.25 on some index futures. To turn a price distance into ticks, divide by syminfo.mintick. For each leg, pass either the price version or the tick version; mixing them for the same leg makes the behaviour harder to reason about, and the rules for that case changed between v5 and v6.

A complete example

This strategy enters on a 20/50 EMA cross, then sets a stop by ATR, percentage or ticks, a target at a multiple of the stop distance, and an optional move to break-even:

//@version=6
strategy("strategy.exit: stop, target and break-even (example)", 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)

stopMode  = input.string("ATR", "Stop type", options = ["ATR", "Percent", "Ticks"])
atrMult   = input.float(1.5, "Stop (x ATR)", minval = 0.1, step = 0.1)
stopPct   = input.float(1.0, "Stop (%)", minval = 0.1, step = 0.1)
stopTicks = input.int(200, "Stop (ticks)", minval = 1)
rr        = input.float(2.0, "Target (x stop distance)", minval = 0.5, step = 0.25)
useBe     = input.bool(true, "Move the stop to break-even at 1R")

atr        = ta.atr(14)
longSignal = ta.crossover(ta.ema(close, 20), ta.ema(close, 50))

stopDistance = switch stopMode
    "ATR"     => atrMult * atr
    "Percent" => close * stopPct / 100
    => stopTicks * syminfo.mintick

var float stopPrice   = na
var float targetPrice = na
var float riskDist    = na

if longSignal and strategy.position_size == 0
    stopPrice   := close - stopDistance
    targetPrice := close + rr * stopDistance
    riskDist    := stopDistance
    strategy.entry("Long", strategy.long)

// Break-even: once the high has reached entry + 1R, lift the stop to the entry price.
if useBe and strategy.position_size > 0 and high >= strategy.position_avg_price + riskDist
    stopPrice := math.max(stopPrice, strategy.position_avg_price)

strategy.exit("Exit", "Long", stop = stopPrice, limit = targetPrice)

plot(strategy.position_size > 0 ? stopPrice : na, "Stop", color = color.new(color.red, 0), style = plot.style_linebr)
plot(strategy.position_size > 0 ? targetPrice : na, "Target", color = color.new(color.teal, 0), style = plot.style_linebr)

Fix the levels when you enter

The most common exit bug looks like this: strategy.exit("Exit", "Long", stop = close - 2 * ta.atr(14)), called on every bar. It compiles and runs, but the stop is recalculated from the latest close and ATR on each bar, so it moves up and down with price instead of staying where the trade idea is invalidated.

The example stores the levels in var variables at the moment of the signal. var keeps a value across bars, so the stop stays put until your code deliberately changes it.

One subtlety: the levels are computed from the signal bar's close, but the entry fills at the next bar's open. If the market gaps, the real distance to the stop differs from what you planned. If that matters for your market, re-anchor the levels to strategy.position_avg_price (the actual fill price) on the first bar of the position. The trade-off is that the stop then only becomes active one bar after the fill.

Moving the stop to break-even

The break-even block runs on every bar while a position is open. Once the bar's high has reached the entry price plus the original risk (1R), it raises stopPrice to the entry price. Because strategy.exit() is called again with the same ID, the stop order is replaced at the new level from the next bar. math.max() makes sure the stop can only move up.

Break-even stops feel safe, but they change a strategy's statistics a lot: more small scratch trades, fewer full losses, and some winners cut off before they develop. Test with and without it.

Trailing stops and partial take-profits

The built-in trailing stop uses ticks. trail_points sets how much profit is needed before trailing starts, and trail_offset how far behind the best price the stop follows. This second example closes half the position at 1R and lets the rest run with a trailing stop that activates at 2R:

//@version=6
strategy("strategy.exit: partial target and trailing stop (example)", 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)

atrMult   = input.float(1.5, "Stop (x ATR)", minval = 0.1, step = 0.1)
trailMult = input.float(1.0, "Trail offset (x ATR)", minval = 0.1, step = 0.1)

atr        = ta.atr(14)
longSignal = ta.crossover(ta.ema(close, 20), ta.ema(close, 50))

var float stopPrice = na
var float tp1       = na
var float trailOn   = na   // profit, in ticks, at which trailing starts
var float trailOff  = na   // trailing distance, in ticks

if longSignal and strategy.position_size == 0
    dist      = atrMult * atr
    stopPrice := close - dist
    tp1       := close + dist
    trailOn   := 2 * dist / syminfo.mintick
    trailOff  := trailMult * atr / syminfo.mintick
    strategy.entry("Long", strategy.long)

// Half the position exits at 1R; the other half keeps the stop and trails once 2R is reached.
strategy.exit("TP1", "Long", qty_percent = 50, stop = stopPrice, limit = tp1)
strategy.exit("Runner", "Long", stop = stopPrice, trail_points = trailOn, trail_offset = trailOff)

Each exit covers its own share of the position, and both carry the same protective stop, so the whole position is always protected. For a trailing stop based on your own rule, such as an ATR trail or the Supertrend line, compute the level yourself each bar and pass it as stop, only ever moving it in the trade's favour.

How the backtester decides what filled

On historical bars TradingView only knows each bar's open, high, low and close, not the path between them. When a stop and a target both sit inside one bar's range, it assumes a path: if the open is closer to the high, price is assumed to go to the high first, then the low; if the open is closer to the low, the reverse. That assumption can flatter or punish tight brackets on higher timeframes.

Two ways to get closer to reality: use the Bar Magnifier option where your plan provides it, which resolves fills with lower-timeframe data, and avoid targets and stops so close that several fit inside a typical bar. More on this in backtesting mistakes that inflate results.

Quick fixes for common problems

SymptomLikely causeFix
Exits never triggerfrom_entry doesn't match the entry IDCopy the ID exactly, including case
Stop moves every barLevel recalculated from current valuesStore it in a var variable at entry
Trailing starts far too early or latePrice distance passed where ticks are expectedDivide the distance by syminfo.mintick
Only part of the position closesqty_percent below 100 with no second exitAdd an exit for the remainder
Fills look too goodStop and target inside the same barBar Magnifier, wider levels, lower timeframe

Sources and further reading

Try it with Edgoro

Describe your exit rules — "stop at 1.5 × ATR fixed at entry, take half off at 1R, move the stop to break-even, trail the rest by 1 × ATR" — and Edgoro writes the Pine Script v6 strategy, or the same exits in an MT5 Expert Advisor. Build your strategy with Edgoro.

These strategies are examples for testing and education, not recommendations. Backtested results are hypothetical, and trading involves substantial risk of loss.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro