millionchart
فارسی

Calls that keep a state

Some calls remember earlier bars. Each place where such a call is written has its own state, and the state moves only when the script reaches that place.

Most calls forget: math.abs(x) gives the same answer for the same x, whatever happened before. A few calls remember. tech.sma(close, 20) needs the last twenty closes, tech.crosses_above(a, b) needs the previous bar, and math.random() needs to know which number comes next. This page calls them calls that keep a state. The reference marks each of them in its Effects line: "keeps a state for each place it is written (a call site) and advances it when execution reaches the call".

The whole rule fits in one sentence. A call that keeps a state has one state for each place it is written, and the state moves exactly when the script reaches that place on a bar. If the script does not reach the place on a bar, the state stays as it was.

What keeps a state#

Keeps a stateDoes not
Every function of tech except tech.tr: averages, bands, oscillators, highs and lows over a window, crossings, pivots (60 of the 61 functions)The prices and bar values: close, bar.index, bar.time
rises(condition) and falls(condition)close[1], and the history of any plain name: the engine already keeps the series, so reading it moves nothing
tf.changed(tf), math.random(...)Arithmetic, math.* other than math.random, text.*, color.*
data.series(...) and data.intrabars(...), which read another timeframeThe methods of an array, a map or a matrix, and the objects of the chart
The history of anything that is not a plain name: (high - low)[1]The members of a declared session and prior(...) and current(...) (see below)
A function of yours that holds a state, or that calls one of the left columnA function of yours that does neither

A state name of your own is different: it is a value you read and write yourself, in plain statements. Variables shows let, mut and state.

One state for each place#

The state belongs to the place in the text where the call is written, not to the function that is called. Two lines that both call tech.ema are two moving averages with two memories. The same goes for a function of yours: the memory is not in the function but in the place that calls it.

Cuprel
// Every place where a call that keeps a state is written has a state of its own. smooth() is one function called twice, with two different series,
// and each call keeps its own running average: the difference to the same average written out directly is zero on every bar.
indicator "One state for each call"
    edition = 1
    display = pane

fn smooth(source: float, length: int): float = tech.ema(source, length)

let from_close = smooth(close, 10)
let from_open = smooth(open, 10)
plot(from_close - tech.ema(close, 10), name = "Close, function minus direct", color = color.green)
plot(from_open - tech.ema(open, 10), name = "Open, function minus direct", color = color.red)
plot(from_close - from_open, name = "Close average minus open average", color = color.gray)

The first two plots are zero because smooth(close, 10) and tech.ema(close, 10) each advance their own state on every bar with the same input. If the two calls of smooth shared one memory, the average of the opens and the average of the closes would be mixed up.

Because the state is tied to a place, the place has to be one that runs a fixed number of times on a bar. That is why a call that keeps a state may not stand in a loop. All the passes of the loop would be the same place and would share one memory, so the engine refuses it with E3301:

Cuprel · error E3301
// Mistake: a request for another timeframe keeps a state per call site, so it may not stand in a loop (E3301).
indicator "Request in a loop"
    edition = 1
    display = pane

mut total = 0.0
for i in 0 to 2
    total += data.series("60", close, symbol = symbol.id)
plot(total, name = "Total")

The same holds for a function of yours that keeps a state and is called inside a loop. Make the call once before the loop and use its value inside, as the corrected program does:

Cuprel
// The fix for E3301: make the request once, outside the loop, and use its value inside.
indicator "Request outside the loop"
    edition = 1
    display = pane

let hourly = data.series("60", close, symbol = symbol.id)
mut total = 0.0
for i in 0 to 2
    total += hourly
plot(total, name = "Total")

Moving only when it is reached#

A place that the script reaches on every bar gives the state every bar, and that is what a call at the top of the script does. A place inside a block that runs on some bars gives the state only those bars. The state does not advance on the bars where the block is skipped, and it does not go back to zero either: it waits where it was.

The checker tells you when it sees this. A call that keeps a state inside an if, else or match block is allowed and is a warning, W3302:

Cuprel · warning W3302
// Warning W3302: a call that keeps a state stands in an `if` block. It is allowed, but its state moves only on the bars where the block runs.
// Here the average of the up bars is smoothed over the up bars only, which is rarely what the line says.
indicator "Stateful call in a block"
    edition = 1
    display = price

mut smoothed: float = na
if close > open
    smoothed = tech.ema(close, 5)
plot(smoothed, name = "Smoothed")

Sometimes that is exactly what you want: an average of the up bars only. Often it is not, and the usual fix is to run the call on every bar above the block and let the block choose what to keep:

Cuprel
// The fix for W3302: run the call on every bar above the block, and let the block only choose what to keep.
indicator "Stateful call above the block"
    edition = 1
    display = price

let smoothed_all = tech.ema(close, 5)
mut smoothed: float = na
if close > open
    smoothed = smoothed_all
plot(smoothed, name = "Smoothed")

Where a call may stand#

Where the call is writtenWhat the language does
At the top level of the script, or on the right of a letReached on every bar. No message.
In the body of an if or else block, or in an arm of a matchAllowed. The warning W3302 says the state moves only where that code runs.
Directly in the right side of and, or or ??, or in an arm of if ... then ... elseRefused: E3302. The side may not run, so the state would move on some bars only. See Lazy operators.
In a for or while loop, directly or through a function of yoursRefused: E3301. The passes would share one state.
In a function of yoursDepends on the place that calls the function: each call of the function is a place with its own states.

The checker reports W3302 and E3302. E3301 comes from the engine when it compiles the script for a run, not from the checker's own pass.

Reads that look like calls and keep none#

Some values are computed by the engine once for every bar, whether or not the script reaches the line that reads them. They have no state of their own for the line, so they may stand anywhere, including the right side of and:

  • the members of a declared session (london.active, london.high and the others): Trading day and sessions;
  • prior(day).high, current(week).open and the other period fields;
  • clock.reached(...), which depends on the time of the bar and the one before it, and bar.time_in(...), which depends on the time of the bar.
Cuprel
// A trading session is two times and a zone. `state session` keeps a value that restarts at the start of each session,
// so this draws the high of the London session so far, and the high of the New York session so far.
indicator "Session highs"
    edition = 1
    display = price

session london = 08:00 .. 16:30 @ "Europe/London"
session newyork = 09:30 .. 16:00 @ "America/New_York"
state session london london_high = high
state session newyork newyork_high = high
if london.active
    london_high = math.max(london_high, high)
if newyork.active
    newyork_high = math.max(newyork_high, high)
plot(london_high, name = "London high so far", color = color.green)
plot(newyork_high, name = "New York high so far", color = color.orange)

What it costs#

A call that keeps a state holds memory for as long as the script runs, but only as much as it reads back. Its memory follows the number of bars it reads (at most 8,192) and never the length of the chart. The memory is counted in the ledger of Limits and budgets, and every call spends steps from the budget of 500,000 for a bar.

See also: Lazy operators, E3302 and W3302, Choices and loops, Functions, Examples: calls that keep a state.