Take-profit and stop-loss
Opening a trade is the easy half; saying in advance where you will leave it is the hard half. This page covers arming both protective levels in one order, what happens when a single bar touches both, and how to size those levels to volatility.
Leave the exit to be decided later and it hangs on whatever your code works out on each new bar. A level you place ahead of time just sits there and acts the moment price arrives. A target and a stop are simply both ends of the trade, drawn before it begins.
Why you arm both levels at once
One exit call carries both the target and the stop. Both sit on the chart as pending orders. Whichever level price reaches first is the one that fills, and the other withdraws itself. That withdraw-the-loser behaviour goes by the name OCO.
- limit is the upper level where you bank the gain; it fills when price rises to it.
- stop is the lower level where you cut the loss; it fills when price drops to it.
- id names the resting exit order, and you must supply it wherever a limit or a stop appears.
- fromEntry says which entry the exit is aimed at; leave it out and every open entry is targeted.
// open a position when the fast average crosses above the slow one enterLong(crossover(sma(close, 10), sma(close, 30)), id='pos') // two protections in one order: the target above, the stop-loss below exit(true, id='hold', fromEntry='pos', limit=close * 1.03, stop=close * 0.98)
A buy mark appears on the crossover bar and the position opens. Two resting exit orders go up with it: a take-profit 3% above that close and a stop 2% below. Whichever level price touches first closes the trade, leaves an exit mark at that price, and cancels the other one.
exit(condition, fromEntry?, qtyPct?, id?, limit?, stop?, comment?, commentProfit?, commentLoss?)
Returns nothing. It closes the position on the bar where the condition is true; pass a limit or a stop and it arms a resting exit order at those levels instead of closing.
Skip the id on an exit that carries a limit or a stop and you get a plain error: a resting exit order needs a name. The reverse is an error too — hand an id to an exit that closes the instant its condition is met and CodeScript rejects it. Names belong to resting orders only.
Pinning the levels to your entry
exit is worked out again on every bar. Build the level from close and it walks along with price — the target keeps meaning "3% above right now", so price can never catch it. To tie a level to where you got in, use positionAvg(): it holds still for as long as the position is open.
enterLong(crossover(sma(close, 10), sma(close, 30)), id='pos') // the entry price stays fixed while the position is open entry = positionAvg() open = position() != 0 // the target is 3% above the entry, the stop-loss 2% below exit(open, id='hold', fromEntry='pos', limit=entry * 1.03, stop=entry * 0.98) plot(entry, "entry price", "#ff9800")
The orange line shows your entry price as a flat level while the trade is on, and breaks off once it closes. Both levels are measured from that fixed line, so they no longer drift as price moves. In the previous example the levels slid on every bar; here they stay put.
positionAvg()
The average entry price of the open position. It returns na while you are flat.
positionAvg() gives you na while flat, and anything multiplied by na stays na. That is why the exit is armed with position() != 0: the levels are only worked out when there is actually a trade to protect.
When one bar touches both
A bar is not one price. It travels a path from open to close, and pending orders are checked step by step along that path. When a single bar reaches the target and the stop alike, that path decides the outcome.
- On an up bar the walk goes: open, low, high, close.
- On a down bar the walk goes: open, high, low, close.
- The first level met along that walk is the one that fills; the other is cancelled.
- Should the two land on exactly the same price, the stop-loss goes first.
- A position-level risk stop given on the entry itself is checked ahead of any resting exit order.
enterLong(crossover(close, sma(close, 20)), id='pos') entry = positionAvg() open = position() != 0 target = entry * 1.02 loss = entry * 0.99 exit(open, id='hold', fromEntry='pos', limit=target, stop=loss) // draw both levels plot(target, "target", "#26a69a") plot(loss, "stop-loss", "#ef5350") // mark the bar that touched both the target and the stop-loss plotshape(open and high >= target and low <= loss, "overlapping bar", style="diamond", location="abovebar", color="#ff9800")
Green marks the target, red marks the stop. An orange diamond flags a bar that reached both, and those are the bars that decide the trade. On an up bar the low is walked first, so the stop fills; on a down bar the high comes first, so the target fills.
When both levels land on one bar, do not assume the target wins. These are exactly the bars that swing a backtest the most. The path order decides, and an exact tie goes to the stop — the cautious side is served first.
enterLong will also take percent stopLoss and takeProfit arguments. Those act as a position-level risk stop and are checked BEFORE any resting exit order on the same bar. Use both together only if you know which one gets the first look.
Levels that follow volatility: atr
A fixed percentage does not mean the same thing on every symbol. On a quiet one a 2% stop sits far away; on a jumpy one it lands inside ordinary noise and gets taken out on the first bar. atr measures the average true range of recent bars in price units — it answers how much ground this symbol has been covering in a single bar.
// a volatility measure: the average true range of the last 14 bars distance = atr(14) enterLong(crossover(close, sma(close, 20)), id='pos') entry = positionAvg() open = position() != 0 // the stop-loss is 2 ATR below, the target 3 ATR above loss = entry - 2 * distance target = entry + 3 * distance exit(open, id='hold', fromEntry='pos', limit=target, stop=loss) plot(target, "target", "#26a69a") plot(loss, "stop-loss", "#ef5350")
In calm stretches the two lines close in on price; in busy ones they open up. Risking 2 ATR to make 3 means a winner brings home one and a half times what a loser costs. The distance is recomputed each bar, so it keeps breathing with volatility while the trade is open.
atr(length)
An average true range series that measures volatility in price units.
Want the distance frozen for the life of the trade? Store the value from the entry bar: valuewhen hands you the atr reading from the last bar your entry condition was true. A breathing distance and a frozen one are two different strategies, and the choice is yours.
Partial exits with closePart
Reaching a target does not oblige you to close everything. closePart shuts a share of the open position and leaves the rest running. You bank some of the gain at the first target and carry the remainder toward a farther one, still fenced in by the target and the stop.
distance = atr(14) enterLong(crossover(close, sma(close, 20)), id='pos') entry = positionAvg() open = position() != 0 firstTarget = entry + 1.5 * distance // close half the position when the first target breaks upward closePart(open and crossover(high, firstTarget), 50) // the remaining half keeps waiting between the far target and the stop-loss exit(open, id='hold', fromEntry='pos', limit=entry + 3 * distance, stop=entry - 2 * distance) // let whatever remains close on the opposite cross closeAll(crossunder(close, sma(close, 20)))
On the bar where price pushes 1.5 ATR above entry, half the position closes and a partial exit mark appears; positionSize() steps from +1 down to +0.5. The other half waits between the 3 ATR target and the 2 ATR stop. If price then crosses back below the average, whatever is left is closed out.
closePart(koşul, oran?)
Returns nothing. On the bar where the condition holds it closes the given share of the open position, or all of it when you give no share.
closeAll(kosul?)
Returns nothing. It closes the whole open position at market on the bar where the condition holds, and does nothing at all when you are flat.
With these two, pass the condition positionally and never by name. Spell the name wrong on closeAll and you get no error: the condition counts as missing and the position is closed on every single bar. A strategy that goes quietly wrong costs far more than one that fails out loud.
A partial exit always works on what is REMAINING at that moment. Repeated 50% closes keep halving the balance without ever emptying it: 1, then 0.5, then 0.25, and so on. To be truly out you still need closeAll or an exit with no share given.
For the same thing at the level of a single entry, write exit(condition, fromEntry='poz', qtyPct=50). The difference: closePart acts on the whole open position, while an exit with qtyPct trims only the entry you named. In a position you built up in stages, that distinction earns its keep.
Mistakes that catch people out
- Building a level from close and never noticing the target runs away with price. Anchor it to positionAvg() instead.
- Forgetting the id on a resting exit, or attaching one to an immediate exit. Both are errors: names are for resting orders alone.
- Assuming the target wins whenever one bar reaches both levels. The price path inside the bar decides that.
- Writing a partial exit on a comparison that stays true. high > target holds bar after bar and halves the position each time; crossover narrows it to the one bar you meant.
- Expecting repeated partial exits to empty the position. Each call only ever takes a share of the remainder.
- Forgetting that an order price never reaches simply does not fill. A pending order waits, and an unfilled one leaves neither a trade nor a mark.