Guide

Stop Loss and Take Profit in Pine Script: A Full Guide

strategy.exit() has two ways to express a stop and two ways to express a target, and mixing them up is why exits land at prices you did not ask for.

13 min read

Almost every confusing exit in Pine Script traces back to one detail: strategy.exit() accepts stops and targets in two different units, and the parameter names do not make that obvious. stop and limit take a price. loss and profit take a number of ticks. Pass a price where ticks were expected and the script still compiles, still runs, and places an exit somewhere absurd.

Short answer

Use strategy.exit() with the stop parameter for a stop loss at a specific price and the limit parameter for a take profit at a specific price. Use loss and profit instead when you want to express the distance in ticks rather than as a price level. Every strategy.exit() call must name the entry it applies to through from_entry, and it must specify at least one exit condition or it will not compile. This article covers the mechanics of coding those exits; it does not suggest where any stop or target belongs, which is entirely your decision.

Key facts

  • strategy.exit() takes stop and limit as absolute price levels, and loss and profit as distances measured in ticks.
  • A tick is syminfo.mintick for the current symbol, so converting a price distance to ticks means dividing by syminfo.mintick.
  • from_entry links the exit to a specific entry id. Without it, the exit applies to the whole position rather than the entry you meant.
  • A strategy.exit() call with no exit condition specified does not compile, which is the one mistake in this area the editor catches for you.
  • In Pine v5, when both an absolute and a relative parameter were given for the same side, the absolute value won. In v6, whichever triggers first wins.
  • The when parameter was removed from the strategy order functions in v6, so a conditional exit is wrapped in an if instead.
  • strategy.close() exits at market on the next bar open rather than at a predetermined level, which is a different tool from strategy.exit().
  • When a bar’s range contains both the stop and the target, a single bar’s OHLC cannot establish which came first, so the result depends on the platform’s fill assumptions rather than on your code.
ParameterUnitMeaningUse when
stopPriceStop loss at this exact priceYou computed a level, such as entry minus 2 ATR
lossTicksStop loss this many ticks from entryYou want a fixed distance regardless of entry
limitPriceTake profit at this exact priceYou computed a target level
profitTicksTake profit this many ticks from entryYou want a fixed distance regardless of entry
from_entryStringWhich entry this exit belongs toAlways, once you have more than one entry id
qty_percentPercentExit part of the positionScaling out in stages

The unit problem, and how to avoid it entirely

Start with the failure, because recognising it saves hours. Suppose you have computed a stop level of 180.50 and you write it into the loss parameter. Pine reads that as 180.5 ticks. On an instrument with a tick size of one cent, that is a stop about a dollar and eighty cents from entry, which is not what you meant but is entirely plausible, so nothing looks wrong. On an instrument with a larger tick size it might be a stop hundreds of points away, which effectively means no stop at all. The strategy runs, produces results, and those results describe a rule you never wrote.

The reverse mistake is louder and therefore less dangerous. Passing a small tick distance such as 40 into the stop parameter asks for a stop at a price of 40, which on most instruments is either far below the market or nonsensical, and the trade behaves so strangely that you investigate immediately.

The habit that removes the whole category of problem is to pick one unit and stay in it. Working in prices is usually clearer, because a stop level is something you can see on the chart and verify against the List of Trades, so use stop and limit and compute the levels explicitly. If you genuinely want a fixed tick distance, convert once, name the variable so the unit is in the name, and never let a price and a tick count share a variable name pattern.

pine
//@version=6
strategy("Stop and target as prices", overlay = true)

atrMult    = input.float(2.0, "Stop distance (x ATR)", minval = 0.1)
rewardMult = input.float(2.0, "Target as multiple of risk", minval = 0.1)

atrValue = ta.atr(14)

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

// Compute the levels as prices, and hold them so they do not drift as
// the bars advance. var means these survive from bar to bar.
var float stopLevel   = na
var float targetLevel = na

if longSignal and strategy.position_size == 0
    stopDistance = atrValue * atrMult
    stopLevel   := close - stopDistance
    targetLevel := close + stopDistance * rewardMult
    strategy.entry("Long", strategy.long)

// stop and limit both take prices, so the units match the variables.
if strategy.position_size > 0
    strategy.exit("Long exit", from_entry = "Long", stop = stopLevel, limit = targetLevel)

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

Two details in that snippet matter more than they look. The levels are stored in var variables so they are fixed at entry rather than recomputed on every bar, which is what stops a stop from drifting as ATR changes. And they are plotted, which is the cheapest verification available: if the plotted line does not sit where you expect, you have found the bug without reading a single trade.

Why exits fire on a bar you did not expect

A backtest works from bar data, and a bar is four numbers: open, high, low, close. If a bar’s range covers both your stop and your target, those four numbers cannot tell anyone which level the price reached first. The information simply is not in the data. This is not a shortcoming of Pine Script or of TradingView; it is a property of aggregated bars, and every backtesting platform faces it.

What follows is that on such a bar the outcome is decided by the platform’s assumptions rather than by your code. That has a practical consequence worth internalising: the tighter your stop and target are relative to the size of a bar, the more of your results are being produced by fill assumptions instead of by your rules. A strategy whose stop and target both sit inside a typical bar’s range is not really being tested, and no amount of correct Pine Script changes that.

The mitigations are worth knowing. Testing on a lower timeframe makes each bar smaller relative to your levels, so fewer bars contain both. TradingView’s Bar Magnifier, available on higher account tiers, uses lower-timeframe data to resolve intrabar sequence during a backtest. And there are declaration-level settings that change when orders are processed, including process_orders_on_close, which fills at the close of the signal bar rather than the next open. Each changes the numbers, so change one at a time and know which one you changed.

from_entry is not optional in practice

The from_entry parameter names which entry an exit belongs to. It is easy to skip when a strategy has a single entry, and skipping it builds a habit that breaks the moment there are two. Once you have separate long and short entries, or two entries with different stop logic, an exit without from_entry does not know which one it is protecting.

The rule that keeps this straight is to give every entry an explicit id string and use that same string in the corresponding exit. It costs nothing and it makes the pairing visible when you read the code. The ids also appear in the List of Trades, which means a meaningful id turns the trade log from a wall of anonymous rows into something you can actually reconcile against the code.

A related point about scaling out. The qty_percent parameter lets one exit close part of a position, which is how a partial target is expressed. Two exits on the same entry, one taking half at a first target and one taking the rest at a second, is a common structure and it works. What causes confusion is forgetting that the remaining half still needs a stop, so the second exit call has to specify one rather than assuming the first one still applies.

pine
//@version=6
strategy("Scaling out with two targets", overlay = true)

atrValue = ta.atr(14)

var float stopLevel    = na
var float firstTarget  = na
var float secondTarget = na

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

if longSignal and strategy.position_size == 0
    risk         = atrValue * 2
    stopLevel   := close - risk
    firstTarget := close + risk
    secondTarget := close + risk * 3
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    // Take half at the first target, and keep a stop on that half.
    strategy.exit("Half out", from_entry = "Long", qty_percent = 50,
         stop = stopLevel, limit = firstTarget)
    // The remainder needs its own exit, with its own stop.
    strategy.exit("Rest out", from_entry = "Long",
         stop = stopLevel, limit = secondTarget)

Both calls repeat the stop level deliberately. An exit call describes a complete set of conditions for the quantity it governs, so the second call is not inheriting anything from the first. Leaving the stop off the second call would leave the remaining half of the position with a target and nothing else.

What changed in v6, and why old code behaves differently

Two v6 changes affect exits directly. The when parameter is gone from the strategy order functions, having been deprecated in v5. Code that used it now fails to compile, and the replacement is to wrap the call in an if statement. That is a mechanical fix and the built-in converter in the Pine Editor handles it.

The second change is subtler and the converter cannot decide it for you. When a single exit call specified both an absolute parameter and a relative one for the same side, for instance both limit and profit, v5 resolved the conflict by letting the absolute value win. In v6, whichever level triggers first wins. A script that specified both, perhaps without the author noticing, can therefore exit at a different level after migration while compiling cleanly and reporting no error at all.

The practical response is to audit any exit call that passes more than one parameter per side. In most cases the right fix is to remove the redundancy: decide whether you are working in prices or in ticks and pass only that one. An exit call that specifies a stop two different ways is ambiguous to a reader regardless of which version resolves it, so removing the ambiguity is an improvement independent of the migration.

strategy.exit or strategy.close

These solve different problems and are not interchangeable. strategy.exit() places conditional orders that sit and wait: a stop and a target that will fill if price reaches them. strategy.close() exits at market, on the next bar’s open by default, when the code decides to call it. One is a level, the other is a decision.

The distinction matters for rules like "exit when the trend reverses". That is not a price level, it is a condition evaluated on each bar, so it belongs in a strategy.close() call inside an if rather than in a stop parameter. Trying to express it as a level means recomputing the level on every bar, which produces a stop that moves around unpredictably and results that cannot be reconciled with the chart.

Many strategies want both, and combining them is fine. A stop and a target from strategy.exit() protect the position at fixed levels, while a strategy.close() call handles a discretionary reversal condition. Just be aware that whichever happens first ends the trade, so a reversal condition that fires frequently will mean the stop and target rarely get tested, which changes what your results are actually measuring.

Verifying the exit rather than trusting it

Plot the levels. This is the highest-value habit in this entire article, and it takes one line per level. A stop plotted on the chart is either where you expect or it is not, and the answer arrives instantly. Use plot.style_linebr and a value of na when flat, so the line appears only while a position is open rather than drawing a misleading horizontal across the whole chart.

Then reconcile one trade by hand. Open the List of Trades, take a single closed trade, and check that its exit price matches the level your rule should have produced from the data at entry. If the exit sits at a price that is neither your stop nor your target, you have learned something specific rather than something vague. A frequent finding is that the exit is at a bar open, which means it came from a strategy.close() rather than the level you were investigating.

Use log.info() for the values you cannot see. Printing the stop level, the target level, and the tick size at the moment of entry turns unit errors from an inference into a fact. This is the fastest way to catch a price-where-ticks-were-expected mistake, because the printed number will be obviously wrong in a way the equity curve never is. Our guide to runtime logging covers the mechanics.

Where a Pine-focused workflow helps

Exit logic is spread across the declaration, the entry, and one or more exit calls, and it has to stay internally consistent. That makes it a poor fit for the paste-and-regenerate loop: asked to fix an exit, a general-purpose chat assistant commonly returns an entire new strategy, and you then cannot tell whether the numbers moved because the exit was fixed or because the entry, the sizing, and the declaration all changed alongside it.

PineScripter is the product we build, and this is the case it targets. It retrieves the Pine Script v6 manual as context, so a suggestion can reference documented behaviour such as which parameters take ticks or how v6 resolves a conflicting pair, rather than guessing from the parameter names. Its edits are line-level and arrive as a diff, which is what you want when a change has to keep three related call sites in agreement.

The limit is worth being clear about. No tool should decide where your stop belongs, and a coding assistant cannot make a strategy behave better. What it can do is keep the translation from rule to code faithful and the edits small enough to review. The Strategy Tester and the plotted levels on your chart remain the only real verification.

Describe the exit rule in plain English, then check the generated levels on the chart

Frequently asked questions

How do I add a stop loss and take profit in Pine Script?

Call strategy.exit() with from_entry naming your entry id, then stop for the stop loss price and limit for the take profit price. If you would rather express the distances in ticks than as prices, use loss and profit instead. A strategy.exit() call must specify at least one exit condition or it will not compile.

What is the difference between stop and loss in strategy.exit()?

Units. stop takes an absolute price level, so you pass the price you want the stop to sit at. loss takes a number of ticks from the entry price. Passing a price into loss compiles and runs but produces a stop at a completely different distance than intended, which is the most common exit bug in Pine Script.

Why did my take profit and stop loss both trigger on the same bar?

A bar’s open, high, low, and close cannot establish the order in which price visited levels inside that bar. When both your stop and your target fall within one bar’s range, the outcome is determined by the platform’s fill assumptions rather than by your code. Testing on a lower timeframe or using TradingView’s Bar Magnifier reduces how often this happens.

How do I convert a price distance into ticks for the loss parameter?

Divide the price distance by syminfo.mintick, which is the tick size of the current symbol. A stop two dollars away on an instrument with a one-cent tick is 200 ticks. Naming the resulting variable something like stopTicks keeps the unit visible at the call site.

Why does my exit not work after migrating to Pine v6?

Most likely the when parameter, which was removed from the strategy order functions in v6. Wrap the call in an if instead. Separately, if an exit call specified both an absolute and a relative parameter for the same side, v5 let the absolute value win while v6 lets whichever triggers first win, so the exit price can change without any error appearing.

Should I use strategy.exit or strategy.close?

Use strategy.exit() for exits at predetermined price levels, such as a stop or a target, because it places orders that wait for price to arrive. Use strategy.close() for exits driven by a condition rather than a level, such as a trend reversal, because it exits at market when the condition is met. Many strategies use both.

The practical takeaway

Exits fail for boring reasons: the wrong unit, a missing from_entry, a level recomputed when it should have been fixed, or a bar that contained both the stop and the target. Pick one unit and name your variables after it, store levels in var at entry, plot them so mistakes are visible, and reconcile a single trade by hand before you believe an equity curve.

Because an exit change has to stay consistent with the entry and the declaration at the same time, PineScripter is our product and shows all of them as one reviewable diff. It will not decide where your stop belongs, and the plotted levels on your chart are still the check that matters.

Sources

Related reading: position sizing in Pine Script, coding a trailing stop, why a strategy takes no trades, how TradingView backtesting works, the ATR stop loss calculator, the risk/reward ratio 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.