Guides

Complete Examples

Two whole indicators you can paste into the Pine Editor: a cached scenario model built on one Memo, and a zone cluster engine that combines five OptiPine structures.

Updated Publisher Alien_AlgorithmsPine Script v6

Each example uses native Pine where it is simpler and OptiPine where it removes real work. Paste either one into a new indicator in the Pine Editor and add it to a chart.

Cached regime stress

The indicator plots a probability-weighted downside estimate for the current trend and volatility regime, and shows both regime values in the Data Window. The EMA and ATR run on every bar. Their rounded regimes change less often, so Memo reruns the 401-scenario model only when one of them changes and serves the cached result in between.

Cached_Regime_Stress.pinepine
//@version=6
indicator("OptiPine - Cached Regime Stress", overlay = false)

import Alien_Algorithms/OptiPine/1 as op

// Test 401 possible moves, giving more weight to common moves.
// This function is pure: its result depends only on its inputs.
estimateDownside(float trendInAtr, float atrPercent) =>
    float result = na

    if not na(trendInAtr) and not na(atrPercent) and atrPercent > 0
        float weightedDownside = 0.0
        float totalWeight = 0.0

        for scenario = -200 to 200
            float standardShock = scenario / 40.0
            float weight = math.exp(-0.5 * standardShock * standardShock)
            float projectedMove = (trendInAtr + standardShock) * atrPercent
            float downside = math.max(-projectedMove, 0.0)

            weightedDownside += downside * weight
            totalWeight += weight

        result := totalWeight > 0 ? weightedDownside / totalWeight : na

    result

// Stateful Pine calculations stay outside the Memo guard.
float ema20 = ta.ema(close, 20)
float ema50 = ta.ema(close, 50)
float atr14 = ta.atr(14)

float trendInAtr = atr14 > 0 ? (ema20 - ema50) / atr14 : na
float atrPercent = close > 0 ? atr14 / close * 100.0 : na

// Quantization makes the dependencies describe a regime, not every tick.
float trendRegime = math.round(
  math.max(-3.0, math.min(3.0, trendInAtr)) * 10.0) / 10.0
float volatilityRegime = math.round(atrPercent * 4.0) / 4.0

var op.FloatMemo downsideStress = op.floatMemo()

if downsideStress.staleOn(trendRegime, volatilityRegime)
    downsideStress.store(
      estimateDownside(trendRegime, volatilityRegime))

float stress = downsideStress.get()

plot(stress, "Expected downside (%)", color.orange, linewidth = 2)
plot(trendRegime, "Trend regime (ATR units)", display = display.data_window)
plot(volatilityRegime, "Volatility regime (%)", display = display.data_window)

How it works

  • The model is pure. estimateDownside() depends only on its two arguments, which is what makes caching it safe.
  • Stateful calls stay outside the guard. ta.ema() and ta.atr() run on every bar and keep their history.
  • Rounding creates the hits. The trend is rounded to tenths of an ATR within plus or minus 3, and the volatility to quarters of a percent. Raw values would change on almost every bar.
  • staleOn() decides. When neither regime changed, the 401-iteration loop does not run at all and get() returns the stored value.

The same shape fits parameter sweeps, numerical solvers and other expensive calculations that reduce to one number. Watch & Memo covers the details.

Zone cluster engine

The indicator draws recent pivot levels, thickens the ones near the current price, plots the strongest price cluster, and reports its statistics in the Data Window.

Zone_Cluster_Engine.pinepine
//@version=6
indicator("OptiPine - Zone Cluster Engine", overlay = true, max_lines_count = 100)

import Alien_Algorithms/OptiPine/1 as op

int pivotLength = input.int(5, "Pivot length", minval = 1)
int maxZones = input.int(32, "Maximum zones", minval = 4, maxval = 100)
int bucketTicks = input.int(25, "Cluster size in ticks", minval = 1)
float bucketSize = syminfo.mintick * bucketTicks

// Stateful Pine calculations remain outside every conditional rebuild.
float pivotHigh = ta.pivothigh(high, pivotLength, pivotLength)
float pivotLow = ta.pivotlow(low, pivotLength, pivotLength)
float pivotStrength = math.max(nz(volume[pivotLength], 1.0), 1.0)
float highlightDistance = ta.atr(14)

var op.StablePool zones = op.stablePool()
var op.IntRowRing zoneOrder = op.intRowRing(maxZones, 1)
var op.DirtySet dirtyZones = op.dirtySet(maxZones)

var array<float> zonePrices = array.new<float>(maxZones, na)
var array<float> zoneStrengths = array.new<float>(maxZones, 0.0)
var array<int> zoneTimes = array.new<int>(maxZones, na)
var array<bool> resistance = array.new<bool>(maxZones, false)
var array<bool> highlighted = array.new<bool>(maxZones, false)
var array<line> zoneLines = array.new<line>(maxZones)

var op.IntBuckets zonesByBucket = op.intBuckets()
var op.IntFloatStore strengthByBucket = op.intFloatStore()

// Retained build storage is resized and overwritten, never cleared and repopulated.
var array<int> bucketKeyByPosition = array.new<int>()
var array<int> aggregateKeys = array.new<int>()
var array<float> aggregateStrengths = array.new<float>()
var int strongestBucketKey = na
var float strongestBucketStrength = na
var int strongestZoneCount = 0

dirtyZones.begin()
bool topologyChanged = barstate.isfirst

// Logical pivot IDs receive stable, reusable physical drawing slots.
for event = 0 to 1
    float level = event == 0 ? pivotHigh : pivotLow
    if barstate.isconfirmed and not na(level)
        int pivotBar = bar_index - pivotLength
        int pivotTime = time[pivotLength]
        int zoneId = pivotBar * 2 + event
        int slot = zones.find(zoneId)

        // Only a new logical pivot enters the retirement queue.
        if slot < 0
            if zoneOrder.rowCount() == maxZones
                int oldestId = zoneOrder.at(0, 0)
                zones.release(oldestId)

            [newSlot, _] = zones.acquire(zoneId)
            slot := newSlot
            zoneOrder.pushValue(zoneId)
            zonePrices.set(slot, level)
            zoneStrengths.set(slot, pivotStrength)
            zoneTimes.set(slot, pivotTime)
            resistance.set(slot, event == 0)
            highlighted.set(slot, false)
            dirtyZones.mark(slot)
            topologyChanged := true

// Proximity can mark a newly created slot again; DirtySet still stores it once.
for slot in zones.slots()
    bool isHighlighted = math.abs(close - zonePrices.get(slot)) <= highlightDistance
    if isHighlighted != highlighted.get(slot)
        highlighted.set(slot, isHighlighted)
        dirtyZones.mark(slot)

// Only changed drawings cross the line API boundary.
for slot in dirtyZones.values()
    float level = zonePrices.get(slot)
    color baseColor = resistance.get(slot) ? color.red : color.lime
    line zoneLine = zoneLines.get(slot)

    if na(zoneLine)
        zoneLine := line.new(zoneTimes.get(slot), level, time, level,
          xloc = xloc.bar_time)
        zoneLines.set(slot, zoneLine)

    line.set_xy1(zoneLine, zoneTimes.get(slot), level)
    line.set_xy2(zoneLine, time, level)
    line.set_extend(zoneLine, extend.right)
    line.set_width(zoneLine, highlighted.get(slot) ? 3 : 1)
    line.set_color(zoneLine,
      color.new(baseColor, highlighted.get(slot) ? 0 : 55))

// Rebuild grouped lookup only after the explicit creation event.
if topologyChanged
    array<int> liveSlots = zones.slots()
    int liveCount = liveSlots.size()

    op.resizeInt(bucketKeyByPosition, liveCount, 0)
    if liveCount > 0
        for position = 0 to liveCount - 1
            int slot = liveSlots.get(position)
            int bucketKey = int(math.round(zonePrices.get(slot) / bucketSize))
            bucketKeyByPosition.set(position, bucketKey)

    // Repeated bucket keys accumulate several physical zone slots.
    zonesByBucket.buildFromPairs(bucketKeyByPosition, liveSlots)

    int bucketCount = zonesByBucket.bucketCount()
    op.resizeInt(aggregateKeys, bucketCount, 0)
    op.resizeFloat(aggregateStrengths, bucketCount, 0.0)
    strongestBucketKey := na
    strongestBucketStrength := na
    strongestZoneCount := 0

    if bucketCount > 0
        for bucketSlot = 0 to bucketCount - 1
            int bucketKey = zonesByBucket.keyAt(bucketSlot)
            [start, count] = zonesByBucket.rangeBySlot(bucketSlot)
            float totalStrength = 0.0

            if count > 0
                for position = start to start + count - 1
                    int zoneSlot = zonesByBucket.valueAt(position)
                    totalStrength += zoneStrengths.get(zoneSlot)

            aggregateKeys.set(bucketSlot, bucketKey)
            aggregateStrengths.set(bucketSlot, totalStrength)
            if na(strongestBucketStrength) or totalStrength > strongestBucketStrength
                strongestBucketKey := bucketKey
                strongestBucketStrength := totalStrength
                strongestZoneCount := count

    strengthByBucket.build(aggregateKeys, aggregateStrengths)

// Query the current price cluster directly and display the strongest cluster.
int currentBucketKey = int(math.round(close / bucketSize))
float nearbyStrength = strengthByBucket.get(currentBucketKey, 0.0)

float strongestClusterPrice = na(strongestBucketKey) ?
  na : strongestBucketKey * bucketSize

plot(strongestClusterPrice, "Strongest zone cluster", color.orange,
  linewidth = 2, style = plot.style_stepline)
plot(nearbyStrength, "Strength near current price", display = display.data_window)
plot(strongestBucketStrength, "Strongest cluster strength",
  display = display.data_window)
plot(strongestZoneCount, "Zones in strongest cluster",
  display = display.data_window)
plot(dirtyZones.size(), "Drawings updated", display = display.data_window)
Component results, not a total
This 32-zone indicator is small. In larger matching workloads in our tests, DirtySet saved 93% with 1% of entries changed, and IntBuckets averaged 19% with 64 keys. For typed lookup, direct addressing saved 21% over a map on compact keys, and a map saved 91% over linear search at 32 entries.

Which structure does what

StructureJob in this script
StablePoolGives each pivot ID a drawing slot that stays put while other zones come and go.
IntRowRingRemembers the order zones were created in, so the oldest one is retired when the limit is reached.
DirtySetQueues the zones whose line must be redrawn, once each, however many reasons marked them.
IntBucketsGroups the live zones into price buckets.
IntFloatStoreLooks up the total strength of the bucket around the current price.

The script still scans every live zone for proximity, which is cheap at this size. DirtySet makes sure only the zones whose state changed reach the line API.

One bar, step by step

  1. 1
    Stateful calculations first

    Pivots, volume and the ATR are computed on every bar, outside every condition.

  2. 2
    New pivots become zones

    Each confirmed pivot gets an ID from its bar and type. A new ID retires the oldest zone when the ring is full, acquires a slot from the pool, writes every payload field at that slot, and marks it dirty.

  3. 3
    Proximity marks changes

    Every live zone is checked against the ATR distance from the close. Only zones whose highlight flipped are marked; a zone marked twice is still queued once.

  4. 4
    Only dirty zones are redrawn

    Each marked slot creates its line the first time and afterwards only updates it with setters. Untouched zones cost nothing here.

  5. 5
    Clusters rebuild only when zones change

    When a zone was created, the live slots are grouped by price bucket, each bucket's strength is summed, and the bucket strengths are built into a store. On other bars the previous build is reused, and the current price's bucket is one lookup.

Storage that is never reallocated

The build arrays are declared with var and sized with op.resizeInt() and op.resizeFloat() before each rebuild, so a rebuild overwrites them instead of allocating new ones. See Storage Utilities.

Adapting the examples

  • Replace estimateDownside() with your own pure model, and pass staleOn() whatever it depends on, rounded to the precision that matters.
  • For more than four float dependencies, or an array, use the Memo's explicit begin() and miss() pass.
  • The zone engine's pattern, a pool for identity, a ring for age, a DirtySet for redraws, carries over to labels, boxes and table cells.
  • To keep more than 100 zones, raise the input's maxval and the declaration's max_lines_count together. The second caps how many lines the script may keep, up to 500.
OptiPine is a performance library for Pine Script. TradingView's memory, execution and collection limits still apply. © Alien_Algorithms.