Getting Started

Quickstart

Import OptiPine, create each structure once, describe what a result depends on, and let the library decide when the work runs again.

Updated Publisher Alien_AlgorithmsPine Script v6

Import the library

Declaration and importpine
//@version=6
indicator("My Script", overlay = false)

import Alien_Algorithms/OptiPine/1 as op

OptiPine 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.

Cached_Regime_Stress.pinepine
//@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

  1. 1
    Stateful calculations run on every bar

    ta.ema() and ta.atr() keep their own history, so they stay outside the guard. Their results become the inputs.

  2. 2
    The 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.

  3. 3
    The Memo is created once

    var op.FloatMemo downsideStress = op.floatMemo() keeps the cached value and the previous dependencies from bar to bar.

  4. 4
    staleOn() 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 var variable. 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.
Keep guarded work pure
Stateful calls such as 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.

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.

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

The last 200 confirmed bars, three values eachpine
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

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