OptiPine Open-source Pine Script optimization library Faster Pine Script by doing less work

OptiPine is a Pine Script v6 library for optimizing TradingView scripts. It caches expensive calculations, updates only what changed, keeps rolling histories and dynamic objects in place, and picks the lookup that fits your keys, so demanding indicators fit inside Pine's execution budget.

Where Pine scripts lose their time
15 to 40%
faster than conventional Pine on the optimized paths, in our tests
Over 90%
faster when a sparse update or an indexed lookup replaces a full scan
93%
faster with DirtySet than clearing and scanning, with 1% of entries changed
391
public calls, from quick defaults to expert control

Where Pine Script spends time that adds nothing

In a small indicator, optimization is often optional. In a rendering engine, a machine learning library, a simulation, a dashboard or an object system, it decides whether a feature runs at all.

The cost is rarely one slow formula. It is the thousands of operations around it. OptiPine's answer, built from techniques used in Pine3D and NeuraLib: do less work, move less data, and let the representation follow the workload. Before changing anything, measure: TradingView's Pine Profiler shows which lines take the time.

  • Recalculating results whose inputs did not change

    Memo and Watch rerun the work only when what it depends on changes.

    How it works
  • Shifting rolling arrays on every bar

    Row rings overwrite the oldest row and still read in time order.

    How it works
  • Moving stored objects when one disappears

    StablePool keeps every object in the slot it was given.

    How it works
  • Scanning a whole collection to find a few changes

    DirtySet visits only the entries that were marked.

    How it works
  • Searching keys one by one

    Typed stores choose direct indexing, a table, a map or binary search.

    How it works
  • Rebuilding state that could be reused

    Double buffers swap in O(1) and Revision skips unchanged producers.

    How it works

Cache expensive calculations in Pine Script

The fastest expensive calculation is the one that never needed to run.

Memo holds one cached int, float, bool, string or color. staleOn() checks up to four floats, two integers, one boolean and one string, store() saves a rebuilt value and get() returns it. Round raw inputs into meaningful regimes first: a model that responds to regimes then reruns only when a regime changes. If the dependencies repeat on nine executions out of ten, it avoids roughly 90% of the runs.

Watch is the lighter choice when the caller already owns the result. changed() is true on the first observation and whenever a value, a primitive array or a row ring changes. For an array it compares with a retained snapshot and rewrites it only after a change, which measured 35% to 60% faster than rewriting the snapshot every time. CadenceGate limits how often work may run at all.

Keep stateful ta.* calls outside the guard. Compute them on every bar, then pass their results in.

Memo.pine
// 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.
Model reranServed from the cache
Model runs
171 of 240
Scenario evaluations
68,571
instead of 96,240
Work avoided
29%
A simulated market, run through the published example: the 401-scenario model reruns only when the rounded trend or volatility regime changes, and the cached result is served on every other bar.

Rolling history without shifting arrays

If an array keeps the latest 200 events, removing the oldest and shifting the other 199 adds nothing to the result.

FloatRowRing and IntRowRing keep fixed-width rows in reusable storage. Once full, the next row overwrites the oldest physical slot while reads stay chronological. A push costs O(width), or O(1) through pushValue() on a width-one ring, and a full chronological read takes at most two native copies, which measured 90% faster than rebuilding a 512-cell, width-four output row by row.

RingCursor gives arrays you own the same circular layout. Ordinary series history such as close[50] should stay native Pine.

Row_Ring.pine
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.
array.shift() then array.push()0 values moved
1
2
3
FloatRowRing, one value per row0 values moved
10
21
32

The small number is each value's place in time, 0 the oldest: what history.at(row, 0) reads.

Each new value pushes out the oldest of the last eight. The array shifts every remaining value one place; the ring writes the new value over the oldest slot, and reads still come back oldest to newest.

Stable slots for objects that come and go

When identity is tied to array position, removing one object shifts every later entry in every parallel array, and every saved position goes stale.

StablePool keeps one association: an object ID chosen by the script points to a reusable slot. The ID answers which zone this is; the slot answers where its data is stored. acquire() returns the slot and whether the ID is new, release() frees the slot without erasing its data, and acquire(), release() and find() are all O(1). One slot can index a zone's price, time, colour, strength and line across several arrays.

SlotCache is the frame-based alternative: begin(), acquire every active key, then finish(), and the keys that were not touched are retired. For a few fixed objects, manual indices are simpler.

StablePool.pine
var op.StablePool zonePool = op.stablePool()

// A receives slot 0. B receives slot 1.
[slotA, _] = zonePool.acquire(ZONE_A_ID)
[slotB, _] = zonePool.acquire(ZONE_B_ID)

// Zone A no longer exists. Its slot becomes available.
zonePool.release(ZONE_A_ID)

// C is a new zone with a new identity, but it can reuse A's old slot.
[slotC, _] = zonePool.acquire(ZONE_C_ID)
var op.StablePool zonePool = op.stablePool()
array.remove() on each arraynothing shifted yet
IndexZonePriceTimeStrength
0
1
StablePoolnothing shifted
IndexZonePriceTimeStrength
0
1

Nothing is stored yet.

The publication's own example: zones A, B and C stored in parallel price, time and strength arrays, once with array.remove() and once with StablePool.

Update only what changed

Large state does not mean large change. A dashboard may hold 10,000 cells while only a few change on a bar.

A dirty-flag array has to be cleared and scanned in full. DirtySet stores only the changed indices, drops repeated marks and begins a new cycle without clearing the whole universe. Several producers can mark work, and one consumer updates each affected cell, drawing or record once. Work scales with the number of changes, not the size of the collection: with 1% of the entries changed, it measured 93% faster than clearing and scanning.

DirtySet.pine
// Start this bar's sparse-work cycle.
dirtySlots.begin()

if zoneGeometryChanged
    [zoneSlot, created] = zones.acquire(zoneId)
    tops.set(zoneSlot, newTop)
    bottoms.set(zoneSlot, newBottom)
    dirtySlots.mark(zoneSlot)

if zoneStyleChanged
    int styleSlot = zones.find(zoneId)
    if styleSlot >= 0
        dirtySlots.mark(styleSlot)  // A second mark of the same slot is ignored.

// Process only the distinct physical slots marked during this bar.
for dirtySlot in dirtySlots.values()
    redrawZone(zones.keyAt(dirtySlot))
Marks this bar
33
4 repeated, ignored
Cells visited
29
one per distinct mark

Bar 1

A dashboard of 2,500 cells. On each bar a geometry producer and a style producer mark the cells they changed, sometimes the same cell twice. The full scan clears and visits every cell; DirtySet keeps one entry per marked cell and visits only those.

Map, array or binary search: let the keys decide

No lookup structure fits every key set. OptiPine sees the completed keys at build time and chooses the one that fits them.

A typed store maps each integer key to one int, float, bool, string or color value. Consecutive keys are addressed directly, a compact range gets a dense table, other unordered keys a native map, and ascending sparse keys binary search, or a map when the expected query count justifies building one. The same store keeps working past Pine's 50,000-pair map limit. Direct addressing measured 21% faster than a map, and a map 91% faster than repeated linear lookup with 32 entries.

IntIndex is the same directory for several payload fields of your own. IntBuckets gives one key many integers, such as the zone slots in one price cell, packed into shared storage: in a 64-key traversal it averaged 19% faster than a wrapper type holding an array.

Typed_Store.pine
var op.IntFloatStore scores = op.intFloatStore()

if barstate.isfirst
    scores.build(
      array.from(410, 411, 412, 413),
      array.from(0.80, 0.30, 0.95, 0.50))

float selected = scores.get(412)

// Output: integer key 412 resolves to float value 0.95.
Key layout
7340917, 120443, 9981201, 55120, 3306611, ...
expectedQueries
Direct index
O(1)
Dense table
O(1)
Native map
O(1)
Binary search
about 9 steps

The keys are unordered and too spread out for a table, so they go into a native map, within Pine's 50,000-pair limit.

The decision IntIndex makes at buildFinish() in automatic mode, with the default settings. Every typed store and IntBuckets is built on it.

Reuse state and sample by weight

IntDoubleBuffer and FloatDoubleBuffer keep a current and a previous array. swap() exchanges them in O(1) and clears the new current one for the next pass.

That suits graph searches, flood fills, iterative clustering and simulations, where one pass must stay readable while the next is built. Revision covers state OptiPine cannot see: the producer bumps it after a change, and each consumer compares its own saved token instead of keeping a snapshot.

WeightedSampler chooses slots in proportion to their weights, for simulations, randomized search and priority sampling. It starts with a cumulative prefix for stable weights, and with weightConfigSparseUpdates() it can move to an update-friendly Fenwick tree while sample() stays the same.

Double_Buffer.pine
var op.IntDoubleBuffer searchFrontier = op.intDoubleBuffer()

if barstate.isfirst
    searchFrontier.current.push(1)

    for depth = 1 to 3
        [nextFrontier, activeFrontier] = searchFrontier.swap()

        for nodeId in activeFrontier
            nextFrontier.push(nodeId * 2)
            nextFrontier.push(nodeId * 2 + 1)

// Output: current contains [8, 9, 10, 11, 12, 13, 14, 15].
//         previous contains [4, 5, 6, 7].
sampler.sample(0.50)  // 11
The publication's three weights. sample() multiplies the fraction by the total weight and finds the slot whose share of the running total contains it. The sampler does not draw the fraction itself.

Three levels of control

High-level code describes the problem, not the mechanism. Start with the quick calls and go deeper only when a measured workload needs it.

Tier 1

Quick

Ready-to-use calls with automatic defaults. Most scripts never need anything else.

  • Watch, Memo and CadenceGate
  • Typed stores
  • StablePool and DirtySet
  • Row rings and double buffers
  • WeightedSampler

Tier 2

Composable

Explicit lifecycles, configuration and representation policies, for when the defaults are not the workload.

  • IntIndex and IntBuckets
  • SlotCache and RingCursor
  • Revision
  • WeightedIndex, WeightedPrefix and FloatFenwick

Tier 3

Expert

Physical addressing, unchecked writes and scoped raw mutation, for hot paths you have measured.

  • reserveRowUnchecked() and writeUnchecked()
  • editBegin(), rawUnsafe() and editFinish()
  • weightsEditBegin() and weightsUnsafe()

Built for engines like Pine3D and NeuraLib

OptiPine packages techniques used in projects such as Pine3D and NeuraLib, where the work around each formula decides what fits.

Its emblem on the publication is a TradingView chart: the knot rendered natively by Pine3D. OptiPine is its own library, imported next to the others, and it fits the same kind of script: one where the bookkeeping, not the formula, decides what ships.

TradingView capture. The OptiPine emblem, rendered natively by Pine3D for the publication.

Complete Pine Script examples you can paste

Two whole indicators from the publication, using native Pine where it is simpler and OptiPine where it removes real work. Add one to a chart, then change it.

Plots a probability-weighted downside estimate for the current trend and volatility regime. The EMA and ATR run on every bar; Memo reruns the 401-scenario model only when a rounded regime changes.

Cached_Regime_Stress.pine

Cached regime stress: Cached_Regime_Stress.pine

//@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)

The OptiPine API at a glance

The main public calls, grouped by what they do. Construct each structure once with var, then call it on every bar.

Open a group to see its calls. Every one of the 391 public calls, with its exact parameters and defaults, is in the plain Markdown reference, ready to paste into an editor or an AI assistant. The source is on the TradingView publication.

Watch and Memo

watch, changed, begin, finish, floatMemo, staleOn, store, get
watch()
A dependency watch. It remembers what it last saw; it does not store the result.
changed(source)
True on the first observation and whenever one value, primitive array or row ring changed. Arrays are compared with a retained snapshot, rings by their revision in O(1).
begin(), watchInt(), watchFloat(), watchBool(), watchString(), watchColor(), watchFloats() ... finish()
An explicit pass over several dependencies. finish() returns true when the completed set changed.
intMemo(), floatMemo(), boolMemo(), stringMemo(), colorMemo()
A cached value of one type, with its own Watch.
staleOn(firstValue, secondValue, thirdValue, fourthValue, firstKey, secondKey, flag, tag)
Up to four floats, two ints, one bool and one string. True when the value must be recomputed.
store(value), get(fallback = na), isValid(), invalidate()
Save, read, check and drop the cached value.
begin(), dependencies.watch*(), miss()
The explicit Memo lifecycle, for collections or a variable dependency list.

Revision and CadenceGate

revision, bump, changedSince, cadenceGate, due, dueWhenChanged
revision(initialValue = 0)
A change token for state OptiPine cannot observe.
bump(), current(), changedSince(snapshot)
The producer bumps after a change; each consumer compares its own saved token.
cadenceGate()
A gate for work that should run at most every so many ticks.
due(tick, interval = 1, force = false)
Consumes the gate and returns true when work is due.
dueWhenChanged(tick, revision, interval = 1, force = false)
Due only when the cadence is open and the producer changed. Changes wait until the cadence opens.
peek(tick, interval = 1, force = false)
Asks the same question without consuming the gate.

Row rings and RingCursor

floatRowRing, intRowRing, push, pushValue, at, newestAt, chronological
floatRowRing(capacity, width, preallocate = true, fillValue = 0.0)
Fixed-width float rows in one flat array. intRowRing() is the int version.
push(row), pushValue(value)
Appends the newest row in O(width), or one value in O(1) on a width-one ring. Once full, the oldest row is overwritten.
at(logicalRow, column), newestAt(offset, column), setAt(logicalRow, column, value)
Read and write in time order: row 0 is the oldest retained row.
chronological(output = na), window(startLogicalRow, rowCount, output = na), gather(logicalRows, output = na)
Copy rows out oldest to newest, a run of rows, or chosen rows.
rowCount(), rowWidth(), rowCapacity(), revision()
The ring's size and its content revision.
ringCursor(capacity), reserve(), physical(logicalIndex), logical(physicalSlot)
The same circular layout for arrays you own.

StablePool and SlotCache

stablePool, acquire, release, find, slots, slotCache
stablePool(settings = na)
Maps object IDs to reusable slots with an explicit lifetime.
acquire(key)
Returns [slot, created]. A live ID gets the same slot back; a new one may reuse a freed slot.
release(key)
Frees the slot without erasing your payload at that slot.
find(key), contains(key), keyAt(slot), slots(), size(), slotCount()
Lookups in O(1), and the live slots as a read-only view.
slotCache(settings = na), begin(), acquire(key), finish()
Frame-based slots: keys not acquired in a frame are retired at finish().
slotPoolConfig(strictLifecycle = true, maxSlots = 50000)
Pool settings. Up to 50,000 live keys.

DirtySet

dirtySet, begin, mark, markMany, markRange, values
dirtySet(universeSize = 0)
A set of changed indices over a universe of up to 100,000.
begin(universeSize = na)
Starts a new cycle without clearing the universe.
mark(index), markMany(indices), markRange(first, last)
Marks work. A second mark of the same index in a cycle is ignored.
values(), valuesCopy(output = na), size(), contains(index)
The distinct marks in discovery order, never a scan of the universe.

Typed stores and IntIndex

intFloatStore, build, get, getMany, intIndex, buildFinish, find
intIntStore(), intFloatStore(), intBoolStore(), intStringStore(), intColorStore()
Integer keys to one value each. Each takes an optional IndexConfig.
build(keys, values, expectedQueries = na)
Pairs the two same-length arrays and chooses the lookup. The hint says how many lookups to expect before the next build.
get(key, fallback = na), set(key, value), contains(key), getMany(queryKeys, output = na)
Lookups, single or batched.
intIndex(settings = na), buildBegin(), add(key), addMany(keyBatch), buildFinish(expectedQueries = na)
A key-to-slot directory for your own payload arrays.
find(key), findMany(queryKeys, output = na), keyAt(slot), mode(), stats()
Slot lookups, the representation in use, and counters.
indexConfigStaticSorted(), indexConfigDense(), indexConfigDictionary(), indexConfigTiny(), indexConfigDynamic()
Presets. Dynamic lets a sorted index promote itself to a map once enough queries arrive.

IntBuckets

intBuckets, buildFromPairs, rangeByKey, valueAt
intBuckets(settings = na, strictLifecycle = true)
One key to many integers, in shared contiguous storage.
buildFromPairs(keyArray, valueArray, expectedQueries = na)
Replaces every group. A repeated key adds a member.
buildBegin(), add(key, value), addMany(keyArray, valueArray), buildFinish(expectedQueries = na)
The same build when pairs arrive one at a time.
rangeByKey(key), rangeBySlot(slot), valueAt(flatIndex)
A group as [start, count] and its members. A missing key has a count of 0.
bucketCount(), keyAt(slot), pairCount()
The groups and their keys.

Double buffers and storage

intDoubleBuffer, swap, swapSized, ensureSize*, resize*, refill*, sameExact*
intDoubleBuffer(), floatDoubleBuffer()
Two retained arrays, current and previous.
swap(), swapSized(size, fillValue)
Exchanges them in O(1) and clears, or sizes and refills, the new current one.
ensureSizeFloat(values, minimumSize, fillValue = 0.0)
Grows a caller-owned array in place. Int, Bool, Color and String versions too.
resizeFloat(), refillFloat(), sameExactFloat()
Resize, refill and compare arrays exactly, without Pine's float rounding.
matrixFromFlat(), flatFromFloatMatrix(), transposeFlatFloat()
Conversions between flat arrays and matrices.

Weighted sampling

weightedSampler, setWeight, sample, probability, weightConfigSparseUpdates
weightedSampler(capacity, exponent = 1.0, settings = na)
Selects slots in proportion to their weights.
setWeight(slot, weight), setWeights(weights), setMany(slots, weights)
Update one weight or a batch.
sample(fraction), sampleMany(fractions, output = na)
Maps a fraction in [0, 1) to a slot. Supply the fraction, for example from math.random().
probability(slot), total()
Each slot's share and the total weight.
weightConfigSparseUpdates()
Lets the sampler move to a Fenwick tree when weights change often.
weightedIndex(capacity, settings = na), prepare(), draw(fraction)
Circular ranges and explicit policy control.

Limits worth knowing before you optimize

OptiPine removes work around a calculation; it does not change what Pine allows. These are the rules that keep an optimized script correct.

Guarded work must be pure
Stateful calls such as ta.ema() or ta.pivothigh() must stay outside Memo and Watch guards. Compute them on every bar and pass the results in.
A comparison is not free
Watch compares an array in O(N). Use it when the calculation it protects costs more, and use a producer's own change flag when it has a reliable one.
A freed slot keeps its data
release() frees a slot without erasing your payload. Overwrite every field when the slot is reused.
Views are borrowed
values(), slots(), keysView() and rowView() are read-only and valid until the structure changes. Use the Copy() calls to keep them.
Pine's own limits still apply
Arrays hold 100,000 elements and maps 50,000 pairs. Pools hold 50,000 live keys, and a ring's capacity times its width stays within 100,000.
Delayed work is delayed
CadenceGate is for work that may wait, such as periodic model fitting, not for results that must update on the bar.
The randomness is yours
WeightedSampler maps the fractions you give it. It does not generate them.
Editor warnings are cosmetic
get(), set(), push() and clear() match Pine's own names on purpose. A shadowing warning does not change which method runs.

Pine Script optimization questions

Short answers about making Pine Script faster, and about OptiPine. The TradingView publication carries the full description and the source.

What is OptiPine?

OptiPine is an open-source performance library for TradingView, published as a Pine Script v6 library by Alien_Algorithms. It provides caching, sparse updates, stable storage, rolling buffers, adaptive keyed lookup and weighted selection, so demanding scripts spend their execution budget on the feature instead of on repeated work.

How do I optimize a Pine Script indicator?

Find the work that does not change the result. The usual costs are recalculating results whose inputs did not change, shifting rolling arrays, scanning a large collection to find a few changes, and moving stored objects when one disappears. Replace each with a cheaper pattern: cache the result and rerun it only when its inputs change, overwrite the oldest slot of a ring instead of shifting, visit only the entries marked as changed, and keep each object in a stable slot. OptiPine provides each of these as a small API.

How do I find what makes my Pine Script slow?

Measure before changing anything. TradingView's Pine Profiler shows how much of a script's run time each line and function takes. Then look at the most expensive parts for repeated work: a calculation that reruns with the same inputs, array.shift() or array.remove() on a large array, or a loop over a whole collection when only a few entries changed.

How do I cache a calculation in Pine Script?

Create a FloatMemo once with var, call staleOn() with the values the result depends on, and store() the new result when it reports stale. get() returns the cached value in between. Round raw inputs into meaningful regimes first, or the cache misses on every small change, and keep stateful ta.* calls outside the guard so they run on every bar.

How do I keep a rolling history without shifting arrays in Pine Script?

Use a ring buffer. OptiPine's FloatRowRing and IntRowRing keep fixed-width rows in reusable storage. Once full, a push overwrites the oldest row while reads stay in chronological order, so a push costs one row's width instead of moving every retained row.

Is a map faster than an array in Pine Script?

For looking up keys, usually yes: in our tests a native map was 91% faster than a repeated linear search with 32 entries. When the keys are consecutive integers, indexing an array directly is faster still, 21% faster than a map. OptiPine's typed stores look at the keys they are built with and choose direct indexing, a dense table, a map or binary search accordingly, and keep working past Pine's 50,000-pair map limit.

How much faster does OptiPine make a script?

It depends on the workload. Against conventional Pine implementations of the same task, OptiPine's optimized paths commonly ran 15% to 40% faster in our measurements, and sparse updates and indexed lookups exceeded 90% when the alternative scanned or searched the full collection. With 1% of the entries changed, DirtySet measured 93% faster than clearing and scanning the whole collection.

Which scripts benefit most from OptiPine?

Scripts where the work around the formula decides whether a feature fits: rendering engines, machine learning libraries, simulations, large dashboards and object systems. The techniques are the ones used in projects such as Pine3D and NeuraLib. For a small indicator, native Pine is often simpler, and ordinary series history such as close[50] should stay native.

Is OptiPine free to use?

Yes. It is open source under CC BY-NC-SA 4.0: free for non-commercial use when Alien_Algorithms is credited for the underlying software. Commercial use needs a licence from Alien_Algorithms.

Can an AI assistant write OptiPine code?

Yes, when it has the reference. Give it alienalgorithms.com/optipine.md, a plain Markdown file that lists every public call with its exact parameters and defaults, the rules for correct scripts, and complete examples.

Spend the budget on the feature

At large scale, optimization is no longer only about speed. It decides whether an ambitious idea can ship at all. Import OptiPine, describe what your results depend on, and let it keep the bookkeeping.

Licensed under CC BY-NC-SA 4.0: free for non-commercial use when Alien_Algorithms is credited for the underlying software. For commercial licensing, contact Alien_Algorithms on TradingView.