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.
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.
// 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.
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.
// 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, andarity()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.
| Policy | Effect |
|---|---|
op.NaCollectionPolicy.exact | The default. na equals na, so two passes holding na in the same position report no change. |
op.NaCollectionPolicy.alwaysChanged | An 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.reject | Stop 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().
// 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, andinvalidate()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.
// 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.0Memo 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.
// 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
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
| Call | What 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.