ox-charts

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:

  1. 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.
  2. Paste from TradingView works. Pine's indentation-based blocks, => functions, :=, tuples and switch parse 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.
  3. Easier where Pine is awkward, without breaking Pine. Keyword arguments on every call, optional { } blocks, series allowed as function parameters, no simple/series qualifier puzzles, no max_bars_back. Every addition is a superset; valid Pine stays valid.
  4. The library is the standard library. Every IndicatorDefinition (68 today, plus the order-flow pack) is a ta.* function. Multi-plot indicators return named tuples. Any indicator can also be instantiated generically by id through ta.indicator("keltner-channels", ...).
  5. 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.
  6. 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:

SectionRule
//@oxscript=1First 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.
Inputsinput.* calls. Evaluated once, before bar 0. May appear anywhere at top level but are hoisted.
BodyRuns once per bar, top to bottom, exactly like Pine.
Outputsplot*, 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, switch and 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 long plot(...) 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 #rrggbb and #rrggbbaa.

3. Types

Every value has a static type. Types are inferred; annotations are optional and Pine-style.

TypeNotes
int, floatint widens to float silently. Division of two ints yields float.
boolif, and, or, not require bool. No truthy numbers.
stringImmutable. + concatenates.
colorcolor.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, tableDrawing 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.
  • var is Pine var: the initialiser runs on the first bar only. This is the fold's state.
  • varip survives 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, works

The 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 - 1

if 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

NameTypeMeaning
open high low close volumeseries<float>The chart's bars.
hl2 hlc3 ohlc4 hlcc4series<float>Derived prices.
timeseries<int>Bar open time, ms since epoch (Pine convention).
bar_indexseries<int>0-based index in the loaded history.
barstate.isfirst isconfirmed islast isrealtime ishistory isnewboolSame meanings as Pine.
syminfo.ticker syminfo.tickerid syminfo.mintick syminfo.typestring/floatFrom the resolved symbol.
timeframe.period timeframe.multiplier timeframe.isintradayFrom the chart resolution.

Order-flow fields exist on every bar and are na when the datafeed did not supply footprint data:

NameTypeMeaning
deltaseries<float>Σ(ask − bid) for the bar. Exact from footprint, proxied from OHLC otherwise via of.proxy.
deltaHigh deltaLowseries<float>Delta extremes while the bar formed.
levelSizefloatPrice granularity of the footprint levels.
levelsseries<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)
styleox-charts series
plot.style_lineline
plot.style_steplinestep-line
plot.style_areaarea
plot.style_histogramhistogram (base = histbase)
plot.style_columnscolumn
plot.style_circles, plot.style_crossline-markers
plot.style_bandband (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

  1. Compile. Source → tokens → AST → static analysis → generated fold. Static analysis produces the IndicatorDefinition shell: params from inputs, plots from output calls, warmup from the longest lookback chain, the source budget from request.*, and the list of ta.* instances to create. Errors carry line and column.
  2. Bar 0..n. The fold runs once per closed bar, top to bottom. var state 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.
  3. Live bar. On every tick the engine restores the one-step snapshot and re-runs the bar (Pine's rollback). varip variables are excluded from the snapshot. barstate.isconfirmed is false during ticks and true on the closing run.
  4. History reload / symbol change. setHistory replays from bar 0. Scripts are pure functions of (inputs, bars), so this is always safe.
  5. 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

LimitValueBehaviour
MAX_PLOTS64Compile error. A series-coloured plot counts as 2, like Pine.
MAX_LOOKBACK5000Compile error for constant indices; runtime na beyond for dynamic ones.
MAX_SOURCES10Compile error; chart's own series is free.
MAX_LOOP_ITERATIONS per bar1,000,000Runtime error, script paused with message.
MAX_TOTAL_FUEL per bar50,000,000 opsSame.
MAX_RECURSION_DEPTH200Same.
MAX_COLLECTION_SIZE100,000Same.
MAX_DRAWINGS_PER_KIND500Oldest evicted.
Wall-clock per bar500 msScript paused.
Worker memorybrowser defaultWorker 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:

PineOxScriptWhy
Indentation blocksSame, plus optional { }Pine scripts paste unchanged.
import user/lib/1Not availableNo TradingView library registry; the error names the import.
simple/series/const qualifiersInferredUsers hit these errors constantly; we only enforce the one rule that matters (lengths known before bar 0).
max_bars_backInferredLargest constant index per variable is computed statically.
Positional-only for most callsKeyword arguments everywhereta.alma(close, length=9, offset=0.85, sigma=6) reads better and matches the settings dialog.
Series cannot be modified inside functions in some contextsSeries parameters are first-classFewer surprises.
request.security(..., lookahead=)No lookaheadOnly the non-repainting behaviour exists.
Cloud executionBrowser WorkerNo account or round-trip; strategies may later run host-side with the same contract.
No order-flow datadelta, levels, of.*The library's footprint feed is a first-class input.
Custom indicators by idta.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:

  1. Name. OxScript, file extension .oxs.
  2. 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.
  3. Built-ins stay native folds in v1. Scripts are for user indicators only.
  4. Strategy API deferred. strategy(...), strategy.entry/exit/close/* and the broker emulator are phase two. The parser reserves the strategy namespace 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.

On this page