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 index array.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 with var so the array survives across bars; without var it 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.

graph TD A[Bar N executes] --> B[array.push closes closeBuffer close] B --> C{array.size closeBuffer gt windowLen} C -- Yes --> D[array.shift closeBuffer removes index 0] C -- No --> E[Buffer still warming up] D --> F[Buffer size equals windowLen] E --> F F --> G{close gt open} G -- Yes --> H[array.push upBarBuffer close] G -- No --> I[Skip upBarBuffer] H --> J[array.avg closeBuffer returns arrayAvg] I --> J J --> K[array.avg upBarBuffer returns upBarAvg] K --> L[plot arrayAvg and upBarAvg on chart]

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:

  1. Push: array.push(closeBuffer, close) — appends the current bar's close to the end. Array size increases by 1.
  2. Shift (conditional): If array.size(closeBuffer) > windowLen, call array.shift(closeBuffer) to remove the oldest element from index 0. Array size returns to windowLen.

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 for loop 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(), and array.min() are implemented natively and are faster than manual loops.
  • Use array.copy() before destructive operations: Functions like array.sort() and array.remove() mutate the array in place. Always copy first if you need the original intact.
  • Memory: Each var array persists 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(), and array.min() operate on the entire array in one call and are both concise and performant — prefer them over manual for loops.
  • 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

  1. Weighted rolling average: Maintain a parallel array<float> of weights (e.g., volume) and compute $\sum w_i x_i / \sum w_i$ manually using array.sum() on both arrays for a volume-weighted mean.
  2. 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.

Related Posts

Comments

Popular posts from this blog

How to Build a Volume Indicator in Pine Script v6: Buying & Selling Pressure Analysis

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

SuperTrend Indicator in Pine Script v6: Canonical Formula, Flip Logic, and Non-Repainting Signals