Common Pitfalls
The mistakes that make a cache stale, a slot show old data, or a script stop with an OptiPine error, and how to fix each one.
A result never updates
The structure is recreated every bar
Without var, a Memo, Watch or any other structure is created again on every bar, empty, and forgets everything it saw. A Memo created that way misses on every bar; a structure that needs history never builds one.
// Wrong: a new, empty Memo on every bar.
op.FloatMemo model = op.floatMemo()
// Right: one Memo for the whole run.
var op.FloatMemo model = op.floatMemo()A dependency is missing
A Memo reruns only when something it watches changes. If the calculation reads a value that is not passed to staleOn() or the explicit pass, a change in that value never invalidates the cache. Pass every input the result depends on, including settings.
DirtySet marks are ignored
op.dirtySet() without a size has an empty universe, and marks outside the universe are silently ignored. Create it with the largest index you will mark plus one, such as op.dirtySet(maxZones), or pass a size to begin().
A CadenceGate never opens
The tick must keep growing. bar_index works; a value that repeats or resets does not. With dueWhenChanged(), the revision must also change, so check that the producer bumps it.
The cache never hits
- Raw inputs. Prices and indicator values change on nearly every bar. Round them to the precision the result responds to before passing them in. See regimes.
- One Watch, several sources.
changed()keeps one baseline. Calling it with two different sources on the same Watch makes each call compare against the other source, so it always reports a change. Give each source its own Watch, or use an explicitbegin()andfinish()pass. - An array with na under alwaysChanged. With
NaCollectionPolicy.alwaysChanged, anynaelement reports a change on every pass. That is its purpose; switch toexactifnais an ordinary value. - The dependency order varies. An explicit pass compares dependencies by position. Add them in the same order on every pass.
Indicator values are wrong
ta.* functions keep their own history. Inside a Memo or Watch guard, a CadenceGate block, or any condition that is false on some bars, they skip those bars and return different values. Pine Script v6 also evaluates and and or lazily, so a call on the right of and can be skipped as well.
// Wrong: ta.atr() only runs on the bars where the cache misses.
if model.staleOn(trendRegime)
model.store(fit(trendRegime, ta.atr(14)))
// Right: ta.atr() runs on every bar, and its rounded value is a dependency.
float atr14 = ta.atr(14)
float atrRegime = math.round(atr14 / close * 400.0) / 4.0
if model.staleOn(trendRegime, atrRegime)
model.store(fit(trendRegime, atrRegime))A slot shows old data
Releasing a StablePool key, or retiring a SlotCache key, frees the slot without erasing your payload arrays. When acquire() returns created as true, the slot may hold the previous occupant's values. Write every field before reading any, or reset the payloads of the slots finish() returns. See StablePool & SlotCache.
The same applies to a released slot's key: keyAt() keeps reporting the last key until the slot is reused, so use contains(key) to test whether an object is still live.
An array changed by itself
Calls such as values(), slots(), valuesView(), keysView(), rowView() and rawView() return the structure's own storage, not a copy. It changes with the structure, and it is only valid until the next call that modifies it.
- Read a view right away; do not keep it in a
varacross bars. - Never sort, push to or clear a view. A StablePool or DirtySet keeps positions that describe its list, and changing the list breaks both.
- When you need to keep or reorder the data, use the
*Copy()call or a call that fills anoutputarray. - Passing a view back into the same structure, such as a store's
valuesView()into its ownbuild(), raises an aliasing error. Copy it first.
Core Concepts lists what every accessor returns.
Runtime errors
Every OptiPine error starts with the structure that raised it. N stands for a number in the message.
| Message | Cause and fix |
|---|---|
OptiPine.IntIndex: find() before buildFinish(). | A store, index or IntBuckets was queried before its first build, or after clear(). Build it first, for example on barstate.isfirst; building from empty arrays is fine. |
OptiPine.IntIndex: add() after buildFinish(). Call buildBegin() first. | Keys were added to a sealed index. Start a new build with buildBegin(). |
OptiPine.IntIndex: buildFinish found N duplicate key(s). | A key appears more than once in a store or index build. Remove the duplicates, or set IndexConfig.duplicates to firstWins or lastWins. |
OptiPine.IntStore: build() expects exactly one value per key. | The key and value arrays passed to build() have different lengths. |
OptiPine.IntIndex: map representation needs N pairs, above Pine's 50,000-pair limit. | A forced map, or indexConfigDictionary(), was built with more than 50,000 keys. Use automatic mode or LookupPolicy.sorted. |
OptiPine.IntIndex: dense policy needs a key span of at most N but the span is N. | A forced dense index was built over keys too far apart. Raise maxDenseSpan or use automatic mode. |
OptiPine.StablePool: slot limit of N reached. | A new key needed a slot when none was free and maxSlots were already allocated. Release keys that are gone, or raise maxSlots up to 50,000. Also raised by SlotCache. |
OptiPine.StablePool: na is not a valid key. | An object ID was na, often from a calculation that is not ready yet. Pools, indexes and buckets all reject na keys. |
OptiPine.SlotCache: begin called while a frame was already open. | The previous frame never reached finish(). Call finish() once per begin(). |
OptiPine.SlotCache: finish called without begin. | A frame was closed that was never opened. |
OptiPine.FloatRowRing: push expects a row of exactly N values. | The row passed to push() is not the ring's width. |
OptiPine.FloatRowRing: pushValue requires width 1; this ring has width N. | pushValue() only works on width-one rings. Use push() with a full row. |
OptiPine.FloatRowRing: capacity * width exceeds the 100,000-element array limit. | The ring is larger than one Pine array can hold. Reduce the capacity or the width. |
OptiPine.FloatRowRing.window: window is outside the retained range. | The requested rows go past what the ring holds. Check rowCount() first. |
OptiPine.FloatRowRing: rawUnsafe requires an open editBegin() scope. | Raw storage was requested for writing outside an edit scope. Use rawView() to read, or open a scope with editBegin(). |
OptiPine.DirtySet: universe size exceeds the 100,000-element array limit. | The universe is larger than one Pine array can hold. |
OptiPine.IntBuckets: add called outside a build. | Call buildBegin() before add(), or use buildFromPairs(). |
OptiPine.Watch.watchFloats: na element at index N is rejected by NaCollectionPolicy.reject. | An array watched with the reject policy holds na. Fill it first, or use exact. |
OptiPine.WeightedIndex: na weight rejected by the configured na policy. | A strict weight configuration received na. Clean the weight first, or use the default policy, which stores 0. |
OptiPine.WeightedIndex: supports at most 99,999 weights. | The sampler or index capacity is too large. |
OptiPine.ensureSizeFloat: values cannot be na. | The array passed in was never created. Declare it with array.new<float>() first. The same applies to every resize and refill helper. |
op.poolConfigLenient(), strictLifecycle = false on an IndexConfig, and op.intBuckets(settings, false) turn those cases into defined fallbacks. Limits and invalid keys still raise.