Docs·Delta DSL Script·Library

Dynamic arrays

First-class mutable collections of scalars — Pine-style array.push / pop / shift / unshift / sort / aggregate, with a sandbox-safe element type set and per-script memory caps.

Dynamic arrays are first-class mutable collections of scalars (number / boolean / string). They cover patterns the bulk-vector model can't express cleanly: rolling pivot ledgers, signal journals, multi-strike trackers, custom rolling stats with median or sort semantics.

Arrays are a distinct runtime type from series — they don't broadcast over bars, they don't propagate NaN automatically, and they're not interchangeable with close / volume / etc. Pass-by-reference: arr2 = arr1 shares the same backing buffer; mutations through one name are visible through the other.

Sandbox policy

Arrays are sandbox-aware. The following element types are allowed:

  • number (including NaN)
  • boolean
  • string

The following are rejected at insert time (throws a clear runtime error):

  • null / undefined — use NaN for "missing number" instead.
  • Series (full bulk-vector arrays) — arrays hold scalars only. Reduce with arrPush(arr, at(series, i)) if you need to capture a per-bar value.
  • Nested arrays — Phase 2 sandbox policy. (May be lifted in a future phase after the memory model is validated.)
  • Functions — non-negotiable per deltadsl-invariants.mdc §1 (sandbox closed by construction).

Memory caps

Per-evaluation budgets enforced by the array arena (lib/arrays.js):

CapValueWhy
Per-array element count10,000Covers 10y daily history × 4 layers comfortably. Throws on arrNew(10001).
Total elements across all arrays100,000~10 large arrays of 10k each. Throws on arrPush when total would exceed.
Array count per script-eval256Kills while(true) arrNew(1) memory bombs. Throws on the 257th arrNew.

The arena is bound at the START of each script eval and unbound in finally — caps are per-eval and don't leak across scripts.

Construction

arrNew(size=0, fill=NaN)

Pre-sized array. fill must pass the sandbox element check.

Delta DSL
empty   = arrNew()                       // size 0
zeros   = arrNew(10, 0)                  // 10 × 0
nans    = arrNew(100)                    // 100 × NaN (default)
labels  = arrNew(5, "")                  // 5 × empty string

arrFrom(v1, v2, …)

Variadic factory:

Delta DSL
strikes  = arrFrom(45000, 50000, 55000, 60000)
flags    = arrFrom(true, true, false)

arrCopy(arr)

Shallow clone. Counts as a fresh allocation against the arena. Use when you need to mutate a derived array without touching the source:

Delta DSL
backup = arrCopy(strikes)
arrSort(strikes, "desc")    // doesn't disturb `backup`

Inspection

FnReturns
arrSize(arr)Element count (scalar)
arrGet(arr, i)Element at i. Negative wraps from end. Throws on out-of-bounds. Equivalent to arr[i] subscript.
arrFirst(arr)arrGet(arr, 0) — NaN if empty
arrLast(arr)arrGet(arr, -1) — NaN if empty
arrIndexOf(arr, v)First match index (===) or -1
arrLastIndexOf(arr, v)Last match index or -1
arrIncludes(arr, v)Boolean
Delta DSL
prices = arrFrom(45000, 50000, 55000)
n      = arrSize(prices)          // 3
hi     = arrLast(prices)          // 55000
lo     = arrFirst(prices)         // 45000
hasMid = arrIncludes(prices, 50000)  // true
mid    = arrGet(prices, 1)        // 50000
mid2   = prices[1]                // 50000 — same thing, subscript syntax
last   = prices[-1]               // 55000 — negative wraps

The subscript form arr[i] is exactly equivalent to arrGet(arr, i) — both go through the runtime type-dispatched Subscript handler. Use whichever reads more naturally.

Mutation

All mutation fns return the modified array (Pine convention) so calls chain naturally — except arrPop, arrShift, and arrRemove which return the REMOVED element (Pine convention too).

arrPush(arr, value) / arrPop(arr)

End-side push / pop:

Delta DSL
ledger = arrNew()
arrPush(ledger, close[0])
arrPush(ledger, close[1])
arrPush(ledger, close[2])
arrSize(ledger)                  // 3
last = arrPop(ledger)            // close[2]; size now 2

arrShift(arr) / arrUnshift(arr, value)

Front-side pop / push — together with push/pop these give you FIFO and LIFO semantics:

Delta DSL
// FIFO queue of the last 5 buy signals:
buys = arrNew()
if longSignal
  arrPush(buys, close)
  if arrSize(buys) > 5
    arrShift(buys)               // drop the oldest
  end
end

arrSet(arr, i, value) / arrInsert(arr, i, value) / arrRemove(arr, i) / arrClear(arr)

Index-driven mutations. arrInsert(arr, size, v) is allowed (equivalent to arrPush); other indices follow the _normIndex rules (negative wraps).

Delta DSL
strikes = arrFrom(45000, 50000, 55000)
arrSet(strikes, 0, 46000)        // [46000, 50000, 55000]
arrInsert(strikes, 1, 48000)     // [46000, 48000, 50000, 55000]
removed = arrRemove(strikes, 2)  // returns 50000; arr = [46000, 48000, 55000]
arrClear(strikes)                // arr = []

Reordering

arrReverse(arr)

In-place reverse:

Delta DSL
arrFrom(1, 2, 3) → arrReverse → [3, 2, 1]

arrSort(arr, dir="asc")

In-place sort. dir is "asc" or "desc" (case-insensitive). Numbers sort numerically, strings lexically, booleans false < true. Mixed-type arrays throw — pure-type sorted output is the contract.

Delta DSL
sizes = arrFrom(0.3, 0.1, 0.5, 0.2)
arrSort(sizes)                   // [0.1, 0.2, 0.3, 0.5]
arrSort(sizes, "desc")           // [0.5, 0.3, 0.2, 0.1]

NaN floats to the end regardless of direction (Pine semantics).

Combinations

arrSlice and arrConcat allocate NEW arrays and count against the arena. arrFill mutates in place.

Delta DSL
all   = arrFrom(1, 2, 3, 4, 5)
mid   = arrSlice(all, 1, 4)              // new: [2, 3, 4]
ends  = arrConcat(arrSlice(all, 0, 1),   // new: [1, 5]
                  arrSlice(all, 4))

zeros = arrFill(arrCopy(all), 0, 1, 4)   // [1, 0, 0, 0, 5]

Aggregation

Reductions to scalars. Booleans, strings, and NaN are skipped (non-numeric elements aren't combined). Empty result → NaN.

FnReturns
arrSum(arr)Sum
arrAvg(arr)Arithmetic mean
arrMin(arr)Min
arrMax(arr)Max
arrMedian(arr)50th percentile (interpolated for even counts)
arrVariance(arr)Population variance (divisor n)
arrStdev(arr)Population stdev
Delta DSL
// Rolling-N median that the standard `stdev` / `mean` rolling
// helpers don't offer:
medianWindow = arrNew()
for i = 0 to bars - 1
  arrPush(medianWindow, at(close, i))
  if arrSize(medianWindow) > 20
    arrShift(medianWindow)
  end
end
finalMedian = arrMedian(medianWindow)    // last-bar 20-period median

Conversion

arrToSeries(arr, length)

Broadcast the array as a series of length bars. Elements beyond arrSize(arr) become NaN; elements beyond length are truncated. Useful for piping per-pivot data into bar-aware drawing primitives that consume series.

Delta DSL
// Array of last-3 pivot prices → bar-shaped series for a plotLine:
pivots    = arrFrom(50000, 51000, 50500)
asSeries  = arrToSeries(pivots, bars)
plotLine(asSeries, color="#F0B90B")     // 3 dots at the start, NaN afterwards

Subscript syntax — arr[i] and series[k] coexist

The [k] subscript syntax dispatches based on the LHS runtime type:

  • LHS is an MrdArray → index access (arrGet(arr, i) shape)
  • LHS is a series → history shift (shift(series, k) shape)
  • LHS is a scalar[0] returns the scalar, [k>0] returns NaN

This means close[1] still means "previous-bar close" exactly as it did before Phase 2; only arr[i] on a user array picks up the new behaviour.

Delta DSL
prev_close   = close[1]              // history shift (series)
swing_high   = swings[0]             // array index (MrdArray)

Reference recipes

Rolling N-pivot ledger

Delta DSL
@input window = input.int(5, "Pivot window", minval=1, maxval=20)

pivots   = arrNew()
pivot_ev = pivothigh(high, window, window)

if pivot_ev
  arrPush(pivots, pivot_ev)
  if arrSize(pivots) > 10
    arrShift(pivots)                 // FIFO — keep last 10
  end
end

if barstate_islast and arrSize(pivots) > 0
  hi_pivot = arrMax(pivots)
  lo_pivot = arrMin(pivots)
  plotLabel(last_bar_time, hi_pivot, "Recent pivot HI " + tostring(hi_pivot, "0.00"),
            color="#FFFFFF", bg=withAlpha(bull_color, 0.78), anchor="left")
end

Trade journal with running expectancy

Delta DSL
@input pnl_take = input.float(2.0, "TP × R")

trade_pnls = arrNew()
entry_px   = stateMachine(longSignal, close, na)
exit_px    = stateMachine(crossunder(close, ema(close, 50)), close, na)

if not isNaN(exit_px) and not isNaN(shift(exit_px, 1)) and exit_px != shift(exit_px, 1)
  r = (exit_px - entry_px) / entry_px * 100
  arrPush(trade_pnls, r)
end

if barstate_islast and arrSize(trade_pnls) > 0
  win_rate     = arrAvg(arrFrom(trade_pnls > 0)) * 100  // would need iteration
  expectancy   = arrAvg(trade_pnls)
  logInfo("Stats: " + tostring(arrSize(trade_pnls)) + " trades, expectancy " + tostring(expectancy, "0.00") + "R")
end

Performance notes

  • All inspection / aggregation fns are O(n) in array size.
  • arrSort is O(n log n) with a single-pass kind-detect pre-pass.
  • arrMedian allocates a sorted copy of the numeric subset internally (does NOT mutate the source).
  • Mutation fns (arrPush / arrPop / arrShift / arrUnshift) are O(1) amortised at the end and O(n) at the front (JS Array semantics).
  • The arena adds 2-3 ns of overhead per allocating call (cap check + counter bump) — negligible vs. the indicator math.
  • Arrays are not garbage-collected mid-script. They live for the entire evaluation and get dropped when the script's evaluate() pass returns. Each evaluation starts with a fresh arena.

Anti-patterns

  • Storing series in arrays: arrPush(arr, close) throws — close is a series, not a scalar. Use arrPush(arr, at(close, i)) or arrPush(arr, close[0]) to pick a single bar's value.
  • Allocating arrays inside a for-loop: for i = 0 to bars - 1; tmp = arrNew(); … end consumes the array-count cap fast. Allocate ONCE outside the loop, mutate inside.
  • Comparing arrays with ==: arr1 == arr2 is reference identity, not deep equal. The DSL has no array equality op — use arrSize + element loop.
  • Math on arrays: arr + 5 throws (Phase 2 design — arithmetic operands are scalars / series only). Project to a scalar first with arrSum / arrAvg / arrGet.

Next

  • Drawing — using array elements as coordinates for lineNew / boxNew / labelNew.
  • Series — the bulk-vector counterpart; bridge via arrToSeries(arr, length).
  • Recipes — full scripts that combine arrays with the rest of the stdlib.