millionchart
فارسی

Determinism and limits

What makes a script give the same output on the same bars: random numbers and seeds, history, budgets, the price grid, and what is not a function of the bars.

A script is a function of its text, its inputs and the bars it runs on. Run it twice on the same bars with the same inputs and it draws the same lines, marks and drawings, and a strategy makes the same trades. This page lists the pieces that keep that true, and the few values that are not a function of the bars.

The documentation checks the promise for its own examples. Every program in the examples is compiled and run twice on the same synthetic, seeded market data each time the documentation is built, and a short fingerprint of what each one drew is compared with the one stored. If the fingerprint of an example moves, the build fails until someone has looked at why.

What is not a function of the bars#

  • clock.now is the current time as the platform knows it. A script that reads it gives a different answer at a different time. Use it only where that is the point.
  • The bars themselves. A chart loads the history it loads; a script that looks back further than the chart has gets na, which is a fact about the data, not a failure of the script.
  • The inputs: a different input value is a different run.
  • The forming bar. While the newest bar is still forming, the script runs on it again with every new price; the closed bars before it do not change. Results that look too good covers what a script may read there.

Everything else is calculated from these and from the text of the script.

Random numbers and their seed#

math.random(min, max) is not random in the sense of a coin: it is a fixed list of numbers, and the script reads the next one at the place where it asks. The list depends on four things and on nothing else:

  1. the identity of the script: the id the platform stores the script under; for a script that is not stored yet, the title in its header; failing that, a fixed word;
  2. the values of the inputs, by their position in the script;
  3. the seed the host gives the run, if any;
  4. the dataset the run is on.

The call has a number too: its place among the math.random calls of the script. A number is drawn by the bar and the place that asks for it, so the numbers of a closed bar do not change when a bar is added or when the chart loads more history, and one call never takes the numbers of another.

What this means in practice, measured on a small script that plots math.random(0, 1):

What you changeThe numbers
Formatting, comments, the name of a variable, the order of unrelated linesthe same
The title of a stored script, or editing it (its stored id stays)the same
The title of a script that has no stored id (the identity is the title)other numbers
An input, even one the call does not readother numbers
The dataset or the host's seedother numbers
Adding or removing a math.random call above another onethe calls after it take another place

If a draw must survive any of these, give the call its own seed. A call with seed = n uses a generator that depends only on n and on the place that asks, so changing the title, the inputs or the dataset leaves it alone:

Cuprel
// math.random is deterministic: the same seed and the same call site give the same numbers on every run and every machine.
// The seed is part of the call, so changing it changes the numbers and nothing else does.
indicator "Seeded noise"
    edition = 1
    display = pane

input seed: int = 7, name = "Seed", min = 0
let noise = math.random(-1, 1, seed = seed)
plot(noise, name = "Noise", style = histogram, color = color.gray)
plot(tech.sma(noise, 20), name = "Average of the noise", color = color.orange)

The history the engine keeps#

A series keeps the bars the text says it reads and no more, so a script uses the same memory on a chart of a thousand bars and of a hundred thousand. A read can go back at most 8,192 bars. A read that goes further is an error, never a silent na:

  • a number written in the script is found before the run (E-LIMIT-HISTORY-STATIC);
  • a number worked out while the script runs stops it on that bar (E-LIMIT-HISTORY);
  • a read that nothing in the text bounds, such as an input without a max, runs and keeps up to 8,192 bars; the editor says so with W-ENGINE-HISTORY-OPEN.
Cuprel
// Reading earlier bars: `close[n]` is the close n bars ago, and the engine keeps a bounded history for every series.
// This compares the close with the close 50 bars ago, 100 bars ago, and the highest close of the last 200.
indicator "Look back"
    edition = 1
    display = pane

let change_50 = close - close[50]
let change_100 = close - close[100]
let from_peak = close - tech.highest(close, 200)
plot(change_50, name = "Change over 50 bars", color = color.blue)
plot(change_100, name = "Change over 100 bars", color = color.purple)
plot(from_peak, name = "From the 200-bar high", color = color.red)
Cuprel · error E-LIMIT-HISTORY-STATIC
// Mistake: a window longer than the history the engine keeps cannot be filled, so the run is refused before it starts (E-LIMIT-HISTORY-STATIC).
indicator "Window beyond the history"
    edition = 1
    display = price

plot(tech.sma(close, 100000), name = "Too long")

Budgets#

Three budgets bound a run, and a script cannot raise them: the steps one bar may spend (500,000), the steps a whole run may spend (600,000,000), and the bytes the script holds and builds (the memory ledger: 134,217,728 held at a moment, 2,147,483,648 built over a run). They count work the engine does, not seconds: a loop pass, a window walk, a drawing, a character of text built. The count is the same on every machine, so a script that goes over a budget stops at the same bar and the same line everywhere, and the message says which. Limits and budgets has the numbers, and the error of a loop that does not end is on that page.

The clock-time limit is the one exception: a run on a chart also stops after 5,000 ms of real time, and that depends on the machine. A script that is near a step budget on a slow machine can meet the time limit first (E-LIMIT-TIME).

The price grid#

A price on a chart lives on a grid whose step is the tick of the symbol (symbol.tick). Prices from the data are on the grid. A price you work out is not: close - 2 * tech.atr(14) is almost never a multiple of the tick. The tester fills an order at the price it is given. It does not round it to the grid, so a stop worked out from the range is filled at that exact number, and a trade can exit at a price the market could never have had.

symbol.snap(price) rounds a price to the nearest tick; symbol.snap_down and symbol.snap_up round to the tick below and above; symbol.ticks(distance) turns a distance into a number of ticks and symbol.price_of(ticks) does the reverse. For a stop or a target, snap the price:

Cuprel
// Prices live on a grid of ticks. symbol.snap rounds a price to the nearest tick, so a stop or a target is a price the market can have.
strategy "Stops on the price grid"
    edition = 1
    display = price

setup:
    capital = 10000
    size = sizing.lots(0.1)

let range = tech.atr(14)
let fast = tech.ema(close, 8)
let slow = tech.ema(close, 21)
if tech.crosses_above(fast, slow)
    orders.enter("Long", side.long)
    orders.exit(
        "Long exit",
        "Long",
        stop = symbol.snap(close - 2 * range),
        limit = symbol.snap(close + 3 * range),
    )
plot(fast, name = "Fast", color = color.green)
plot(slow, name = "Slow", color = color.red)

Numbers#

The functions that two machines are allowed to compute a little differently (math.log, math.exp, math.pow and the trigonometric functions) are written in the engine itself and do not call the machine's own. Addition, subtraction, multiplication, division, square root and rounding are fixed by the number format. The statement and its measured limit are on Numbers.

The examples of this page#

The examples of determinism run the programs above and a few more on the documentation's synthetic data: the seed, the history, the price grid and the budget of steps.

See also: Limits and budgets, Calls that keep a state, Engine messages.