Language reference
The full Delta DSL grammar — file structure, types, operators, broadcasting rules, comments, errors. Everything that is NOT a function lives here.
The Delta DSL grammar is small. Read this page once and you have seen every syntactic shape the language supports. Function-shaped APIs (math, drawing, alerts) live in the dedicated reference pages — this page is purely about syntax, types, and the rules the runtime enforces.
File structure
A script file is read top-to-bottom in two passes:
- Analysis pass — directives (
@version,@name,@pane,@input) are extracted from comments / declarations and forwarded to the engine. Nothing else runs. - Evaluation pass — every non-directive statement runs in order, producing variable bindings and queueing draw calls.
A typical file looks like:
// 1. Language version (optional but recommended for new scripts)
@version 1
// 2. Header directives
@name "My Indicator"
@pane "below"
// 3. Inputs
@input length = input.int(14, "Length", minval=2, maxval=500)
@input src = input.source("close", "Source")
// 4. Calculations (assignments, branches, loops)
r = rsi(src, length)
ob = 70
os = 30
// 5. Drawing
paneRange(0, 100)
paneLine(r, color="#F0B90B", width=1.5)
paneHline(ob, color="rgba(255, 255, 255, 0.25)")
paneHline(os, color="rgba(255, 255, 255, 0.25)")
// 6. Alerts
alertcondition(crossover(r, os), title="RSI exit oversold")
alertcondition(crossunder(r, ob), title="RSI exit overbought")
The blocks aren't enforced by the parser — they're a convention. You can interleave them with one exception: @version MUST appear before any executable statement if you declare it at all (other directives like @name and @input may sit on either side of it). Scripts read better when directives come first, calculations come next, and side-effecting calls (drawing, alerts) come last.
Omitting @version is fine — the script defaults to v1 (the current language version). Adding the directive is the right choice for new scripts because it locks in the language semantics you wrote against; future runtime updates to v2 won't disturb your v1 code. See Versioning & Migration for the full story.
Comments
// Single-line comment. Runs to end of line.
ma = sma(close, 20) // Trailing comment after a statement.
Block comments (/* … */) are not supported. Use multiple // lines.
Types
Delta DSL has a small, mostly-implicit type system. Every value is one of:
| Type | Examples | Where it comes from |
|---|---|---|
number | 1, -3.14, 0.5e-3 | Literal, arithmetic, indicator math |
boolean | true, false, close > open | Comparisons, logical ops |
string | "text", "#F0B90B", 'hello' | String literal, tostring(...) |
series<number> | close, sma(close, 20), change(close) | Built-in series, vector math |
series<boolean> | close > open, crossover(a, b) | Comparisons over series |
series<string> | iff(close > open, upColor, dnColor) | iff with string branches |
na | na, nz(value, default) | NaN constant + helpers |
There are no explicit type annotations and no class system. The type of a value is determined by the expression that produced it.
Series vs scalar
A scalar is a single value; a series is a vector indexed by bar number.
Most stdlib functions and operators are broadcast-aware — they accept either a scalar or a series and return the matching shape:
ma = sma(close, 20) // series in, series out
diff = close - ma // series - series → series (element-wise)
big = close > 100 // series > scalar → series of booleans
mid = (high + low) / 2 // already a built-in: hl2
When mixing shapes, the rule is:
scalar OP scalar→scalarseries OP scalar→series(broadcast scalar to every bar)series OP series→series(element-wise; lengths must match — the runtime enforces this)scalar OP series→series(same as above; commutative)
NaN ("na")
NaN propagates through arithmetic — NaN + 1 == NaN, NaN > 0 == false. Use nz(value, default) to substitute a default and fixnan(value) to carry the last finite value forward across NaN gaps. iff(cond, value, na) masks bars where cond is false to NaN, which the renderer treats as a gap.
Numeric literals
0 1 -1 42
1.5 -3.14 .25 100.
1e6 2.5e-3 -1.0e+10
No integer / float distinction exists — every number is a 64-bit float. No hexadecimal / binary / octal literals. No underscore separators.
String literals
"text" 'text' "with \"escapes\""
"#F0B90B" "rgba(255, 100, 0, 0.5)"
Single and double quotes are interchangeable. Escapes: \\, \", \', \n, \t. No template literals (`), no ${} interpolation. Use tostring(...) plus the + operator for dynamic strings:
plotLabel(time[0], close[0], "RSI: " + tostring(r[0], "0.0"), color="#F0B90B")
Boolean literals
true and false. Comparisons return booleans (or boolean series). The boolean operators are and, or, not — not &&, ||, !.
breakout = (close > highest(high, 20)) and (volume > sma(volume, 20))
Variables and assignment
ma = sma(close, 20) // create binding
ma = ema(close, 20) // re-bind (allowed)
length := 50 // walrus — same as `length = 50`
Both = and := are accepted. = and := mean exactly the same thing; the walrus is included so Pine-Script-flavoured scripts compile unchanged.
Identifier rules:
- Start with a letter or underscore.
- Subsequent characters: letters, digits, underscore.
- Case-sensitive (
maandMAare different). - Cannot collide with reserved words (see below).
Reassignment is allowed
Unlike Pine, you can reassign a variable freely, and the type can change:
clr = "#F0B90B"
clr = iff(close >= ma, "#0ECB81", "#F6465D") // now a series<string>
There's no var / let distinction. Every assignment lives in the current scope.
Reserved words
| Group | Keywords |
|---|---|
| Boolean | true, false, and, or, not |
| Branching | if, elseif, else, end |
| Loops | for, to, step |
| Functions | fn, return |
if, for, fn, and end are block keywords — they MUST sit at the start of a statement (whitespace-only prefix) and they MUST be paired with a closing end.
Built-in identifiers (open, high, low, close, volume, time, bar_index, hl2, hlc3, ohlc4, bars, pi, e, na) are not reserved — you can shadow them in a local scope — but doing so is a confusing pattern. Avoid it.
Operators
Listed in decreasing precedence — top binds tighter than bottom. Operators of the same row are left-associative unless noted.
| Precedence | Operator | Meaning | Example |
|---|---|---|---|
| 1 | ( … ) | Grouping | (a + b) * c |
| 2 | f(args), series[k], obj.field | Call, history-shift, member access | sma(close, 20), close[1], d.kind |
| 3 | unary -, +, not | Negation, identity, boolean NOT | -x, not breakout |
| 4 | *, /, % | Multiplication, division, modulo | a * b, n % 2 |
| 5 | +, - | Addition / subtraction (or string concat for +) | a + b |
| 6 | <, <=, >, >= | Numeric comparison | close > open |
| 7 | ==, != | Equality | dir == 1 |
| 8 | and | Logical AND | a and b |
| 9 | or | Logical OR | a or b |
| 10 | cond ? a : b | Ternary if (right-associative) | dir == 1 ? upColor : dnColor |
Arithmetic
+, -, *, /, % are element-wise on series and broadcast scalars. Division by zero produces Infinity (not an error); modulo with zero produces NaN.
Important: + is string concatenation when EITHER operand is a string:
"RSI: " + tostring(r[0], "0.0") // "RSI: 67.4"
1 + 2 // 3
"a" + 1 // "a1"
Without this rule, "RSI: " + tostring(r) would silently coerce both operands to numbers and produce NaN. The grammar mirrors JavaScript / Python here.
Comparison
<, <=, >, >=, ==, != work on numbers, strings, and booleans. Comparisons broadcast over series and return a boolean series.
NaN comparisons all return false (including NaN == NaN). To check for NaN:
isMissing = not (x == x) // true when x is NaN
// or use fixnan / nz to fill instead of test
Logical
and, or, not are short-circuit for scalars and bitwise for series — both branches always run when at least one operand is a series, because the runtime evaluates the whole vector in one pass.
// Scalar — short-circuit:
if length > 0 and source != na
// safe: source is never read if length <= 0
end
// Series — both sides evaluated:
breakout = (close > highest(high, 20)) and (volume > sma(volume, 20))
For per-bar gating, prefer iff(cond, value, fallback) over if cond ... end blocks — see Control flow.
Ternary
clr = close >= ma ? upColor : dnColor
Right-associative, so a ? b : c ? d : e parses as a ? b : (c ? d : e). Identical semantics to iff(cond, a, b). Use whichever reads better — iff is friendlier for nested branches and accepts series-valued conditions cleanly.
History shift series[k]
series[k] returns the value of series k bars ago — close[0] is the current bar, close[1] the previous, close[5] five bars back. Negative k raises a runtime error. Reading past the available history returns NaN.
prevClose = close[1]
gap = open - close[1] // simple gap-up / gap-down detector
series[k] returns a scalar at the right edge of the visible window. Inside a for loop iterating over historical bars, use at(series, i) instead — it returns the scalar at absolute index i, not a relative shift.
Member access obj.field
The only objects you encounter are user-drawing entries from getUserDrawings():
for d in getUserDrawings()
if d.kind == "trendline"
plotVLine(d.anchors[0].t, color="#F0B90B")
end
end
Member access reads a frozen field (no assignment back). See Drawing.
Comments and whitespace
- Whitespace is significant only in that it separates tokens. A statement is one logical line, so
ma = sma(close, 20)is one statement. - A statement can span multiple physical lines if every line break is inside unmatched brackets:
plotLine(
ma,
color="#F0B90B",
width=2,
)
- Trailing commas inside argument lists are allowed and ignored (per the bracket-line-continuation rule above).
- Indentation is purely cosmetic. Convention: 2 spaces inside
if/for/fnblocks.
Function calls
Two argument forms are accepted, in any order, as long as positionals come before keywords:
plotLine(ma, "#F0B90B", 2) // positional
plotLine(ma, color="#F0B90B", width=2) // mixed
plotLine(series=ma, color="#F0B90B", width=2) // all keyword
Keyword names must match the function's parameter list. Unknown keywords raise an error at parse time.
User-defined functions (fn name(a, b) = …) follow the same rules. See Control flow.
Errors
Errors fall into three buckets:
- Parse error — bad syntax. Raised before any code runs. Surfaces in the editor's error gutter with a line / column.
- Analysis error — semantic mistake the parser caught (e.g. unknown directive, missing
end, bad input shape). - Runtime error — division-by-zero corner cases all return
NaN/Infinityinstead of raising. The runtime DOES raise on:- Negative history shift (
series[-1]). - Mismatched series lengths in arithmetic.
- Loop budget exhaustion (5 000 000 iter).
- Recursion depth exhaustion (64 frames).
- Draw record overflow (50 000 records).
- Calling an unknown function.
- Negative history shift (
When a runtime error fires, the script halts cleanly, the indicator legend shows a red error chip with the message, and the chart stays responsive. No mid-frame partial paint.
What's NOT in the language
For reference — these features intentionally do not exist:
- No classes / objects / records (read-only
getUserDrawings()aside). - No
null/undefinedkeywords. Usena(NaN) instead. - No exceptions /
try/catch. Useiff/nzto handle absent data. - No global mutable state across script invocations. Persistent drawings are slot-based via
labelNew/boxNew/lineNew(the slot's identity persists, the script does not). - No
var(Pine "carry-forward" variable). Every script invocation starts fresh; previous-bar values are read viaseries[1]. - No
import/module/package. The standard library is implicit; everything else lives in the script file.
Next
- Control flow —
if/for/fn/return. - Inputs & directives —
@name,@pane,@input. - Built-in variables — what
close,time,bar_indexactually contain.