Execution Model
How scripts are initialized and evaluated across history and realtime updates.
Lifecycle
Scripts follow this flow:
- Create a fresh script context and register globals/constants.
- Install plotting helpers and built-ins.
- Execute your script once (top-level scope).
- Repeatedly call
onBar(index)during history/realtime evaluation.
Top-level code is where you define metadata, inputs, and subscriptions.
//@version=2
indicator("My Script", false)
const len = input.int("Length", 20)
const candles = subscribe(data.OHLCV)
function onBar(index) {
plot("SMA", ta.sma(candles.close(), len))
}onBar(index) Semantics
onBar(index) receives a relative index from newest bar in the current evaluation window:
index = 0means newest bar in that runindex = 1means previous bar- etc.
History execution and realtime execution both use this distance-from-newest convention.
Higher-Timeframe Subscriptions
onBar(index) still runs on the script's primary/chart timeframe.
If you subscribe to a higher timeframe with subscribe(..., { timeframe }), that subscription only advances when its own timeframe rolls. Between those boundaries, accessors like htf.close() keep returning the current higher-timeframe bar.
If you need derived values to advance on the higher-timeframe clock, use subscription.calc(...) on that accessor instead of recalculating from htf.close() inside every lower-timeframe onBar() call.
Using higher-timeframe subscriptions does not change the script's primary execution clock:
onBar(index)still advances on the chart timeframecontext.timeframestill refers to the chart timeframe- global
unix(offset?)still refers to the chart series htf.unix(offset?)refers to the subscribed higher-timeframe series
That distinction matters for boundary detection:
//@version=2
indicator("Execution Model HTF Example", false)
const htf = subscribe(data.OHLCV, { timeframe: "30m" })
function onBar(index) {
const newHtfBarOnChart = unix() === htf.unix()
const prevHtfBar = htf.unix(1)
if (newHtfBarOnChart) {
plot("HTF Boundary", htf.close(), { color: color.orange })
}
}New Bar vs Realtime Update
isNewBar()istruewhen current unix timestamp differs from previous evaluation.isRealtimeBar()is the inverse (!isNewBar()).- If unix is unchanged, the current bar is updated in-place.
Context State Per Bar
context flags update every bar:
context.isFirstcontext.isHistorycontext.isLastcontext.isNewcontext.isRealtime
See Context for full definitions.
Top-Level Only Calls
These are not allowed inside onBar():
indicator(...)subscribe(...)subscription.calc(...)input.*(...)
Important
Calling top-level-only APIs inside onBar() throws an error.
subscription.calc(...) has its own restriction layer as well. Inside a calc callback, the runtime disallows plotting, entities, logging, inputs, additional subscriptions, and chart-bar helper functions. Keep calc callbacks side-effect free and return only numbers or flat object snapshots of numbers. Use NaN for missing numeric values.
Plot Channel Freeze After Warmup
After history warmup, plot channel declarations can be locked. If you create a new plot label after lock, execution throws:
plot channel declarations are frozen after history warmup
Practical rule: declare all labels you will use during history.
Runtime Error Behavior
Uncaught script errors move the script session into an error state. Reloading/recreating resets state and allows execution again.