CODESCRIPT

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.
CODESCRIPT
// hizli ortalama yavas ortalamayi yukari kesince pozisyon ac
enterLong(crossover(sma(close, 10), sma(close, 30)), id='poz')

// tek emirde iki koruma: ustte hedef, altta zarar-kes
exit(true, id='koru', fromEntry='poz', 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.

condition
condition
Required. On an immediate exit it means "close now"; on a resting order it means "place the order now", which is why you usually see true there.
fromEntry
constant text
Optional. Aims the exit at the entry carrying this name and no other.
qtyPct
number
Optional. What percent of the targeted entry's REMAINING size to close. It must fall between 0 and 100.
id
constant text
The resting exit order's name. Mandatory alongside limit or stop, and forbidden on an immediate exit.
limit
price
Optional. The take-profit level.
stop
price
Optional. The stop-loss level.
CAUTION

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 the engine 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.

CODESCRIPT
enterLong(crossover(sma(close, 10), sma(close, 30)), id='poz')

// pozisyon acikken giris fiyati sabit kalir
giris = positionAvg()
acik = position() != 0

// hedef girisin %3 ustu, zarar-kes girisin %2 alti
exit(acik, id='koru', fromEntry='poz', limit=giris * 1.03, stop=giris * 0.98)
plot(giris, "giris fiyati", "#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.

NOTE

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.
CODESCRIPT
enterLong(crossover(close, sma(close, 20)), id='poz')

giris = positionAvg()
acik = position() != 0
hedef = giris * 1.02
zarar = giris * 0.99

exit(acik, id='koru', fromEntry='poz', limit=hedef, stop=zarar)

// iki seviyeyi de ciz
plot(hedef, "hedef", "#26a69a")
plot(zarar, "zarar-kes", "#ef5350")

// ayni bar hem hedefi hem zarar-kesi gorduyse isaretle
plotshape(acik and high >= hedef and low <= zarar, "cakisan 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.

CAUTION

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.

NOTE

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.

CODESCRIPT
// oynaklik olcusu: son 14 barin ortalama gercek araligi
mesafe = atr(14)

enterLong(crossover(close, sma(close, 20)), id='poz')

giris = positionAvg()
acik = position() != 0

// zarar-kes 2 ATR asagida, hedef 3 ATR yukarida
zarar = giris - 2 * mesafe
hedef = giris + 3 * mesafe

exit(acik, id='koru', fromEntry='poz', limit=hedef, stop=zarar)
plot(hedef, "hedef", "#26a69a")
plot(zarar, "zarar-kes", "#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.

length
number
How many bars to average over; 14 is the usual choice. It reads high, low and close on its own and takes no separate source argument.
TIP

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.

CODESCRIPT
mesafe = atr(14)

enterLong(crossover(close, sma(close, 20)), id='poz')

giris = positionAvg()
acik = position() != 0
ilkHedef = giris + 1.5 * mesafe

// ilk hedef yukari kirildiginda pozisyonun yarisini kapat
closePart(acik and crossover(high, ilkHedef), 50)

// kalan yari uzak hedef ile zarar-kes arasinda beklemeye devam eder
exit(acik, id='koru', fromEntry='poz', limit=giris + 3 * mesafe, stop=giris - 2 * mesafe)

// kalan ne varsa ters kesisimde kapansin
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.

koşul
condition
The condition that triggers the close. Passing it positionally is the safest habit: this argument's name is spelled in Turkish here and has no English alternative.
oran
number
An optional percentage from 0 up to and including 100. It has to be a constant number. If you prefer to name it, qtyPct does the same job.

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.

kosul
condition
The close condition. Again, pass it positionally: this function spells the argument in plain ASCII while its sibling closePart wants the Turkish spelling.
CAUTION

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.

CAUTION

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.