Docs·Delta DSL Script·Language

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:

  1. Analysis pass — directives (@version, @name, @pane, @input) are extracted from comments / declarations and forwarded to the engine. Nothing else runs.
  2. Evaluation pass — every non-directive statement runs in order, producing variable bindings and queueing draw calls.

A typical file looks like:

Delta DSL
// 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

Delta DSL
// 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:

TypeExamplesWhere it comes from
number1, -3.14, 0.5e-3Literal, arithmetic, indicator math
booleantrue, false, close > openComparisons, 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
nana, 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:

Delta DSL
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 scalarscalar
  • series OP scalarseries (broadcast scalar to every bar)
  • series OP seriesseries (element-wise; lengths must match — the runtime enforces this)
  • scalar OP seriesseries (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

Delta DSL
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

Delta DSL
"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:

Delta DSL
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, notnot &&, ||, !.

Delta DSL
breakout = (close > highest(high, 20))  and  (volume > sma(volume, 20))

Variables and assignment

Delta DSL
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 (ma and MA are different).
  • Cannot collide with reserved words (see below).

Reassignment is allowed

Unlike Pine, you can reassign a variable freely, and the type can change:

Delta DSL
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

GroupKeywords
Booleantrue, false, and, or, not
Branchingif, elseif, else, end
Loopsfor, to, step
Functionsfn, 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.

PrecedenceOperatorMeaningExample
1( … )Grouping(a + b) * c
2f(args), series[k], obj.fieldCall, history-shift, member accesssma(close, 20), close[1], d.kind
3unary -, +, notNegation, identity, boolean NOT-x, not breakout
4*, /, %Multiplication, division, moduloa * b, n % 2
5+, -Addition / subtraction (or string concat for +)a + b
6<, <=, >, >=Numeric comparisonclose > open
7==, !=Equalitydir == 1
8andLogical ANDa and b
9orLogical ORa or b
10cond ? a : bTernary 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:

Delta DSL
"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:

Delta DSL
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.

Delta DSL
// 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

Delta DSL
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.

Delta DSL
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():

Delta DSL
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:
Delta DSL
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 / fn blocks.

Function calls

Two argument forms are accepted, in any order, as long as positionals come before keywords:

Delta DSL
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/Infinity instead 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.

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 / undefined keywords. Use na (NaN) instead.
  • No exceptions / try / catch. Use iff / nz to 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 via series[1].
  • No import / module / package. The standard library is implicit; everything else lives in the script file.

Next