Storage

StablePool & SlotCache

Stable array slots for objects that come and go. A pool gives each object ID a reusable position, so removing one object never shifts the others or breaks a saved index.

Updated Publisher Alien_AlgorithmsPine Script v6

Identity tied to position

Scripts that track zones, levels or trades often keep one array per field and use the array position as the object's identity. Removing one object then shifts every later entry, the same removal has to happen in every parallel array, and any position saved elsewhere in the script now points at the wrong object.

A StablePool separates the two. An ID you choose answers "which zone is this?", and the slot the pool assigns answers "where is its data stored?". Released slots are reused for new objects, and nothing ever shifts.

IDs and slots

The example has three different zones, A, B and C. Their IDs, 1001, 1002 and 1003, are arbitrary unique values chosen for readability. Real IDs may come from a pivot bar, a timestamp, an order number or a counter.

A released slot is reused by a new zonepine
const int ZONE_A_ID = 1001
const int ZONE_B_ID = 1002
const int ZONE_C_ID = 1003

var op.StablePool zonePool = op.stablePool()

// This example never has more than two active zones.
var array<float> prices = array.new<float>(2, na)

if barstate.isfirst
    // A receives slot 0. B receives slot 1.
    [slotA, _] = zonePool.acquire(ZONE_A_ID)
    [slotB, _] = zonePool.acquire(ZONE_B_ID)
    prices.set(slotA, 100.0)
    prices.set(slotB, 200.0)

    // Zone A no longer exists. Its slot becomes available.
    zonePool.release(ZONE_A_ID)

    // C is a new zone with a new identity, but it can reuse A's old slot.
    [slotC, _] = zonePool.acquire(ZONE_C_ID)
    prices.set(slotC, 300.0)

// Output: B keeps slot 1. C has ID 1003 but reuses A's released slot 0.
//         prices is [300.0, 200.0].

Why C needs a new ID: C is a different zone, even though it occupies the position A once used. Reusing 1001 would describe A returning, not a new zone. IDs preserve identity; slots are only reusable storage addresses.

Several fields, one slot

In practice one slot addresses every field that belongs to the object. Continuing the same lifecycle with four parallel arrays:

Four payload arrays share one slotpine
const int ZONE_A_ID = 1001
const int ZONE_B_ID = 1002
const int ZONE_C_ID = 1003

var op.StablePool zonePool = op.stablePool()
var array<float> zonePrices = array.new<float>()
var array<int> zoneTimes = array.new<int>()
var array<float> zoneStrengths = array.new<float>()
var array<color> zoneColors = array.new<color>()

if barstate.isfirst
    [slotA, _] = zonePool.acquire(ZONE_A_ID)
    [slotB, _] = zonePool.acquire(ZONE_B_ID)

    // Grow every payload array to cover the allocated slots.
    int required = zonePool.slotCount()
    op.ensureSizeFloat(zonePrices, required, na)
    op.ensureSizeInt(zoneTimes, required, na)
    op.ensureSizeFloat(zoneStrengths, required, na)
    op.ensureSizeColor(zoneColors, required, na)

    zonePrices.set(slotA, 100.0)
    zoneTimes.set(slotA, 10)
    zoneStrengths.set(slotA, 0.40)
    zoneColors.set(slotA, color.blue)

    zonePrices.set(slotB, 200.0)
    zoneTimes.set(slotB, 20)
    zoneStrengths.set(slotB, 0.80)
    zoneColors.set(slotB, color.red)

    zonePool.release(ZONE_A_ID)
    [slotC, _] = zonePool.acquire(ZONE_C_ID)

    // C reuses A's slot, so every field at that slot must be overwritten.
    zonePrices.set(slotC, 300.0)
    zoneTimes.set(slotC, 30)
    zoneStrengths.set(slotC, 0.60)
    zoneColors.set(slotC, color.lime)

// Output: B keeps slot 1 in every array. C owns slot 0 in every array.
//         Nothing is removed or shifted.

The pool never touches your arrays. Its slots can index parallel arrays, as here, or a single array of user-defined types. When acquire() reports a new ID, grow each payload array with the ensureSize helpers.

Acquire and release

  • acquire(key) returns [slot, created]. Calling it again for a live key returns the same slot with created false.
  • A new key takes the most recently released slot first, and a new slot only when none is free.
  • release(key) returns the freed slot, or -1 when the key was not live. releaseSlot(slot) does the same by position and returns whether it released anything.
  • find(key) returns the slot or -1, and keyAt(slot) the key assigned to a slot. A released slot keeps reporting its last key until it is reused.
  • slots() is a read-only view of the live slots. Release swaps the freed slot with the last one, so the order is unspecified after any release.
  • acquire(), release() and find() are O(1).
Released data stays in place
Release frees the slot but does not erase your payloads. When createdis true, the slot may be recycled storage that still holds the previous occupant's values, so write every field before you read any of them.

For a few fixed objects, manual indices are simpler. A pool earns its place when IDs appear and disappear over time, several payload arrays share the same slots, or other parts of the script keep those positions. Pair it with a DirtySet to rebuild only the slots that changed.

SlotCache: one frame at a time

A SlotCache keeps stable slots for keys that must be seen again to stay alive. Each frame, usually one bar, opens with begin(), acquires every key that is still active, and closes with finish(). Keys from the previous frame that were not acquired are retired, without any explicit release.

Per-key smoothing for the keys seen this barpine
// Pseudocode: activeKeys holds the keys present on this bar,
// and readingFor() returns each key's new value.
var op.SlotCache cache = op.slotCache()
var array<float> smoothed = array.new<float>()

cache.begin()
for key in activeKeys
    [slot, created] = cache.acquire(key)
    if created
        op.ensureSizeFloat(smoothed, slot + 1, na)
        smoothed.set(slot, na)
    float reading = readingFor(key)
    float previous = smoothed.get(slot)
    smoothed.set(slot, na(previous) ? reading : previous + 0.2 * (reading - previous))

// Keys that were not acquired this frame are retired here.
array<int> retiredSlots = cache.finish()
for slot in retiredSlots
    smoothed.set(slot, na)
  • finish() returns the retired slots in the order they were acquired, so the payloads can be reset. Leaving them is also fine, as long as a new key overwrites them.
  • A new key reuses the most recently freed slot first. The order is part of the contract, so you can rely on it when reasoning about payload reuse.
  • active() lists the slots acquired in the frame in progress, and retired() the ones the last finish() retired. Both are borrowed views, valid until the next begin(); activeCopy() and retiredCopy() return copies you own.
  • acquire() is O(1), and finish() costs the number of slots active in the previous frame.

StablePool or SlotCache

AspectStablePoolSlotCache
LifetimeUntil you release the key.Until a frame passes without the key.
Removalrelease() or releaseSlot().Automatic at finish().
SuitsObjects with explicit creation and removal events, such as zones that break.Sets rebuilt each bar, such as the levels or labels currently in range.
Tier1, Quick2, Composable

Configuration

Both structures take an optional SlotPoolConfig:

  • op.slotPoolConfig(strictLifecycle = true, maxSlots = 50000) sets the lifecycle mode and the most slots the structure may allocate. Values above 50,000 are clamped, because the key directory is a Pine map.
  • op.poolConfigFrameRetained() is the default configuration. op.poolConfigLenient() turns strict lifecycle off.
  • Strict mode stops the script when a SlotCache calls begin() while a frame is open, or finish() without one. In lenient mode the first closes the open frame before starting a new one, and the second does nothing.
  • Acquiring a new key beyond maxSlots raises in either mode.

Calls

CallWhat it does
op.stablePool(settings = na), op.slotCache(settings = na)Create a pool or a frame cache.
pool.acquire(key), cache.acquire(key)Return [slot, created] for a key. On a SlotCache it also marks the key active in this frame.
pool.release(key), pool.releaseSlot(slot)Free a slot by key or by position.
cache.begin(), cache.finish()Open a frame, then close it and return the retired slots.
find(key), contains(key), keyAt(slot)Look up a slot or a key without changing anything. Both structures.
pool.slots(), cache.active(), cache.retired()Borrowed, read-only views of the slot lists.
cache.activeCopy(output = na), cache.retiredCopy(output = na)Copies of the slot lists that you own.
size(), slotCount(), freeCount(), clear()Live keys, slots ever allocated, free slots, and emptying. Both structures.
cache.frame()The frame counter, for diagnostics.
op.slotPoolConfig(strictLifecycle = true, maxSlots = 50000)Configuration for both structures.

Every signature is in the API Reference. Next: DirtySet.

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