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(includingNaN)booleanstring
The following are rejected at insert time (throws a clear runtime error):
null/undefined— useNaNfor "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):
| Cap | Value | Why |
|---|---|---|
| Per-array element count | 10,000 | Covers 10y daily history × 4 layers comfortably. Throws on arrNew(10001). |
| Total elements across all arrays | 100,000 | ~10 large arrays of 10k each. Throws on arrPush when total would exceed. |
| Array count per script-eval | 256 | Kills 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.
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:
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:
backup = arrCopy(strikes)
arrSort(strikes, "desc") // doesn't disturb `backup`
Inspection
| Fn | Returns |
|---|---|
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 |
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:
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:
// 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).
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:
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.
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.
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.
| Fn | Returns |
|---|---|
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 |
// 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.
// 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.
prev_close = close[1] // history shift (series)
swing_high = swings[0] // array index (MrdArray)
Reference recipes
Rolling N-pivot ledger
@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
@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.
arrSortis O(n log n) with a single-pass kind-detect pre-pass.arrMedianallocates 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. UsearrPush(arr, at(close, i))orarrPush(arr, close[0])to pick a single bar's value. - ❌ Allocating arrays inside a for-loop:
for i = 0 to bars - 1; tmp = arrNew(); … endconsumes the array-count cap fast. Allocate ONCE outside the loop, mutate inside. - ❌ Comparing arrays with
==:arr1 == arr2is reference identity, not deep equal. The DSL has no array equality op — usearrSize+ element loop. - ❌ Math on arrays:
arr + 5throws (Phase 2 design — arithmetic operands are scalars / series only). Project to a scalar first witharrSum/arrAvg/arrGet.