VP Script
The language
VP Script is TypeScript with the type annotations erased before it runs. What is worth learning is not the syntax but the shape of the data: everything is a whole-array series, and a series is allowed to have holes.
Everything is a series
A series is one value per bar, in bar order. close is a series; ta.sma(close, 20) is a series; the result of comparing two series is a series. Your script runs once, over all of them.
const fast = ta.sma(close, 10) // one value per barconst slow = ta.sma(close, 30)const spread = math.sub(fast, slow)A gap is a value, and it spreads
null means there is no value for that bar: the first nineteen bars of a 20-bar average, a foreign feed that has not reported, a division that had nothing to divide. It is not zero, and it must not become zero. A moving average that reads 0 before it warms up draws a cliff that never happened.
This is why math.add and friends exist. Plain a + b on a null gives NaN, which draws as a break; the math.* helpers propagate the gap instead, so the result is null exactly where an input was.
const a = ta.sma(close, 50)const b = ta.sma(close, 200) const wrong = a.map((v, i) => (v ?? 0) - (b[i] ?? 0)) // invents zerosconst right = math.sub(a, b) // null until both existLoops are ordinary, and bounded
Write the loop you mean. There is no bar-by-bar execution model to work around, so a state machine over the bars is just a for loop and a couple of variables.
indicator('Bars since a new high', { pane: 'separate' }) const hh = ta.highest(high, 50)const since = new Array(barCount).fill(null)let count = 0for (let i = 0; i < barCount; i++) { count = high[i] !== null && high[i] === hh[i] ? 0 : count + 1 since[i] = count}plot(since, { style: 'histogram' })Every loop carries an iteration budget, decremented as it runs. A script that would spin forever is stopped with an error naming the line rather than freezing the chart. You will not meet the budget doing anything reasonable over a chart’s worth of bars.
What a script may not do
Some constructs are refused when the script compiles, with a line number, rather than failing strangely later:
importandexport. A script is self-contained.async,await,yield. A script is synchronous, so there is nothing to wait for: the bars are already in hand.class,enum,namespace. Not available; plain functions and objects are.evalandFunction. Evaluating text as code.DateandIntl. See below.
No clock, no locale, no randomness
A script must be a pure function of its bars and its inputs. Two runs over the same bars must produce the same drawing, for you and for every reader.
That rules out the wall clock and locale-dependent formatting, so Date, Intl, Math.random and performance are not available. Both of the first two are refused at compile time with an explanation, because reaching for them is a reasonable mistake rather than an attempt at anything.
Every temporal fact you need is already on the bar. Read bars[i].time instead of asking what time it is now, and format numbers with toFixed rather than a locale formatter that would render differently in another country.
Where your script runs
Your own scripts run in the page while you are editing them. Anything installed from the community shop runs somewhere stricter: an isolated frame on a separate domain, with no network, no cookies, no access to your account, and a watchdog that stops a script that stops responding. Nothing you write can reach another reader’s data, and nothing they write can reach yours.