OpenScriptv0.5.0Documentation
GitHub

Operators

Every operator in OpenScript with its precedence, associativity, operand types, result type and what it does when an operand is absent.

On this page
  1. Precedence and associativity
  2. Grouping, calls, history and members
  3. Unary operators
  4. Arithmetic
  5. Comparison
  6. Logic
  7. The ternary
  8. Assignment
  9. What each operator does with none
  10. Operators that do not exist
  11. Functions that spell out an operator
  12. Index of marks

This page is the complete list of operator marks in OpenScript: arithmetic, comparison, logic, the ternary, history and member access, and the assignment forms. For each one it gives the types it accepts, the type it produces and what it does when an operand is none, the absent value.

Operators are where a script's arithmetic meets its warmup bars (the first bars, where an indicator does not have enough data yet), so the absence rules below decide what your first plotted bars look like. If you are learning the language, read the precedence table and the section on absent operands; the rest is reference.

version 1
study("Body strength", precision = 2)

body     = close - open                          // binary minus
barRange = high - low
share    = barRange > 0 ? body / barRange : none // the ternary skips a zero range
avgVol   = sma(volume, 20)
heavy    = volume > 1.5 * avgVol and not isNone(share)

plot(share * 100, "Body as a percent of range", aqua, style = "histogram")
plot(heavy ? share * 100 : none, "On heavy volume", orange, style = "histogram")
level(0, "Zero", gray)

Precedence and associativity

Operators on a higher row bind more tightly. Each row lists its associativity, which decides how a chain of operators from the same row groups.

LevelOperatorsAssociativityNotes
1(expr), f(args), a[i], a.bLeftGrouping, call, history or element, member
2unary -, unary +, notRight
3*, /, %Left
4binary +, binary -Left+ also joins strings
5<, <=, >, >=NoneCannot be chained
6==, !=NoneCannot be chained
7andLeftShort-circuits
8orLeftShort-circuits
9cond ? a : bRightOnly the chosen arm runs
a = 2
b = 3
c = 4
x = a + b * c                 // a + (b * c), which is 14
y = -a % b                    // (-a) % b, which is -2
z = close > open ? "up" : close < open ? "down" : "flat"   // groups to the right
plot(x + y, "Sum, which is 12")
plot(str.length(z), "Length of the direction word")

Assignment is not in the table. = and the compound forms such as += are statements, not operators, and they are listed below.

How each associativity reads

AssociativityOperatorsa op b op c means
Left*, /, %, binary +, binary -, and, or(a op b) op c
Rightunary -, unary +, not, the ternarynot not x is not (not x); p ? a : q ? b : c is p ? a : (q ? b : c)
None<, <=, >, >=, ==, !=Not allowed: a < b < c is OS1008. Write a < b and b < c

A comparison and an equality are on different levels, so a < b == true is legal: the comparison runs first. Two operators from the same non-chaining level are the only form refused.

Grouping, calls, history and members

( and ): grouping and calls

Parentheses group an expression to override precedence, at no cost at run time. After a name, they hold a call's arguments.

mid = (high + low) / 2
plot(mid, "Midpoint")

A call takes positional arguments, named arguments, or positional followed by named. The compiler checks every call before the first bar runs:

MistakeError
Too many argumentsOS3001
A required argument left outOS3012
A named argument that does not existOS3002, and the message lists the names that do
The same argument given twiceOS3013
A positional argument after a named oneOS3005
An argument of the wrong typeOS3011

[ and ]: history, elements and array literals

One pair of brackets does three jobs, and the compiler always knows which from what is on the left.

WrittenWhenMeansResult
src[n]src is a seriesThe value n bars ago; src[0] is srcThe series' value type
arr[i]arr is an arrayElement i, counting from 0The element type
[a, b, c]At the start of an expressionAn array literalarray<T>
prev  = close[1]              // history: the previous bar's close
var recent: array<number> = []
push(recent, close)
if size(recent) > 20
    shift(recent)
first = recent[0]             // element: the oldest value kept
plot(close - prev, "Change")
plot(first, "Oldest of the last 20")

A value has history, and so accepts [n], when it is a built-in series such as close, a name declared at the top level of the file, a call that returns a series, or a series parameter of your own function. Anything else is OS2004: give the value a name at the top level and read that name.

History caseResult
n is more than the bars so farnone, not an error and not the oldest bar
n is nonenone
n is negative or not a whole numberOS4001 when the bar runs
n is deeper than the retained historyOS4002, naming the depth that was kept

An array index outside 0 to size - 1, or one that is not a whole number, is OS4004. It is an error rather than an absent value because an array has a size you chose, so an index past it is a mistake. When one line does both jobs, the explicit forms history() and element() say which is meant.

.: member access

Reads a member of a namespace, such as bar.isConfirmed, chart.lotSize or date.hour(time). The namespaces are bar, chart, session, date, str, math, pos, order, leg, book, draw and req. A member that does not exist is OS2009, and the fix suggests the nearest name.

if bar.isConfirmed and close > high[1]
    signal("BREAKOUT")

,: separator

Separates the arguments of a call, the elements of an array literal and the values of a case. A comma at the end of a line continues the statement onto the next line, which is how a long call is spread over several lines.

levels = [22000.0, 22500.0, 23000.0]
plot(close, "Close",
     color = aqua,
     width = 2)
plot(size(levels), "Levels")

Unary operators

Unary - and +

Unary - negates a number. Unary + returns it unchanged. Both take a number and give a number; given none, both give none. A negative literal such as -2 is this operator applied to 2, and it parses as you expect inside an argument list.

drop = -change(close)
plot(drop, "Fall since the last bar")

not

Boolean negation: bool to bool, and not none is none. It is right associative, so not not x is legal. ! is not an operator; !cond is OS1001, and the fix names not.

if not bar.isConfirmed
    background(fade(yellow, 90))

Arithmetic

OperatorOperandsResultWith a zero or an absent operand
*number * numbernumbernone if either side is absent, even none * 0
/number / numbernumbernone when the divisor is zero, including 0 / 0
%number % numbernumber, the remainder of truncated divisionnone when the divisor is zero
+number + number, or string + stringnumber, or the joined stringnone if either side is absent
-number - numbernumbernone if either side is absent

A number in OpenScript is always finite. An operation with no finite result produces none instead of infinity, so a bad bar draws a gap rather than a spike and never poisons an average.

* multiplication

atrPercent = atr(14) / close * 100
plot(atrPercent, "ATR as a percent of price")

/ division

Division by zero is none, not an error, because one bad bar must not stop a study that is correct over fifty thousand others. You do not need a guard to avoid a crash; the plot simply has a gap on that bar. Reach for orElse() or a ternary only when you want a definite value there instead of a gap.

gain  = rma(max(change(close), 0), 14)
loss  = rma(max(-change(close), 0), 14)
ratio = gain / loss            // none on a bar where the average loss is 0
plot(ratio, "Average gain over average loss")

% remainder

The remainder of truncated division, so its sign follows the left operand: -7 % 3 is -1. The library's mod() is the floored remainder, whose sign follows the right operand, so mod(-7, 3) is 2. The two agree whenever the right operand is positive, which covers wrapping an index or a bar count.

everyFifth = bar.index % 5 == 0
background(everyFifth ? fade(silver, 92) : none)

+ addition and concatenation

Adds two numbers or joins two strings, and nothing else. "a" + 5 is OS2003: there is no implicit conversion anywhere in the language. Convert explicitly with text(). Joining follows the absence rule too, so "a" + none is none; to write an absent value into text on purpose, use text(none), which is the string "none".

panel = table("Last close", 1, 1, position = "bottomRight")
message = "Close at " + text(close, 2)
if bar.isLast
    cell(panel, 0, 0, message)

- subtraction

body = close - open
plot(body, "Body", style = "histogram")

There is no exponent operator. Write pow(): pow(x, 2).

Comparison

<, <=, >, >=: ordering

OperandsResult
number against number, or string against stringbool, or none when either side is absent

Strings compare by Unicode code point, which is the same on every machine and in every locale. It is not an alphabetical sort for people.

If either side is none, the result is none, not false. This keeps a > b and a <= b exact opposites on every bar. During warmup both are none, so neither branch runs, instead of one of them running on a guess.

r = rsi(close, 14)
if r > 70
    background(fade(red, 92))
if r <= 30
    background(fade(lime, 92))
plot(r, "RSI", purple)

Comparisons cannot be chained: a < b < c is OS1008, and the fix is a < b and b < c.

== and !=: equality

OperandsResult
Two values of the same type, or any value against nonebool, never none

Equality is the one place absence does not spread. none == none is true, none == 5 is false and 5 != none is true, so x == none is a working test, the same as isNone().

That rule has a consequence worth seeing once. On bar 0, dir[1] is none, so dir != dir[1] is true there. The example guards the first bar so a flip is reported only between two real readings:

dir  = close > open ? 1 : -1
flip = not isNone(dir[1]) and dir != dir[1]
if flip
    signal("FLIP")
plot(dir, "Direction")
ComparingRule
Two values of different typesOS2003
Two coloursEqual when all four channels match
Two arrays, or two drawing objectsEqual only when they are the same one. arrayEqual() compares array contents
Two plot, fill or level handlesNot allowed

Logic

and, or, not

and and or combine bool values with three-valued logic, where none means unknown. Both short-circuit: the right side runs only when it can still change the answer.

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

An unknown side is absorbed only when the other side settles the answer alone: a false under and, a true under or. Both operators give the same value with their sides swapped, so order never changes a result. Order does decide what runs:

ExpressionLeft side isRight side runs
a and bfalseNo
a and btrue or noneYes
a or btrueNo
a or bfalse or noneYes
// time[1] is absent on bar 0, but bar.isFirst is true there, so it is never read.
newDay = bar.isFirst or not date.isSameDay(time, time[1])
background(newDay ? fade(aqua, 90) : none)

A call that keeps state, such as ema() or a function holding a var, does not advance on a bar where it is skipped, and it is absent for that bar. The compiler warns with OS8001. Compute such a call on its own line first, then combine the results. && and || do not exist; the operators are the words.

The ternary

? and :

Form: cond ? a : b. The condition is a bool or none. Both arms have the same type, or one arm is none; arms of different types are OS2012. Only the chosen arm runs. A none condition takes the second arm, the same rule as if.

ema20 = ema(close, 20)
trending = ema20 > ema(close, 50)
plot(trending ? ema20 : none, "EMA 20 while trending", aqua)

The ternary is how you hide a plot on some bars: plot() is top level only, and wrapping it in an if is OS3006. It is also the place for a guard, such as a value you only want on some bars. Keep stateful calls out of the arms for the same reason as with and and or: an arm that is not chosen does not advance, and the compiler warns with OS8001.

Assignment

Assignment is a statement, never an expression, so if x = 5 is OS1006 with the fix "write == to compare".

FormMeans
name = expressionDeclare the name in this scope, or update it if it already exists in an enclosing scope
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 the first assignment that gives it a definite value, and assigning another type later is OS2003.

version 1
study("Running volume")

var total = 0.0
total += orElse(volume, 0)      // one absent bar would otherwise make the total absent for good
plot(total, "Cumulative volume", silver, style = "area")

What each operator does with none

OperatorWith an absent operand
Unary -, unary +none
*, /, %, +, -none if either side is absent
+ on stringsnone if either side is absent
<, <=, >, >=none if either side is absent
==, !=Never absent. none == none is true
notnone
and, orSee the three-valued table
? : conditionTakes the second arm
? : armWhatever the chosen arm gives, absent included
src[n] with n absentnone

none * 0 is none, not zero: the operand was unknown, not zero. One uniform rule means an absent value travels to the plot, where it draws a gap you can see, instead of turning into a number that looks right.

Operators that do not exist

Each of these is refused when the script compiles. The table gives the error you see and what to write instead.

WrittenErrorWrite instead
!xOS1001not x
a && b, a || bOS1001a and b, a or b
x ^ y, x ** yOS1001pow(x, y)
x++OS1001x += 1
x--OS1022x -= 1
Bitwise &, |, ~OS1001Nothing; there are no bitwise operators in version 1
<<, >>OS1008, read as two comparisonsNothing; there are no shift operators
a < b < cOS1008a < b and b < c
a; b on one lineOS1007One statement per line
{ ... }OS1001Indentation. There are no braces
/* ... */OS1026// on each line

Functions that spell out an operator

Where an operator could be misread by a person, the library has a named function that says exactly what is meant.

FunctionAlways means
history()History, n bars back
element()Element i of an array
pow()Raise to a power
mod()Remainder whose sign follows the divisor, unlike %
arrayEqual()Arrays with equal contents, unlike ==
isNone()The same test as x == none
orElse()The value when present, a fallback when absent

Index of marks

MarkLevelMeans
( )1Grouping, or a call's arguments
[ ]1History, element access, or an array literal
.1Member of a namespace
- unary, + unary, not2Negation, identity, boolean not
* / %3Multiply, divide, remainder
+ -4Add or join strings, subtract
< <= > >=5Ordering, absent if either side is. < and > also enclose the element type in array<number>
== !=6Equality, never absent
and7Conjunction
or8Disjunction
? :9The ternary. : also introduces a type annotation, as in var x: number = 0
=StatementAssignment. Also a named argument (color = aqua) and a parameter default (len = 20)
+= -= *= /= %=StatementCompound assignment
,NoneSeparates arguments, elements and case values
//NoneA comment to the end of the line
\NoneContinues a statement on the next line
=>NoneSeparates a function's parameters from its body

Related: Keywords, Types, Operators in the language guide, Absent values, Bars and history, General functions.