Getting Started

Core Concepts

The ideas every OptiPine structure shares: three levels of control, who owns which array, the lifecycles that keep state correct, and the limits Pine sets underneath.

Updated Publisher Alien_AlgorithmsPine Script v6

Three tiers

OptiPine is layered so that high-level code describes the problem rather than the mechanism. Each type's documentation states its tier.

TierWhat it offersStructures
1, QuickReady-to-use calls with automatic defaults.Watch, Memo, CadenceGate, typed stores, StablePool, DirtySet, row rings, double buffers, WeightedSampler
2, ComposableExplicit lifecycles, configuration and representation policies.IntIndex, IntBuckets, SlotCache, RingCursor, Revision, WeightedIndex, WeightedPrefix, FloatFenwick, IndexConfig, WeightConfig
3, ExpertPhysical addressing, unchecked writes and scoped raw mutation for measured hot paths.Ring reserveRowUnchecked(), writeUnchecked(), writePhysical() and edit scopes; markUnchecked(); WeightedIndex setWeightUnchecked() and weight edit scopes

Start with Tier 1. Most scripts never need anything else. Move a layer down only when a measured workload needs it, and keep Tier 3 calls inside the code path you measured.

Ownership and views

Every accessor makes one of three promises about the array it gives you:

  • A value. Scalars such as memo.get(), store.get(key) or ring.at(row, column) return a value. Changing your copy changes nothing inside the structure.
  • A borrowed view. Calls such as dirty.values(), pool.slots(), store.valuesView(), index.keysView(), ring.rowView() and ring.rawView() return the structure's own array. It is read-only by contract and valid only until the structure changes. Do not keep it across bars, sort it or resize it.
  • Your own copy. The *Copy() calls, such as dirty.valuesCopy() or cache.activeCopy(), and the calls that take an output array return storage you own.
Reuse output arrays
Calls such as ring.chronological(output = na), index.findMany(queryKeys, output = na) or store.getMany(queryKeys, output = na) clear and refill an array you pass in, and only allocate a new one when you pass none. Keep a var output array and pass it every time.

Why a view can break

A view is the live storage. pool.slots(), for example, removes a released slot by swapping it with the last one, and a pool keeps each slot's position in that list. Reordering the list from outside would desynchronize both. Reading it is always safe while nothing else changes the pool.

Lifecycles

Several structures work in passes or builds. Each has a fixed order of calls:

StructureOrderWhat it means
Watchbegin(), typed watch*() calls, finish()One comparison pass over several dependencies. finish() reports whether the set changed.
MemostaleOn(), or begin(), dependencies.watch*(), miss()One-call check, or an explicit pass for collections and variable dependency lists.
DirtySetbegin() once per cycle, then mark()A new cycle starts without clearing the universe.
SlotCachebegin(), acquire() each active key, finish()One frame. Keys not acquired in it are retired.
IntIndexbuildBegin(), add() or addMany(), buildFinish()The representation is chosen at buildFinish(). Lookups come after.
IntBucketsbuildFromPairs(), or buildBegin(), add(), buildFinish()Every build replaces all previous groups.
Typed storesbuild(keys, values, expectedQueries)The whole build in one call. set() can change values but not the key set.
Row rings (Tier 3)editBegin(), rawUnsafe(), editFinish()Raw writes are allowed only inside the scope. editFinish() bumps the revision.

IndexConfig, SlotPoolConfig and IntBuckets carry a strictLifecycle flag, on by default. Strict mode stops the script with a descriptive error on a call out of order. With it off, the same mistake degrades to a defined fallback instead, for scripts that must never halt.

Revisions

Row rings and typed stores keep a content revision, a counter that advances when their content changes. Writes that store the same value again leave it alone. That is what makes watch.changed(ring) O(1): the Watch compares one integer, not the ring's contents. For state OptiPine cannot see at all, a Revision token does the same job by hand.

What each call costs

StructureCost
WatchO(1) for one value or a ring; O(N) to compare an array, with the O(N) copy paid only when it changed.
MemoO(number of dependencies) per check.
Row ringspush() is O(width); pushValue() and every single-value read or write are O(1).
StablePoolacquire(), release() and find() are O(1).
SlotCacheacquire() is O(1); finish() is O(slots active in the previous frame).
DirtySetbegin() and mark() are O(1); traversing values() costs the number of marks, never the universe.
IntIndexfind() is O(1) for dense and map, O(log n) for binary search, O(n) for a linear scan.
Double buffersswap() is O(1) plus clearing the new current array.
WeightedSamplersetWeight() is O(1) with the prefix sum and O(log n) with a Fenwick tree. A draw is O(log n); with the prefix sum, the first draw after a weight change also rebuilds it in O(n).

na and empty values

  • Watch compares exactly. na equals na, and floats are compared without Pine's rounding. For arrays, NaCollectionPolicy decides whether an na element is ordinary, always counts as a change, or raises.
  • Memo starts empty. get() returns its fallback before the first store(): na, or false for a BoolMemo. isValid() tells an empty memo from one holding the empty value.
  • Stores return an empty value on a miss. When a stored value may equal it, use read(key), which returns [found, value], or contains(key).
  • Keys are never na. Pools, indexes and buckets reject an na key with an error.

Limits underneath

OptiPine removes work around a calculation. It does not change what Pine allows:

  • An array holds at most 100,000 elements, and a map at most 50,000 pairs.
  • StablePool and SlotCache hold at most 50,000 live keys, because their key directory is a map.
  • A ring's capacity times its width, and a DirtySet's universe, stay within 100,000.
  • IntIndex holds up to 100,000 keys, or 50,000 when it uses a map. IntBuckets holds 100,000 pairs and 50,000 distinct keys.
  • WeightedIndex and FloatFenwick hold at most 99,999 weights.
  • A Revision wraps to 0 after 2,000,000,000 bumps.

The library exports two of these as constants: op.MAX_ARRAY_ELEMENTS and op.MAX_MAP_PAIRS.

Method names

Methods such as get(), set(), push() and clear() match Pine's own collection vocabulary on purpose. The Pine Editor may show a shadowing warning for them. It is cosmetic: the type of the object before the dot decides which method runs.

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