OpenScriptv0.5.0Documentation
GitHub

Variables and scope

How a name is declared by its first assignment, updated by later ones, kept across bars with var, and seen only inside the scope it was declared in. The ban on shadowing, and the rules for names.

On this page
  1. A complete example
  2. Declaring and updating
  3. A plain assignment is recomputed on every bar
  4. var keeps a value across bars
  5. The three scopes
  6. Declaration versus update
  7. What a block can see and keep
  8. There is no shadowing
  9. Order of declaration
  10. Loop variables
  11. switch arms declare nothing that outlives them
  12. Names inside a function
  13. Patterns worth copying
  14. Naming rules
  15. Errors and warnings you may meet

A variable in OpenScript is a name you assign a value to. There is no keyword to declare an ordinary name: the first assignment creates it, and every later assignment updates it. This page covers how a name is declared, when it is recomputed and when it survives into the next bar, which lines can see it (its scope), and why the language refuses to let two variables share a name. With these rules you can point at any name in a script and say where it was declared, what it holds and whether it carries forward.

A complete example

version 1
study("Spread", precision = 2)

lookback = input(20, "Average length", min = 2, max = 500)

spread = high - low  // declares spread
spread = spread / close * 100  // updates it, as a percentage of price

var widest = 0.0  // survives from bar to bar
if spread > widest
    widest = spread  // updates the file-scope widest

avgSpread = sma(spread, lookback)

plot(spread, "Spread, percent of price", aqua, width = 2)
plot(avgSpread, "Average spread", orange)
plot(widest, "Widest so far", gray, style = "step")

spread is declared and updated on every bar. widest is declared once with var and keeps its value between bars. The if block updates widest rather than creating a second one, because a name that already exists outside a block is updated from inside it.

Declaring and updating

The first assignment to a name in a scope declares it. Every later assignment updates it.

Two rules follow immediately.

A name's type is fixed by its first assignment. Assigning a value of a different type later is error OS2003, even when the two lines are pages apart. A name is one thing for the life of the script, so a reader can look at one line and know what it holds. A first assignment of none fixes no type, because none belongs to every type; the type then comes from the first assignment that gives a definite value.

len = 14
len = "fourteen"

A name must be assigned above the line that reads it. The file runs top to bottom on every bar, so reading a name before its assignment is error OS2001, not an absent value. Function declarations are the one exception, covered below.

plot(slow, "Slow", orange)
slow = ema(close, 21)

A plain assignment is recomputed on every bar

A name assigned without var is computed afresh on every bar. Its previous value is still readable through the history operator, as name[1], but it is not the starting point for this bar's computation.

seen = seen + 1

seen does not exist yet on this bar when seen + 1 is read, so that line is OS2001. Reading the previous bar instead compiles, and never produces a value:

seen = 0
seen = seen[1] + 1  // absent on bar 0, and on every bar after it
plot(seen, "Never draws")

On bar 0 there is no previous bar, seen[1] is absent, and the sum is absent. Every later bar reads an absent predecessor, so one absent bar at the start poisons the whole run. That is what var fixes.

var keeps a value across bars

var name = initial declares a name whose initial value is set once and which then keeps whatever it holds from one bar to the next.

version 1
study("Running high", overlay = true, precision = 2)

// Set to none once, on bar 0, then kept from bar to bar.
var highestSeen = none
if isNone(highestSeen) or high > highestSeen
    highestSeen = high

plot(highestSeen, "Highest high so far", aqua, style = "step")
RuleConsequence
The initial value is set once, on the first bar the declaration is reachedA var inside an if that is false for the first hundred bars is set on bar 100
var may appear at the top level, inside a block or inside a functionPersistence is available wherever a value is
A var inside a block is still scoped to that blockHow long a value lives and where its name can be seen are separate questions
var rolls back on the forming barRunning the newest bar ten times gives the same answer as running it once
The declaration needs an initial valuevar seen alone is OS1011. Write var seen = none for an empty start

Rollback, and why a running total is safe

While the market is open the newest bar is still forming, and the engine runs the script on it again on every update. Before each rerun, every var is restored to what it held at the end of the previous bar (this is called rollback). So a counter counts bars, not updates, and a chart shows the same numbers as a backtest over the same data.

version 1
study("Bars, not updates")

var seen = 0
seen += 1

plot(seen, "Bars seen", aqua, width = 2)

live var is the same except that it does not roll back, so it keeps counting across the updates of the forming bar. It exists for one purpose, counting or accumulating within a bar, and it is spelled with an extra word because a script that uses one produces different numbers on a real-time chart than in a backtest. The compiler reports warning OS8011 to make sure that is what you meant. Persistence covers var, live var and rollback in full.

History and persistence are different questions

WrittenMeans
close[1]History: what close was one bar ago
var x = 0Persistence: x carries into the next bar
x[1]Both: what the persistent x was one bar ago

A useful consequence: at any line of the file, a var still holds the previous bar's value until the line that reassigns it. So a script can compare this bar's value with the last one by reading the var before it changes:

version 1
study("Up-close streaks", precision = 0)

var streak = 0

// At this line streak still holds the count the previous bar left in it.
prevStreak = streak

streak = close > close[1] ? streak + 1 : 0

// A run of three or more up closes ended on this bar.
if prevStreak >= 3 and streak == 0
    signal("STREAK ENDED")

plot(streak, "Up closes in a row", aqua, style = "step")

The three scopes

A scope is a region of the script in which a name can be seen.

ScopeHoldsCreated by
GlobalThe library: built-in series, functions and colour namesThe language
FileEvery name assigned at the top level, and every fnThe file
BlockNames first assigned inside itEach if block, else block, for or while body, case or default arm, and function body

Blocks nest, and a block can see everything its enclosing blocks and the file can see. A function's parameters belong to its body's scope.

Declaration versus update

This is the whole of scoping in two sentences:

A name is declared by its first assignment in a scope. An assignment to a name that already exists in an enclosing scope updates that name and does not create a new one.

version 1
study("Threshold")

volatile = atr(14) > sma(atr(14), 50)

threshold = 70  // declared in the file scope
if volatile
    threshold = 80  // updates the file-scope name

plot(threshold, "Threshold", aqua)  // 80 on volatile bars, 70 on the rest
volatile = atr(14) > sma(atr(14), 50)
if volatile
    scratch = high - low  // declared in the block scope
plot(scratch, "Scratch", aqua)  // OS2001: scratch is not visible here

Together those rules mean you never have to ask which of two variables a line writes to. There is exactly one threshold, and exactly one place scratch can be read.

What a block can see and keep

A name first assigned inside a block is visible from its assignment to the end of that block, including blocks nested inside it. Nothing else.

Where it is first assignedVisible to
The top level of the fileEvery line below it, including inside every block and every function
Inside an if blockThe rest of that block and blocks nested in it. Not the else, not a sibling if, not anything after the block
Inside an else blockThe rest of that block only
Inside a for or while bodyThe rest of that body, on that iteration. The next iteration starts fresh
Inside a case or default armThe rest of that arm. No other arm, and nothing after the switch
Inside a function bodyThe rest of that body

Three consequences catch everyone once:

  • A name declared in an if cannot be read in its else. They are two blocks.
  • A name declared in a loop body does not accumulate. It is a fresh name each time round. To carry a value between iterations, declare it above the loop.
  • A name declared in a block has no history. History is kept only for names at the top level of the file, so inner[1] inside a block is OS2004. Name the value at the top level instead.
version 1
study("Mean candle body", precision = 2)

lookback = input(20, "Lookback", min = 2, max = 500)

// Declared above the loop: a name declared inside the body would be a fresh
// name on every iteration and would carry nothing between them.
total = 0.0
seen = 0

for i = 0 to lookback - 1
    body = abs(close[i] - open[i])  // a fresh name on each iteration
    if isNone(body)
        continue
    total += body
    seen += 1

plot(seen > 0 ? total / seen : none, "Mean body", aqua, width = 2)

There is no shadowing

Shadowing means declaring a second variable with the same name as one in an enclosing scope, so that one name refers to two things. OpenScript does not allow it: declaring a name in an inner scope when the same name already exists in an enclosing scope is error OS2002. The message gives the line of the outer declaration.

Inside an if or a loop a plain assignment cannot shadow, because an assignment to an outer name updates it; only a var that reuses an outer name can, and it is OS2002. Inside a function body it is easier to do by accident: a function may read names from the file scope, but an assignment inside the body always declares a name of the function's own. So assigning a file-scope name inside a function is a second declaration of that name, and is OS2002.

len = 20

fn smooth(src) =>
    len = 9
    sma(src, len)

The fix is to rename:

len = 20

fn smooth(src) =>
    innerLen = 9
    sma(src, innerLen)

plot(smooth(close), "Smoothed", aqua)
plot(sma(close, len), "SMA", orange)

A function parameter follows the same rule: a parameter named after a file-scope name, or after a library name such as close, is OS2002.

Shadowing is banned because the most expensive bug in a per-bar script is a value that is right in one place and stale in another, and two variables sharing one name is the shortest path to it.

Library names are taken

The library's names (built-in series, functions and colours) live in the global scope, so assigning to one is a shadowing attempt and is OS2002 as well.

close = 5

The names that bite most often are the short, obvious ones a script wants for its own values: count, sum, avg, min, max, size, change, variance, stdev, level, fill, signal, time, open, high, low, close and volume. If a name feels natural enough that the library probably took it, it probably did: upCount, totalVolume and dayHigh are free.

Order of declaration

Within the file scope a name must be assigned before it is read, reading top to bottom, because the file is the body of the per-bar loop and runs in source order.

Functions are the exception. A function declaration is not a per-bar statement, and the compiler collects every one before it checks any body, so a fn may be called above its declaration:

version 1
study("Helper below", overlay = true, precision = 2)

plot(helper(close), "Smoothed", aqua, width = 2)

fn helper(src) => sma(src, 9)

Loop variables

The loop variable of a for belongs to the loop: it does not exist after the loop, and it may not be assigned in the body. Assigning it is OS2006; to leave early, use break.

for i = 0 to 9
    if close[i] > high
        i = 9

The for x in arr form scopes x the same way, and assigning x in the body is OS2006 too.

switch arms declare nothing that outlives them

Because a name first assigned inside an arm belongs to that arm, a name the arms of a switch set must be declared before the switch. This makes the "declared in one arm only" bug impossible to write, and it puts the default where a reader sees it first.

version 1
study("Selectable length", overlay = true, precision = 2)

method = input("medium", "Speed", options = ["fast", "medium", "slow"])

// Declared here, so it exists whatever the switch does.
len = 21

switch method
    case "fast"
        len = 9
    case "slow"
        len = 50

plot(sma(close, len), "SMA", aqua, width = 2)

Names inside a function

A function body is a block scope. Its parameters live there, and anything it assigns is its own. It can read file-scope names, and the ban on shadowing means it can never accidentally declare a second one.

A function body may use var, and may call library functions that keep state. State belongs to the call site (the place in the source where the function is called), not to the function, so two calls in two places are two independent pieces of state. User functions covers this in full.

version 1
study("Bars since", precision = 0)

fn sinceTrue(cond) =>
    var n = none
    if cond
        n = 0
    else if not isNone(n)
        n = n + 1
    n

sinceUp = sinceTrue(close > open)  // its own counter
sinceHigh = sinceTrue(high > high[1])  // a separate counter

plot(sinceUp, "Bars since an up close", aqua)
plot(sinceHigh, "Bars since a higher high", orange)

Patterns worth copying

Declare above, refine inside

Give a name its "we do not know yet" value above the branch that refines it, so warmup is an explicit state rather than an accident:

r = rsi(close, 14)

zone = "unknown"
if not isNone(r)
    zone = r > 70 ? "high" : (r < 30 ? "low" : "mid")

// Warmup bars stay "unknown", so they are never shaded as overbought.
background(zone == "high" ? fade(red, 85) : none)

Reset at the start of each day

State that belongs to a trading day resets on the day's first bar. Test for it with the calendar, not with a bar count, because the number of bars in a day changes with the interval and with holidays and short sessions.

version 1
study("Day extremes", overlay = true, precision = 2)

// The first bar of a new calendar day in the chart's time zone, or bar 0.
newDay = bar.isFirst or not date.isSameDay(time, time[1])

var dayHigh = none
var dayLow = none

if newDay
    dayHigh = high
    dayLow = low
else
    dayHigh = max(dayHigh, high)
    dayLow = min(dayLow, low)

plot(dayHigh, "Day high", aqua, width = 2, style = "step")
plot(dayLow, "Day low", orange, width = 2, style = "step")

Where the application running the script states the instrument's session hours, session.isFirstBar marks the first bar of each session instead; see Sessions and time.

Store a time, not a bar index

bar.index is a position in the data the engine was given. Loading older history renumbers every bar, so an index stored in a var and compared later is compared against something that moved. Store time, which does not move:

version 1
study("Minutes since the last cross up", precision = 0)

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

var crossTime = none  // survives more history being loaded
if crossUp(fast, slow)
    crossTime = time

// Absent until the first cross, because time - none is none.
plot((time - crossTime) / 60000, "Minutes since the last cross up", aqua)

Subtracting two indices from the same run is still fine, because both come from the same numbering.

Naming rules

Names are ASCII, start with a letter or an underscore, and are case sensitive. The convention, not enforced, is camelCase for names and functions and UPPER_SNAKE for values a script treats as constants.

The reserved words cannot be used as names: and, array, as, bool, break, case, color, continue, default, else, false, fn, for, if, import, in, is, live, map, matrix, none, not, number, or, return, series, step, string, strategy, study, switch, to, true, type, var and while. Using one is OS1019. Some are reserved for later versions and do nothing today; reserving them now means adding them later cannot break a script that used one. Keywords describes each one.

Errors and warnings you may meet

CodeMeansFix
OS1011var with no initial valuevar name = none is the empty start
OS1019A reserved word used as a nameRename it
OS2001The name is not defined at this pointAssign it above this line, or fix the spelling
OS2002The name already exists in an enclosing scopeRename the inner one, or update the outer one from a block instead
OS2003The name changed typeUse a second name
OS2004The value has no historyName it at the top level and read that name
OS2005A function calls itselfWrite a loop
OS2006The loop variable was assigned in the bodyUse break, or a separate name
OS8010A name is assigned and never readUse it, or delete the line
OS8011A live var makes real-time and backtest differUse var, unless counting updates within a bar is the intent

Related. Execution model, Persistence, Types and values, Control flow, User functions, Style guide