OpenScriptv0.5.0Documentation
GitHub

Operators

Arithmetic, comparison, equality, logical and conditional operators, the history and element operator, and assignment. What binds first and what each operator does with an absent value.

On this page
  1. A worked example
  2. Precedence
  3. Arithmetic
  4. Joining strings
  5. Comparison
  6. Equality
  7. Logical operators
  8. The ternary
  9. History and element access
  10. Member access
  11. Assignment
  12. Operators that do not exist
  13. Absent operands at a glance

Operators combine values into expressions: close - open, r > 70, trending and volume > avgVolume. OpenScript has a short list of them, nine precedence levels in all, and every one has an exact rule for what it does when an operand is absent. This page teaches you to read any expression the way the compiler reads it: what binds first, what division gives you, what a comparison against an absent value returns, and when the right side of an and or an or is evaluated at all. The Operators reference lists every operator mark with its operand and result types.

A worked example

version 1
study("Down bars on heavy volume", precision = 2)

avgVolume = sma(volume, 20)
body = close - open
weighted = body + body[1] * 2  // body + (body[1] * 2)
heavyDown = not (close > open) and volume > avgVolume  // (not ...) and (...)

plot(weighted, "Weighted body", aqua)
plot(heavyDown ? 1 : isNone(heavyDown) ? none : 0, "Heavy down bar", orange, style = "histogram")

Read the last line with the absence rules in mind. avgVolume is absent for the first nineteen bars, so the comparison volume > avgVolume is absent there. On a down bar the left side of and is true and heavyDown is absent; on an up bar the left side is false, which decides the answer alone, and heavyDown is false. The nested ternary plots a gap where the answer is unknown instead of a confident zero.

Precedence

Precedence decides which operator takes its operands first when an expression has several, as multiplication does before addition in arithmetic. The table lists the tightest binding first. Every level groups left to right except where the notes say otherwise.

LevelOperatorsNotes
1(expr), f(args), a[i], a.bGrouping, call, history or element, member
2unary -, unary +, notRight to left
3*, /, %
4+, -
5<, <=, >, >=At most one per expression
6==, !=At most one per expression
7andShort-circuits
8orShort-circuits
9cond ? a : bRight to left
a = 1
b = 2
c = 3
x = 4
y = 5

r1 = a + b * c  // a + (b * c), 7
r2 = -x % y  // (-x) % y, -4
r3 = close[1] * 2  // (close[1]) * 2
r4 = x > 0 ? 1 : x < 0 ? -1 : 0  // x > 0 ? 1 : (x < 0 ? -1 : 0), 1

plot(r1 + r2 + r3 + r4, "Sum")

Assignment is not in the table because it is not an operator. It is a statement, which is why if x = 5 does not compile: that is OS1006, with the fix naming ==.

The one precedence trap

not binds tighter than comparison, so not close > open means (not close) > open. not needs a bool, and close is a number, so the line is error OS2011 rather than the test you meant. Write the parentheses:

downBar = not close > open
downBar = not (close > open)
plot(downBar ? 1 : 0, "Down bar")

The presence guard not isNone(x) and x > 5 needs no extra parentheses, because the call's own brackets already group isNone(x): it reads as (not isNone(x)) and (x > 5).

Arithmetic

+, -, *, / and % work on number. There is one numeric type, so there is one division, and / is always real division: 7 / 2 is 3.5. There is no integer division operator. When you want a whole number, say which way to round with a call the reader can see.

ExpressionValueWhy
7 / 23.5Division is always real
-7 / 2-3.5The same
floor(7 / 2)3floor() rounds toward negative infinity
floor(-7 / 2)-4So a negative value goes down
trunc(-7 / 2)-3trunc() rounds toward zero
round(-7 / 2)-4round() rounds to nearest, halves away from zero
7 % 21The remainder after division
-7 % 3-1% takes the sign of the left operand
7 % -31The same
mod(-7, 3)2mod() takes the sign of the right operand
mod(7, -3)-2The same
7 / 0, 0 / 0, 7 % 0noneDivision by zero has no answer

Two remainders exist because both are wanted. % suits a "distance past a multiple" calculation; mod suits an index into a repeating cycle. The two agree whenever both operands are positive, which covers wrapping a bar count or a position in a session.

There is no power operator: pow(x, y) is the power function. See pow().

Every arithmetic operator propagates absence: if either operand is absent, so is the result, including none * 0. Arithmetic with no finite answer gives none rather than infinity or an error. Absent values covers both rules.

version 1
study("Stop a few ticks under the low", overlay = true, precision = 2)

steps = input(3, "Distance, in ticks", min = 1, max = 100)

// chart.tickSize is absent when the application running the script does not
// state one. The product is then absent, and so is the level, so nothing is
// drawn rather than a price the exchange would not accept.
offset = steps * chart.tickSize
stopLevel = roundToTick(lowest(low, 20) - offset)

plot(stopLevel, "Stop level", red, width = 2, style = "step")

Joining strings

+ also joins two strings, and does nothing else. "a" + 5 is OS2003; convert the number with text() first.

t = table("Last close", 1, 1)
message = "Close " + text(close, 2) + " on " + chart.symbol

if bar.isLast
    cell(t, 0, 0, message)

An absent operand makes the whole string absent, so a message with one absent part is no message at all: if chart.symbol were absent, the cell above would be blank. text(x) with one argument writes an absent value as the string "none", which is the way to show an absence in the output rather than lose the whole string.

Comparison

<, <=, > and >= compare two numbers or two strings. Strings compare by Unicode code point, which is the same in every locale; it is not a dictionary order.

A comparison cannot be chained. 30 < r < 70 is error OS1008. Write the middle value twice:

r = rsi(close, 14)
inBand = 30 < r < 70
r = rsi(close, 14)
inBand = 30 < r and r < 70
plot(inBand ? 1 : 0, "RSI between 30 and 70")

Chaining is refused rather than given the mathematical meaning because a reader could take it two ways, and a form with two plausible meanings has no place in a language that places orders.

Ordered comparison propagates absence. If either side is absent, the result is absent, not false. That keeps not (a > b) equal to a <= b for every input: during warmup both are absent and both branches are skipped. A comparison written against none itself is therefore absent on every bar, and is warning OS8012; test with isNone() instead.

Equality

== and != always return true or false, never absent. That is the deliberate exception to propagation, because a question that could not be answered would be no use.

CaseResult
none == nonetrue
none == 5false
5 != nonetrue
Two coloursEqual when all four channels match
Two arraysEqual when they are the same array, not when their contents match
Two values of different typesOS2003, except against none, which is always allowed

arrayEqual() compares the contents of two arrays. == compares identity, because an array is a reference: b = a gives two names for one array.

Because equality is total, x != x[1] is true on bar 0, where x[1] is absent, and a marker for a change of state would fire on the first bar of every chart. The same trap waits at the end of warmup, when a value that was absent becomes present. Keep the state absent until it means something, and require the previous value to be present:

version 1
study("Direction flips", overlay = true, precision = 2)

fast = ema(close, 9)
slow = ema(close, 21)

// Absent until both averages exist. Without the guard the ternary would read
// -1 through the warmup, and the first real reading could look like a flip.
dir = isNone(slow) ? none : (fast > slow ? 1 : -1)

if not isNone(dir[1]) and dir != dir[1]
    signal(dir == 1 ? "TREND UP" : "TREND DOWN")

plot(fast, "Fast", aqua, width = 2)
plot(slow, "Slow", orange, width = 2)

Logical operators

The logical operators are the words and, or and not. They take bool operands and use three-valued logic, where none means "unknown".

aba and ba or b
truetruetruetrue
truefalsefalsetrue
truenonenonetrue
falseanyfalseb
nonetruenonetrue
nonefalsefalsenone
nonenonenonenone

not none is none, and not not x is legal and means x.

An unknown operand is absorbed exactly when the other one decides the answer by itself: a false under and, a true under or.

Both operators are commutative. a and b equals b and a, and a or b equals b or a, for every combination of true, false and none, so the order you write the operands in never changes the answer. The usual rules for negating a combination hold too, absent operands included: not (a and b) is not a or not b. What decides whether a guard works is the operator, not the side a test sits on: isNone(x) and x > 5 is never true, however it is written.

Short-circuit evaluation

An operand is evaluated only if it can change the result. Skipping the rest of an expression once its answer is settled is called short-circuiting.

ExpressionLeft side isRight side evaluatedResult
a and bfalseNofalse
a and btrueYesb
a and bnoneYesfalse when b is false, otherwise none
a or btrueNotrue
a or bfalseYesb
a or bnoneYestrue when b is true, otherwise none

An absent left side settles nothing on its own, so the right side is evaluated in both cases. Only a left side that decides alone skips the right side.

Order does not change what an expression means, but it does change what runs. Put the cheaper or more often decisive test on the left, so its answer saves the work on the right. This line saves the right side on bar 0, because bar.isFirst is true there:

newDay = bar.isFirst or not date.isSameDay(time, time[1])
background(newDay ? fade(silver, 85) : none)

Keep stateful calls out of the right side. A call that keeps state between bars, such as rsi(), ema() or a user function with a var in it, does not advance on a bar where it is skipped, and its value is absent there. The compiler reports warning OS8001:

useFilter = input(true, "Use the filter")
if useFilter and rsi(close, 14) > 70
    signal("HIGH")

Compute it at the top level, where it runs on every bar, and use the result in the guard:

version 1
study("Filtered overbought", precision = 2)

useFilter = input(true, "Use the filter")
len = input(14, "RSI length", min = 2, max = 200)

r = rsi(close, len)

if useFilter and r > 70
    signal("HIGH")

plot(r, "RSI", purple, width = 2)
level(70, "Overbought", fade(red, 50))

&&, || and ! do not exist; the operators are the words.

The ternary

cond ? a : b (the ternary, or conditional operator) chooses a value. The condition must be a bool or absent, and an absent condition takes the false arm. Both arms must have the same type, or one arm may be none, and only the taken arm is evaluated.

r = rsi(close, 14)
tint = close > open ? lime : red

// A chain nests to the right, so it reads as a list of cases with the last one
// the default. The first case keeps the warmup bars absent.
zone = isNone(r) ? none : r > 70 ? 1 : r < 30 ? -1 : 0

plot(r, "RSI", tint)
plot(zone, "Zone: 1 above 70, -1 below 30")

Arms of different types are OS2012. There is no expression form of switch: the ternary chooses a value, and switch chooses a block.

Because only the taken arm runs, the ternary is a safe guard for a division: down > 0 ? up / down : none. For the same reason a stateful call inside an arm only advances on the bars that take that arm, and is warning OS8001. Take the call at the top level and put its result in the arm:

version 1
study("Up-volume share", precision = 2)

flow = sum(close > open ? volume : 0, 20)
traded = sum(volume, 20)
share = traded > 0 ? flow / traded : none

plot(share, "Share of volume on up bars", aqua)

The ternary is also how a plot is hidden on some bars, since plot() must stay at the top level: plot(trending ? ema20 : none, "EMA 20", aqua).

History and element access

a[i] means one of two things, chosen at compile time from the type of a:

a isa[i] meansExample
A seriesThe value i bars backclose[1], the previous close
An arrayElement i, counting from 0levels[0], the first element
version 1
study("History and elements", overlay = true, precision = 2)

var recent: array<number> = []
push(recent, close)
if size(recent) > 20
    shift(recent)

oldest = recent[0]  // element: the first item in the array
back19 = close[19]  // history: the close 19 bars ago

plot(oldest, "Oldest close in the array", silver)
plot(back19, "Close 19 bars back", orange)

Once the array holds its twenty closes, the two lines are one line. Before that, on the first nineteen bars, recent[0] is the very first close while close[19] is absent: element access reads what the array holds, and history reads what the data holds.

SituationResult
x[n] where n is greater than bar.indexnone: not clamped, not zero, not an error
x[n] where n is absentnone
x[n] where n is negative or not wholeOS4001, which stops the script at that bar
x[n] deeper than the history the engine keepsOS4002, naming the limits(history = ...) that raises it
arr[i] outside 0 to size - 1OS4004, an error

Reading past the start of history is absence, because that value never existed. Reading past the end of an array is an error, because the script asked for something it never created.

Where a reader could doubt which meaning a line uses, write the explicit form: history() always reads a series some bars back, and element() always reads an element of an array. Bars and history covers the history operator in full.

Member access

a.b reads a member of a namespace, such as bar.isConfirmed, chart.symbol or session.isFirstBar, or calls one, such as date.dayOfWeek(). A name after the dot that the namespace does not have is OS2009.

Assignment

Assignment is a statement, never an expression.

FormMeans
name = expressionDeclare the name in this scope, or update it if it already exists in an enclosing one
name += expressionname = name + expression
name -= expressionname = name - expression
name *= expressionname = name * expression
name /= expressionname = name / expression
name %= expressionname = name % expression

The compound forms obey every rule of the long form, including absence: x += none leaves x absent. A name's type is fixed by its first assignment, and a different type later is OS2003.

version 1
study("Cumulative volume", precision = 0)

var total = 0.0

// Without the guard, one bar with no volume would make total absent for ever.
if not isNone(volume)
    total += volume

plot(total, "Cumulative volume", silver, style = "area")

Operators that do not exist

You might writeWrite insteadError
!condnot condOS1001
a && b, a || ba and b, a or bOS1001
a ^ b, a ** bpow(a, b)OS1001
i++i += 1OS1001
i--i -= 1OS1022
a & b, a | b, ~aNothing: there are no bitwise operatorsOS1001
a < b < ca < b and b < cOS1008
if x = 5if x == 5OS1006
a; bTwo linesOS1007
{ ... }IndentationOS1001
up = close > open
down = !up

The console under the editor shows each of these with its line, its code and the fix:

The Scripts panel console listing a compile error with its line, code and fix
Every save compiles. The console under the editor names the line, the error code and the fix.

Absent operands at a glance

OperatorWith an absent operand
unary -, unary +, notAbsent
*, /, %, +, -Absent if either operand is
+ on stringsAbsent if either operand is
<, <=, >, >=Absent if either operand is
==, !=Never absent: none == none is true
and, orThree-valued, see the table above
? : conditionAn absent condition takes the false arm
a[n] with n absentAbsent

Related. Types and values, Absent values, Control flow, Bars and history, Operators reference, Math