Language reference
The OxScript language definition: anatomy, types, control flow, bar data, inputs, the ta.* library, output, execution model and limits.
OxScript is the scripting language for ox-charts. A Pine Script user should be able to read an OxScript file without a manual and write one after skimming the "Coming from Pine" table. Every indicator the library ships is reachable from a script as a builtin, and every builtin is defined in terms of the same fold engine the native indicators use, so a script indicator and a native indicator are indistinguishable to the chart, the legend, the settings dialog and the datafeed.
Design rules, in priority order:
- Pine names, Pine semantics. If Pine has a name for it (
ta.ema,input.int,plot,plotshape,hline,fill,bgcolor,barcolor,var,na,nz,bar_index,barstate.*,[n]history), OxScript uses the same name with the same meaning. - Paste from TradingView works. Pine's indentation-based blocks,
=>functions,:=, tuples andswitchparse unchanged. A v5/v6 indicator script that uses the supported builtins compiles without edits; unsupported calls fail with a line-and-column error naming the call. - Easier where Pine is awkward, without breaking Pine. Keyword arguments on every call,
optional
{ }blocks, series allowed as function parameters, nosimple/seriesqualifier puzzles, nomax_bars_back. Every addition is a superset; valid Pine stays valid. - The library is the standard library. Every
IndicatorDefinition(68 today, plus the order-flow pack) is ata.*function. Multi-plot indicators return named tuples. Any indicator can also be instantiated generically by id throughta.indicator("keltner-channels", ...). - Static before dynamic. Inputs, plots, sources and warm-up are extracted from the AST without running the script. That powers the settings dialog, legend and the resources readout before the first bar executes.
- Bounded. Loops, recursion, drawings, collections, memory and wall-clock are all capped. A script cannot hang the chart.
1. Anatomy of a script
//@oxscript=1
indicator("EMA Cross", overlay=true)
fast = input.int(9, "Fast length", minval=1)
slow = input.int(21, "Slow length", minval=1)
src = input.source(close, "Source")
fastMa = ta.ema(src, fast)
slowMa = ta.ema(src, slow)
plot(fastMa, "Fast", color=color.green)
plot(slowMa, "Slow", color=color.red)
plotshape(ta.crossover(fastMa, slowMa), "Long", style=shape.triangleup, location=location.belowbar, color=color.green)
plotshape(ta.crossunder(fastMa, slowMa), "Short", style=shape.triangledown, location=location.abovebar, color=color.red)Structure, top to bottom:
| Section | Rule |
|---|---|
//@oxscript=1 | First line; the current OxScript version. No header means the latest OxScript. //@version=5 / 6 is Pine compatibility mode; //@version=1–4 and unknown @oxscript versions are refused. |
indicator(...) or strategy(...) | Exactly one, before any statement that produces output. |
| Inputs | input.* calls. Evaluated once, before bar 0. May appear anywhere at top level but are hoisted. |
| Body | Runs once per bar, top to bottom, exactly like Pine. |
| Outputs | plot*, hline, fill, bgcolor, barcolor, drawings, alertcondition. |
The header:
indicator(title, shorttitle=title, overlay=false, format=format.inherit, precision=auto,
scale=scale.right, timeframe=na)overlay=true draws in the price pane; otherwise the script gets its own pane. format.price,
format.volume, format.percent control legend and axis formatting.
2. Lexical structure
- Comments
// to end of line. - Statements end at a newline. A statement continues on the next line when the line ends with an operator, a comma, or an open bracket, or when the next line starts with a closing bracket. Semicolons are accepted and ignored.
- Blocks are indentation-based, exactly as in Pine: the body of
if,else,for,while,switchand a multi-line=>function is the run of lines indented deeper than the header line. Four spaces or one tab per level; mixed indentation inside one block is an error. Braces{ }are accepted as an alternative and may be mixed with indentation blocks file by file, not inside one block. Pine's line-continuation rule (a continued line is indented by an amount that is not a multiple of four) is honoured, so longplot(...)calls paste unchanged. - Identifiers
[A-Za-z_][A-Za-z0-9_]*. Namespaces are dotted:ta.ema,input.int,color.red,math.abs,str.tostring,barstate.islast,strategy.entry. - Literals integers, floats (
1.5,2e-3), strings in"or',true/false,na, colours#rrggbband#rrggbbaa.
3. Types
Every value has a static type. Types are inferred; annotations are optional and Pine-style.
| Type | Notes |
|---|---|
int, float | int widens to float silently. Division of two ints yields float. |
bool | if, and, or, not require bool. No truthy numbers. |
string | Immutable. + concatenates. |
color | color.red, #ff0000, color.new(c, transp), color.rgb(r,g,b,a). |
series<T> | Any value that can differ per bar. close, ta.ema(...), any expression containing one. |
T[] | Arrays: array.new<float>(), literal [1, 2, 3]. Bounded by MAX_COLLECTION_SIZE. |
map<K,V> | map.new<string, float>(). |
| tuples | [a, b, c] = ta.macd(close, 12, 26, 9) destructures. Tuples are not first-class values. |
label, line, box, table | Drawing handles. label.new(...) returns one; methods mutate it. |
na is the missing value for every type. na(x) tests it, nz(x, y) replaces it, fixnan(x)
carries the last non-na value forward. Arithmetic with na yields na. Comparisons with na
yield false, which is what Pine does and what users expect.
There is no simple/series/const/input qualifier vocabulary to learn. The compiler tracks
whether a value is per-bar; the only place it matters is the length argument of a lookback
function, which must be known before bar 0 (an input or a literal or an expression of those). The
error message says exactly that.
4. Variables
x = close * 2 // recomputed every bar
var count = 0 // initialised once on bar 0, then persists across bars
varip ticks = 0 // persists across bars AND across live-bar updates
count := count + 1 // reassignment uses := (Pine)=declares. Redeclaring a name in the same scope is an error.:=assigns to an existing variable. Assigning to a name that was never declared is an error.varis Pinevar: the initialiser runs on the first bar only. This is the fold's state.varipsurvives the live-bar rollback. Use it for tick counters. Rare; documented as advanced.
Every variable is a series and can be indexed with history:
prev = close[1]
wasHigher = high[2] > high[1]
smoothed = nz(smoothed[1]) * 0.9 + close * 0.1 // self-reference: Pine idiom, worksThe compiler finds the largest constant index used on each variable and allocates a ring buffer of
that depth in the fold state. There is no max_bars_back. A dynamic index (close[n] with n a
series) is allowed up to MAX_LOOKBACK (5000) and is rejected at compile time beyond it.
5. Control flow and functions
if fastMa > slowMa
trend := 1
else if fastMa < slowMa
trend := -1
else
trend := 0
sig = switch
rsi > 70 => -1
rsi < 30 => 1
=> 0
for i = 0 to len - 1 // inclusive, like Pine
sum := sum + close[i]
for [k, v] in someMap
...
while x > 0
x := x - 1if and switch are expressions when every branch ends with a value:
col = if up ? color.green : color.red or the multi-line Pine form. The brace form of any block
is also accepted: if up { trend := 1 } else { trend := -1 }.
Functions:
f(x, y) => x + y // single expression, Pine style
wavg(src, len) => // multi-line body, last expression is the return value
sum = 0.0
for i = 0 to len - 1
sum := sum + src[i] * (len - i)
sum / (len * (len + 1) / 2)
wavg(close, 10)method declarations (method f(series float src) => ...) and export are parsed and accepted;
import of TradingView libraries is rejected with a clear error.
Functions accept series parameters, including ones with history (src[i] above). Each call site
gets its own state for any var or ta.* inside, exactly as Pine promises. Recursion is capped at
MAX_RECURSION_DEPTH (200). Functions cannot call plot* or input.*; those are top-level only.
6. Bar data
| Name | Type | Meaning |
|---|---|---|
open high low close volume | series<float> | The chart's bars. |
hl2 hlc3 ohlc4 hlcc4 | series<float> | Derived prices. |
time | series<int> | Bar open time, ms since epoch (Pine convention). |
bar_index | series<int> | 0-based index in the loaded history. |
barstate.isfirst isconfirmed islast isrealtime ishistory isnew | bool | Same meanings as Pine. |
syminfo.ticker syminfo.tickerid syminfo.mintick syminfo.type | string/float | From the resolved symbol. |
timeframe.period timeframe.multiplier timeframe.isintraday | From the chart resolution. |
Order-flow fields exist on every bar and are na when the datafeed did not supply footprint data:
| Name | Type | Meaning |
|---|---|---|
delta | series<float> | Σ(ask − bid) for the bar. Exact from footprint, proxied from OHLC otherwise via of.proxy. |
deltaHigh deltaLow | series<float> | Delta extremes while the bar formed. |
levelSize | float | Price granularity of the footprint levels. |
levels | series<level[]> | Footprint ladder for the bar. level has price, bid, ask, total, delta. |
of.poc() | series<float> | Price of the highest-volume level. |
of.imbalance(ratio=3) | series<int> | Count of diagonal imbalances at the ratio. |
Requesting other data:
htfClose = request.security(syminfo.tickerid, "60", close) // higher timeframe, Pine signature
spot = request.security("NSE:NIFTY", timeframe.period, close)request.security shares the chart's datafeed. Each distinct (symbol, timeframe) pair costs one
source slot from a budget of MAX_SOURCES (10); the chart's own series is free.
lookahead is not offered; the higher-timeframe value is the confirmed value as of the current bar,
which is the non-repainting behaviour. barmerge.gaps_on is supported.
7. Inputs
len = input.int(14, "Length", minval=1, maxval=500, step=1, group="Core", tooltip="Lookback")
mult = input.float(2.0, "Multiplier", minval=0.1, step=0.1)
show = input.bool(true, "Show bands")
mode = input.string("Close", "Mode", options=["Close", "HL2", "Typical"])
src = input.source(close, "Source")
col = input.color(color.blue, "Line colour")
tf = input.timeframe("60", "Timeframe")Every input.* compiles to an IndicatorParamSpec and appears in the standard settings dialog
with the existing controls; group becomes a section. input.source offers the same source list
the native indicators use plus any plot of another active indicator (the engine's
instance:plot sources). Input values are simple, so they are valid as lookback lengths.
The legend title interpolates inputs Pine-style: indicator("EMA", shorttitle="EMA {len}").
8. The standard library: ta.*
Every library indicator is a ta. function. Naming follows Pine where Pine has the indicator, and
the library id (camel-cased) where it does not. Positional order is (source, length, ...) in
Pine's order. All arguments also accept keywords.
Single-plot (return series<float>):
ta.sma(src, len) ta.ema(src, len) ta.wma(src, len) ta.rma(src, len) ta.smma(src, len)
ta.hma(src, len) ta.dema(src, len) ta.tema(src, len) ta.vwma(src, len) ta.lsma(src, len)
ta.alma(src, len, offset=0.85, sigma=6) ta.kama(src, len, fast=2, slow=30) ta.mcginley(src, len)
ta.rsi(src, len) ta.cci(src, len) ta.cmo(src, len) ta.roc(src, len) ta.mom(src, len)
ta.wpr(len) ta.mfi(src, len) ta.tsi(src, short, long) ta.uo(short=7, mid=14, long=28)
ta.atr(len) ta.tr(handle_na=true) ta.stdev(src, len) ta.variance(src, len)
ta.obv() ta.adl() ta.cmf(len) ta.emv(len) ta.forceIndex(len) ta.bop()
ta.nvi() ta.pvi() ta.chop(len) ta.hv(len) ta.trix(len) ta.dpo(len)
ta.fisher(len) ta.coppock(wma=10, longRoc=14, shortRoc=11) ta.ao() ta.psar(start, inc, max)
ta.vwap(src=hlc3, anchor="session") ta.zigzag(deviation=5)
ta.highest(src, len) ta.lowest(src, len) ta.highestbars(src, len) ta.lowestbars(src, len)
ta.change(src, n=1) ta.cum(src) ta.sum(src, len) ta.percentrank(src, len) ta.linreg(src, len, offset)
ta.correlation(a, b, len) ta.crossover(a, b) ta.crossunder(a, b) ta.cross(a, b)
ta.rising(src, len) ta.falling(src, len) ta.barssince(cond) ta.valuewhen(cond, src, n)
ta.pivothigh(src, left, right) ta.pivotlow(src, left, right)Multi-plot (return a tuple; destructure or take a field):
[macdLine, signalLine, hist] = ta.macd(src, 12, 26, 9)
[basis, upper, lower] = ta.bb(src, 20, 2)
[basis, upper, lower] = ta.kc(src, 20, 2, atrLength=10)
[upper, basis, lower] = ta.donchian(20)
[basis, upper, lower] = ta.envelope(src, 20, percent=10)
[k, d] = ta.stoch(high, low, close, 14, 3, 3)
[k, d] = ta.stochrsi(src, rsiLength=14, stochLength=14, k=3, d=3)
[adx, plusDI, minusDI] = ta.dmi(14, adxSmoothing=14)
[up, down] = ta.aroon(14)
[st, dir] = ta.supertrend(3, 10)
[tenkan, kijun, senkouA, senkouB, chikou] = ta.ichimoku(9, 26, 52, displacement=26)
[viPlus, viMinus] = ta.vortex(14)
[kst, sig] = ta.kst(...)
[rvi, sig] = ta.rvi(10)
[klinger, sig] = ta.klinger(34, 55, 13)
[pp, r1, r2, r3, s1, s2, s3] = ta.pivotPoints(period=78, variant="standard")
[hi, lo] = ta.highLow(52)Field access works too: ta.bb(close, 20, 2).upper. Field names are the library plot keys.
Order-flow pack:
of.delta() // per-bar delta (same as the `delta` field)
of.cvd(reset="session", sessionOffsetMinutes=330)
of.deltaPercent()Generic access to any library indicator by id, for indicators that have no ta. alias yet and for
host-registered custom definitions:
kc = ta.indicator("keltner-channels", length=20, atrLength=10, multiplier=2)
plot(kc.upper); plot(kc.lower)Every ta.* call compiles to an engine instance of the corresponding IndicatorDefinition bound
to the given source. The script never re-implements the math, so a script ta.ema and the native
EMA in the picker plot the identical line, seeded the same way (TradingView-style). Per-call-site
state is guaranteed: ta.ema(close, 20) in a loop body or a function gets one instance per site
and per function invocation context, like Pine.
Math and helpers: math.abs ceil floor round(x, precision) max min pow sqrt log log10 exp sign sin cos tan avg sum and math.pi; str.tostring(x, format) tonumber length contains replace upper lower format("{0} / {1}", a, b); color.new(c, transp) rgb(r,g,b,a) from_gradient(value, lo, hi, cLo, cHi); array.* and map.* with Pine's names (push, get, set, size,
sum, avg, max, min, sort, slice, includes).
9. Output
Plots bind to the library's series types; every plot has a legend row with its live value.
plot(series, title="", color=, linewidth=1, style=plot.style_line, offset=0, display=display.all,
overlay=na, trackprice=false, histbase=0, editable=true)style | ox-charts series |
|---|---|
plot.style_line | line |
plot.style_stepline | step-line |
plot.style_area | area |
plot.style_histogram | histogram (base = histbase) |
plot.style_columns | column |
plot.style_circles, plot.style_cross | line-markers |
plot.style_band | band (used by fill) |
color may be a series: plot(hist, color=hist >= 0 ? color.green : color.red) compiles to a
per-point colour on histogram and column series, the same path Volume Delta uses today.
overlay= per plot overrides the header, for a script that mixes price-pane and pane plots.
hline(price, title="", color=, linestyle=hline.style_solid, linewidth=1)
fill(plot1, plot2, color=, title="") // also fill(hline1, hline2, ...)
bgcolor(color, offset=0) // background band per bar
barcolor(color) // recolour the candle
plotshape(cond, title, style=shape.circle, location=location.abovebar, color=, size=size.small, text="")
plotchar(cond, title, char="•", location=, color=, size=)
plotarrow(series, title, colorup=, colordown=)
plotcandle(o, h, l, c, title, color=, wickcolor=)
plotbar(o, h, l, c, title, color=)Drawings map to the library's drawing tools and follow Pine's object API:
l = line.new(bar_index[10], low[10], bar_index, low, color=color.blue, width=2, extend=extend.right)
line.set_xy2(l, bar_index, close)
b = box.new(left, top, right, bottom, bgcolor=color.new(color.orange, 80))
lb = label.new(bar_index, high, "POC", style=label.style_label_down, textcolor=color.white)
line.delete(l)Handles created on a bar are re-created on every bar unless stored in a var, as in Pine. Object
counts are capped per kind (MAX_DRAWINGS_PER_KIND, 500); the oldest is garbage-collected first,
which is Pine's behaviour too. table.new / table.cell render an anchored table, used for
per-bar readouts like Volume Bubbles' data window.
Alerts are declared, not fired, by the script:
alertcondition(ta.crossover(fastMa, slowMa), "Long cross", "EMA fast crossed above slow on {{ticker}}")The host receives the list of conditions and their per-bar bool column and decides how to deliver.
10. Execution model
- Compile. Source → tokens → AST → static analysis → generated fold. Static analysis
produces the
IndicatorDefinitionshell:paramsfrom inputs,plotsfrom output calls,warmupfrom the longest lookback chain, the source budget fromrequest.*, and the list ofta.*instances to create. Errors carry line and column. - Bar 0..n. The fold runs once per closed bar, top to bottom.
varstate and ring buffers live in the fold state;ta.*results are engine instances feeding the fold through the existing DAG. Outputs land in Float64Array columns index-aligned with bars, NaN during warm-up, exactly as native indicators do. - Live bar. On every tick the engine restores the one-step snapshot and re-runs the bar
(Pine's rollback).
varipvariables are excluded from the snapshot.barstate.isconfirmedisfalseduring ticks andtrueon the closing run. - History reload / symbol change.
setHistoryreplays from bar 0. Scripts are pure functions of (inputs, bars), so this is always safe. - Where. The generated fold runs in a Worker owned by the shell, with a fuel counter in every loop and function entry, and a terminate watchdog. Bars cross as transferable typed arrays. The main thread only sees columns and drawing lists.
11. Limits
| Limit | Value | Behaviour |
|---|---|---|
MAX_PLOTS | 64 | Compile error. A series-coloured plot counts as 2, like Pine. |
MAX_LOOKBACK | 5000 | Compile error for constant indices; runtime na beyond for dynamic ones. |
MAX_SOURCES | 10 | Compile error; chart's own series is free. |
MAX_LOOP_ITERATIONS per bar | 1,000,000 | Runtime error, script paused with message. |
MAX_TOTAL_FUEL per bar | 50,000,000 ops | Same. |
MAX_RECURSION_DEPTH | 200 | Same. |
MAX_COLLECTION_SIZE | 100,000 | Same. |
MAX_DRAWINGS_PER_KIND | 500 | Oldest evicted. |
| Wall-clock per bar | 500 ms | Script paused. |
| Worker memory | browser default | Worker terminated, script paused. |
12. Coming from Pine
Nothing to relearn:
//@version, indicator/strategy, = vs :=, var, varip, na/nz/fixnan, [n]
history, bar_index, barstate.*, syminfo.*, timeframe.*, ta.* names and argument order,
input.* names and options, plot/plotshape/plotchar/plotarrow/hline/fill/bgcolor/
barcolor, label/line/box/table object APIs, color.*, math.*, str.*, array.*,
map.*, request.security, alertcondition, for … to, switch, => functions, tuples.
What is different, and why:
| Pine | OxScript | Why |
|---|---|---|
| Indentation blocks | Same, plus optional { } | Pine scripts paste unchanged. |
import user/lib/1 | Not available | No TradingView library registry; the error names the import. |
simple/series/const qualifiers | Inferred | Users hit these errors constantly; we only enforce the one rule that matters (lengths known before bar 0). |
max_bars_back | Inferred | Largest constant index per variable is computed statically. |
| Positional-only for most calls | Keyword arguments everywhere | ta.alma(close, length=9, offset=0.85, sigma=6) reads better and matches the settings dialog. |
| Series cannot be modified inside functions in some contexts | Series parameters are first-class | Fewer surprises. |
request.security(..., lookahead=) | No lookahead | Only the non-repainting behaviour exists. |
| Cloud execution | Browser Worker | No account or round-trip; strategies may later run host-side with the same contract. |
| No order-flow data | delta, levels, of.* | The library's footprint feed is a first-class input. |
| Custom indicators by id | ta.indicator(id, ...) | Any library or host-registered definition is scriptable without a new alias. |
13. Worked examples
RSI with bands and fill (own pane):
//@oxscript=1
indicator("RSI", format=format.price, precision=2)
len = input.int(14, "Length", minval=1)
src = input.source(close, "Source")
ob = input.int(70, "Overbought")
os = input.int(30, "Oversold")
r = ta.rsi(src, len)
p = plot(r, "RSI", color=color.purple)
hOb = hline(ob, "Overbought", color=color.gray)
hOs = hline(os, "Oversold", color=color.gray)
fill(hOb, hOs, color=color.new(color.purple, 90))
bgcolor(r > ob ? color.new(color.red, 90) : r < os ? color.new(color.green, 90) : na)
alertcondition(ta.crossover(r, ob), "RSI overbought")Footprint imbalance markers (overlay, order-flow data):
//@oxscript=1
indicator("Stacked imbalances", overlay=true)
ratio = input.float(3, "Imbalance ratio", minval=1.5, step=0.5)
minStack = input.int(3, "Min stacked", minval=2)
var stackedBuy = 0
stackedBuy := 0
for i = 1 to array.size(levels) - 1
lvl = array.get(levels, i)
below = array.get(levels, i - 1)
buyImb = lvl.ask >= below.bid * ratio and below.bid > 0
stackedBuy := buyImb ? stackedBuy + 1 : 0
plotshape(stackedBuy >= minStack, "Stacked buy", style=shape.triangleup, location=location.belowbar, color=color.green)
plot(of.cvd(reset="session"), "CVD", overlay=false, style=plot.style_histogram, color=of.cvd() >= 0 ? color.green : color.red)Composing library indicators:
//@oxscript=1
indicator("Keltner squeeze", overlay=true)
[bbBasis, bbUp, bbLo] = ta.bb(close, 20, 2)
[kcBasis, kcUp, kcLo] = ta.kc(close, 20, 1.5, atrLength=20)
squeeze = bbUp < kcUp and bbLo > kcLo
barcolor(squeeze ? color.orange : na)
plot(kcUp, "KC upper", color=color.new(color.blue, 40))
plot(kcLo, "KC lower", color=color.new(color.blue, 40))14. Open decisions
Decided 2026-09-21:
- Name. OxScript, file extension
.oxs. - Paste from TradingView is a requirement. Indentation blocks are the primary syntax; the parser targets Pine v5/v6 indicator scripts verbatim. A conformance suite of public Pine indicators (TradingView built-ins, common community scripts) is part of the compiler's tests.
- Built-ins stay native folds in v1. Scripts are for user indicators only.
- Strategy API deferred.
strategy(...),strategy.entry/exit/close/*and the broker emulator are phase two. The parser reserves thestrategynamespace now and reports "strategies are not supported yet" instead of a generic error.
Still open:
- Which Pine builtins are out of scope for v1 (candidates:
request.financial,request.dividends,polyline,linefill,matrix.*,ticker.*). Scripts using them get a named error.