Stops, targets and strategy.exit
Lesson 17 · about 12 min
strategy.exit() is how a strategy places the protective orders that a real trade would have: a stop loss, a profit target, or a trailing stop, attached to a specific entry. It is more capable and more confusing than strategy.close, and most backtests that "can't be right" have a strategy.exit argument in the wrong units.
The call
strategy.exit(id, from_entry, qty, qty_percent, profit, limit, loss, stop, trail_price, trail_points, trail_offset, comment, alert_message)
id: the exit's own label.from_entry: the entry ID this exit protects. Omit it to attach to every open entry.stopandlimit: absolute prices for the stop loss and the take profit.lossandprofit: the same levels expressed as a distance in ticks from the entry price.trail_pointsandtrail_offset: trailing stop activation and distance, in ticks.trail_price: activation as a price.qtyorqty_percent: exit part of the position.
Prices versus ticks is the trap. loss=50 on a stock that trades in cents is a 50-cent stop; on an index future with a 0.25 tick it is a 12.5-point stop. Use stop= and limit= with prices you have computed, and you will always know what you meant.
Attaching exits to an entry
//@version=6
strategy("Breakout with ATR stop", overlay=true, initial_capital=10000,
default_qty_type=strategy.percent_of_equity, default_qty_value=10, pyramiding=0)
len = input.int(20, "Breakout length", minval=2)
atrMult = input.float(2.0, "ATR stop multiple", minval=0.5, step=0.25)
rr = input.float(2.0, "Reward to risk", minval=0.5, step=0.25)
atr = ta.atr(14)
breakout = close > ta.highest(high, len)[1]
var float stopPrice = na
var float targetPrice = na
if breakout and strategy.position_size == 0
stopPrice := math.round_to_mintick(close - atr * atrMult)
targetPrice := math.round_to_mintick(close + (close - stopPrice) * rr)
strategy.entry("Long", strategy.long)
strategy.exit("Exit", "Long", stop=stopPrice, limit=targetPrice)
plot(strategy.position_size > 0 ? stopPrice : na, "Stop", color=color.red, style=plot.style_linebr)
plot(strategy.position_size > 0 ? targetPrice : na, "Target", color=color.green, style=plot.style_linebr)
Calling strategy.exit in the same block as the entry is correct. The exit is tied to the entry ID and becomes active once the entry fills at the next open; until then it waits. The stop and target are computed from the signal bar's close, but the fill is at the next open, so the actual risk per trade differs slightly from the plan. That is realistic; you would face the same gap trading it by hand.
Rounding with math.round_to_mintick matters because the emulator rejects prices that are not multiples of the tick.
Exit orders persist
Once placed, an exit stays active until it fills or you cancel it (strategy.cancel("Exit"), or the position closes some other way). You do not have to call strategy.exit on every bar. Calling it every bar with a changing price is how you build a manual trailing stop:
var float trail = na
if strategy.position_size > 0
trail := na(trail) ? stopPrice : math.max(trail, close - atr * atrMult)
strategy.exit("Exit", "Long", stop=trail, limit=targetPrice)
else
trail := na
Each call replaces the previous exit with the same ID, so the stop ratchets up and never down.
The built-in alternative is trail_points (how far in profit, in ticks, before the trail activates) with trail_offset (how far behind price it follows). It is convenient and hard to reason about across symbols; the manual version above is explicit and works anywhere.
Both levels in one bar
If a bar's range covers both the stop and the target, the emulator has to guess which was hit first. Its rule: if the bar's high is closer to the open than its low is, price is assumed to have gone open, high, low, close; otherwise open, low, high, close. That is a coin toss dressed as a rule, and on volatile bars it decides wins versus losses. The bar magnifier option in module 7 replaces the guess with lower-timeframe data. Until then, treat strategies with tight stops and targets that both fit inside typical bars as unreliable.
Pyramiding
pyramiding=3 allows three entries in the same direction. Each strategy.entry with the same ID adds to the position; different IDs let you exit legs separately. A strategy.exit without from_entry covers all of them. Adding to winners is a legitimate technique; adding to losers ("averaging down") in a strategy is a fast way to build a backtest that survives everything except the one drawdown that ends it. The tester will not stop you.
Time stops
Exiting after N bars needs no special function:
if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 10
strategy.close("Long", comment="Time stop")
strategy.opentrades.entry_bar_index(0) is the bar index where the oldest open trade entered.
Key idea:
strategy.exit("Exit", "Long", stop=price, limit=price)attaches a stop and a target to an entry, in prices, and stays active until filled; calling it again with the same ID replaces it, which is how trailing stops are built.
Try it: Add the breakout strategy to a daily chart and open the trade list. Find a trade that exited at the stop and check that the exit price equals the plotted stop line (or the next open if it gapped). Then change
stop=stopPricetoloss=50and see what happens to every stop.
Recap
stopandlimittake prices;lossandprofittake ticks. Prefer prices you computed and rounded withmath.round_to_mintick.- Place
strategy.exitin the same block as the entry; it activates when the entry fills. - Exits persist; re-calling with the same ID replaces the order, which is how manual trailing stops work.
- When one bar contains both stop and target, the emulator guesses the order of touches unless the bar magnifier is on.
pyramidingallows multiple same-direction entries; time stops usestrategy.opentrades.entry_bar_index.
See it drawn
Original diagrams for the ideas on this page. Illustrative, not real market data.