Storage

Row Rings & RingCursor

A ring buffer for Pine Script: a fixed number of equal-width rows in one array. Once full, the next row overwrites the oldest one, and reads still come back in time order.

Updated Publisher Alien_AlgorithmsPine Script v6

Why not shift the array

A rolling history usually keeps the latest N entries with array.shift() and array.push(). Once the array is full, every new entry moves all the others one place: keeping the latest 200 events shifts 199 values per event, and the result is no different.

A ring writes the new row over the oldest physical slot and moves nothing. A push costs O(width), or O(1) through pushValue() on a width-one ring. Rings suit pivots, completed trades, sampled features and other fixed event histories.

Ordinary series history stays native
close[50] and other series references are already O(1) in Pine. A ring is for histories you build yourself: events, rows of features, objects in arrival order.

Create a ring

The last 200 confirmed bars, three values eachpine
var op.FloatRowRing history = op.floatRowRing(200, 3)

float atr14 = ta.atr(14)

if barstate.isconfirmed
    history.push(array.from(close, volume, atr14))

float oldestPrice = history.at(0, 0)
float latestPrice = history.newestAt(0, 0)

// Output: after a confirmed push, these are the oldest and newest retained closes.
  • op.floatRowRing(capacity, width) and op.intRowRing(capacity, width) create a ring of floats or ints. Capacity times width may not exceed 100,000.
  • Storage is allocated up front by default. With preallocate = false it grows on demand instead, trading a smaller start for growth work during the first cycle.
  • push(row) takes exactly width values. pushValue(value) takes one, without building an array, and needs a width of 1.
  • A RingConfig from op.ringConfig() holds the same settings for op.floatRowRingFrom() and op.intRowRingFrom().

Read in time order

Reads use logical rows, which always run in time order: row 0 is the oldest retained row and rowCount() - 1 the newest. Where it physically sits in the array does not matter.

  • at(logicalRow, column) reads one value, counting from the oldest. newestAt(offset, column) counts back from the newest: offset 0 is the latest row.
  • Both return na for a row that is not occupied yet.
  • setAt(logicalRow, column, value) writes one value in place and returns whether the row was occupied.
  • rowView(logicalRow) returns one row as a read-only slice of the storage, without copying it.

Copy rows out

Every copy comes back flat and row by row: width values for the first row, then the next.

  • chronological(output = na) copies every row, oldest to newest, using at most two native contiguous copies. In our tests that ran 90% faster than rebuilding a 512-cell, width-four output row by row.
  • window(startLogicalRow, rowCount, output = na) copies a run of consecutive rows, and raises when the run is outside what the ring holds.
  • gather(logicalRows, output = na) copies chosen rows in the order you ask for them. An unoccupied row contributes width values of na.

Pass a var output array to reuse it; without one, each call allocates a new array.

Detect changes

A ring keeps a content revision that advances when its content or membership changes. setAt() with the value already there leaves it alone. Because of that revision, another part of the script can watch the ring in O(1), whatever its size:

Rebuild a summary when the history changedpine
var op.Watch historyWatch = op.watch()
var float summary = na

if historyWatch.changed(history)
    // Pseudocode: summarize() reads the ring.
    summary := summarize(history)

This is useful when several producers can change one ring. revision() reads the counter directly, for example for a CadenceGate.

RingCursor for your own arrays

When several arrays you own must share one circular layout, for example a price, a time and a label per event, a RingCursor keeps the layout without holding any data. reserve() advances it and returns the physical slot to write; once full, that is the oldest slot.

Three caller-owned arrays, one ring layoutpine
var op.RingCursor events = op.ringCursor(100)
var array<float> eventPrices = array.new<float>(100, na)
var array<int> eventTimes = array.new<int>(100, na)
var array<string> eventNames = array.new<string>(100, na)

// Series calls run on every bar, before any condition uses them.
bool crossed = ta.cross(close, ta.sma(close, 50))

if barstate.isconfirmed and crossed
    int slot = events.reserve()
    eventPrices.set(slot, close)
    eventTimes.set(slot, time)
    eventNames.set(slot, close > open ? "up" : "down")

// The newest event's price, when there is one.
float lastEventPrice = events.count() > 0 ? eventPrices.get(events.newest()) : na
  • physical(logicalIndex) and logical(physicalSlot) translate between time order and storage position. newest(offset) and oldest(offset) locate rows from either end.
  • Each returns -1 for a position that is not occupied, so check count() first.
  • For a flat array of rows, op.copyChronologicalFloatRows(), op.copyLogicalFloatWindow(), op.gatherLogicalFloatRows() and op.copyLogicalFloatRow() copy rows out in time order, with Int versions alongside.
Compute series values before the condition
Pine Script v6 evaluates and and or lazily, so a ta.* call written after barstate.isconfirmed and would be skipped on some bars and lose its history. That is why the example computes crossed on its own line first.

Physical access

For a measured hot path, rings expose their storage directly. These calls are Tier 3:

  • reserveRowUnchecked() advances the ring and returns the slot of the new newest row without writing anything. Write every column with writeUnchecked(slot, column, value) before the row is read.
  • writePhysical(slot, row) overwrites a whole row in place without advancing the ring.
  • rawView() is the whole storage, row-major in physical order, read-only. physicalRow(logicalRow) maps a logical row to its slot.
  • To write the storage directly, open a scope with editBegin(), write through rawUnsafe(), and close it with editFinish(), which advances the revision the raw writes could not.

Calls

CallWhat it does
op.floatRowRing(capacity, width, preallocate = true, fillValue = 0.0)Creates a float ring. op.intRowRing() takes an int fill value.
ring.push(row), ring.pushValue(value)Append the newest row. Both return the physical slot written.
ring.at(logicalRow, column), ring.newestAt(offset, column)Read one value, from the oldest or the newest end.
ring.setAt(logicalRow, column, value)Write one value in place. True when the row was occupied.
ring.chronological(output = na), ring.window(startLogicalRow, rowCount, output = na), ring.gather(logicalRows, output = na)Copy all rows, a run of rows, or chosen rows.
ring.rowView(logicalRow), ring.rawView()Read-only views of one row or the whole storage.
ring.rowCount(), ring.rowWidth(), ring.rowCapacity(), ring.revision(), ring.clear()Size, shape, content revision, and emptying.
op.ringCursor(capacity)A ring layout for arrays you own.
cursor.reserve(), cursor.physical(logicalIndex), cursor.logical(physicalSlot)Advance, and translate between time order and storage.
cursor.newest(offset = 0), cursor.oldest(offset = 0), cursor.full(), cursor.count(), cursor.limit(), cursor.clear()Locate rows and read the size.

Every signature is in the API Reference. Next: StablePool & SlotCache.

OptiPine is a performance library for Pine Script. TradingView's memory, execution and collection limits still apply. © Alien_Algorithms.