OptiPine: High-Performance Caching and Data PipelinesOptiPine is a high performance architecture library for Pine Script™, built for algorithms that push beyond ordinary indicator workloads. It turns caching, sparse updates, reusable storage and workload-aware data structures into practical APIs that stay small at the call site.
In a small indicator, optimization is often optional. In a rendering engine, machine learning library, simulation, dashboard or object system, it can determine whether a feature runs at all. The problem is rarely one slow formula. It is the thousands of unnecessary operations around it: recalculating unchanged results, shifting rolling arrays, scanning large collections for a few changes, and moving stored objects when one disappears.
OptiPine attacks that layer with techniques used in projects such as Pine3D and NeuraLib . The idea is simple: do less work, move less data, and let the representation follow the workload.
Compared with conventional Pine implementations of the same task, OptiPine's optimized paths commonly ran 15% to 40% faster . Sparse updates and indexed lookups exceeded 90% when the alternative scanned or searched the full collection.
Most users can stay entirely within the high-level API. A Memo cache with several dependencies looks like this:
// 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.
Memo owns the previous dependencies, first-run state, validity and cached result. The caller only declares what the result depends on.
----------------------------------------------------------------------------------------------------------------
🔷 DO NOT CALCULATE THE SAME THING TWICE
The fastest expensive calculation is the one that never needed to run. Models, simulations and generated geometry often remain valid across many script executions.
Memo is the direct choice when the cached result is an 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.
Many models respond to regimes rather than every tiny change in raw data. Round the inputs into meaningful regimes, pass them to staleOn() , and the model runs only when a regime changes.
In practice: Memo is useful for scenario models, parameter sweeps, numerical solvers and other expensive pure calculations that reduce to one primitive result. If its dependencies repeat on nine out of ten executions, it avoids roughly 90% of those model runs.
For collections or a variable dependency list, use Memo's explicit begin() , dependencies.watch*() and miss() lifecycle.
Keep guarded work pure: Stateful ta.* and similar history-dependent calls must remain outside Memo and Watch guards. Compute them every bar, then pass their results into the guarded calculation.
🔸 WATCH: CHANGE DETECTION WITHOUT RESULT STORAGE
Watch is the lighter choice when the caller already owns the result. Several consumers can observe the same producer independently by giving each its own Watch. changed() returns true on the first observation and whenever one scalar, primitive array or OptiPine row ring changes. Row rings expose an internal revision, so checking them is O(1).
For a single source, the dependency check should take less attention than the calculation it protects. Here another component supplies one caller-owned feature array:
// Pseudocode: getFeatureSnapshot() supplies an array.
array 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.
Because OptiPine does not own features , it compares the array with a retained snapshot and rewrites that snapshot only after a change. Supported row rings use their internal revision instead. The call stays the same, and this compare-first array pattern measured roughly 35% to 60% faster than rewriting the snapshot every time.
The array comparison is still O(N), so use it when the avoided calculation costs more than the comparison. If the producer already provides one reliable change flag, use the flag directly.
For several dependencies, use an explicit pass. begin() starts the comparison, the typed watch*() methods add dependencies, and finish() returns true if the completed set changed. A Watch remembers dependencies; it does not store the result.
// Pseudocode dependencies: int length, float multiplier,
// and array 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.
Construct the Watch once with var , then run begin() and finish() on every comparison pass. For one dependency, changed(source) is the shorter path.
In practice: Watch fits module boundaries: a model can observe a feature array, a renderer can observe a managed ring, or a cache can observe several mixed settings without duplicating the producer's change logic.
CadenceGate limits how often work may run. due() is periodic; dueWhenChanged() also requires a producer revision and remembers changes until the cadence opens. Use it for intentionally delayed work such as periodic model fitting, not results that must update immediately.
----------------------------------------------------------------------------------------------------------------
🔷 ROLLING HISTORY WITHOUT SHIFTING IT
Rolling histories often perform work that adds nothing to the result. If an array keeps the latest 200 events, removing the oldest one and shifting the other 199 entries is unnecessary.
FloatRowRing and IntRowRing keep fixed-width rows in reusable storage. Once full, the next row overwrites the oldest physical slot while reads remain chronological.
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.
A push costs O(width), or O(1) through pushValue() for a width-one ring. Rings also provide chronological windows and gathered rows. When several producers can mutate a ring, a separate consumer can detect its revision with Watch.changed(ring) in O(1).
In practice: Row rings fit pivots, completed trades, sampled features and other fixed event histories.
Performance: A full ring overwrites one row instead of shifting every retained row. Its chronological output uses at most two native contiguous copies, which measured 90% faster than rebuilding a 512-cell, width-four output row by row.
Use RingCursor when several caller-owned arrays need the same circular layout. Ordinary series history such as close should remain native Pine.
----------------------------------------------------------------------------------------------------------------
🔷 KEEP DYNAMIC OBJECTS STABLE
Dynamic objects become surprisingly expensive when identity is tied to array position. If one object is removed from several parallel arrays, every later entry shifts, every synchronized payload array needs the same removal, and every external reference to those positions becomes fragile.
StablePool is not the zone storage itself. It keeps one association: an object ID supplied by the script points to a reusable array slot. The ID answers "which zone is this?" while the slot answers "where is this zone's data stored?"
The example has three different logical zones named A, B and C. Their IDs, 1001, 1002 and 1003, are arbitrary unique values chosen for readability. Real IDs may come from a pivot bar, timestamp, order number or incrementing counter.
const int ZONE_A_ID = 1001
const int ZONE_B_ID = 1002
const int ZONE_C_ID = 1003
var op.StablePool zonePool = op.stablePool()
// This example never has more than two active zones.
var array prices = array.new(2, na)
if barstate.isfirst
// A receives slot 0. B receives slot 1.
= zonePool.acquire(ZONE_A_ID)
= zonePool.acquire(ZONE_B_ID)
prices.set(slotA, 100.0)
prices.set(slotB, 200.0)
// 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.
= zonePool.acquire(ZONE_C_ID)
prices.set(slotC, 300.0)
// Output: B keeps slot 1. C has ID 1003 but reuses A's released slot 0.
// prices is .
Why C needs a new ID: C is a different zone, even though it occupies the same array position A once used. Reusing 1001 would describe A returning, not a new zone C. IDs preserve object identity; slots are only reusable storage addresses.
Several fields, one slot: In production, the same slot usually addresses every field belonging to the object. Continuing the A, B and C lifecycle with four parallel arrays:
const int ZONE_A_ID = 1001
const int ZONE_B_ID = 1002
const int ZONE_C_ID = 1003
var op.StablePool zonePool = op.stablePool()
var array zonePrices = array.new()
var array zoneTimes = array.new()
var array zoneStrengths = array.new()
var array zoneColors = array.new()
if barstate.isfirst
= zonePool.acquire(ZONE_A_ID)
= zonePool.acquire(ZONE_B_ID)
// Grow every payload array to cover the allocated slots.
int required = zonePool.slotCount()
op.ensureSizeFloat(zonePrices, required, na)
op.ensureSizeInt(zoneTimes, required, na)
op.ensureSizeFloat(zoneStrengths, required, na)
op.ensureSizeColor(zoneColors, required, na)
zonePrices.set(slotA, 100.0)
zoneTimes.set(slotA, 10)
zoneStrengths.set(slotA, 0.40)
zoneColors.set(slotA, color.blue)
zonePrices.set(slotB, 200.0)
zoneTimes.set(slotB, 20)
zoneStrengths.set(slotB, 0.80)
zoneColors.set(slotB, color.red)
zonePool.release(ZONE_A_ID)
= zonePool.acquire(ZONE_C_ID)
// C reuses A's slot, so every field at that slot must be overwritten.
zonePrices.set(slotC, 300.0)
zoneTimes.set(slotC, 30)
zoneStrengths.set(slotC, 0.60)
zoneColors.set(slotC, color.lime)
// Output: B keeps slot 1 in every array. C owns slot 0 in every array.
// Nothing is removed or shifted.
acquire(id) returns the slot and whether the ID was newly added. Calling it again for an active ID returns the same slot. release(id) frees the slot, but does not erase its array data, so every field must be overwritten when that slot is reused.
The example preallocates two values because it has at most two active zones. A dynamic script can grow its payload arrays with ensureSize*() whenever acquire() reports a new ID. zonePool.slots() returns the currently active slots as a read-only view.
In practice: One zone slot can index its price, time, color, strength and line across several arrays. In the complete example later, the pivot bar and event type form each zone ID. Releasing one zone frees its slot without shifting other zones or breaking saved positions.
Performance: StablePool is independent of payload layout: its slots can index parallel arrays or one array of UDTs. acquire() , release() and find() are O(1), and releasing an object never shifts caller-owned payloads.
For a few fixed objects, manual indices are simpler. StablePool becomes useful when IDs appear and disappear over time, several payload arrays share the same slots, or other parts of the script retain those positions.
SlotCache is the frame-based alternative. Call begin() , acquire every active key, then call finish() ; previously active keys that were not touched are retired automatically.
----------------------------------------------------------------------------------------------------------------
🔷 UPDATE ONLY WHAT CHANGED
Large state does not imply large change. A dashboard may contain 10,000 cells while only a few change on one bar, or a large object system may need to refresh only a handful of entries.
A conventional dirty-flag array must be cleared and scanned in full. DirtySet stores only the changed indices, removes duplicate marks and begins a new cycle without clearing the entire universe. It is a work list, not payload storage or an ID-to-slot map.
Here StablePool resolves zoneId , the arrays store zone data, and DirtySet schedules the slots that need rebuilding. The event values are pseudocode:
int MAX_ZONES = 50000
var op.StablePool zones = op.stablePool()
var op.DirtySet dirtySlots = op.dirtySet(MAX_ZONES)
var array tops = array.new()
var array bottoms = array.new()
var array midpoints = array.new()
// Start this bar's sparse-work cycle.
dirtySlots.begin()
if zoneGeometryChanged
// StablePool converts the logical ID into a reusable physical slot.
= zones.acquire(zoneId)
if created
op.ensureSizeFloat(tops, zoneSlot + 1, na)
op.ensureSizeFloat(bottoms, zoneSlot + 1, na)
op.ensureSizeFloat(midpoints, zoneSlot + 1, na)
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()
float midpoint = (tops.get(dirtySlot) + bottoms.get(dirtySlot)) * 0.5
midpoints.set(dirtySlot, midpoint)
redrawZone(zones.keyAt(dirtySlot), midpoint)
// Output: one zone is rebuilt once even if geometry and style both mark it.
Repeated marks are deduplicated, and unmarked zones are never visited. Work scales with the number of changed slots, not the size of the collection. If the natural address is already a dense index, mark it directly without StablePool.
In practice: Several producers can mark work, then one consumer updates each affected cell, drawing or record once. With 1% of entries changed, this measured 93% faster than clearing and scanning the full universe.
----------------------------------------------------------------------------------------------------------------
🔷 KEYED LOOKUP WITHOUT GUESSWORK
Keyed lookup appears throughout object systems, caches and grouped data, but no structure fits every key set. Distribution, rebuild frequency and query volume change the best choice. OptiPine sees the completed keys at build() , then selects the lookup shape that fits them.
🔸 TYPED STORES: ONE VALUE PER KEY
A typed store maps each integer key to one primitive value. build() pairs entries at matching positions in the key and value arrays. Consecutive IDs allow direct addressing:
var op.IntFloatStore scores = op.intFloatStore()
if barstate.isfirst
// Four entries are shown for readability; both arrays may be much larger.
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.
Lookup is one-way: get(412) returns 0.95 , but values may repeat, so get(0.95) has no general meaning.
What automatic mode chooses:
Consecutive ascending keys: Direct arithmetic indexing.
Compact key ranges: A dense lookup table.
Other unordered keys: A native map when within Pine's map limit.
Ascending sparse keys: Binary search, or a map within that limit when expectedQueries justifies its build cost.
Linear lookup remains available for unusual workloads that rebuild far more often than they query. Automatic mode only selects it for non-empty stores when linearMaxEntries is deliberately configured.
The same API avoids hashing when direct addressing fits, uses a map when it pays, and remains usable beyond Pine's map capacity. Automatic mode is the normal default. Use op.indexConfigDynamic() when future query volume is unknown and the store may need to promote itself later.
build(keys, values, expectedQueries) accepts two same-length arrays. The optional hint tells OptiPine how many lookups to expect before the next build. Stores support int, float, bool, string and color values. Use one IntIndex for several payload fields, or IntBuckets when a key owns several integers.
In practice: Batch-build IDs to scores, states or metadata, then query them without committing to a representation. Direct integer addressing measured 21% faster than a map, while a map measured 91% faster than repeated linear lookup with 32 entries.
🔸 INTBUCKETS: ONE KEY TO MANY INTEGER VALUES
A Store returns one value for each key. IntBuckets returns a group of integers, usually object IDs or physical slots. Repeating a key adds another member instead of replacing the previous one.
var op.IntBuckets cellMembers = op.intBuckets()
var array matches = array.new()
if barstate.isfirst
// Six (cell, object slot) pairs. Cell 7 appears three times.
array cellKeys = array.from(7, 2, 7, 5, 2, 7)
array objectSlots = array.from(101, 205, 412, 990, 777, 888)
cellMembers.buildFromPairs(cellKeys, objectSlots)
// Read cell 7's group from flat storage. matches is only demo output.
= cellMembers.rangeByKey(7)
if count > 0
for position = start to start + count - 1
matches.push(cellMembers.valueAt(position))
// Output: matches contains , the object slots assigned to cell 7.
What happens: Each key is paired with the slot at the same array position. Cell 7 appears three times, so its group contains 101, 412 and 888. rangeByKey() returns where that group starts and how many values it contains. A missing key returns a count of 0.
Lifecycle: buildFromPairs() replaces all previous groups. Use buildBegin() , add() and buildFinish() only when pairs arrive one at a time.
In practice: A price cell can own several zone slots, a graph node can own several neighbors, or a category can own several record IDs. One query visits only that group.
Why use it: A native map stores one value per key, and Pine does not allow an array directly as that value. Giving one key several values therefore requires a small wrapper UDT containing an array. IntBuckets provides that relationship directly, packing every group into shared contiguous storage. It suits batch rebuilds followed by repeated traversal, while the wrapper approach is more convenient when individual groups change constantly. In the tested 64-key traversal workload, IntBuckets averaged 19% faster across four runs.
----------------------------------------------------------------------------------------------------------------
🔷 REUSE STATE INSTEAD OF REBUILDING IT
IntDoubleBuffer and FloatDoubleBuffer retain current and previous arrays. swap() exchanges their references in O(1), preserves the old result and clears the new current buffer for reuse. That clear still costs O(N).
This is useful when one pass must remain readable while the next is built. In this small search, node n has children 2n and 2n + 1 . Each pass reads the active level and writes the next one:
var op.IntDoubleBuffer searchFrontier = op.intDoubleBuffer()
if barstate.isfirst
searchFrontier.current.push(1)
for depth = 1 to 3
= searchFrontier.swap()
for nodeId in activeFrontier
nextFrontier.push(nodeId * 2)
nextFrontier.push(nodeId * 2 + 1)
// Output: current contains .
// previous contains .
What happens: swap() makes the completed level available as activeFrontier and returns the other retained array, already empty, as nextFrontier . No level is copied and no replacement array is created. The same pattern supports graph searches, flood fills, iterative clustering and simulations. Use swapSized() when every pass needs a fixed-size output.
A var array can also be reused. The ensureSize*() , resize*() and refill*() families modify existing storage, while sameExact*() compares primitive arrays without Pine's float-comparison rounding.
Revision handles caller-owned state that OptiPine cannot observe. The producer calls bump() after a change; each consumer compares its own saved token with changedSince() instead of keeping a snapshot.
----------------------------------------------------------------------------------------------------------------
🔷 WEIGHTED SELECTION FOR STATIC AND DYNAMIC SYSTEMS
Weighted selection chooses entries in proportion to their weights. It is useful in simulations, randomized search and priority sampling.
WeightedSampler is the high-level interface. Set weights, then supply a fraction to select a slot. The sampler does not generate randomness; use math.random() or a repeatable fraction sequence:
var op.WeightedSampler sampler = op.weightedSampler(512)
if barstate.isfirst
sampler.setWeight(10, 0.25)
sampler.setWeight(11, 0.80)
sampler.setWeight(12, 0.10)
float fraction = 0.50
int selected = sampler.sample(fraction)
// Output: selected is 11 for the supplied fraction of 0.50.
The default cumulative prefix suits stable weights. Pass op.weightConfigSparseUpdates() and the sampler can move to an update-friendly Fenwick tree as the workload changes. sample() stays the same. Use WeightedIndex for circular ranges or explicit policy control.
In practice: Each slot can represent a candidate model, simulation outcome or work item. Update its weight when its score changes, then sample repeatedly through the same interface.
----------------------------------------------------------------------------------------------------------------
🔷 THREE LEVELS OF CONTROL
OptiPine is layered so high-level code describes the problem rather than the mechanism. Start with Tier 1 and move deeper only when the workload requires more control:
Tier 1, Quick: Ready-to-use APIs with automatic defaults, including Watch, Memo, CadenceGate, typed stores, StablePool, DirtySet, row rings, double buffers and WeightedSampler.
Tier 2, Composable: Explicit lifecycles, configuration and representation policies through IntIndex, IntBuckets, SlotCache, RingCursor and Revision.
Tier 3, Expert: Physical addressing, unchecked operations and scoped raw mutation for measured hot paths. Ordinary read-only views are not Tier 3.
Editor warnings: Methods such as get() , set() , push() and clear() intentionally match Pine's collection vocabulary. Any shadowing-method warning is cosmetic; the receiver's type determines which method runs.
----------------------------------------------------------------------------------------------------------------
🔷 COMPLETE, COPY-PASTE EXAMPLES
The fragments above isolate one idea at a time. These two copy-paste indicators combine them in practical workflows, using native Pine where it is simpler and OptiPine where it removes real work.
🔸 Complete example 1: high-level cached stress model
What it does: The indicator plots a probability-weighted downside estimate for the current trend and volatility regime, while exposing both regime values in the Data Window.
The EMA and ATR calculations run normally on every bar. Their rounded regimes change less often, so Memo recalculates the 401-scenario model only when one of those regimes changes and serves the cached result between changes.
//@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)
🔸 Complete example 2: advanced zone-cluster engine
What it does: The indicator draws recent pivot levels, thickens those near the current price, plots the strongest price cluster and reports its key statistics in the Data Window.
StablePool preserves drawing slots, the ring tracks retirement order, DirtySet queues redraws, IntBuckets forms price clusters and IntFloatStore looks up their strength.
Relevant benchmarks: These are component results, not a total for this 32-zone indicator. In larger matching workloads, DirtySet saved 93% at 1% dirty and IntBuckets averaged 19% with 64 keys. For typed lookup, direct addressing saved 21% over a map on compact keys, while a map saved 91% over linear search at 32 entries. Automatic mode selects the representation.
StablePool and the ring manage recycling. The script still scans live zones for proximity changes, then DirtySet avoids unnecessary drawing updates.
//@version=6
indicator("OptiPine - Zone Cluster Engine", overlay = true, max_lines_count = 100)
import Alien_Algorithms/OptiPine/1 as op
int pivotLength = input.int(5, "Pivot length", minval = 1)
int maxZones = input.int(32, "Maximum zones", minval = 4, maxval = 100)
int bucketTicks = input.int(25, "Cluster size in ticks", minval = 1)
float bucketSize = syminfo.mintick * bucketTicks
// Stateful Pine calculations remain outside every conditional rebuild.
float pivotHigh = ta.pivothigh(high, pivotLength, pivotLength)
float pivotLow = ta.pivotlow(low, pivotLength, pivotLength)
float pivotStrength = math.max(nz(volume , 1.0), 1.0)
float highlightDistance = ta.atr(14)
var op.StablePool zones = op.stablePool()
var op.IntRowRing zoneOrder = op.intRowRing(maxZones, 1)
var op.DirtySet dirtyZones = op.dirtySet(maxZones)
var array zonePrices = array.new(maxZones, na)
var array zoneStrengths = array.new(maxZones, 0.0)
var array zoneTimes = array.new(maxZones, na)
var array resistance = array.new(maxZones, false)
var array highlighted = array.new(maxZones, false)
var array zoneLines = array.new(maxZones)
var op.IntBuckets zonesByBucket = op.intBuckets()
var op.IntFloatStore strengthByBucket = op.intFloatStore()
// Retained build storage is resized and overwritten, never cleared and repopulated.
var array bucketKeyByPosition = array.new()
var array aggregateKeys = array.new()
var array aggregateStrengths = array.new()
var int strongestBucketKey = na
var float strongestBucketStrength = na
var int strongestZoneCount = 0
dirtyZones.begin()
bool topologyChanged = barstate.isfirst
// Logical pivot IDs receive stable, reusable physical drawing slots.
for event = 0 to 1
float level = event == 0 ? pivotHigh : pivotLow
if barstate.isconfirmed and not na(level)
int pivotBar = bar_index - pivotLength
int pivotTime = time
int zoneId = pivotBar * 2 + event
int slot = zones.find(zoneId)
// Only a new logical pivot enters the retirement queue.
if slot < 0
if zoneOrder.rowCount() == maxZones
int oldestId = zoneOrder.at(0, 0)
zones.release(oldestId)
= zones.acquire(zoneId)
slot := newSlot
zoneOrder.pushValue(zoneId)
zonePrices.set(slot, level)
zoneStrengths.set(slot, pivotStrength)
zoneTimes.set(slot, pivotTime)
resistance.set(slot, event == 0)
highlighted.set(slot, false)
dirtyZones.mark(slot)
topologyChanged := true
// Proximity can mark a newly created slot again; DirtySet still stores it once.
for slot in zones.slots()
bool isHighlighted = math.abs(close - zonePrices.get(slot)) <= highlightDistance
if isHighlighted != highlighted.get(slot)
highlighted.set(slot, isHighlighted)
dirtyZones.mark(slot)
// Only changed drawings cross the line API boundary.
for slot in dirtyZones.values()
float level = zonePrices.get(slot)
color baseColor = resistance.get(slot) ? color.red : color.lime
line zoneLine = zoneLines.get(slot)
if na(zoneLine)
zoneLine := line.new(zoneTimes.get(slot), level, time, level,
xloc = xloc.bar_time)
zoneLines.set(slot, zoneLine)
line.set_xy1(zoneLine, zoneTimes.get(slot), level)
line.set_xy2(zoneLine, time, level)
line.set_extend(zoneLine, extend.right)
line.set_width(zoneLine, highlighted.get(slot) ? 3 : 1)
line.set_color(zoneLine,
color.new(baseColor, highlighted.get(slot) ? 0 : 55))
// Rebuild grouped lookup only after the explicit creation event.
if topologyChanged
array liveSlots = zones.slots()
int liveCount = liveSlots.size()
op.resizeInt(bucketKeyByPosition, liveCount, 0)
if liveCount > 0
for position = 0 to liveCount - 1
int slot = liveSlots.get(position)
int bucketKey = int(math.round(zonePrices.get(slot) / bucketSize))
bucketKeyByPosition.set(position, bucketKey)
// Repeated bucket keys accumulate several physical zone slots.
zonesByBucket.buildFromPairs(bucketKeyByPosition, liveSlots)
int bucketCount = zonesByBucket.bucketCount()
op.resizeInt(aggregateKeys, bucketCount, 0)
op.resizeFloat(aggregateStrengths, bucketCount, 0.0)
strongestBucketKey := na
strongestBucketStrength := na
strongestZoneCount := 0
if bucketCount > 0
for bucketSlot = 0 to bucketCount - 1
int bucketKey = zonesByBucket.keyAt(bucketSlot)
= zonesByBucket.rangeBySlot(bucketSlot)
float totalStrength = 0.0
if count > 0
for position = start to start + count - 1
int zoneSlot = zonesByBucket.valueAt(position)
totalStrength += zoneStrengths.get(zoneSlot)
aggregateKeys.set(bucketSlot, bucketKey)
aggregateStrengths.set(bucketSlot, totalStrength)
if na(strongestBucketStrength) or totalStrength > strongestBucketStrength
strongestBucketKey := bucketKey
strongestBucketStrength := totalStrength
strongestZoneCount := count
strengthByBucket.build(aggregateKeys, aggregateStrengths)
// Query the current price cluster directly and display the strongest cluster.
int currentBucketKey = int(math.round(close / bucketSize))
float nearbyStrength = strengthByBucket.get(currentBucketKey, 0.0)
float strongestClusterPrice = na(strongestBucketKey) ?
na : strongestBucketKey * bucketSize
plot(strongestClusterPrice, "Strongest zone cluster", color.orange,
linewidth = 2, style = plot.style_stepline)
plot(nearbyStrength, "Strength near current price", display = display.data_window)
plot(strongestBucketStrength, "Strongest cluster strength",
display = display.data_window)
plot(strongestZoneCount, "Zones in strongest cluster",
display = display.data_window)
plot(dirtyZones.size(), "Drawings updated", display = display.data_window)
----------------------------------------------------------------------------------------------------------------
🔷 API REFERENCE
This is a compact index of the main public entry points.
🔸 Watch and Memo: changed(source) handles one scalar, primitive array or row ring. For several dependencies, use begin() , watch*() and finish() . Typed Memos add staleOn() , store() , get() and invalidate() .
🔸 Revision and Cadence: revision() exposes bump() , current() and changedSince() for manual change tracking. cadenceGate() provides due() and change-aware dueWhenChanged() scheduling.
🔸 Row Rings: floatRowRing() and intRowRing() provide push() , width-one pushValue() , at() , setAt() , newestAt() , chronological() and gather() .
🔸 RingCursor: Circular addressing for caller-owned arrays. Use reserve() to advance, physical() and logical() to translate positions, and newest() or oldest() to locate retained rows.
🔸 StablePool: acquire() and release() manage stable key-to-slot assignments. Lookup and traversal use find() , contains() , keyAt() , slots() and size() . Recycled slots retain their caller-owned payload until overwritten.
🔸 SlotCache: Frame-based stable allocation follows begin() , acquire() , finish() . active() , retired() and size() expose its state.
🔸 DirtySet: begin() starts a cycle; mark() , markMany() and markRange() add entries. Read the distinct work list with values() and size() .
🔸 Typed Stores: intIntStore() , intFloatStore() , intBoolStore() , intStringStore() and intColorStore() map integer keys to primitive values. Build with build() , then use get() , set() , contains() or getMany() .
🔸 IntIndex: A shared integer key-to-slot directory for custom payloads and explicit lookup policy. Build with buildBegin() , add() or addMany() and buildFinish() ; query with find() , keyAt() and findMany() . IndexConfig controls representation and duplicate policy.
🔸 IntBuckets: A one-key-to-many-integers index. Build directly with buildFromPairs() , or incrementally with buildBegin() , add() or addMany() and buildFinish() . Read groups with rangeByKey() and valueAt() .
🔸 Double Buffers: intDoubleBuffer() and floatDoubleBuffer() retain current and previous arrays. swap() exchanges them; swapSized() also sizes and refills the new current buffer.
🔸 Weighted Sampling: weightedSampler() provides weight updates, sample() , sampleMany() , probability() and total() . It maps caller-supplied fractions; it does not generate randomness. weightedIndex() adds circular ranges and explicit policy control.
🔸 Storage Utilities: ensureSize*() , resize*() , refill*() and sameExact*() handle primitive arrays. Other helpers cover flat/matrix conversion, transposition and bulk ring reads.
----------------------------------------------------------------------------------------------------------------
🔷 WHY OPTIPINE EXISTS
Pine's limits are real, but standard architecture often reaches them long before the idea itself has to. Repeating unchanged calculations, shifting rolling storage, scanning mostly untouched collections and rebuilding state all consume the same execution budget the feature needs to exist.
OptiPine reclaims that budget. Expensive models can run only when their inputs change. Large dashboards can refresh only what moved. Dynamic object systems can grow and recycle storage without reorganizing everything around them. The APIs stay approachable, while the architecture underneath is built for workloads that would normally force a Pine project to scale back.
At large scale, optimization is no longer simply about feature speed. It is the factor that dictates whether an ambitious idea can ship at all.
----------------------------------------------------------------------------------------------------------------
This work is licensed under (CC BY-NC-SA 4.0) , meaning usage is free for non-commercial purposes given that Alien_Algorithms is credited in the description for the underlying software. For commercial use licensing, contact Alien_Algorithms
The publication diagram has been rendered natively by Pine3D .
Library

Session Edge Profiler | Flux ChartsGENERAL OVERVIEW:
The Session Edge Profiler is a statistical dashboard indicator that profiles up to five configurable trading sessions (Asia, London, NY AM, NY Lunch, NY PM by default) across the available completed trading days loaded on the chart. The indicator records each session's range, volume, directional outcome, and smart money structure (Fair Value Gaps, swing breaks, higher highs, lower lows) on every completed day, then surfaces the resulting statistics in a configurable on-chart dashboard with progress bars and best value markers.
For every metric, the indicator filters history by the selected weekdays. Range-based metrics are normalized against the previous daily ATR for cross-volatility comparison, while volume, directional, extreme, and structure metrics are calculated directly from completed session records. The indicator also computes percentile rankings of the current session range against its historical distribution. Session boxes can be plotted for visual reference, and a live label tracks the active session's running range against its historical average and percentile rank in real time. The indicator is statistical, session based, dashboard driven, and includes one alert condition for sessions exceeding the 90th percentile of their historical range distribution.
WHAT IS THE THEORY BEHIND THE INDICATOR?:
Markets do not move uniformly across the day. Each trading session carries different participant types, different volume profiles, and different structural behaviors. The Asia session tends to be range bound and accumulative. The London session frequently sweeps overnight liquidity. NY AM often produces the largest expansions of the day. NY Lunch is typically the lowest volume window. NY PM frequently reverses or extends NY AM moves into the close.
These tendencies are widely cited but rarely measured per instrument. The Session Edge Profiler quantifies them. By recording per session statistics across the historical window available on the chart, and by filtering by selected weekdays, the indicator builds an empirical profile of how each session has actually behaved on a specific symbol rather than relying on generalized assumptions. The result is a session level statistical profile that can be compared against the current session in real time, identifying when a given session is behaving unusually large, unusually quiet, or consistent with its historical edge.
SESSION EDGE PROFILER FEATURES:
◇ Session tracking with customizable times, names, and colors
◇ Statistical dashboard with up to thirteen configurable metrics
◇ ATR normalized range comparison across sessions
◇ Today percentile ranking of the live session range
◇ Daily extremes tracking (HOD %, LOD %)
◇ Directional statistics (Bull %, Continuation %)
◇ Volume profiling (Vol Share %, Avg Vol)
◇ Smart money structure analytics (FVGs, Swing Breaks, FVG Survival, HH, LL)
◇ Active session live label with real time percentile and average comparison
◇ Session range boxes with current and historical display
◇ Weekday filtering applied uniformly across all statistics
◇ Dashboard theming (Dark or Light), nine position options, and five text sizes
◇ High percentile range alert
SESSION TRACKING AND RANGE BOXES:
🔹What is Session Tracking?
Session Tracking is the foundation of the indicator. Five configurable session windows are monitored on every bar. When price enters a session window, the indicator opens an active tracking object that records the session's high, low, open price, total volume, and structural events. When price leaves the session window, the active object is closed and its values are committed to the historical record for that session.
🔹Why is Session Tracking important?
Every statistic computed by the indicator depends on accurately segmenting the trading day into sessions. Without a reliable session lifecycle, range comparisons, HOD/LOD attribution, volume share, and structure counts would be inconsistent. The session lifecycle also defines what gets drawn on the chart: the live range box for the current session and, optionally, persistent boxes for historical sessions.
🔹How is Session Tracking detected and calculated?
Every bar is checked against the configured session time windows in New York time. The moment price enters a session window, a new session opens: the session's high, low, open, and volume start fresh, and the FVG, swing break, HH, and LL counters reset to zero. While the session is active, the high updates to the running maximum, the low updates to the running minimum, and volume accumulates with each new bar. When price leaves the session window, the session is closed: the final high, low, open, close, and volume are committed and the session is marked complete for the day.
A trading day boundary is determined by shifting time forward by 6 hours and comparing the resulting calendar date in New York time. This shift causes a new day to register at 18:00 NY time, aligning the trading day with the start of the Asia session at 19:00 NY. When a new trading day begins, the completed session statistics from the previous day are added to each session's history along with the weekday they were recorded on, the daily fields reset, and a new tracking cycle begins.
🔹Settings: Sessions Group
◇ Enable Toggle: Turns the session on or off. Disabled sessions are excluded from the dashboard, the live label, and all calculations.
◇ Session Name: Custom label used in the dashboard column header, on the session box, and in the active session label. Defaults: Asia, London, NY AM, NY Lunch, NY PM.
◇ Session Time: The session window in NY time using HHMM,HHMM format. Defaults: Asia 1900,0200, London 0200,0830, NY AM 0830,1200, NY Lunch 1200,1330, NY PM 1330,1600.
◇ Session Color: Color applied to the dashboard column header (when active), the session box border and background, and the active session label.
🔹Customization
Display Group
◇ Show Session Ranges: When enabled, plots a translucent box around the current session showing its running high and low, with the session name labeled in the top left corner. Historical session boxes are also retained on the chart for visual reference.
◇ Show Active Session Stats: When enabled, plots a live label next to the most recent bar of the active session displaying the session name, current range, current range as a percentage of historical average, and current percentile rank.
◇ Label Size: Sets the text size of the active session label. Options: Tiny, Small, Normal, Large, Huge.
STATISTICAL DASHBOARD:
🔹What is the Statistical Dashboard?
The Statistical Dashboard is a configurable table that summarizes the historical statistical profile of every enabled session. Rows correspond to metrics. Columns correspond to sessions. Each cell shows the metric value for that session, optionally rendered with a unicode progress bar and a star marker (★) for the session with the highest value on metrics where "highest" is the meaningful target.
🔹Why is the Statistical Dashboard important?
The dashboard is where the indicator's measurements surface. Rather than requiring a trader to scroll through chart history and visually estimate session behavior, the dashboard reduces the entire weekday filtered history of every session to a compact table of directly comparable numbers. The header line shows the active weekday filter and the maximum number of historical days used in any cell, providing immediate context for the statistical sample size.
🔹How is the Statistical Dashboard calculated?
On the most recent bar of the chart, the indicator reviews each enabled session's stored history. For every past session, it checks whether the weekday it was recorded on is included in the selected weekday filter. If yes, the session contributes to the running totals: range sums, volume sums, HOD/LOD counts, bull counts, continuation counts, FVG counts, swing break counts, HH counts, LL counts, and volume share. After the review, totals are converted to averages or percentages and written to the dashboard cells.
Best value markers are computed by tracking the maximum value across all enabled sessions for the metrics where "highest" is the intended target: Avg Range, HOD %, LOD %, Avg FVGs, and FVG Survival %. For metrics where directional bias matters (Bull %, Continuation %) or where higher is not strictly better (Vol Share %, Avg Swing Breaks, Avg HH, Avg LL), no best marker is shown.
[Screenshot: Full dashboard table screenshot in Dark Mode with every metric row enabled. Header line showing the active weekday filter and sample size, column headers in each session's color, progress bars rendered in percentage cells, and the SMART MONEY divider row visible separating the structural metrics from the range and directional metrics above.
🔹Settings: Dashboard Group
◇ Show Dashboard: Master toggle for the entire dashboard. When disabled, no table is rendered.
◇ Theme: Dark Mode or Light Mode. Controls background, row, header, and text colors. The best value highlight cell uses a deeper accent color on the selected theme.
◇ Position: Table placement on the chart. Options cover all nine combinations of vertical (Top, Middle, Bottom) and horizontal (Left, Center, Right) anchoring.
◇ Text Size: Tiny, Small, Normal, Large, Huge. Affects every cell.
◇ Show Progress Bars: When enabled, percentage and percentile cells render an 8 segment unicode bar alongside the numeric value, scaling from 0% to 100%. When disabled, only the numeric value is shown.
🔹Customization
Metric Toggles
Each of the following dashboard rows can be independently shown or hidden:
◇ Avg Range (ATR%)
◇ Vol Share %
◇ Avg Vol
◇ HOD %
◇ LOD %
◇ Bull %
◇ Continuation %
◇ Today Percentile
◇ Avg FVGs
◇ Avg Swing Breaks
◇ FVG Survival %
◇ Avg HH
◇ Avg LL
🔹Signal Colors
◇ High: Color applied to high tier values (Today Percentile at or above 75, FVG Survival at or above 70). Default: green.
◇ Mid: Color applied to mid tier values (Today Percentile between 25 and 75, FVG Survival between 40 and 70). Default: orange.
◇ Low: Color applied to low tier values (Today Percentile at or below 25, FVG Survival below 40). Default: red.
ATR NORMALIZED RANGE STATISTICS:
🔹What is ATR Normalized Range?
The Avg Range (ATR%) metric expresses each session's average range as a percentage of the daily Average True Range. A value of 45% means the session, on average, covered 45% of a full day's ATR.
🔹Why is ATR Normalized Range important?
Raw range values cannot be compared across instruments or across volatility regimes. A 200 point range means very different things in calm versus volatile markets. Normalizing by daily ATR removes that distortion: the resulting percentage is directly comparable between sessions, between symbols, and between months of history.
🔹How is ATR Normalized Range calculated?
For each completed session, the raw range (session high minus session low) is divided by the daily ATR value of the previous completed day. The daily ATR uses a configurable length (default 14) and is always read from the previous daily bar, which means the value is fixed for the entire current trading day and never repaints. The session's normalized range is stored alongside its weekday in the history. When the dashboard renders, the indicator averages all normalized ranges from sessions whose weekday passes the filter, then multiplies by 100 to produce the displayed percentage.
🔹What is Today Percentile?
Today Percentile expresses where the current session's live range sits within the historical distribution of that same session's past ranges. The comparison stays within the session: today's London is compared only against past Londons, today's NY AM only against past NY AMs, and so on, all filtered by the selected weekdays. A value of 80 means the live range is larger than 80% of past occurrences of the same session on those weekdays.
🔹How is Today Percentile calculated?
For each enabled session, the indicator computes the current normalized range (current session range divided by daily ATR). It then walks through that session's own past history, counting how many past sessions have a normalized range less than or equal to the current value, while skipping any past session whose weekday is not enabled in the filter. The percentile is the percentage of qualifying past sessions at or below the current value.
The cell color reflects the tier: at or above 75 uses the High color, at or below 25 uses the Low color, otherwise the Mid color. The numeric value is rendered with an ordinal suffix (1st, 2nd, 3rd, 4th, and so on) for readability, and the progress bar segments scale from 0 to 100.
🔹Settings:Filters Group
◇ ATR Length: Lookback for the daily ATR used in normalization. Range: 5 to 50. Default: 14.
DAILY EXTREMES TRACKING:
🔹What are HOD % and LOD %?
HOD % measures how often a given session contained the day's highest price. LOD % measures how often it contained the day's lowest price. Both are expressed as a percentage of the total weekday filtered days in history.
🔹Why are HOD/LOD statistics important?
Knowing which session historically sets the daily extreme on a given instrument helps frame intraday liquidity expectations. A session with a high HOD % is the session that most frequently posts the day's selling extreme. A session with a high LOD % most frequently posts the day's buying extreme. On many instruments NY AM dominates both, but the ratio shifts by symbol and by weekday, which is why measuring rather than assuming is useful.
🔹How are HOD % and LOD % calculated?
While the trading day is in progress, the indicator continuously tracks the day's running high and running low across all bars, not just within session windows. When a new trading day begins, every completed session from the previous day is checked: if the session's recorded high matches the day's high, that session is tagged as the HOD session; if its low matches the day's low, it is tagged as the LOD session. These tags are stored with the session in history. When the dashboard renders, it counts how many sessions in the weekday filtered history carry each tag and converts those counts to percentages. The session with the highest HOD % across all enabled sessions receives a star marker, and the same applies to LOD %.
DIRECTIONAL STATISTICS:
🔹What are Bull % and Continuation %?
Bull % is the percentage of historical sessions that closed higher than they opened. Continuation % is the percentage of historical sessions whose direction matched the previous occurrence of the same session.
🔹Why are directional statistics important?
Bull % captures the session's directional skew. A session with Bull % consistently above 60% on a particular instrument and weekday set has a measurable upward tendency. Continuation % captures the session's persistence: a high continuation rate means the session frequently extends the previous day's same session direction, while a low rate suggests the session tends to reverse the prior day's bias.
🔹How are Bull % and Continuation % calculated?
For each completed session, Bull is true when the session's close (the chart close at the bar where the session ended) exceeds its open. Continuation is true when the previous occurrence of the same session was bullish in the same direction (both bullish or both bearish). The very first occurrence in history has no previous reference and is excluded from the continuation calculation. The dashboard divides the bullish session count by the total session count for Bull %, and the matched continuation count by the continuation eligible count for Continuation %.
No best value marker is shown for either metric, since "highest" is not inherently better: directional bias and continuation are interpretive measurements rather than competitive ones across sessions.
VOLUME PROFILING:
🔹What is Volume Profiling?
The indicator tracks two volume metrics per session: Vol Share % (the session's average share of total daily volume) and Avg Vol (the session's average absolute volume).
🔹Why is Volume Profiling important?
Volume distribution across the day reveals participant activity. Sessions that historically account for a disproportionate share of daily volume are the sessions where flow is most concentrated. Sessions with low volume share (typically NY Lunch) are statistical low conviction windows where moves are more likely to be lower quality.
🔹How is Volume Profiling calculated?
While each session is active, the indicator accumulates bar volume into the session's running total. When the trading day rolls over, total day volume is computed as the sum of all completed session volumes for that day. Each session's Vol Share is then computed as its session volume divided by total day volume, multiplied by 100, and saved into the session's history alongside the absolute volume. When the dashboard renders, Avg Vol is the simple weekday filtered mean of recorded session volumes, and Vol Share % is averaged across the weekday filtered history.
SMART MONEY STRUCTURE ANALYTICS:
🔹What are the Smart Money metrics?
The Smart Money section of the dashboard surfaces four structural counters per session:
◇ Avg FVGs: average number of Fair Value Gaps formed during the session.
◇ Avg Swing Breaks: average instances where the close pierces a previously confirmed pivot high or pivot low.
◇ FVG Survival %: percentage of FVGs that were not invalidated within the same session in which they formed.
◇ Avg HH and Avg LL: average count of new higher highs and lower lows in pivot structure during the session.
🔹Why are Smart Money metrics important?
These metrics quantify the structural activity of each session. High FVG counts indicate aggressive displacement and gap creation. High Swing Break counts indicate liquidity sweeps and structural inflection. FVG Survival measures how often gaps formed during the session are respected (not immediately filled in the opposite direction), giving a session level reliability score for the FVG concept. HH and LL counts profile each session's tendency to extend structure in one direction versus the other.
🔹How are Smart Money metrics calculated?
A Fair Value Gap is detected as a 3 bar pattern: a bullish FVG forms when the current bar's low sits above the high from two bars ago, and a bearish FVG forms when the current bar's high sits below the low from two bars ago. Whenever an FVG forms during an active session, the session's FVG counter increments and the gap level (the high from two bars ago for a bullish FVG, the low from two bars ago for a bearish FVG) is added to a list of active gaps for that session, along with its direction.
On every later bar within the same session, the indicator checks each active gap. If price closes below a bullish FVG's level, or closes above a bearish FVG's level, the gap is treated as invalidated and removed from the active list, and the session's invalidation counter increments. At the end of the session, FVG Survival % is computed as the count of total FVGs minus invalidated FVGs, divided by total FVGs, expressed as a percentage. The cell is color coded by tier: at or above 70 uses High, at or above 40 uses Mid, otherwise Low.
Swing breaks use a configurable pivot strength (default 5 bars on each side). When a pivot high confirms and price subsequently closes above that pivot level, a bullish swing break fires and the pivot is consumed (cleared from active tracking). The same applies symmetrically for pivot lows. Each break increments the active session's Swing Break counter.
HH and LL counts use the same pivot detection. When a new pivot high confirms with a level greater than the session's previous tracked pivot high, the session's HH counter increments. When a new pivot low confirms with a level less than the session's previous tracked pivot low, the LL counter increments.
🔹Settings: Filters Group
◇ Pivot Strength: Bars on each side required to confirm a pivot high or pivot low for the swing break and HH/LL calculations. Range: 2 to 20. Default: 5. Higher values produce fewer, more significant pivots; lower values produce more frequent, noisier pivots.
ACTIVE SESSION LIVE LABEL:
🔹What is the Active Session Live Label?
A floating label that appears next to the most recent bar of the active session, displaying live statistics for the session currently in progress.
🔹Why is the Active Session Live Label important?
The dashboard summarizes completed historical sessions. The live label answers a different question: how does the session that is currently developing compare to history, right now? It allows a trader to see, mid session, whether the current session is tracking above, near, or below its average range and what percentile it currently occupies, without waiting for the session to close.
🔹How is the Active Session Live Label calculated?
The label content includes the session name, the live range (current session high minus current session low), the ratio of the live normalized range to the historical average normalized range expressed as a percentage, and the current percentile. The percentile is computed by iterating the session's weekday filtered history and counting how many records have a normalized range at or below the live value.
The label position updates every bar to track the right edge of the current session at its current high. When the session ends, the label is deleted.
🔹Settings
◇ Show Active Session Stats: Toggle for the label.
◇ Label Size: Sets text size. Options: Tiny, Small, Normal, Large, Huge.
WEEKDAY FILTERING:
🔹What is Weekday Filtering?
A set of seven toggles (Sunday through Saturday) that determines which weekdays contribute to every statistic on the dashboard, the live label, and the alert condition.
🔹Why is Weekday Filtering important?
Session behavior is not uniform across the week. Monday open behavior differs from midweek behavior. Friday afternoon often shows reduced participation. By filtering history to only the selected weekdays, traders can profile each session under conditions that match the current trading day, rather than averaging in unrelated days.
🔹How is Weekday Filtering applied?
Each session in history is tagged with the weekday it was recorded on. Every calculation in the dashboard, the live label, and the alert checks that weekday against the user's selection and skips any session whose weekday is not enabled. The dashboard header line displays a compact label of the active filter: "All" when every weekday is enabled, "Weekdays" when only Monday through Friday are enabled, or a custom combination such as "M/Tu/W" otherwise. The header also shows the largest number of sessions any column was able to use after filtering, which serves as the sample size indicator.
🔹Settings: Filters Group
◇ Sun, Mon, Tue, Wed, Thu, Fri, Sat: Individual toggles. Defaults: Mon, Tue, Wed, Thu, Fri enabled; Sun and Sat disabled.
ALERTS:
🔹What alerts are available?
A single alert condition is provided:
◇ Range > 90th Percentile: Fires when an active session's current normalized range exceeds the 90th percentile of its weekday filtered historical normalized range distribution.
🔹When does it fire?
On every bar where at least one enabled, active session has a current normalized range above which 90% of its history sits. The alert fires once per qualifying bar, allowing traders to be notified when a session is in the process of becoming statistically large relative to its own history.
IMPORTANT NOTES:
◇ All session times are evaluated in New York time regardless of the chart's display timezone. Adjust session times if profiling instruments where session timing conventions differ from the defaults.
◇ Trading day boundaries are anchored to 18:00 NY time (the 6 hour shift before midnight) so that the Asia session opens at the start of each new trading day. This is the convention used for HOD/LOD attribution and for pushing completed session records to history.
◇ Daily ATR is always read from the previous completed daily bar. This means the value used for normalization is fixed for the current trading day and does not repaint as new bars print, while still giving the live percentile calculations a stable reference.
◇ The session history for each session is built progressively as the chart loads. Sessions on the very first day on the chart cannot contribute to continuation statistics because no earlier occurrence of the same session exists to compare against.
◇ Best value markers (★) are shown only on metrics where "highest" is the meaningful target: Avg Range, HOD %, LOD %, Avg FVGs, and FVG Survival %. Other metrics intentionally omit the marker.
◇ FVG Survival counts gaps that survive to the end of the session in which they formed. A gap that survives the session but is invalidated on a later day is still counted as survived for the session that created it.
UNIQUENESS:
The Session Edge Profiler distinguishes itself from common session indicators in several ways. Most session tools plot boxes and stop there, while this indicator extends session tracking into a full statistical profile with thirteen configurable metrics per session, reducing the entire history of every session to a single, scannable table. Range comparisons use ATR normalization rather than raw point values, making the dashboard meaningful across volatility regimes and instruments without per chart recalibration, and percentile ranking of the live session against history provides a single number answer to a question many traders ask intuitively: is this session unusually large or unusually small for this time and this weekday? FVG and swing break tracking are integrated into the session profile rather than treated as separate indicators, allowing direct comparison of which session produces the most structural activity and how reliable that structure tends to be on a given instrument. FVG Survival % quantifies a concept that is rarely measured anywhere else: how often each session's FVGs actually hold within their own session, converting a qualitative idea into a session level reliability score. Weekday filtering applies uniformly to every statistic on the dashboard, the live label, and the alert, allowing traders to profile sessions only on days that match the current trading day rather than diluting the sample with unrelated weekdays. Best value markers and progress bars make the dashboard scannable at a glance, with the strongest session per metric immediately visible without parsing numbers. Finally, the active session live label provides real time positional context that complements the historical dashboard: the dashboard answers what a session usually does, while the label answers what the session is doing right now, with both views driven by the same underlying statistical model. Indicator

Liquidity Sweep Profiler | Flux ChartsGENERAL OVERVIEW:
The Liquidity Sweep Profiler is a multi-source liquidity tracking and outcome statistics indicator. It automatically identifies key liquidity levels across three categories (intraday sessions, higher timeframe key levels, and chart structure), monitors each level for sweep events (wick pierces with rejection), and then tracks what happens after each sweep over a configurable watch window. Every resolved sweep is recorded in an internal history that powers a dashboard showing, by liquidity type, the average reversal magnitude, average breach magnitude, and an Edge ratio between the two. The dashboard highlights the liquidity type with the strongest historical edge on the current chart and instrument.
The indicator plots session high/low lines (Asia, London, NY AM, NY Lunch, NY PM), previous-period highs and lows (PDH/PDL, PWH/PWL, PMH/PML), and chart structure liquidity (Swing Highs/Lows, EQH/EQL clusters). When a level is swept, it draws a Sweep Zone box marking the rejection range and an x marker at the wick extreme. An Active Sweep Tracker label shows the live performance of the most recent unresolved sweep against its historical baseline. The indicator is multi-timeframe, session-based, statistical, and rules-based. Optional quality filters let the user restrict the statistics to sweeps that meet specific volume, wick, or delta thresholds.
Screenshot: a hero shot showing session, key level, and structure liquidity lines on one chart with several Sweep Zones marked, plus the dashboard visible in a corner.
WHAT IS THE THEORY BEHIND THE INDICATOR?
In intraday and swing trading, certain price levels function as "liquidity pools", areas where a critical mass of resting orders (stop losses, breakout buy/sell orders, and pending entries) tends to accumulate. The high of yesterday, the low of last week, the high of the London session, and a recent swing high are all examples of such levels. When price reaches these levels, the resting orders get triggered, which can produce one of two outcomes: a sustained breakout where the order flow continues past the level, or a sweep where price briefly pierces the level, triggers the orders, and then reverses back through it. The sweep outcome is what this indicator is designed to detect and study.
Different liquidity types behave differently. On some instruments, swept session highs and lows tend to reverse cleanly. On others, sweeps of weekly or monthly extremes are more reliable. On yet others, sweeps of equal highs and lows (clustered pivots) outperform sweeps of standalone swing points. The behavioral pattern can also vary by day of the week and by whether the sweep candle showed strong rejection characteristics (high relative volume, large rejection wick, strong intrabar volume imbalance toward the rejection direction). The Liquidity Sweep Profiler treats every sweep as a data point, records the recovery and breach magnitudes that followed it, and aggregates the data by liquidity type to surface which type has shown the strongest reversal tendency on the specific chart and instrument the trader is using.
This is a statistical profile, not a prediction. The dashboard reports what has happened historically on the current chart. The trader uses that profile to focus attention on the liquidity types with the strongest empirical edge, while remaining aware that future behavior can deviate from past behavior. Every sweep is treated as evidence to be aggregated, and the indicator surfaces the resulting profile for the trader to interpret.
FEATURES:
◇ Multi-source liquidity detection (sessions, higher timeframe key levels, structure)
◇ Sweep detection with configurable confirmation window
◇ Sweep Zone boxes and x markers
◇ Outcome tracking (recovery and breach magnitudes over a watch window)
◇ Statistics dashboard with per-type sweep counts, averages, and Edge ratios
◇ Best-Edge banner highlighting the top-performing liquidity type
◇ Active Sweep Tracker for live monitoring of the most recent sweep
◇ Quality filters (relative volume, wick %, intrabar delta %)
◇ Trading-day filter (per-weekday inclusion)
◇ Display unit selector (ATR, Price, Pips, Ticks)
◇ Configurable label, line, zone, and theme styling
◇ Built-in alerts for new sweeps and high-edge sweeps
Screenshot: a clean overview showing one example from each liquidity category (a session line, a PDH, EQL/EQL line) with their distinct color coding visible.
LIQUIDITY LEVEL DETECTION
🔹 What are liquidity levels?
A liquidity level is a price where resting orders tend to accumulate. The Liquidity Sweep Profiler tracks three categories:
◇ Session liquidity: the high and low formed during each defined intraday session (Asia, London, NY AM, NY Lunch, NY PM). Each session is a configurable time window.
◇ Key levels: the high and low of the previous completed day (PDH/PDL), week (PWH/PWL), and month (PMH/PML).
◇ Structure liquidity: pivot-based swing highs and lows detected on the current chart, plus EQH/EQL clusters where two or more recent pivots formed at approximately the same price.
🔹 Why do these levels matter?
Each category captures a different participant base. Session highs and lows matter to intraday traders working specific market hours. Daily, weekly, and monthly extremes matter to swing traders and institutional desks that operate on those reference points. Swing pivots and equal highs/lows matter to participants who place orders relative to recent chart structure. By tracking all three in one indicator, the trader can observe which category produces the most reliable sweep behavior on the specific instrument.
🔹 How are levels detected?
Session levels are tracked in real time during each session window. The session detector evaluates whether the current bar's New York time falls inside the session's start-end string. While the session is active, the indicator maintains a running high and low, updating both the level and the bar index of each extreme on every new high or low. When the session window closes (the next bar is outside the session), both the final high and the final low are stored as liquidity levels, with the bar index of the actual extreme preserved as the level's anchor bar.
Previous-period levels are fetched from the daily, weekly, and monthly timeframes. The indicator requests the prior period's high and low (offset by one period, so the value is stable and never references the still-developing current period). Each time the fetched value changes (which happens once per new day, week, or month), the new level is added to tracking.
Structure liquidity uses standard pivot detection with a configurable lookback length (default 5 bars on each side). When a new pivot high or pivot low forms, it is checked against the most recent prior pivots in the same direction: if it falls within an ATR-based threshold (default 0.1 x ATR) of one of the last three same-side pivots, it is classified as an EQH or EQL. Otherwise it is recorded as a standalone Swing High or Swing Low.
For each category, a Track Last input controls how many of the most recent levels of each type are kept on the chart simultaneously. When a new level of a given type is added, the oldest level of that same type is trimmed from the tracking array if the count exceeds the limit.
Screenshot: showing session-derived levels (dashed lines), HTF key levels (solid lines), and structure levels (dotted lines)
🔹 Settings
◇ Session enable toggles, names, time windows (in New York timezone), and per-session colors for all five sessions.
◇ Track Last (Sessions): how many days of session highs and lows to keep tracked. Default 1.
◇ Enable PDH/PDL, PWH/PWL, PMH/PML with individual color pickers.
◇ Track Last (Previous Periods): number of previous periods kept per type (days for PDH/PDL, weeks for PWH/PWL, months for PMH/PML). Default 1.
◇ Pivot Length: number of bars on each side used for pivot detection. Default 5.
◇ EQH/EQL Threshold (ATR): two pivots within this multiple of ATR distance count as equal. Default 0.1.
◇ Track Last (Structure): number of structure levels kept per type. Default 5.
🔹 Customization
◇ Per-category visibility toggles under Visual Overlays (Session Liq, PDH/PDL, PWH/PWL, PMH/PML, Swings, EQH/EQL).
◇ Day Suffix toggle: appends (Today), (Yest), or (-Nd) to session labels when tracking more than one day of session liquidity.
◇ Boxes toggle: optionally renders the live session range as a translucent box while the session is active.
SWEEP DETECTION
🔹 What is a sweep?
A sweep occurs when price reaches a tracked liquidity level, briefly trades beyond it with its wick, and then closes back through it within a defined confirmation window. The wick pierces the level (triggering the resting orders), but the candle body closes back on the original side, indicating that the move past the level was rejected. This is the canonical stop-run-and-reverse pattern.
🔹 Why does the confirmation window matter?
A pure same-bar sweep requires the same candle to both pierce the level with its wick and close back through it. This is the strictest definition and captures the cleanest rejections. Allowing one or more additional bars for the close to come back through captures sweeps that take a slightly longer time to resolve, at the cost of including weaker rejections. The trader picks the trade-off they prefer using the Sweep Confirmation Window input.
🔹 How are sweeps detected?
Each tracked level carries two state flags: pierced and taken. On every new bar, the indicator walks the list of untaken levels and evaluates two conditions per level:
◇ Wick-through: for a high-side level, the bar's high exceeds the level. For a low-side level, the bar's low falls below the level.
◇ Closed-back: for a high-side level, the bar's close is below the level. For a low-side level, the close is above it.
The flow is:
◇ If the level is not yet pierced and the wick-through condition is true on this bar, the level is marked pierced, the piercing bar index is stored, and the wick extreme is recorded. If the closed-back condition is also true on the same bar, the level is immediately marked taken (a same-bar sweep).
◇ If the level was already pierced on a previous bar, the indicator first updates the wick extreme if the current bar exceeded the previous extreme. It then checks how many bars have elapsed since the pierce. If the elapsed count exceeds the confirmation window, the level is marked taken with the broken flag set (clean breakout, no rejection). Otherwise, if closed-back is true on the current bar, the level is marked taken with broken cleared (confirmed sweep).
When a sweep confirms (taken, broken = false), the indicator captures a snapshot of the sweep candle's context:
◇ Relative volume: current bar's volume divided by the 20-bar simple moving average of volume.
◇ Wick percentage: the rejection wick's share of the candle's total range. For a high sweep, this is (high − max(open, close)) / (high − low) x 100. For a low sweep, (min(open, close) − low) / (high − low) x 100.
◇ Intrabar volume delta: the share of lower-timeframe volume on the rejecting side. The indicator requests lower-timeframe up-volume (close > open) and down-volume (close < open) for the bar, sums both, and computes the rejecting side's share. For a high sweep, that's down-volume / total. For a low sweep, up-volume / total.
These three values are stored on the sweep record and become the basis for the optional quality filters.
🔹 Bullish Example (low sweep)
A Swing Low at 1.0850 sits on the chart. Price drops to 1.0840 on a single candle (wick extreme), then closes at 1.0855, back above the original level. The level is marked as swept (low sweep), a green Sweep Zone box is drawn from the wick extreme up to the level, and an x marker is plotted at 1.0840.
🔹 Bearish Example (high sweep)
A PDH sits at 1.0950. Price rallies to 1.0965 on the wick, then closes at 1.0945, back below the original level. The level is marked as swept (high sweep), a red Sweep Zone box is drawn from the level up to the wick extreme, and an x marker is plotted at 1.0965.
Screenshot: bullish low sweep and one bearish high sweep visible on the same chart, both with their Sweep Zone boxes and x markers rendered.
🔹 Settings
◇ Sweep Confirmation Window: number of additional bars allowed after the wick pierce for the close to come back through. 0 = same-bar rejection only. 1 = same-bar or next bar. Default 0.
🔹 Customization
◇ Show Sweep Zones: toggle Sweep Zone box rendering.
◇ High Sweep Zone Color / Low Sweep Zone Color: customize the fill color for high-sweep and low-sweep zones.
◇ Show Sweep x Mark: toggle the x marker plotted at the wick extreme of each confirmed sweep.
OUTCOME TRACKING
🔹 What is outcome tracking?
Detecting that a sweep occurred is only half the picture. To know whether a particular liquidity type tends to produce reversals worth trading, the indicator also needs to measure what happened after the sweep. Outcome tracking does this by monitoring each confirmed sweep for a fixed number of bars and recording two values:
◇ Recovery: the maximum favorable excursion away from the swept level (in the rejecting direction). For a high sweep, this is how far price fell below the sweep candle's close. For a low sweep, how far the price rose above it.
◇ Breach: the maximum adverse excursion past the swept level (in the original sweep direction). For a high sweep, how far price went above the sweep candle's close. For a low sweep, how far the price went below it.
🔹 Why measure both?
Recovery alone could mislead. A liquidity type might produce large reversals on average but also large breaches when the sweep fails, which is information the trader needs. Tracking both Recovery and Breach, and then computing their ratio as an Edge value (Recovery / Breach), captures the full risk-reward profile of sweeps on that level type. An Edge above 1 indicates the type tends to deliver more reversal magnitude than breach magnitude on average.
🔹 How is outcome tracking calculated?
When a sweep confirms, the indicator stores the sweep candle's close price, the ATR value at that moment (using the configured ATR length, default 14), and the other metadata snapshot. From the next bar onward, for the configured Outcome Watch Window (default 20 bars), it computes two per-bar values:
◇ Recovery on the current bar = (sweep_close − low) / sweep_ATR for high sweeps, or (high − sweep_close) / sweep_ATR for low sweeps.
◇ Breach on the current bar = (high − sweep_close) / sweep_ATR for high sweeps, or (sweep_close − low) / sweep_ATR for low sweeps.
The running maximum of each is updated bar by bar. When the watch window expires (bars since sweep ≥ watch window), the sweep is marked completed and its final maxRecovery, maxBreach, and metadata snapshot are pushed into the indicator's history array along with the resolution day's weekday. This history is what powers the dashboard.
If the level was classified as broken instead of swept (the confirmation window expired without a close-back-through), the record is not added to the history, since the indicator only counts confirmed sweep outcomes.
Recovery and Breach are stored in the history as ATR multiples to keep them comparable across different volatility regimes. The dashboard's Display Unit input converts them to ATR multiples, Price, Pips, or Ticks for display at render time. Pip conversion uses mintick x 10 (or x 100 for JPY pairs), and Tick conversion uses raw mintick.
Screenshot: A swept level showing two arrows. One marks how far price moved back (Recovery). The other marks how far price moved past the level (Breach).
🔹 Settings
◇ Outcome Watch Window: number of bars to track each sweep for measuring Recovery and Breach. Default 20.
◇ ATR Length: ATR period used for the volatility snapshot at sweep time. Default 14.
STATISTICS DASHBOARD
🔹 What is the dashboard?
The dashboard is the analytic output of the indicator. It aggregates every completed sweep in the history array and displays per-type statistics in a table grouped by category. The columns are:
◇ Type: the liquidity type (Asia High, PDH, Swing Low, etc.).
◇ Total Sweeps: number of completed sweep records for that type that passed all active filters.
◇ Avg Recovery: average maximum favorable excursion, in the selected Display Unit.
◇ Avg Breach: average maximum adverse excursion, in the selected Display Unit.
◇ Edge: Avg Recovery / Avg Breach. A value above 1 means recovery has typically exceeded breach on that type.
The row with the highest Edge (subject to a minimum sample count of 5) is highlighted, and a Best Edge banner above the table calls out the winning type explicitly. Types with fewer than 5 samples can appear in the table but are excluded from the Best Edge competition.
🔹 Why aggregate by type?
The whole point of the indicator is to surface which liquidity types behave reliably on the current chart. A flat list of every sweep is not actionable. Grouping by type and computing aggregate statistics turns the raw sweep records into a usable trading profile.
🔹 How are statistics calculated?
For each liquidity type, the indicator walks the history array and filters by the active toggles (trading-day filter, relative volume filter, wick % filter, delta % filter). For records that pass all filters, it converts each stored ATR-multiple to the current Display Unit and sums the Recovery and Breach values. Avg Recovery and Avg Breach are computed by dividing the running sums by the filtered count. Edge is the ratio of the resulting averages.
To pick the Best Edge across all categories, the indicator runs the same aggregation for every active liquidity type (sessions, key levels, structure), filters out types with fewer than 5 samples, and selects the one with the highest Edge. The selection is independent of category, so a Swing High can win over an Asia Low if its Edge is higher and its sample count qualifies.
In the table itself, Avg Recovery is colored green when it exceeds Avg Breach for that row. Avg Breach is colored red when it exceeds Avg Recovery. The Edge cell is colored green at 1.5 or above, neutral between 1.0 and 1.5, and red below 1.0. The Best Edge row gets a green background and a star marker.
Screenshot: a close-up of the dashboard table showing all three category sections (Sessions, Key Levels, Structure) populated with realistic data, with the Best Edge banner visible and one row highlighted as the winner.
🔹 Settings
◇ Show Dashboard: master toggle.
◇ Theme: Dark Mode or Light Mode.
Screenshot: Showing Light Mode Theme
◇ Position: nine-position selector for table placement (Top Left, Top Center, Top Right, Middle Left, Middle Center, Middle Right, Bottom Left, Bottom Center, Bottom Right).
◇ Text Size: Tiny, Small, Normal, Large, Huge.
◇ Display Unit: ATR (volatility-normalized multiples), Price (raw price excursion), Pips (mintick x 10 for forex, x 100 for JPY pairs), Ticks (mintick units).
Screenshot: the dashboard configured to show only the liquidity types the user enabled. Sessions section shows only London High and London Low. Key Levels shows only PDH and PDL. Structure shows only EQH and EQL. Disabled types are filtered out of the table entirely.
QUALITY FILTERS
🔹 What are quality filters?
Quality filters restrict the sweeps that get counted in the dashboard statistics. Each one is independently togglable, and any combination can be active at once.
◇ Volume Spike Multiplier: the sweep candle's volume must be at least this multiple of its 20-bar volume average. Default 1.5x.
◇ Sweep Wick %: the rejection wick must be at least this percent of the candle's total range. Default 50%.
◇ Sweep Delta %: the rejecting side's intrabar volume share must be at least this percent of total intrabar volume. The lower timeframe used to compute delta is configurable (default 1 minute).
🔹 Why filter quality?
Not every sweep is equal. A sweep that occurs on heavy volume, with a long rejection wick, and with the rejecting side dominating intrabar volume is a fundamentally stronger rejection than one without those characteristics. By filtering the dashboard to only count high-quality sweeps, the trader can see whether quality-filtered sweeps produce a meaningfully different Edge than the unfiltered set. This is useful both for refining a setup definition and for evaluating which characteristics matter on the current instrument.
🔹 How do filters interact with the dashboard?
The total sweeps count shown in the dashboard title reflects the filtered count. The Best Edge banner and per-row statistics are also computed against the filtered set. Toggling any filter on or off triggers an immediate recomputation of the dashboard.
Screenshot: a before-and-after dashboard pair showing how the statistics change when quality filters are applied
🔹 Settings
◇ Volume Spike Multiplier: enable toggle and threshold (default 1.5x).
◇ Sweep Wick %: enable toggle and minimum percent (default 50).
◇ Sweep Delta %: enable toggle, minimum percent (default 60), and intrabar timeframe (default 1 minute).
TRADING DAY FILTER
🔹 What is the Trading Day Filter?
A row of seven weekday checkboxes that controls which days of the week are included in the dashboard statistics. Each sweep's resolution day is stored with the record. The filter excludes records whose weekday is unchecked.
🔹 Why filter by weekday?
Sweep behavior frequently varies by day of the week. Monday opens often produce different patterns than Wednesday midweek sessions or Friday closes. Letting the trader exclude specific weekdays makes it possible to test whether the Edge values on each liquidity type are weekday-dependent.
🔹 Settings
◇ Sun, Mon, Tue, Wed, Thu, Fri, Sat: each is an independent on/off toggle. Defaults: Mon through Fri on, Sat and Sun off.
ACTIVE SWEEP TRACKER
🔹 What is the Active Sweep Tracker?
A floating label rendered near the current bar that shows the live performance of the most recent unresolved sweep. It updates each bar while the watch window is still open. The label displays:
◇ The liquidity type that was swept.
◇ Bars elapsed since the sweep, against the watch window total.
◇ Current Recovery and Breach magnitudes (running max plus current-bar excursion).
◇ The historical average Recovery, Breach, and Edge for that type, if there are at least 5 samples for that type.
🔹 Why does it matter?
The dashboard shows aggregate historical statistics, but during a live setup the trader wants to know how the current move is tracking against the baseline. The Active Sweep Tracker makes this comparison explicit on the chart: at any moment, the trader can see whether the active sweep is matching, exceeding, or underperforming what that type has typically delivered.
🔹 How is the tracker calculated?
On the last bar of the chart, the indicator scans the levels array for any level that is taken, has broken = false, and is not yet completed. Among those, it picks the one with the highest takenBar (the most recent unresolved sweep). For that sweep, it computes the current-bar Recovery and Breach using the same formulas as outcome tracking, takes the maximum of the running max and the current-bar value (so the displayed value reflects either the historical peak or the live excursion, whichever is larger), and pulls the historical stats for that sweep type using the same filter pipeline as the dashboard.
The label's background color reflects which side is winning in real time. Bull color when the higher of the two excursions is on the Recovery side, bear color otherwise.
Screenshot: a chart with an active unresolved sweep, the Sweep Zone visible, and the Active Sweep Tracker label rendered near the current bar showing the live Bar x / Y count, current excursion values, and historical baseline comparison.
🔹 Settings
◇ Show Active Sweep Tracker: master toggle.
◇ Text Size: Tiny, Small, Normal, Large, Huge.
DISPLAY AND STYLING
🔹 Label and line styling
Liquidity levels render as horizontal lines extended to the right. Each category uses a distinct line style: solid for key levels, dashed for session levels, dotted for structure levels. Each type has its own color, customizable from the Sessions, Previous Periods, and Structure input groups. Labels render at the right edge with the type name and an optional day suffix for session levels when tracking more than one day.
🔹 Hide-on-Swept behavior
By default, swept levels remain drawn on the chart. With Hide on Swept enabled, swept levels are removed from the chart after a configurable grace period (Keep Swept Levels For). The grace period is measured in bars from when the level resolved (either taken or broken). This is useful for keeping the chart focused on the active liquidity once the historical sweep map becomes dense.
🔹 Settings
◇ Extend Right: number of bars to extend liquidity lines past the current bar. Default 3.
◇ Label Size: Tiny, Small, Normal, Large, Huge.
◇ Hide on Swept: toggle removal of swept levels.
◇ Keep Swept Levels For: grace period in bars before removal (applies when Hide on Swept is enabled). Default 5.
ALERTS
🔹 New Sweep
Fires when any tracked liquidity level is freshly swept on the current bar (taken status set this bar, broken = false). The alert message includes ticker and timeframe.
🔹 High-Edge Sweep
Fires when a new sweep occurs on a liquidity type whose historical Edge meets or exceeds the High-Edge Alert Threshold, provided that type has at least 5 historical samples. The Edge value is computed using the same filter pipeline as the dashboard, so any active quality filters and weekday filters are respected when evaluating whether the alert qualifies.
🔹 Settings
◇ High-Edge Alert Threshold: Edge value at or above which the High-Edge alert qualifies. Default 1.5.
IMPORTANT NOTES:
◇ The Sweep Delta % filter relies on lower-timeframe volume data, which may be unavailable for some instruments (forex pairs with no native volume, certain crypto exchanges, etc.). On those instruments, the delta filter can be left disabled.
◇ Statistics displayed in the dashboard reflect the sweeps visible in the historical data the chart has access to. Loading more historical bars (by scrolling left on lower timeframes or increasing the chart's bar limit) will increase the sample size and may shift the Edge rankings.
◇ The Best Edge banner requires a minimum of 5 sweeps per type to qualify. Types with fewer sweeps are shown in the dashboard but do not compete for the banner.
◇ Past sweep behavior on a given liquidity type does not guarantee future sweep behavior. The dashboard provides a statistical profile of historical sweeps; trade decisions remain the user's responsibility.
◇ Session times are interpreted in the America/New_York timezone regardless of the chart's session timezone. The default windows correspond to common Asia / London / NY conventions but can be edited freely.
UNIQUENESS:
The Liquidity Sweep Profiler is built around a feedback loop that most liquidity-tracking indicators do not provide. It detects liquidity levels, monitors them for sweeps, measures what happened after each sweep, and aggregates the results into a per-type statistical profile on the current chart. The trader sees not just where liquidity sits, but which categories of liquidity have actually produced clean reversals on the specific instrument and timeframe in question. Most competing tools stop at plotting the levels and flagging sweeps, leaving the trader to estimate behavior by eye. The Sweep Profiler turns this into structured data with sample counts, average magnitudes, and an Edge ratio that captures the recovery-to-breach trade-off.
The indicator also combines several feature categories that are usually distributed across multiple tools: intraday session tracking, higher timeframe key levels, chart-structure liquidity (swings and equal highs/lows), volume and delta quality filters, weekday filtering, and a live Active Sweep Tracker that compares the current unresolved sweep to its historical baseline in real time. All of this is unified into one dashboard with a Best Edge banner that surfaces the strongest-performing liquidity type at a glance. Display values can be expressed in ATR, raw price, pips, or ticks, so the same indicator reads naturally on indices, forex, futures, and crypto without manual conversion. Sweep detection itself is configurable from strict same-bar rejection to multi-bar close-back-through, letting the trader tune the detection logic to match the rejection style they actually trade. Indicator

Liquidity Delta Profiler [LuxAlgo]The Liquidity Delta Profiler indicator identifies major buy-side and sell-side liquidity levels and visualizes internal buyer/seller activity through volume delta-filled quadrants, providing a complete toolkit for analyzing liquidity sweeps and potential reversals.
🔶 USAGE
The indicator detects significant swing highs and lows to plot liquidity zones, representing areas where stop-loss orders or breakout orders are likely clustered.
🔹 Volume Delta Quadrants
Unlike standard liquidity indicators, this tool splits each zone into four horizontal quadrants. As price trades within these quadrants, the script calculates the volume delta (the difference between buying and selling pressure) for each specific section.
Buy Delta Fill : Indicates aggressive buyers were more active in that specific price slice.
Sell Delta Fill : Indicates aggressive sellers dominated that section.
Intensity : The color's opacity represents the relative magnitude of the volume delta compared to other sections of the zone.
🔹 Reversal Detection
The script includes an advanced detection system that identifies unusual volume patterns during liquidity sweeps. These signals are plotted as bubbles with hoverable tooltips:
ABS (Absorption) : Occurs when aggressive market orders at the extreme edge of a zone are absorbed by large limit orders in the opposite direction.
EXH (Exhaustion) : Occurs when a sweep happens on very low relative volume, suggesting no follow-through.
DIV (Divergence) : Identified when high volume pushes into the edge of a zone (FOMO) but price fails to close outside the level.
REJ (Snapback Rejection) : Triggered when a sweep candle shows high delta in the opposite direction of the sweep and closes back inside the zone.
🔹 Time-Based Performance Dashboard
To evaluate signal reliability, the indicator includes a real-time dashboard that tracks the historical performance of each reversal signal type using a time-based validation logic.
A "Win" is recorded if, within the Eval Window , the price reverses from the sweep and remains in profit (on the correct side of the signal entry) for a specific number of consecutive bars ( Hold Time ). This method filters for signals that generate sustained pressure rather than just temporary wicks.
🔹 Zone Decay (Health)
Active zones feature a "Health" percentage label. This tracks the cumulative volume traded within the zone relative to its capacity. As more volume is transacted at these levels, the liquidity is considered "consumed," and the percentage drops toward 0%.
🔶 DETAILS
The indicator utilizes a pivot-based detection system. When a swing high is confirmed, a Buy-Side Liquidity (BSL) zone is created; a swing low creates a Sell-Side Liquidity (SSL) zone.
The script includes "Filter Overlaps" logic to ensure chart clarity. If a new, more significant pivot forms within the range of an existing active zone, the tool can automatically update to the most relevant level, preventing the clutter of multiple overlapping boxes.
🔶 SETTINGS
🔹 Main
Pivot Length : Lookback/lookforward period for detecting swing highs and lows.
Max Zones per Type : Maximum number of active and historical zones to keep on the chart.
Show Swept Zones : Keeps zones visible with dashed outlines after they have been breached.
Filter Overlapping Zones : Prevents the creation of new zones that overlap with existing active zones.
🔹 Decay & Reversals
Show Zone Decay : Toggles the health percentage labels.
Zone Volume Capacity : Multiplier for average volume to determine how much volume a zone can absorb.
Enable Reversal Detection : Toggles the signal bubbles for reversal patterns.
🔹 Dashboard
Show Dashboard : Toggles the performance tracking table.
Eval Window (Bars) : The maximum number of bars the script waits for a reversal to manifest.
Hold Time (Bars) : The number of consecutive bars price must stay in profit to be considered a successful reversal.
Position/Size : Customizes the UI placement and scale of the dashboard.
🔹 Style
Colors : Customize colors for BSL/SSL outlines and the positive/negative volume delta fills. Indicator

Structural Leg Profiler [LuxAlgo]The Structural Leg Profiler indicator is a comprehensive structural analysis tool that dynamically maps lower-timeframe volume distributions onto major market swings to reveal where the most significant trading activity occurs within a trend.
🔶 USAGE
The tool is designed to bridge the gap between traditional price action swings and order flow analysis. By using an ATR-based swing detection mechanism, the indicator automatically identifies structural "legs" (upward or downward price movements) and generates a detailed volume distribution profile for each one.
Users can leverage this tool to identify high-interest zones (Point of Control) and determine whether a move is supported by aggressive buying or selling volume through the Delta coloring mode.
🔹 Interpreting the Profiles
Each structural leg displays a volume profile across its duration, constructed from lower-timeframe data to ensure precision.
Volume Gradient Mode: Highlights nodes based on total activity. The colors transition from low-volume areas to high-volume nodes, with the Point of Control (POC) being the most prominent. Delta Mode: Colors the profile blocks based on the net difference between buying and selling volume. Bright green indicates heavy aggressive buying, while bright red indicates heavy aggressive selling. Summary Labels: Each leg includes a summary label showing the Total Leg Volume, the Net Delta, and the exact POC price.
🔹 Volume Anomalies
The script automatically detects and highlights candles with unusual volume spikes using dynamic "bubbles." This helps in identifying potential exhaustion points or strong breakout momentum.
Standard Bubbles: Indicate volume that is significantly above the 20-period average. Large Bubbles with Values: Indicate extreme volume spikes, with the exact volume printed inside the bubble for immediate context.
🔶 DETAILS
🔹 Untested POC Extensions
A core feature of this script is the "Naked POC" logic. When a structural leg is completed, its Point of Control is projected forward as a dashed line. These levels often act as high-probability support or resistance zones. The line continues to extend until price eventually "tests" or crosses it, at which point it automatically terminates to keep the chart clean.
🔹 Lower Timeframe Precision
Unlike standard profiles that use only chart-resolution data, this script utilizes
request.security_lower_tf
to pull granular data. This provides a much more accurate view of how volume was distributed within each leg compared to simple OHLC-based approximations.
🔶 SETTINGS
🔹 Swing Detection
ATR Period: The lookback period used to calculate volatility for swing detection. Swing Multiplier (ATR): Controls the sensitivity of the legs. Higher values capture major trends; lower values capture micro-swings.
🔹 Profile Settings
Max Profile Boxes: Defines the vertical resolution and maximum number of rows in the profiles. Profile Alignment: Determines where the volume bars are anchored (Left, Right, or Center) within the leg area. Show Volume Value: Toggles the visibility of numeric volume values inside the profile boxes. Extend Untested POCs: Enables the forward projection of POC lines until they are mitigated by price.
🔹 Volume Anomalies
Show Volume Bubbles: Toggles the volume anomaly visualization. Spike Threshold: The multiplier relative to the 20-period average volume that triggers an anomaly bubble.
🔹 Style & Colors
Box Color Mode: Choose between "Volume Gradient" (Total Volume focus) or "Delta" (Buy vs. Sell focus). Up/Down Leg Colors: Customizable color gradients for both bullish and bearish structural legs. Indicator

Indicator

Oscillator Profile IndicatorDescription:
The Oscillator Profile Indicator (OPI) is designed to provide insights into market trends and potential reversal points by profiling the value distribution of an oscillator or the price chart over a specified lookback period.
The OPI works by calculating the Point of Control (PoC) for the oscillator values or prices in the given lookback period. This PoC, essentially a median, is considered the fair value where most trading activities have happened. Along with this, OPI also calculates lower and upper boundaries by taking the specified percentile of the sorted distribution of values. These boundaries outline the value area within which a significant portion of trading activity has occurred.
The main feature of the OPI is the interpretation of PoC movement and how it relates to general market trends. If the PoC moves above 0 on the oscillator, it's a potential indication that we are in a general uptrend. Conversely, if the PoC moves below 0, this can be a signal for a general downtrend.
Usage:
While OPI can be used on both price charts and oscillators, its effectiveness is more pronounced when used on oscillators. Applying this indicator to oscillators such as the Relative Strength Index (RSI) or the Moving Average Convergence Divergence (MACD) can provide useful insights.
How to Read:
PoC line: The line represents the median of the past 'n' periods. Its movement above or below 0 can be used to identify general uptrends or downtrends respectively.
Upper and Lower Boundary lines: These lines represent the specified percentile of the value distribution in the lookback period.
Colored Fills: The fills between the upper and lower boundary lines visually represent the value area. The color changes based on the relative position of the source value (price or oscillator value) to the PoC.
Signals:
An uptrend is indicated when the PoC moves above 0 on the oscillator, especially when coupled with an upward crossover of the source value through the PoC.
A downtrend is signaled when the PoC drops below 0 on the oscillator, particularly when paired with a downward crossover of the source value through the PoC.
(!) Note: Like all indicators, OPI should be used in conjunction with other technical analysis tools for the best results. It is also advisable to backtest this indicator with your strategy before using it in live trading. Indicator

benchLibrary "bench"
A simple banchmark library to analyse script performance and bottlenecks.
Very useful if you are developing an overly complex application in Pine Script, or trying to optimise a library / function / algorithm...
Supports artificial looping benchmarks (of fast functions)
Supports integrated linear benchmarks (of expensive scripts)
One important thing to note is that the Pine Script compiler will completely ignore any calculations that do not eventually produce chart output. Therefore, if you are performing an artificial benchmark you will need to use the bench.reference(value) function to ensure the calculations are executed.
Please check the examples towards the bottom of the script.
Quick Reference
(Be warned this uses non-standard space characters to get the line indentation to work in the description!)
```
// Looping benchmark style
benchmark = bench.new(samples = 500, loops = 5000)
data = array.new_int()
if bench.start(benchmark)
while bench.loop(benchmark)
array.unshift(data, timenow)
bench.mark(benchmark)
while bench.loop(benchmark)
array.unshift(data, timenow)
bench.mark(benchmark)
while bench.loop(benchmark)
array.unshift(data, timenow)
bench.stop(benchmark)
bench.reference(array.get(data, 0))
bench.report(benchmark, '1x array.unshift()')
// Linear benchmark style
benchmark = bench.new()
data = array.new_int()
bench.start(benchmark)
for i = 0 to 1000
array.unshift(data, timenow)
bench.mark(benchmark)
for i = 0 to 1000
array.unshift(data, timenow)
bench.stop(benchmark)
bench.reference(array.get(data, 0))
bench.report(benchmark,'1000x array.unshift()')
```
Detailed Interface
new(samples, loops) Initialises a new benchmark array
Parameters:
samples : int, the number of bars in which to collect samples
loops : int, the number of loops to execute within each sample
Returns: int , the benchmark array
active(benchmark) Determing if the benchmarks state is active
Parameters:
benchmark : int , the benchmark array
Returns: bool, true only if the state is active
start(benchmark) Start recording a benchmark from this point
Parameters:
benchmark : int , the benchmark array
Returns: bool, true only if the benchmark is unfinished
loop(benchmark) Returns true until call count exceeds bench.new(loop) variable
Parameters:
benchmark : int , the benchmark array
Returns: bool, true while looping
reference(number, string) Add a compiler reference to the chart so the calculations don't get optimised away
Parameters:
number : float, a numeric value to reference
string : string, a string value to reference
mark(benchmark, number, string) Marks the end of one recorded interval and the start of the next
Parameters:
benchmark : int , the benchmark array
number : float, a numeric value to reference
string : string, a string value to reference
stop(benchmark, number, string) Stop the benchmark, ending the final interval
Parameters:
benchmark : int , the benchmark array
number : float, a numeric value to reference
string : string, a string value to reference
report(Prints, benchmark, title, text_size, position)
Parameters:
Prints : the benchmarks results to the screen
benchmark : int , the benchmark array
title : string, add a custom title to the report
text_size : string, the text size of the log console (global size vars)
position : string, the position of the log console (global position vars)
unittest_bench(case) Cache module unit tests, for inclusion in parent script test suite. Usage: bench.unittest_bench(__ASSERTS)
Parameters:
case : string , the current test case and array of previous unit tests (__ASSERTS)
unittest(verbose) Run the bench module unit tests as a stand alone. Usage: bench.unittest()
Parameters:
verbose : bool, optionally disable the full report to only display failures Library

Indicator
