Pine Script Arrays: Push, Remove, and Average Values Across Multiple Bars (v6 Guide)
When a single series value per bar is not enough, Pine Script's array type lets you store, manipulate, and aggregate arbitrary collections of numbers at runtime — enabling rolling windows, dynamic buffers, and multi-value statistics that the built-in ta.* functions cannot express alone.
1. What Is an Array in Pine Script?
An array in Pine Script v6 is an ordered, zero-indexed collection of elements of a single type (e.g., array<float>, array<int>, array<bool>). Unlike a series, which automatically stores one value per historical bar, an array is a mutable object whose size and contents you control explicitly with built-in array.* functions.
Key Characteristics
- Zero-indexed: The first element is at index
0; the last is at indexarray.size(arr) - 1. - Mutable: Elements can be added, removed, or overwritten at any time.
- Typed: All elements must share the same declared type.
- Persistent with
var: Declare withvarso the array survives across bars; withoutvarit is re-created on every bar.
2. Core Array Functions
The table below lists the most commonly used array.* functions for building a dynamic rolling buffer. All names are verified against the official Pine Script v6 Reference Manual.
| Function | Purpose | Returns |
|---|---|---|
array.new<float>(size, initial_value) |
Create a new array with optional pre-fill | array<float> |
array.push(id, value) |
Append an element to the end | void |
array.shift(id) |
Remove and return the first element | element type |
array.pop(id) |
Remove and return the last element | element type |
array.remove(id, index) |
Remove element at a specific index | element type |
array.get(id, index) |
Read element at a specific index | element type |
array.set(id, index, value) |
Overwrite element at a specific index | void |
array.size(id) |
Return current number of elements | series int |
array.avg(id) |
Arithmetic mean of all elements | series float |
array.sum(id) |
Sum of all elements | series float |
array.max(id) |
Maximum value in the array | series float |
array.min(id) |
Minimum value in the array | series float |
3. The Rolling Buffer Pattern
The most common array use-case is a fixed-length rolling buffer: push a new value each bar, then remove the oldest value when the buffer exceeds the desired window size. This is mathematically equivalent to a sliding window of length $N$.
The arithmetic mean of the buffer at any bar $t$ is:
$$\bar{x}_t = \frac{1}{N} \sum_{i=0}^{N-1} x_{t-i}$$
This is identical to ta.sma(source, N), but the array approach lets you filter, transform, or selectively include elements before averaging — something a plain SMA cannot do.
4. Complete Annotated Example
The script below builds a rolling buffer of the last N closing prices using array.push and array.shift, then plots the array average alongside the equivalent ta.sma for verification. A second buffer collects only up-bar closes (where close > open) to demonstrate selective insertion.
🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator("Array Rolling Buffer Demo", overlay = true)
// ─── Inputs ───────────────────────────────────────────────────────────────────
int windowLen = input.int(20, title = "Window Length", minval = 2, maxval = 500)
// ─── Persistent Arrays ────────────────────────────────────────────────────────
// `var` ensures the array is created ONCE on bar 0 and persists across all bars.
// Without `var`, a brand-new empty array would be created on every bar.
var array<float> closeBuffer = array.new<float>() // stores last N closes
var array<float> upBarBuffer = array.new<float>() // stores closes of up-bars only
// ─── Push New Value Each Bar ──────────────────────────────────────────────────
// array.push() appends to the END of the array (highest index).
array.push(closeBuffer, close)
// ─── Enforce Fixed Window: Remove Oldest Element ──────────────────────────────
// array.shift() removes and returns the element at index 0 (the oldest value).
// We only shift when the buffer has grown beyond the desired window length.
if array.size(closeBuffer) > windowLen
array.shift(closeBuffer)
// ─── Selective Insertion: Up-Bars Only ────────────────────────────────────────
// Only push to upBarBuffer when the current bar closed higher than it opened.
if close > open
array.push(upBarBuffer, close)
// Keep the up-bar buffer at the same maximum window length.
if array.size(upBarBuffer) > windowLen
array.shift(upBarBuffer)
// ─── Compute Averages ─────────────────────────────────────────────────────────
// array.avg() returns the arithmetic mean of all current elements.
// Returns na when the array is empty, so no division-by-zero risk.
float arrayAvg = array.avg(closeBuffer)
float upBarAvg = array.size(upBarBuffer) > 0 ? array.avg(upBarBuffer) : na
// Built-in SMA for cross-verification (should match arrayAvg exactly).
float builtinSma = ta.sma(close, windowLen)
// ─── Additional Statistics ────────────────────────────────────────────────────
float bufferMax = array.max(closeBuffer) // highest close in the window
float bufferMin = array.min(closeBuffer) // lowest close in the window
float bufferSum = array.sum(closeBuffer) // sum of all closes in the window
int bufferSize = array.size(closeBuffer) // current number of elements
// ─── Plots ────────────────────────────────────────────────────────────────────
plot(arrayAvg, title = "Array Avg (all bars)", color = color.blue, linewidth = 2)
plot(builtinSma, title = "ta.sma (verification)", color = color.orange, linewidth = 1)
plot(upBarAvg, title = "Array Avg (up-bars)", color = color.green, linewidth = 2)
plot(bufferMax, title = "Buffer Max", color = color.red, linewidth = 1)
plot(bufferMin, title = "Buffer Min", color = color.purple, linewidth = 1)
// ─── Debug Label on the Last Bar ─────────────────────────────────────────────
// Display buffer statistics as a label on the most recent bar only.
if barstate.islast
string msg = "Size: " + str.tostring(bufferSize) +
"\nSum: " + str.tostring(math.round(bufferSum, 2)) +
"\nAvg: " + str.tostring(math.round(arrayAvg, 4)) +
"\nMax: " + str.tostring(math.round(bufferMax, 4)) +
"\nMin: " + str.tostring(math.round(bufferMin, 4))
label.new(bar_index, high, text = msg,
style = label.style_label_left,
color = color.new(color.blue, 80),
textcolor = color.black,
size = size.small)
5. Step-by-Step Logic Walkthrough
5.1 Declaration with var
The line var array<float> closeBuffer = array.new<float>() creates an empty array exactly once — on the very first bar (bar index 0). On every subsequent bar, Pine Script skips the initializer and reuses the existing object. Without var, a fresh empty array would be created on every bar, discarding all previously pushed values.
5.2 Push → Shift Cycle
Each bar executes two operations in sequence:
- Push:
array.push(closeBuffer, close)— appends the current bar's close to the end. Array size increases by 1. - Shift (conditional): If
array.size(closeBuffer) > windowLen, callarray.shift(closeBuffer)to remove the oldest element from index 0. Array size returns towindowLen.
After the warm-up period (first windowLen bars), the array always contains exactly windowLen elements representing the most recent closes.
5.3 Selective Filtering
The upBarBuffer demonstrates that you can apply any boolean condition before pushing. Only bars where close > open contribute to this buffer. The resulting upBarAvg is the mean of the last windowLen bullish closes — a statistic that no standard built-in function can compute directly.
5.4 array.avg() vs Manual Mean
The built-in array.avg(id) computes:
$$\text{avg} = \frac{\text{array.sum}(id)}{\text{array.size}(id)}$$
It returns na when the array is empty, so no guard is needed for the all-bars buffer. For the upBarBuffer, an explicit size check (array.size(upBarBuffer) > 0) is shown as a best-practice pattern to make intent clear.
6. Removing Elements by Index
Beyond the push/shift pattern, Pine Script provides two additional removal functions worth understanding:
| Function | Removes From | Typical Use Case |
|---|---|---|
array.shift(id) |
Index 0 (oldest / front) | FIFO rolling window — most common |
array.pop(id) |
Last index (newest / back) | Stack (LIFO) operations |
array.remove(id, index) |
Any arbitrary index | Removing outliers or specific events |
The following snippet shows how to remove the element at a computed index — for example, removing the maximum value from the buffer before averaging (a trimmed mean):
🔽 [Click to expand] Trimmed Mean Example
//@version=6
indicator("Trimmed Mean via array.remove", overlay = true)
int N = input.int(20, "Window", minval = 3)
var array<float> buf = array.new<float>()
array.push(buf, close)
if array.size(buf) > N
array.shift(buf)
float trimmedMean = na
if array.size(buf) == N
// Copy the buffer so we don't mutate the original.
array<float> tmp = array.copy(buf)
// Find the index of the maximum element.
int maxIdx = array.indexof(tmp, array.max(tmp))
// Remove the maximum element.
array.remove(tmp, maxIdx)
// Find the index of the minimum element in the modified copy.
int minIdx = array.indexof(tmp, array.min(tmp))
// Remove the minimum element.
array.remove(tmp, minIdx)
// Average the remaining N-2 elements.
trimmedMean := array.avg(tmp)
plot(trimmedMean, title = "Trimmed Mean (excl. max+min)", color = color.teal, linewidth = 2)
plot(ta.sma(close, N), title = "SMA", color = color.gray, linewidth = 1)
7. Performance Considerations
Arrays in Pine Script are heap-allocated objects. Keep the following engineering constraints in mind:
- Avoid large arrays in tight loops: Iterating over an array of size $N$ inside a
forloop that itself runs $N$ times per bar gives $O(N^2)$ complexity per bar — this will trigger Pine Script's execution time limit on long histories with large $N$. - Prefer built-in aggregation functions:
array.avg(),array.sum(),array.max(), andarray.min()are implemented natively and are faster than manual loops. - Use
array.copy()before destructive operations: Functions likearray.sort()andarray.remove()mutate the array in place. Always copy first if you need the original intact. - Memory: Each
var arraypersists for the lifetime of the script execution. Avoid creating many large arrays unnecessarily.
8. Common Pitfalls
| Mistake | Symptom | Fix |
|---|---|---|
Declaring array without var |
Array is always empty; avg returns na |
Add var to the declaration |
| Accessing index out of bounds | Runtime error on historical bars | Guard with array.size(arr) > index before array.get() |
Calling array.shift() on empty array |
Runtime error | Check array.size(arr) > 0 first |
| Mutating array inside a loop without a copy | Incorrect results; index drift | Use array.copy() before destructive operations |
Using myArr[1] (series history) on an array |
Compile error — arrays are not series | Use array.get(myArr, index) instead |
9. Conclusion
- Persistent declaration: Always use
var array<type> name = array.new<type>()so the array survives across bars; the push → shift cycle then implements an exact fixed-length rolling window. - Built-in aggregation:
array.avg(),array.sum(),array.max(), andarray.min()operate on the entire array in one call and are both concise and performant — prefer them over manualforloops. - Selective filtering: Because you control which values are pushed, arrays unlock statistics (e.g., mean of up-bar closes, trimmed mean) that no standard
ta.*function can compute, making them the correct tool for custom multi-bar aggregation logic.
Ideas for Further Development
- Weighted rolling average: Maintain a parallel
array<float>of weights (e.g., volume) and compute $\sum w_i x_i / \sum w_i$ manually usingarray.sum()on both arrays for a volume-weighted mean. - Dynamic anomaly detection: Compute
array.stdev()on the rolling buffer each bar and flag closes that exceed $\mu \pm k\sigma$ as statistical outliers, building a purely array-driven Z-score indicator.
Comments
Post a Comment