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.
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.
//@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()andta.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.
//@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)Which structure does what
| Structure | Job in this script |
|---|---|
| StablePool | Gives each pivot ID a drawing slot that stays put while other zones come and go. |
| IntRowRing | Remembers the order zones were created in, so the oldest one is retired when the limit is reached. |
| DirtySet | Queues the zones whose line must be redrawn, once each, however many reasons marked them. |
| IntBuckets | Groups the live zones into price buckets. |
| IntFloatStore | Looks 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
- 1Stateful calculations first
Pivots, volume and the ATR are computed on every bar, outside every condition.
- 2New 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.
- 3Proximity 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.
- 4Only 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.
- 5Clusters 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 passstaleOn()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()andmiss()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
maxvaland the declaration'smax_lines_counttogether. The second caps how many lines the script may keep, up to 500.