Quickstart
Import OptiPine, create each structure once, describe what a result depends on, and let the library decide when the work runs again.
Import the library
//@version=6
indicator("My Script", overlay = false)
import Alien_Algorithms/OptiPine/1 as opOptiPine needs no special declaration settings. Imports are pinned to a version number, so use the latest one shown on the TradingView publication. With the alias op, constructors are called as op.floatMemo(), and methods on the object they belong to.
Your first cached model
This complete indicator plots a probability-weighted downside estimate for the current trend and volatility regime. The model tests 401 scenarios. The EMA and ATR run on every bar, but their rounded regimes change less often, so Memo reruns the model only when a regime 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)What each part does
- 1Stateful calculations run on every bar
ta.ema()andta.atr()keep their own history, so they stay outside the guard. Their results become the inputs. - 2The inputs are rounded into regimes
The trend is rounded to tenths of an ATR within plus or minus 3, and the volatility to quarters of a percent. Rounding is what makes the inputs repeat; raw values would change on every bar and the model would run every time.
- 3The Memo is created once
var op.FloatMemo downsideStress = op.floatMemo()keeps the cached value and the previous dependencies from bar to bar. - 4staleOn() decides
staleOn()compares the regimes with the ones it last saw. It returns true on the first call and whenever one changed; only then is the model run and the result stored.get()returns the cached value on every bar.
The lifecycle
- Create once: keep every OptiPine object in a
varvariable. One created on every bar starts empty every bar. - Describe the dependencies: pass a Memo or a Watch the values the result depends on, rounded to the precision that matters.
- Rebuild when stale: run the expensive work only when the check says so, and store the result.
- Read every bar: the cached value is there whether or not the work ran.
ta.ema(), ta.pivothigh() or request.security() must never sit inside a Memo or Watch guard. They would skip bars and lose their history. Compute them every bar, then pass their results in.Watch an array
When the script already owns the result, a Watch is the lighter choice: it remembers what it last saw and reports a change, without storing anything else.
// 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.changed() compares the array with a snapshot it keeps, and rewrites that snapshot only after a change. Watch & Memo covers several dependencies at once.
Keep a rolling history
var op.FloatRowRing history = op.floatRowRing(200, 3)
float atr14 = ta.atr(14)
if barstate.isconfirmed
history.push(array.from(close, volume, atr14))
float oldestPrice = history.at(0, 0)
float latestPrice = history.newestAt(0, 0)
// Output: after a confirmed push, these are the oldest and newest retained closes.Once 200 rows are stored, each push overwrites the oldest row instead of shifting the other 199. Reads stay in time order: row 0 is the oldest. See Row Rings.
Next steps
- Core Concepts: tiers, borrowed views, lifecycles and limits.
- Optimizing Pine Script: find where a script spends its time and pick the structure that removes it.
- Complete Examples: a zone cluster engine that combines five structures.