◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / How to Add Stop-Loss and Take-Profit in Pine Script
Pine Script

How to Add Stop-Loss and Take-Profit in Pine Script

A practical, code-first guide to adding stop-loss and take-profit to a Pine Script v6 strategy using strategy.exit with stop= and limit=. Covers fixed-distance versus ATR-based levels, long and short handling, and the difference between a backtest result and a live trading outcome. This is educational material only, not financial advice; trading involves real risk of loss, and a tester result never guarantees future performance.

Why a strategy without exits is only half a strategy

When you build a strategy() in Pine Script, it's easy to focus entirely on entries: the moving-average cross, the breakout, the oscillator condition. But an entry rule alone tells the backtester only when to open a position, not when to close it. Without exit logic, your strategy holds every trade open until an opposite signal appears, which can mean sitting through enormous adverse moves on a single position. That is rarely how anyone intends to trade, and it makes the backtest behaviour almost meaningless as a guide to how the rules actually manage risk.

A stop-loss is the price level at which you exit to cap a losing trade. A take-profit (also called a target or limit) is the price level at which you exit to close a position in profit. In Pine Script v6, both are expressed through a single function, strategy.exit(), which can attach a protective stop and a target to an open position in one call. Understanding how that function maps your intended risk onto actual price levels is the core skill this article builds — the whole topic of a Pine Script stop loss take profit bracket comes down to it.

A quick framing before any code: everything here is for education only. Adding a stop-loss and take-profit changes the shape of your results, but it does not make a strategy profitable, and nothing in this article is a recommendation to trade. Markets move against well-built rules all the time. Trading involves a real risk of loss, and the goal here is correctness and clarity of mechanics, not returns.

strategy.exit: how stop= and limit= actually work

The function that does the work is strategy.exit(). The two parameters that matter most for risk control are stop= (the stop-loss price) and limit= (the take-profit price). Both expect an absolute price level, not a distance and not a percentage. This trips up many beginners: if you want a stop "20 points below entry," you have to compute entryPrice - 20 yourself and pass that result to stop=.

Every exit also needs an id and a from_entry reference so the engine knows which open position it is protecting. The from_entry string must match the id you used in your strategy.entry() call. When both stop= and limit= are supplied in the same strategy.exit() call, they form a bracket: whichever level price reaches first closes the trade, and the other order is cancelled automatically. That bracket behaviour is exactly what you want for a clean one-stop-one-target setup.

A few mechanics worth internalising. The pyramiding and order-sizing settings live in strategy(), not in the exit. The stop and target levels should be computed from a captured entry price (using strategy.position_avg_price once filled) rather than re-derived from a moving close, so they don't drift bar to bar. And because strategy.exit() registers standing orders that the engine checks against the price action itself, you generally do not gate the exit call on barstate.isconfirmed — you want those protective orders live. Confirmation gating belongs on the entry signal, so you aren't acting on an unconfirmed, still-repainting condition on the developing bar.

A clean v6 strategy with a fixed stop and target

Here is a complete, compilable Pine Script v6 strategy. The entry is a simple EMA cross used only as a vehicle; the focus is the risk block. Note how the entry signal is confirmed with barstate.isconfirmed to avoid acting on a repainting bar, while the exit orders are registered unconditionally so the protection is always active.

//@version=6
strategy("Fixed SL/TP Example", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// Inputs for risk distances, expressed in price points
stopPoints   = input.float(20.0, "Stop distance (points)", minval=0.0)
targetPoints = input.float(40.0, "Target distance (points)", minval=0.0)

// Entry vehicle: a confirmed EMA cross (not the point of the article)
fastEma = ta.ema(close, 9)
slowEma = ta.ema(close, 21)
longSignal = ta.crossover(fastEma, slowEma) and barstate.isconfirmed

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

// Compute absolute SL/TP levels from the average fill price
entryPrice = strategy.position_avg_price
longStop   = entryPrice - stopPoints
longTarget = entryPrice + targetPoints

// Attach the protective bracket while a long is open
if strategy.position_size > 0
    strategy.exit("Exit Long", from_entry="Long", stop=longStop, limit=longTarget)

plot(strategy.position_size > 0 ? longStop : na, "Stop", color.red, style=plot.style_linebr)
plot(strategy.position_size > 0 ? longTarget : na, "Target", color.green, style=plot.style_linebr)

Walking through it: stopPoints and targetPoints are user inputs in price units. On a confirmed cross with no open position, strategy.entry() opens a long. Once filled, strategy.position_avg_price gives the real average entry, from which the absolute longStop and longTarget are derived. The strategy.exit() call then brackets the position — first level touched wins, the other cancels. Plotting the two levels with plot.style_linebr lets you see them on the chart and sanity-check that they sit where you expect.

ATR-based stops: adapting distance to volatility

Fixed point distances have an obvious weakness: 20 points might be a tight, easily-stopped distance on a volatile instrument and an absurdly wide one on a quiet one. A common alternative is to size the stop relative to recent volatility using the Average True Range, ta.atr(). The idea is to express the stop as a multiple of ATR so the distance breathes with the market rather than staying constant.

The change to the previous example is small and local — you replace the fixed point distances with ATR multiples and recompute the levels:

//@version=6
strategy("ATR SL/TP Example", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

atrLen    = input.int(14, "ATR length", minval=1)
stopMult  = input.float(1.5, "Stop ATR mult", minval=0.0)
tpMult    = input.float(3.0, "Target ATR mult", minval=0.0)

atr = ta.atr(atrLen)

fastEma = ta.ema(close, 9)
slowEma = ta.ema(close, 21)
longSignal = ta.crossover(fastEma, slowEma) and barstate.isconfirmed

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

entryPrice = strategy.position_avg_price
longStop   = entryPrice - atr * stopMult
longTarget = entryPrice + atr * tpMult

if strategy.position_size > 0
    strategy.exit("Exit Long", from_entry="Long", stop=longStop, limit=longTarget)

Notice the structure is identical — only the level arithmetic changed. With stopMult at 1.5 and tpMult at 3.0, the target distance is twice the stop distance, giving a 2:1 reward-to-risk geometry on each trade. Be careful with that phrasing, though: a 2:1 distance ratio describes the shape of the bracket, not an expectation of profit. Whether price reaches the target more often than the stop depends entirely on the market and the entry, and is something a backtest can only describe historically, never promise forward.

If your strategy can go short as well, you simply mirror the maths: for a short, the stop sits above entry (entryPrice + atr * stopMult) and the target below it (entryPrice - atr * tpMult), attached with a separate strategy.exit() referencing your short entry's id. A quick self-audit catches the slips that creep in here — an inverted short stop, or a level computed from close instead of the fill price — before it quietly corrupts your results. Tooling such as the Audit and Fixer features at ForexCodes is built to flag exactly those mistakes, but a careful manual read of the level arithmetic will find most of them too.

A tester result is not a live result

Once your stop and target are in place, TradingView's Strategy Tester will report metrics: net profit, win rate, max drawdown, profit factor. It is genuinely useful to see how exits reshape these numbers — a strategy that looked fine without risk control often looks very different once a stop is enforced. But it is critical to read these as a historical description, not a forecast.

Several gaps separate a backtest from live trading. The tester fills your stop and limit orders at idealised prices; live markets have slippage, spreads, and gaps that can fill you worse — or skip your level entirely on a fast move. Commission and funding costs are easy to under-model. And the deeper problem is that any rule tuned to look good on past data may simply be fitting noise that won't recur. A stop-loss caps the size of a known historical loss; it does nothing to guarantee the distribution of future outcomes resembles the past.

So treat the workflow honestly. Use the stop-and-target mechanics in this article to make your strategy's intent explicit and its risk-per-trade bounded. Validate the logic — confirm the levels plot where you expect, that shorts mirror longs correctly, that nothing repaints. Then hold the performance numbers loosely. None of this is financial advice, and adding risk control does not make any strategy a money-maker. The honest framing is the most valuable thing you can take away: trading carries a real risk of loss, a passing backtest is not a live edge, and good code is necessary but never sufficient.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro