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
| Parameter | Unit | Meaning |
|---|---|---|
stop | Price | Stop-loss at this exact price |
limit | Price | Take-profit at this exact price |
loss | Ticks | Stop-loss this many ticks from the entry price |
profit | Ticks | Take-profit this many ticks from the entry price |
trail_price | Price | Trailing stop activates when price reaches this level |
trail_points | Ticks | Trailing stop activates after this much profit |
trail_offset | Ticks | Distance 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Exits never trigger | from_entry doesn't match the entry ID | Copy the ID exactly, including case |
| Stop moves every bar | Level recalculated from current values | Store it in a var variable at entry |
| Trailing starts far too early or late | Price distance passed where ticks are expected | Divide the distance by syminfo.mintick |
| Only part of the position closes | qty_percent below 100 with no second exit | Add an exit for the remainder |
| Fills look too good | Stop and target inside the same bar | Bar Magnifier, wider levels, lower timeframe |
Sources and further reading
- Pine Script v6 reference: strategy.exit() — every parameter, including
stop,limit,loss,profitand the trailing options. - Pine Script User Manual: Strategies — how the broker emulator decides which orders fill within a bar.
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.