OpenScriptv0.5.0Documentation
GitHub

Orders

buy, sell, close, exit, cancel and the order namespace. Market, limit, stop and stop-limit orders, tags, working orders, reversing, and every refusal an order can meet.

On this page
  1. A complete example
  2. The order calls
  3. Default or absent
  4. Market, limit, stop and stop-limit
  5. One position, and no order crosses zero
  6. One leg in version 0.5.0
  7. Tags: naming an order
  8. When the next signal arrives and an order is still working
  9. Exiting
  10. Reversing
  11. When the script computes its side
  12. Refusals
  13. Pitfalls

This page covers every call a strategy uses to place, name, cancel and close an order: buy(), sell(), close(), exit(), cancel(), cancelAll() and the order namespace. You need it as soon as a strategy does more than enter at the market and flatten on the opposite signal: a limit entry that waits for a pullback, a stop entry above an opening range, a partial exit, a reversal.

A complete example

This strategy bids for a pullback in an uptrend with a limit order, cancels the bid if it has not filled within a few bars, and exits at the market when the trend turns. It works on any instrument; on an NSE stock it trades one share.

version 1
strategy("Pullback limit", overlay = true, precision = 2,
         capital = 500000, qty = 1)

offsetAtr = input(0.5, "Bid this far below the close, in ATR", min = 0.1, max = 5)
waitBars  = input(3,   "Cancel the bid after this many bars", min = 1, max = 50)

atrValue = atr(14)
trend    = ema(close, 50)
upTrend  = close > trend

// Every price that reaches an order is rounded to the tick and tested for
// absence first.
wanted   = close - offsetAtr * atrValue
bidPrice = isNone(wanted) ? none : roundToTick(wanted)

// The price and bar of the resting bid, none while nothing rests.
var restingAt = none
var placedAt  = none

// Once the bid has filled there is nothing resting any more.
if not pos.isFlat
    restingAt = none
    placedAt  = none

stale = not isNone(placedAt) and bar.index - placedAt >= waitBars

if stale
    cancel("pullback")
    restingAt = none
    placedAt  = none
else if upTrend and pos.isFlat and isNone(placedAt) and not isNone(bidPrice)
    buy(limit = bidPrice, tag = "pullback")
    restingAt = bidPrice
    placedAt  = bar.index
else if pos.isLong and not upTrend
    close()

plot(trend, "Trend", orange, width = 2)
plot(restingAt, "Resting bid", aqua, style = "step")

Read the three branches in order. The cancel is tested first, so on the bar a bid goes stale the script cancels it and does not immediately place another at a stale price. The single if chain also means no two orders from this script can ever go out on the same bar. And the plot draws the price that was actually sent, held in a var (a variable that keeps its value from one bar to the next), not a price recomputed from today's volatility.

A backtest lists every trade the run made:

The trade list of a backtest with side, entry, exit and net result
Every trade the run made, with its entry, exit and net result.

The order calls

Everything below works only in a strategy() file; in a study the compiler refuses it with OS7001.

CallForStatus
buy(qty, limit, stop, tag)Enter or add to a long positionRuns
sell(qty, limit, stop, tag)Enter or add to a short position, or reduce a longRuns
close(tag, qty)Flatten the position, or the part one tag enteredRuns
exit(tag, qty, limit, stop, profit, loss)Set the position's stop and targetAccepted, not acted on in /trading yet; see Exits and brackets
cancel(tag)Cancel a working order that has not filledRuns
cancelAll()Cancel every working order this strategy placedRuns
order.place(side, qty, type, price, trigger, tag)The general form, for a script that computes its sideRuns
order.reverse(qty, tag)Close the position and open the same size the other wayRuns
order.bracket(tag, profit, loss)Set the stop and target as distances from the entryAccepted, not acted on in /trading yet; see Exits and brackets
order.working(), order.pendingWhether a tag is working, and how many orders arePlanned
order.status(), order.filled(), order.avgFill(), order.id(), order.rejection()Reading one order back from the strategy's own ledgerPlanned
order.modify(), order.oco()Changing a working order in place, and one-cancels-otherPlanned

Six bare names cover almost every script, and the order namespace holds the rest. A planned name is refused at the call with OS2020, so a script cannot compile around one by accident.

Default or absent

Leave an argument out and you get its default: buy() uses the declaration's qty, and buy() with neither price is a market order. Pass an argument whose value comes out absent (no value on this bar) and you get something else entirely: the order is refused with OS7002, naming the argument, and the run stops.

That difference is deliberate. buy(stop = lowest(low, 20)) on bar 5 is not a market order at a price nobody chose; it is refused, because the 20-bar window has not filled yet. The fix is a guard, computed once at the top level:

version 1
strategy("Stop entry, guarded", overlay = true, precision = 2, qty = 1)

rangeHigh = highest(high, 20)[1]
trendUp   = close > ema(close, 50)
trigger   = isNone(rangeHigh) ? none : roundToTick(rangeHigh)

if not isNone(trigger) and trendUp and pos.isFlat
    buy(stop = trigger, tag = "breakout")

That script shows the guard and nothing else. It still has a flaw the section on working orders below fixes: every flat bar places another stop order, because a working order is not a position. pyramiding does not catch it either, because it counts filled entries: in a backtest the stops resting at that level trigger together when price reaches it, and the position comes out several times the size the script meant.

Market, limit, stop and stop-limit

There is one entry function per direction, and the kind of order is decided by which prices you pass. A limit order buys at its price or lower (sells at its price or higher). A stop order waits until the market reaches its trigger price, then becomes a market order. A stop-limit waits for the trigger, then rests as a limit.

limitstopKindIn a backtest it fills
absentabsentMarketAt the next bar's open, or at this bar's close with fillOn = "close", worsened by the slippage
givenabsentLimitOnce a bar trades beyond the limit, at the limit, or at the open when the bar opens beyond it. No slippage
absentgivenStopOnce a bar reaches the trigger, at the trigger, or at the open when the bar gaps through it, worsened by the slippage
givengivenStop-limitOnce the trigger is reached and a bar then trades beyond the limit. Until then it rests as a limit

An order with a price is first tested against the bar after the one that placed it. A limit that the bar's low only touches is not filled, because touching a price is not proof your order was reached in the queue. Costs and fills covers these rules.

The trader's decision is direction; the price is a qualifier on it. You decide to buy, and whether you buy at the market or wait for a pullback is the next thought.

Two rules apply to every price you pass:

  • A price must fall on a tick. A limit between two ticks cannot exist at the exchange, so it is refused with OS7006, naming the instrument, its tick and the price. The engine does not round it for you, because that would move the order off the level your script computed. Round it yourself with roundToTick().
  • roundToTick() is absent when the host has stated no tick size. An order given the absent result is refused with OS7002. Test the rounded price once and use the result everywhere, as the examples on this page do.

Here is a stop entry placed once a session, above the high of the first fifteen minutes (09:15 to 09:30), and cancelled at 11:00 if it has not triggered. Every clock test names the zone, because the Backtest panel does not state the chart's timezone to the script, and a window with no zone has no value there:

version 1
strategy("Opening range stop entry", overlay = true, precision = 2,
         capital = 500000, qty = 1, product = "intraday")

// A new IST date is a new session.
newDay   = isNone(time[1]) or not date.isSameDay(time, time[1], "Asia/Kolkata")
forming  = session.isIn("0915-0930", "Asia/Kolkata")
canEnter = session.isIn("0930-1100", "Asia/Kolkata")
lateDay  = not session.isIn("0915-1500", "Asia/Kolkata")

var rangeHigh = none
var placed    = false
var working   = false

if newDay
    rangeHigh = none
    placed    = false

if forming
    rangeHigh = isNone(rangeHigh) ? high : max(rangeHigh, high)

trigger = isNone(rangeHigh) ? none : roundToTick(rangeHigh)

// A filled stop is a position, not a working order.
if not pos.isFlat
    working = false

if working and not canEnter
    cancel("orb")
    working = false
else if canEnter and not placed and pos.isFlat and not isNone(trigger)
    buy(stop = trigger, tag = "orb")
    placed  = true
    working = true
else if pos.isLong and lateDay
    close()

plot(rangeHigh, "Range high", aqua, style = "step")

A deployment from the Strategies panel cannot read the clock like this in version 0.5.0; Sessions and time explains why, and what works instead.

One position, and no order crosses zero

buy(qty) adds to the position, sell(qty) subtracts from it, and close() flattens it. All three count this strategy's own settled fills and nothing else.

sell() does not mean "close a long". It means "subtract", which closes a long if one is open and keeps going into a short if the quantity is larger. To flatten, say so with close().

No order crosses zero. In a strategy that counts in units, an instruction that would take the position from long to short is sent as two orders: one that closes the outgoing position and one that opens the new one. Each carries its own position reference, so a fill that arrives late can still say which position it belongs to.

version 1
strategy("Two orders, not one", overlay = true, qty = 1)

if bar.index == 100
    buy(qty = 5, tag = "long")

// Long 5 here, so this sends two orders: sell 5 to close, then sell 3 to open
// a short of 3.
if bar.index == 110
    sell(qty = 8, tag = "flip")

if bar.index == 120
    close()

Direction comes from the function, never from the sign of the quantity. A negative quantity is a calculation that went the wrong way, and a quantity of zero is never what a script means; both are refused with OS7004. Sizing towards a target position from pos.size is fine, because pos.size describes nothing but this strategy:

version 1
strategy("Target three units", overlay = true, qty = 1)

wantedSize = 3 - pos.size

if wantedSize > 0
    buy(qty = wantedSize)
else if wantedSize < 0
    sell(qty = -wantedSize)

One leg in version 0.5.0

A strategy trades legs, and a leg is one contract. In version 0.5.0 a file declares no legs, so it has exactly one: the instrument on its chart. Every order acts on it and none of them names it. The leg argument that every order call accepts is there for the planned multi-leg strategies, and writing it today is refused with OS3023, whatever you pass:

version 1
strategy("A leg that does not exist", overlay = true, qty = 1)

goLong = crossUp(ema(close, 9), ema(close, 21))

if goLong and pos.isFlat
    buy(qty = 1, leg = "main")

The fix is to take the argument out.

Tags: naming an order

An order is named by a tag, a string your script chooses. A tag is how a later bar cancels an order that is still working, how close(tag = ...) picks out one part of a position, and what the trade list and every refusal message quote back at you.

The script chooses the name rather than the engine because an order function places nothing at the moment it runs: the request is applied at the end of the bar, and only if the bar is confirmed. There is no order yet to have an identifier, and a tag is a name the script already knows.

What a tag argument means is written in its default.

KindDefaultCallsNaming nothing
A label""buy(), sell(), exit(), order.place(), order.reverse(), order.bracket()Ordinary: the tag rides along to the destination and the report
A referencerequired, or nonecancel(), close()A mistake: it names something the strategy must already have

A close whose tag no order in the file is placed with can never close anything, so the compiler refuses it with OS7016 before any bar runs. It is almost always a typo:

version 1
strategy("A typo in a tag", overlay = true, qty = 1)

fast = ema(close, 9)
slow = ema(close, 21)
goLong = crossUp(fast, slow)
goFlat = crossDown(fast, slow)

if goLong and pos.isFlat
    buy(tag = "entry")
else if goFlat and pos.isLong
    close(tag = "entyr")

cancel is checked while the run is going instead, because only the run knows what is working: cancelling a tag with no working order is refused with OS7009, and the run stops. Closing a tag that has already flattened is not an error. It sends nothing and says nothing, which is what makes a close(tag = "entry") safe to write on every bar of an exit condition.

Two habits pay for themselves: give every order a tag, even when the script has only one, and make the tag describe the intention ("entry", "pullback", "reversal") rather than the bar it was placed on.

When the next signal arrives and an order is still working

A working order is one that has been placed and has neither filled nor been cancelled, such as a limit waiting for its price. This is the case that separates a strategy that survives real market conditions from one that does not. The language fixes three facts and leaves the fourth to you:

  1. A working order is not a position. pos.size counts settled fills. A resting limit changes nothing in pos.* until it fills.
  2. Nothing is cancelled for you. A new buy() does not replace a working buy(). If both fill, you hold both, even with pyramiding = 1, because the pyramiding limit counts filled entries and neither had filled when it was placed.
  3. Two opposite orders on one bar are refused, both of them, with OS7013 naming both lines. Source order is an accident of layout, so neither is honoured. Two orders on the same side are not this: they are two orders.
  4. What happens to the old order is a trading decision, and yours to write.

There are three sane policies. Write the script so a reader can tell which one it uses:

PolicyWritten asSuits
Cancel and replacecancel(tag) then place the new order, on the same bar or the nextA resting order that tracks a moving level
First come, first servedRemember that an order is working and place nothing new until it fills or is cancelledAn entry taken at its price or not at all
Age outCount the bars an order has been working and cancel itA signal that goes stale, like the complete example above

order.working() and order.pending will answer "is this order still working" from the ledger. Both are planned, so in version 0.5.0 a script keeps that fact itself in a var, set when the order is placed and cleared when the position opens or the order is cancelled. The pullback and opening range examples above both do exactly that.

cancelAll() is for the moments a script has lost confidence in everything it has working: the session ending, a risk switch turned off in the inputs. It cancels working orders only, and does not close a position; an order that has filled is not working any more. With nothing working it sends nothing and refuses nothing, so it is safe to call on any bar.

Exiting

ToCallNotes
Flatten the positionclose()Whatever is held, long or short
Flatten part of itclose(qty = n)n is positive whichever way the position points
Flatten the part one tag enteredclose(tag = "runner")The tag must be one an order in the file is placed with
Leave at a stop or a targetA rule that tests the level and calls close()See Exits and brackets

A close is measured against what is left to close: what has settled, less everything already on its way out. Two bare closes on one bar send one order between them, and a close on the bar after one the destination has not answered yet sends nothing. That is what stops close() under if pos.size > 0 from sending the whole position again on every bar while a slow destination is still working the first one.

A quantity written on a close is held to a ceiling. close(qty = 5) against a position of 3 would flatten it and open a short under a call named close, so it is refused with OS7017, naming what you asked for and what is left. A close with no quantity asks for whatever is there and cannot be wrong, which is why close(tag = "runner") on a flattened tag is silent while close(tag = "runner", qty = 1) on it is refused. If a scale-out can fire twice on one position, guard it rather than sizing it and hoping. This one takes half off at a first target fixed when the position opens, and closes the rest on the opposite cross:

version 1
strategy("Scale out", overlay = true, precision = 2,
         capital = 500000, qty = 2)

atrValue = atr(14)
fast     = ema(close, 9)
slow     = ema(close, 21)
goLong   = crossUp(fast, slow)
goFlat   = crossDown(fast, slow)

// The first target, fixed once the position is open rather than recomputed
// from each bar's ATR.
var target = none
// One scale out per position, not one per bar above the target.
var scaled = false

if pos.isFlat
    target = none
    scaled = false
else if isNone(target) and not isNone(atrValue)
    target = pos.avgPrice + 2 * atrValue

if goLong and pos.isFlat
    buy(qty = 2, tag = "entry")
else if pos.isLong and goFlat
    close()
else if pos.isLong and not scaled and high > target
    // abs because pos.size is signed and an order quantity never is.
    half = floor(abs(pos.size) / 2)
    if half > 0
        close(qty = half)
        scaled = true

plot(target, "First target", lime, style = "step")

Reversing

There are three ways, and they are not the same trade:

WayWhat happensUse it when
order.reverse()One decision, two orders: close the position, open the same size the other wayA stop-and-reverse system that is never flat
sell(qty = abs(pos.size) + newQty)One instruction the engine splits into two orders, because no order crosses zeroThe new size differs from the old, in a strategy counting in units
close(), then sell() on a later barTwo decisions, with at least one bar flat between themThe reversal deserves a second look

order.reverse() says in one call what the other two spell out, and a reader does not have to check any arithmetic to see that the size is unchanged:

version 1
strategy("Stop and reverse", overlay = true, precision = 2,
         capital = 500000, qty = 1, pyramiding = 1)

factor = input(3.0, "Band width, in ATR", min = 0.5, max = 20)
atrLen = input(10,  "ATR length", min = 1, max = 200)

// supertrend returns [line, direction]: -1 while the trend is up, 1 while
// it is down.
bands   = supertrend(factor, atrLen)
band    = bands[0]
dir     = bands[1]
prevDir = dir[1]

// Both readings must exist. On the first bar with a direction the previous
// one is absent, and != against an absent value reads as a change.
flipped = not isNone(dir) and not isNone(prevDir) and dir != prevDir

if flipped and pos.isFlat
    if dir == -1
        buy(tag = "long")
    else
        sell(tag = "short")
else if flipped
    order.reverse(tag = "reversal")

plot(dir == -1 ? band : none, "Stop, long",  lime, width = 2)
plot(dir == 1  ? band : none, "Stop, short", red,  width = 2)

When the script computes its side

order.place() is the general form, for a script whose direction comes out of a calculation rather than out of two branches. side is "buy" or "sell", and type is "market", "limit", "stop" or "stopLimit". The type and the prices must agree: "limit" takes price, "stop" takes trigger, "stopLimit" takes both, and "market" takes neither. A type that names a limit or a stop without the price it needs is refused with OS7007 rather than filled in from the bar's close, since the report would say "limit" and the fill would say "market". A value written outside either list is a compile error (OS3008).

version 1
strategy("Signed model", overlay = true, capital = 500000, qty = 1)

score = ema(close, 9) - ema(close, 21)
side  = isNone(score) ? "" : (score > 0 ? "buy" : "sell")

if side != "" and pos.isFlat
    order.place(side, 1, tag = "model")
else if pos.isLong and side == "sell"
    close()
else if pos.isShort and side == "buy"
    close()

Use the bare functions where the direction is written in the source, and order.place where it is not. A reader can see a buy() without running anything; a reader of order.place(side, ...) has to work out what side holds.

Refusals

Every refused order reports a code and a reason, naming the line that placed it. In version 0.5.0 a refusal while the run is going also stops the run at that bar: nothing the bar decided is sent, and no later bar executes. The Backtest panel then reports only the trades made before the refusal, and does not show the error itself. Codes marked "At compile time" are caught before any bar runs.

CodeMeansUsual causeIn 0.5.0
OS7001An order call in a studyThe declaration says studyAt compile time
OS7002An order argument is absentA price or size from a window that has not warmed upRaised
OS7003An order call inside a request expressionA function that places an order, passed to req.timeframe()At compile time
OS7004Quantity is zero or negativeA size computed from a difference that went the wrong wayRaised
OS7005Quantity is not a whole number of lotsUnits passed to an instrument that trades in lotsNot raised yet
OS7006Price is not on a tickA limit computed as a percentage and never roundedRaised
OS7007A resting order has no priceorder.place with type = "limit" and no priceRaised
OS7008The entry was refused by pyramidingNo position guard on the entryRaised
OS7009No working order has that tagCancelling an order that has already filled or endedRaised
OS7010A stop or target on the wrong side of the positionA stop above a long's entryRaised
OS7011The order needs more capital than the strategy hasFixed sizing against a small capitalNot raised yet
OS7012The instrument is outside its sessionAn order at a time the exchange is closedNot raised yet
OS7013Two opposite orders on one barTwo independent if blocks that can both be trueRaised
OS7014The destination rejected the orderA product or margin the account cannot tradeNot raised yet
OS7015The strategy has no order destinationNothing configured to receive ordersNot raised yet
OS7016A close names a tag nothing placesA typo in a tagAt compile time
OS7017A close states more than it is closingA scale-out fired twiceRaised
OS3023An order names a legleg = ... written in version 0.5.0At compile time

OS7008 is the pyramiding limit doing its job. Silently building a position the declaration forbade would report a return the stated rules never earned, so the order is refused instead. The order error pages give a before and after for each code.

Pitfalls

SymptomCauseFix
A sell went short instead of flatteningsell subtracts, it does not closeclose()
Two entries where the script meant oneGuarded on pos.isFlat alone while a resting order was still workingRemember the working order in a var, as the examples do
The run stops with OS7009cancel on an order that has already filledClear the "working" flag when the position opens
The run stops with OS7013Two if blocks placing opposite orders on one barOne if chain with else if
No orders at all in the Backtest panelA clock test written without a zone, such as session.isIn("0930-1100")Name the zone: session.isIn("0930-1100", "Asia/Kolkata")
OS3023 on every orderA leg argumentTake it out; the order acts on the chart's instrument
Orders appear on history and not on the forming barThe condition is true inside the bar and false at its closeNothing to fix: orders wait for the bar to confirm

Related. Overview, Exits and brackets, Position and sizing, Costs and fills, Reading the books, Strategy orders reference, order.* reference