You have a strategy with a stop loss that works. Then you add one more rule: once the trade has moved far enough in your favour, pull the stop up to the entry price so the position can no longer close at a loss. It sounds like a one-line change. In practice it is the point where many working exit blocks quietly stop working, because a stop that moves is no longer a fixed number you can pass to strategy.exit() once and forget.
This guide builds a break-even stop in Pine Script v6 from the ground up. We start with why the logic needs state, write a long-only version, extend it to both directions with a small buffer, and then look at the parts that trip people up: which bar the move takes effect on, what the Strategy Tester can and cannot know about the order of events inside a bar, and why the move changes your statistics in ways you should measure rather than assume. If you have not yet coded a fixed stop, read Stop Loss and Take Profit in Pine Script first, because everything here builds on strategy.exit().
Why a break-even stop needs state
A fixed stop is a pure function of the entry. You compute entry - 2 * atr once, hand it to strategy.exit(), and every later bar sees the same order. A break-even stop depends on something that happens after the entry: price reaching a trigger level. Pine Script runs your code once per bar, from the top, so on the bar after the trigger is hit the script has no memory that it was hit unless you stored that fact yourself.
That stored fact is a variable declared with var, which keeps its value from bar to bar instead of being reset. If var is new to you, var, varip, and Persistent State in Pine Script explains the rules. For this build we need three persistent values: the current stop level, the trigger level, and a flag saying whether the stop has already been moved. Without the flag, the script would keep recomputing the stop from the original formula on every bar and undo the move as soon as it made it.
There is a second reason state matters. The entry price is not known when the entry condition is true. A market order placed by strategy.entry() fills at the open of the next bar, so the real fill price only exists once the position does. The reliable source is strategy.position_avg_price, which holds the average entry price of the open position. Anything that should be measured from the entry, including the initial stop, the trigger, and the break-even level itself, should be computed from that value after the position exists, not from the close of the signal bar.
The anatomy of the move
Every break-even rule is made of the same four decisions, and it helps to name them before writing code.
The first is the initial stop, the protection you have before the trigger. Here it is an ATR multiple, because a distance that scales with volatility is easy to reason about; ATR in Pine Script covers ta.atr() in detail. The second is the trigger, the price that must be reached before the stop moves. Expressing it as a multiple of the initial risk, so that a trigger of 1.0 means "price has moved as far in your favour as the stop was away against you", keeps the rule consistent whatever the volatility is. The third is the destination, the level the stop moves to. Plain break-even is the entry price. In practice you usually add a small buffer so that costs do not turn a break-even exit into a small loss. The fourth is the confirmation, whether the trigger counts when the bar's high touches it or only when the bar closes beyond it.
Each of these is an input in the code below. That is deliberate. None of them has a correct value in the abstract, and the only honest way to choose is to test several and look at what changes.
A long-only break-even stop
Here is the smallest version that does the whole job. It enters long on a moving average crossover, which is only a stand-in so that the script produces trades. Replace the entry with your own logic; the exit block does not care where the entry came from.
//@version=6
strategy("Break-Even Stop (Long)", overlay = true, default_qty_type = strategy.percent_of_equity, default_qty_value = 10)
fastLen = input.int(20, "Fast EMA length", minval = 1)
slowLen = input.int(50, "Slow EMA length", minval = 1)
atrLen = input.int(14, "ATR length", minval = 1)
stopMult = input.float(2.0, "Initial stop distance (ATR multiples)", minval = 0.1, step = 0.1)
triggerMult = input.float(1.5, "Break-even trigger (ATR multiples)", minval = 0.1, step = 0.1)
atr = ta.atr(atrLen)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
var float stopLevel = na
var float triggerLevel = na
var bool atBreakEven = false
inLong = strategy.position_size > 0
if not inLong
stopLevel := na
triggerLevel := na
atBreakEven := false
if ta.crossover(fast, slow) and not inLong
strategy.entry("Long", strategy.long)
if inLong
entryPrice = strategy.position_avg_price
if na(stopLevel)
stopLevel := entryPrice - atr * stopMult
triggerLevel := entryPrice + atr * triggerMult
if not atBreakEven and high >= triggerLevel
stopLevel := entryPrice
atBreakEven := true
strategy.exit("Long exit", from_entry = "Long", stop = stopLevel)
plot(inLong ? stopLevel : na, "Stop", color = atBreakEven ? color.green : color.red, style = plot.style_linebr)Read it in the order the script executes. The first if resets the three variables whenever you are flat, so each new trade starts clean. The entry fires only when flat. Once a position exists, the first bar sets the initial stop and the trigger from the real average entry price, using the ATR value on that bar and then freezing it. Freezing matters: if the stop kept following the live ATR, it would drift every bar and you would be testing a different rule from the one you described.
After that, the check high >= triggerLevel asks whether this bar reached the trigger. If it did and the stop has not been moved yet, the stop is reassigned to the entry price and the flag is set. Finally, strategy.exit() is called on every bar in the position with the current stopLevel. Calling it repeatedly is correct, not wasteful: each call updates the pending exit order, which is exactly how a moving stop is expressed in Pine Script. The plot at the bottom draws the stop in red until it moves and in green afterwards, so you can see the move on the chart rather than infer it from trade lists.
Note what the script does not do. It does not call strategy.exit() once on entry and hope. A single early call would leave the original stop in place forever. The updated call on each bar is the whole mechanism.
Choosing the trigger and its confirmation
The trigger is the rule most worth thinking about, because it decides how often the move happens and how often it gets in the way. A trigger set close to the entry moves the stop almost immediately, which means ordinary pullbacks, the kind a trade has to survive, will take you out at break-even before the idea has had room to work. A trigger set far away moves the stop rarely, so the rule changes little. Neither is wrong. They measure different things.
| Convention | How it works | What to watch for |
|---|---|---|
| High or low touch | Trigger counts when the bar's high (long) or low (short) reaches the level | A single wick can trigger the move; the close may be nowhere near |
| Close beyond the level | Trigger counts only when the bar closes past the level | Slower to act; the bar that confirms it may already have retraced |
| Fixed ticks or percent | Distance is a constant, not tied to volatility | Behaves very differently across symbols and timeframes |
| Multiple of initial risk | Trigger distance is a multiple of the initial stop distance | Consistent across symbols, but depends on how the initial stop was set |
Before choosing, it is worth knowing how your position sizing relates to the stop distance, because the initial risk is the number most of these conventions are built on. Position Size in Pine Script shows how to turn a stop distance into a quantity, and the ATR Stop Loss Calculator lets you check the arithmetic of a stop at a given multiple without writing any code.
Both directions, a buffer, and a switchable confirmation
The long-only script has two limitations. It ignores shorts, and break-even at exactly the entry price is rarely break-even once you count costs. A trade that closes at the entry price still paid commission on the way in and on the way out, so it finishes slightly negative. Adding a buffer of a few ticks past the entry moves the stop to a level that offsets part of that cost. The version below handles both directions with a single direction multiplier and exposes the confirmation style as an input.
//@version=6
strategy("Break-Even Stop (Long and Short)", overlay = true, default_qty_type = strategy.percent_of_equity, default_qty_value = 10)
fastLen = input.int(20, "Fast EMA length", minval = 1)
slowLen = input.int(50, "Slow EMA length", minval = 1)
atrLen = input.int(14, "ATR length", minval = 1)
stopMult = input.float(2.0, "Initial stop distance (ATR multiples)", minval = 0.1, step = 0.1)
triggerR = input.float(1.0, "Trigger (multiples of initial risk)", minval = 0.1, step = 0.1)
bufferTicks = input.int(2, "Break-even buffer (ticks)", minval = 0)
useClose = input.bool(false, "Confirm on close instead of high/low")
atr = ta.atr(atrLen)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
goLong = ta.crossover(fast, slow)
goShort = ta.crossunder(fast, slow)
var float stopLevel = na
var float triggerLevel = na
var float beLevel = na
var bool atBreakEven = false
dir = math.sign(strategy.position_size)
if dir == 0
stopLevel := na
triggerLevel := na
beLevel := na
atBreakEven := false
if goLong and dir == 0
strategy.entry("Long", strategy.long)
if goShort and dir == 0
strategy.entry("Short", strategy.short)
if dir != 0
entryPrice = strategy.position_avg_price
if na(stopLevel)
riskDist = atr * stopMult
stopLevel := math.round_to_mintick(entryPrice - dir * riskDist)
triggerLevel := math.round_to_mintick(entryPrice + dir * riskDist * triggerR)
beLevel := math.round_to_mintick(entryPrice + dir * syminfo.mintick * bufferTicks)
if not atBreakEven
probe = dir > 0 ? (useClose ? close : high) : (useClose ? close : low)
reached = dir > 0 ? probe >= triggerLevel : probe <= triggerLevel
if reached
stopLevel := beLevel
atBreakEven := true
if dir > 0
strategy.exit("Long exit", from_entry = "Long", stop = stopLevel)
if dir < 0
strategy.exit("Short exit", from_entry = "Short", stop = stopLevel)
plot(dir != 0 ? stopLevel : na, "Stop", color = atBreakEven ? color.green : color.red, style = plot.style_linebr)
plot(dir != 0 and not atBreakEven ? triggerLevel : na, "Trigger", color = color.gray, style = plot.style_linebr)The trick that keeps the code short is dir, which is math.sign(strategy.position_size): 1 for long, -1 for short, 0 when flat. Multiplying a distance by dir flips it to the correct side for shorts, so the same three lines compute the stop, the trigger, and the break-even level in both directions. For a long, entryPrice - dir * riskDist is below the entry; for a short it is above. The buffer is added in the direction of profit, so for a short the break-even level sits slightly below the entry, which is the protected side.
math.round_to_mintick() snaps each level to the symbol's tick size. It is a small safety measure: a stop price that is not a valid tick can behave differently from the one you plotted, and rounding makes the plotted level and the order level the same number. The buffer is expressed in ticks through syminfo.mintick so that it means the same thing on a symbol quoted in whole numbers as on one quoted to several decimals.
One more guard is worth knowing about even though the code does not need it. If you later add a second rule that can also move the stop, such as a trailing stop that takes over after break-even, make sure it can only tighten the stop, never loosen it. For a long that means math.max(stopLevel, newLevel). A rule that can move the stop backwards will eventually do so on a bar you did not expect. Trailing Stops in Pine Script covers how to build that second stage.
Which bar the move takes effect on
Here is the part that surprises people. The script runs on a bar's close when it is evaluated historically, and orders it places take effect on the next bar. So when high >= triggerLevel is true on bar N, the stop is moved during bar N's calculation, but the order that uses the new level can only act from bar N+1. The same is true of the initial stop: it is placed after the entry has filled, so the entry bar itself is not covered by the script's stop.
That has a consequence for the case where a single bar both reaches the trigger and falls back to the old stop. Inside real trading, the stop would have moved the moment price touched the trigger and you would have been taken out at break-even. In the Strategy Tester, the stop is still at the old level for that bar, so the loss at the original stop is what gets recorded. The test is not wrong, exactly. It has no way to know the order of events inside a bar from open, high, low, and close alone, and it has to rely on assumptions about the path price took through them.
Two settings change how this is handled, and both are worth reading about before you trust a break-even backtest. Using the Bar Magnifier lets the Strategy Tester look at a lower timeframe to resolve what happened inside a bar; Pine Script Bar Magnifier: What It Changes in Backtests walks through it. Setting calc_on_every_tick changes when the script is evaluated on a live bar, which affects real-time behaviour rather than history; calc_on_every_tick in Pine Script explains why that difference is a common source of backtest and live mismatch. Neither is a requirement for a break-even stop, but a rule that is entirely about what happens within a bar is exactly the kind that depends on how those within-bar details are modelled.
What a break-even stop does to your numbers
Adding a break-even stop changes the shape of your results, not just the total. Trades that would have pulled back, recovered, and then reached a target now end at a scratch instead. Trades that went straight to the target are unchanged. The count of trades that end at roughly zero rises, and so does the number of cases where you were stopped just before the move you were waiting for.
Because of that, the headline statistics shift in ways that can look like improvement or damage depending on which you read. A higher fraction of trades that end flat or slightly positive can raise the win rate while shrinking the average gain per trade, which is why Win Rate Is a Trap is worth reading before you compare two versions of a strategy. The number that survives the change is expectancy, the average result per trade across all outcomes, and Expectancy: The Only Number That Predicts Long-Term Trading Results shows how to compute it. A fair comparison runs the same entries with and without the break-even rule and compares expectancy, drawdown, and the trade count together. Nothing about the rule guarantees a better outcome; it is a different distribution of outcomes, and whether it suits your strategy is something only your own tests can show.
Costs deserve their own mention. A break-even exit still pays commission and slippage, and a rule that produces many scratch trades multiplies those costs. How Slippage and Commission Actually Eat Your Returns shows the arithmetic, and it is the reason the buffer input exists in the second script.
Checking that the stop really moves
A script like this can look fine in the trade list and still be doing the wrong thing, so check the behaviour directly. Three checks catch most mistakes. First, look at the plotted stop on a handful of trades: it should sit below the entry for a long, stay flat, then step up to the entry and turn green. If it never turns green, the trigger is too far or the high comparison is wrong. If it is green from the first bar, the trigger is effectively at the entry. Second, confirm the stop is reset between trades; a stop that carries over from a previous trade is the most common state bug here, and the reset block at the top of the script is what prevents it. Third, hover over trades that closed at break-even and check the exit price against the plotted level plus the buffer.
When the behaviour is not obvious from the chart, step through it. In PineScripter, the chart runs your script on real market data and Replay steps through it one bar at a time, so you can watch the stop line and see exactly which bar the flag flipped on. That is faster than guessing from a list of exits. The chart there covers crypto spot markets, so for stocks or futures you would verify the same way inside TradingView.
Adding this to a strategy you already have
If you already have a strategy with a stop, resist the urge to bolt the code above on at the end. The thing most likely to break is the existing exit. A script that already calls strategy.exit() with a fixed stop, and then gains a second strategy.exit() call for the break-even level under the same id, can end up with two competing exits whose interaction is hard to follow. Replace the fixed stop with the variable stopLevel in the one exit call, move the initial-stop calculation into the first-bar block, and keep a single exit order per entry.
This is the kind of change where an editor that checks the script as it goes is useful. PineScripter is an AI editor for Pine Script v6: you describe the change in plain English, it checks the result for compile errors the way TradingView does, fixes them itself, and edits only the lines that change, so your existing entry logic is not rewritten around the new exit. The Code Changes view shows each edit line by line, which makes it easy to confirm the only difference is the stop handling. It does not run trades or tell you where a stop should sit; for the Strategy Tester results you still paste the script into TradingView.
Disclaimer
PineScripter is a coding tool, not a financial advisor. This article explains how to write Pine Script and does not recommend any stop, trigger, or trading approach. Past results from any test do not predict future results, and trading carries risk of loss.
Frequently asked questions
How do I move a stop to break-even in Pine Script?
Store the stop level in a var variable, set it from strategy.position_avg_price once the position exists, and reassign it to the entry price when price reaches your trigger. Then call strategy.exit() with that variable on every bar in the position, so the pending order is updated each time.
Why does my break-even stop not trigger in the backtest?
The usual causes are a stop level that is recomputed every bar and overwrites the move, a missing flag that records the move has already happened, or a trigger set so far from the entry that price never reaches it. Plot the stop level and the trigger on the chart to see which one it is.
Should the break-even stop sit exactly at the entry price?
Many people add a small buffer of a few ticks past the entry, because a trade closed at the entry price still pays commission and slippage and ends slightly negative. The right size depends on your costs and symbol, so treat it as an input and test it rather than fixing a value.
Does the stop move on the same bar that hits the trigger?
No. Orders placed by the script take effect from the next bar. If one bar reaches the trigger and then falls back to the original stop, the Strategy Tester records the original stop, because it cannot know the order of events inside a bar. The Bar Magnifier can help resolve that.
Can I combine a break-even stop with a trailing stop?
Yes. Move the stop to break-even first, then let a trailing rule take over. Make sure the trailing rule can only tighten the stop, for example with math.max for a long position, so that nothing can move the stop back to a worse level.
Does a break-even stop make a strategy better?
Not by itself. It changes the distribution of outcomes, turning some pullback-and-recover trades into scratches and adding exits that pay costs. Compare the same entries with and without the rule using expectancy, drawdown, and trade count, and decide from your own results.
Describe your indicator in plain English and get Pine Script that compiles →
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.