Caching

Watch & Memo

The fastest expensive calculation is the one that never needed to run. Watch notices when inputs change; Memo also keeps the result, so a model reruns only when what it depends on is different.

Updated Publisher Alien_AlgorithmsPine Script v6

Watch or Memo

  • Memo when the cached result is one int, float, bool, string or color. It owns the previous dependencies, the first-run state, the validity and the result.
  • Watch when your script already owns the result, or the result is not a single primitive. A Watch remembers dependencies; it does not store the result.

Both compare exactly: na equals na, and floats are compared without Pine's rounding, so a tiny real change is still a change.

Watch one source

changed(source) is true on the first observation and whenever the source differs from the last one it saw, and it adopts the new value as the baseline. It accepts an int, float, bool, string or color, any of the five primitive arrays, and a FloatRowRing or IntRowRing.

Rebuild a score only when its features changepine
// Pseudocode: getFeatureSnapshot() supplies an array<float>.
array<float> features = getFeatureSnapshot()

var op.Watch featureWatch = op.watch()
var float modelScore = na

if featureWatch.changed(features)
    modelScore := evaluateModel(features)

// Output: modelScore is rebuilt only when the features array changes.
  • Arrays are compared with a snapshot the Watch keeps, in O(N). The snapshot is rewritten only after a change, so a steady array costs one comparison per bar and no writes. In our tests this compare-first pattern ran 35% to 60% faster than rewriting the snapshot every time.
  • Row rings expose a content revision, so checking one is O(1) whatever its size. Row Rings explains when it advances.
  • One source per Watch. A Watch keeps one baseline for changed(). Several consumers can observe the same producer, each with its own Watch.
Is the comparison worth it?
Comparing an array costs O(N). Use it when the work it protects costs more than that. If the producer already gives you one reliable change flag, use the flag.

Watch several dependencies

For several dependencies, run an explicit pass: begin() starts it, the typed watch*() calls add each dependency in a fixed order, and finish() returns true if the completed set changed.

Mixed settings and a feature arraypine
// Pseudocode dependencies: int length, float multiplier,
// and array<float> features.
var op.Watch settingsWatch = op.watch()
var float result = na

settingsWatch.begin()
settingsWatch.watchInt(length)
settingsWatch.watchFloat(multiplier)
settingsWatch.watchFloats(features)

bool dependenciesChanged = settingsWatch.finish()
if dependenciesChanged
    result := rebuild(length, multiplier, features)

// Output: result is rebuilt when any observed dependency changes.

finish() returns true when any dependency changed, when the number of dependencies changed, on the very first pass, and after invalidate(). Construct the Watch once with var, then run begin() and finish() on every pass. One array counts as one dependency, however long it is.

Forcing and resetting

  • invalidate() makes the next observation report a change, through either API.
  • reset() discards every stored dependency while keeping the allocations. The next observation reports a change.
  • isChanged() says whether the pass in progress has already seen a change, and arity() how many dependencies the last completed pass recorded.

na in watched arrays

watchInts(), watchFloats(), watchColors() and watchStrings() take a policy for na elements. An na array is treated as empty.

PolicyEffect
op.NaCollectionPolicy.exactThe default. na equals na, so two passes holding na in the same position report no change.
op.NaCollectionPolicy.alwaysChangedAn array containing any na reports a change on every pass. Use it when na means "not computable yet" and a result built from it must never be reused.
op.NaCollectionPolicy.rejectStop the script on the first na element.

Cache a result with Memo

There is one Memo per value type: op.intMemo(), op.floatMemo(), op.boolMemo(), op.stringMemo() and op.colorMemo().

Rerun a model only when its regime changespine
// Pseudocode: trendRegime and volatilityRegime are floats;
// rebuildModel() is a pure calculation.
var op.FloatMemo model = op.floatMemo()

if model.staleOn(trendRegime, volatilityRegime)
    model.store(rebuildModel(trendRegime, volatilityRegime))

float result = model.get()

// Output: rebuildModel() runs once, then only when either regime changes.
  • staleOn() checks up to four floats, two integers, one boolean and one string, and returns true when the value must be recomputed: on the first call, after any dependency changed, and while nothing is stored.
  • It always watches the same eight slots, so leaving out trailing arguments does not register as a change.
  • store(value) saves the rebuilt value. get(fallback = na) returns it, or the fallback before the first store.
  • isValid() tells an empty memo from one holding the empty value, and invalidate() drops the value so the next check misses.

Round inputs into regimes

Many models respond to regimes rather than to every tiny change in raw data. Round the inputs to the precision that matters before passing them in, and the model runs only when a regime changes. If the dependencies repeat on nine executions out of ten, Memo avoids roughly 90% of the model runs. Raw prices would change on almost every bar, and the cache would almost never hit.

From the publication's complete examplepine
// 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

Memo suits scenario models, parameter sweeps, numerical solvers and other expensive pure calculations that reduce to one primitive result. The full indicator is on the Quickstart.

Memo over collections

For arrays or a dependency list that varies, use the Memo's explicit pass. begin() starts it, the Memo's own Watch in dependencies takes the dependencies, and miss() ends it and returns true when the value must be recomputed.

A Memo that watches an arraypine
// Pseudocode: features is an array<float>; fitModel() is pure.
var op.FloatMemo fit = op.floatMemo()

fit.begin()
fit.dependencies.watchInt(length)
fit.dependencies.watchFloats(features)

if fit.miss()
    fit.store(fitModel(length, features))

float score = fit.get()

Keep guarded work pure

Stateful calls stay outside every guard
ta.* functions and other history-dependent calls keep their own state from bar to bar. Inside a Memo or Watch guard they would skip the bars where the guard is false and return wrong values. Compute them on every bar, then pass their results into the guarded calculation.

For work that may be delayed on purpose, such as refitting a model every few bars, see CadenceGate.

Calls

CallWhat it does
op.watch()Creates a Watch.
watch.changed(source)True on the first observation and whenever the source changed. One overload per primitive, primitive array and row ring.
watch.begin(), watch.finish()Start and complete an explicit pass. finish() returns true when the set changed.
watch.watchInt(value), watchFloat(), watchBool(), watchString(), watchColor()Add one scalar dependency to the pass.
watch.watchInts(values, naPolicy = NaCollectionPolicy.exact), watchFloats(), watchColors(), watchStrings(), watchBools(values)Add one array dependency to the pass.
watch.invalidate(), watch.reset(), watch.isChanged(), watch.arity()Force a change, discard the baseline, read the pass state, count dependencies.
op.intMemo(), op.floatMemo(), op.boolMemo(), op.stringMemo(), op.colorMemo()Create a Memo for one value type.
memo.staleOn(firstValue, secondValue, thirdValue, fourthValue, firstKey, secondKey, flag, tag)One-call check over four floats, two ints, one bool and one string. All default to na, the bool to false.
memo.store(value), memo.get(fallback = na)Save and read the cached value.
memo.isValid(), memo.invalidate()Check and drop the cached value.
memo.begin(), memo.dependencies.watch*(), memo.miss()The explicit pass for collections and variable dependency lists.

Every signature, with its defaults, is in the API Reference.

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