smc_context_engine_private.pine
Copy
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © preuss_steffen
//@version=6
// @description SMC live context library (bus-v3 track). Derives per-bar domain
// frames from the engine primitives instead of the dead per-symbol snapshot:
// StructureFrame (BOS/CHoCH/trend/S-R via eng.detect_structure), ImbalanceFrame
// (FVG lifecycle + BPR + liquidity void via eng.fvgs_objects) and ZoneFrame
// (order-block lifecycle via eng.track_obs). Detection is the engine's SSOT in
// all three and is NOT golden-gated against Python. The imbalance rule layer
// (mitigation %, BPR overlap, void %, zone-bias count delta, state precedence)
// mirrors the golden parity contract in tests/fixtures/smc_context_golden.json;
// structure and zone are spec-only (Column B) — see docs/smc-context-semantics.md.
// Sweep/Pool scoring mirrors the Python golden while their live candidate
// detection is Pine-only and explicitly specified in docs/smc-context-semantics.md.
// Session classification uses independent IANA zones (Tokyo, London, New York),
// and build_context_frame() composes all six domains without running a second
// structure detector for the dependent ZoneFrame.
library("smc_context_engine_private", overlay = true)
// Pine requires every library referenced by a *used* type's fields to be imported
// (CE10293), and those libraries must come before the engine that pulls them into
// its types — so the engine is imported last, matching SMC_Long_Dip_Suite.
// ct — eng.FVG and eng.OrderBlock declare ct.LevelBreakMode fields.
// d — both declare d.SmcBox / d.SmcLine / d.SmcLabel fields.
// pe — eng.OrderBlock declares a pe.Profile field. NOTHING here calls pe.*:
// build_zone_frame leaves every profile feature off, so the engine never
// creates a Profile. Kept a narrow type edge by the allowlist in
// tests/test_pine_library_import_permissions.py and enforced by
// tests/test_pine_context_library_contract.py.
// u — smc_lib_atr for the OB size filter. A real call; public library.
import preuss_steffen/smc_core_types/5 as ct
import preuss_steffen/smc_utils/4 as u
import preuss_steffen/smc_draw/3 as d
import preuss_steffen/smc_profile_engine/4 as pe
import preuss_steffen/smc_engine_private/5 as eng
// ─────────────────────────────────────────────────────────────────────────────
// Rule-layer thresholds.
//
// These are REAL constants, not comments, and they carry the exact names used in
// tests/fixtures/smc_context_golden.json._meta.thresholds. That is deliberate:
// tests/test_pine_context_library_contract.py parses the declarations below and
// asserts each value equals the frozen golden. Previously these numbers were
// inlined at their use-sites with the names only in comments — so the Python
// reference could move, the golden would regenerate, every Python test would stay
// green, and this library would be silently wrong. A comment cannot be checked;
// a named const can.
// ─────────────────────────────────────────────────────────────────────────────
// --- golden-checked (names must match _meta.thresholds) ---
const float PARTIAL_MIT_PCT = 0.5 // gap filled >= 50% -> partial mitigation
const float FULL_MIT_PCT = 1.0 // gap fully closed -> no longer active.
// Handed to the engine as the FVG fill
// target, which is what makes
// eng.FVG.filled mean "fully mitigated".
const float LIQ_VOID_MIN_SIZE_PCT = 2.0 // gap >= 2% of mid -> liquidity void
const float SWEEP_DEPTH_MIN_PCT = 0.1
const float SWEEP_DEPTH_STOP_HUNT_PCT = 0.3
const int SWEEP_RECLAIM_MAX_BARS = 5
const float SWEEP_VOLUME_RATIO_MIN = 1.2
const float IMBALANCE_SIG_THRESHOLD = 0.3
const float PROXIMITY_NEAR_PCT = 1.0
const int CLUSTER_STRONG_COUNT = 3
// --- Pine-only (Column B spec, no Python golden) ---
const int ZONE_BIAS_COUNT_DELTA = 2 // |bull-bear count| >= 2 -> directional bias
// (ImbalanceFrame.zone_bias over FVGs,
// ZoneFrame.ob_bias over order blocks)
// --- Zone-frame OB size filter — mirrors SMC_Long_Dip_Suite.pine lines 904-906 ---
const int ATR_LEN_MAIN = 50 // u.smc_lib_atr length. NOT 200 — that is
// an unrelated engine-internal impulse ATR.
const float OB_FILTER_MIN_MULT = 0.5 // min block size = atr * 0.5
const float OB_FILTER_MAX_MULT = 2.5 // max block size = atr * 2.5
const int SWEEP_TYPE_STOP_HUNT = 1
const int SWEEP_TYPE_GRAB = 2
const int SWEEP_TYPE_INDUCEMENT = 3
const int SESSION_ASIA = 1
const int SESSION_LONDON = 2
const int SESSION_NY_AM = 3
const int SESSION_NY_PM = 4
const int CONTEXT_BIAS_MIN_VOTES = 2
const int OPENING_RANGE_MINUTES = 30
// ── Frame types ──────────────────────────────────────────────────────────────
// Per-bar structure state. Prices are `na` (never 0) when unavailable; anchor
// bar indices are `na` when there is no pivot. `trend` persists between events.
//
// Confirmed-bar only: the frame is rebuilt when barstate.isconfirmed and held
// between ticks, so every field describes the last CLOSED bar. The engine's
// BOS/CHoCH alerts fire intrabar and can vanish before the close, which is why
// the Suite gates the same alerts through resolve_confirmed_signal.
export type StructureFrame
int trend // +1 bullish, -1 bearish, 0 unknown (no confirmed event yet)
bool bos // a BOS fired on this confirmed bar (either side)
bool choch // a CHoCH fired on this confirmed bar (either side)
int dir_event // +1 bullish event, -1 bearish event, 0 none (this bar)
float resistance // active trailing swing high level
float support // active trailing swing low level
int resistance_x // bar_index anchor of the last swing-high pivot
int support_x // bar_index anchor of the last swing-low pivot
int bars_since_event // age: confirmed bars since the last BOS/CHoCH (na until first)
bool fresh // event age within the freshness window
// Per-bar imbalance state (FVG lifecycle + BPR + liquidity void). Prices `na`
// when inactive; counts default 0. `state`: 0 NONE, 1 FVG_BULL, 2 FVG_BEAR,
// 3 BPR, 4 LIQ_VOID (precedence BPR > VOID > FVG_BULL > FVG_BEAR > NONE).
export type ImbalanceFrame
bool bull_fvg_active
bool bear_fvg_active
float bull_fvg_top
float bull_fvg_bottom
float bear_fvg_top
float bear_fvg_bottom
float bull_fvg_mit_pct // engine fill_current_ratio of the newest active bull FVG
float bear_fvg_mit_pct
bool bull_fvg_partial // mit_pct >= 0.5
bool bear_fvg_partial
int bull_fvg_count // active bull FVGs
int bear_fvg_count
bool bpr_active
int bpr_dir // +1 bull, -1 bear, 0 none
float bpr_top
float bpr_bottom
bool liq_void_active
float liq_void_top
float liq_void_bottom
int zone_bias // +1 bull, -1 bear, 0 neutral (FVG count-delta rule)
int state // composite code, see above
// Per-bar order-block state (eng.track_obs lifecycle). Direction-neutral: the v2
// engine bus carries only the product's best *bull* OB, this frame reports both
// sides. Prices/ids are `na` when that side has no active OB; counts default 0.
// `state`: 0 NONE, 1 OB_BULL, 2 OB_BEAR, 3 OB_BOTH (precedence BOTH > BULL >
// BEAR > NONE). `*_new` / `*_broken` are edges — true only on the event bar.
// `ob_bias` is the OB count delta and is NOT ImbalanceFrame.zone_bias (FVGs).
export type ZoneFrame
bool bull_ob_active
bool bear_ob_active
int bull_ob_id // newest active bull OB (engine id)
int bear_ob_id
float bull_ob_top
float bull_ob_bottom
float bear_ob_top
float bear_ob_bottom
int bull_ob_count // active bull OBs
int bear_ob_count
bool bull_ob_new // an OB was confirmed on this confirmed bar
bool bear_ob_new
bool bull_ob_broken // an OB broke on this confirmed bar
bool bear_ob_broken
int ob_bias // +1 bull, -1 bear, 0 neutral (OB count-delta rule)
int state // composite code, see above
// Recent confirmed liquidity sweep. Direction is +1 bullish (sell-side
// liquidity taken), -1 bearish (buy-side taken), 0 none/ambiguous. Sweep type:
// 0 NONE, 1 STOP_HUNT, 2 LIQUIDITY_GRAB, 3 INDUCEMENT.
export type SweepFrame
bool recent_bull_sweep
bool recent_bear_sweep
int sweep_type
int direction
float zone_top
float zone_bottom
bool reclaim_active
int liquidity_taken_direction // +1 buy-side, -1 sell-side, 0 none/both
float depth_pct
float volume_ratio
int quality_score
int bars_since_event
bool fresh
// Confirmed pivot-cluster liquidity pools. Buy-side levels sit above price,
// sell-side levels below it. Magnet direction: +1 UP, -1 DOWN, 0 NONE.
export type PoolFrame
float buy_side_level
float sell_side_level
int buy_side_strength
int sell_side_strength
float proximity_pct
int cluster_density
int untested_buy_pools
int untested_sell_pools
float imbalance
int magnet_direction
int quality_score
// Session state evaluated in each market's own IANA timezone. Session code:
// 0 NONE, 1 ASIA, 2 LONDON, 3 NY_AM, 4 NY_PM. NY wins the London overlap,
// matching the Python session-context precedence.
export type SessionFrame
int session_code
bool in_killzone
bool mss_bull
bool mss_bear
int structure_state
bool fvg_bull_active
bool fvg_bear_active
bool bpr_active
float range_top
float range_bottom
float mean
float vwap
float target_bull
float target_bear
bool opening_range_active
float opening_range_top
float opening_range_bottom
int direction_bias
int context_score
// Session-level MSS: the confirmed close crossing the PREVIOUS session's
// extreme (one-bar pulse). Distinct from mss_bull/mss_bear above, which
// are structure events (BOS/CHoCH) while a session is active — those are
// already derivable from the structure channels. Parity target is
// SMC_Session_Context.pine's "Session MSS Bull/Bear Confirmed".
bool prev_session_break_bull
bool prev_session_break_bear
// One confirmed-bar aggregate. Nested frames retain domain provenance; the
// aggregate adds only an explicit six-domain vote and availability summary.
export type ContextFrame
StructureFrame structure
ImbalanceFrame imbalance
ZoneFrame zone
SweepFrame sweep
PoolFrame pool
SessionFrame session
int bias
int directional_score
int available_domains
int quality_score
// ── Private helpers (defined before the exported builders) ───────────────────
// Newest active FVG in a buffer, by id. Returns [found, top, bottom, mit_pct].
// The engine already keeps only unfilled FVGs in the active buffer; `not filled`
// is a defensive guard. `mit_pct` is the engine's fill_current_ratio.
_newest_active_fvg(array<eng.FVG> arr) =>
int best_id = na
float top = na
float bottom = na
float mit = na
if not na(arr)
for f in arr
if not f.filled
if na(best_id) or f.id > best_id
best_id := f.id
top := f.left_top.price
bottom := f.right_bottom.price
mit := f.fill_current_ratio
[not na(best_id), top, bottom, mit]
// First active FVG whose gap is at least `min_pct`% of `mid`. Returns
// [found, top, bottom]. Mirrors the golden "any FVG >= 2% of mid" void rule.
_first_void(array<eng.FVG> arr, float mid, float min_pct) =>
bool found = false
float top = na
float bottom = na
if not na(arr) and mid > 0
for f in arr
if not f.filled
float gap_pct = (f.left_top.price - f.right_bottom.price) / mid * 100.0
if gap_pct >= min_pct
found := true
top := f.left_top.price
bottom := f.right_bottom.price
break
[found, top, bottom]
// Newest active order block in a buffer, by id. Returns [found, id, top, bottom].
// The engine removes broken blocks from the tracking buffer inside update_broken,
// so every element here is active by construction — no `broken` guard needed.
// Geometry is the raw left_top/right_bottom: build_zone_frame keeps every profile
// feature off, so no value-area/POC edge alignment is ever applied.
_newest_active_ob(array<eng.OrderBlock> arr) =>
int best_id = na
float top = na
float bottom = na
if not na(arr)
for b in arr
if not na(b)
if na(best_id) or b.id > best_id
best_id := b.id
top := b.left_top.price
bottom := b.right_bottom.price
[not na(best_id), best_id, top, bottom]
// Project the engine's OB lifecycle onto a ZoneFrame. Takes the structure tuple
// rather than calling eng.detect_structure itself: track_obs must be fed the SAME
// structure instance a caller already has, and this seam lets the aggregated
// build_context_frame() detect structure ONCE instead of spinning up a second
// detector per frame.
_build_zone(simple int swing_len, float hh, float swing_low, bool bull_bos, bool bull_choch, bool bear_bos, bool bear_choch, simple int atr_len, float min_mult, float max_mult) =>
float atr = u.smc_lib_atr(atr_len)
float min_block = atr * min_mult // OB_FILTER_MIN_MULT
float max_block = atr * max_mult // OB_FILTER_MAX_MULT
// Profile features stay at their `false` defaults, so the engine never builds
// a pe.Profile — that keeps the pe import a pure type-resolution edge.
[active_bull, active_bear, new_bull, new_bear, _broken_bull, _broken_bear, broken_new_bull, broken_new_bear, _disc_bull, _disc_bear] = eng.track_obs(swing_len, hh, swing_low, bull_bos, bull_choch, bear_bos, bear_choch, min_block, max_block)
[has_bull, bull_id, bull_top, bull_bottom] = _newest_active_ob(active_bull)
[has_bear, bear_id, bear_top, bear_bottom] = _newest_active_ob(active_bear)
int bull_count = na(active_bull) ? 0 : array.size(active_bull)
int bear_count = na(active_bear) ? 0 : array.size(active_bear)
// Edges, not latches: new_* is a per-bar local in the engine (na unless an OB
// confirmed this bar); broken_new_* is cleared at the top of every update_broken.
bool bull_new = not na(new_bull)
bool bear_new = not na(new_bear)
bool bull_brk = not na(broken_new_bull) and array.size(broken_new_bull) > 0
bool bear_brk = not na(broken_new_bear) and array.size(broken_new_bear) > 0
int count_delta = bull_count - bear_count
int ob_bias = count_delta >= ZONE_BIAS_COUNT_DELTA ? 1 : count_delta <= -ZONE_BIAS_COUNT_DELTA ? -1 : 0
// Composite precedence: OB_BOTH > OB_BULL > OB_BEAR > NONE.
int state = has_bull and has_bear ? 3 : has_bull ? 1 : has_bear ? 2 : 0
ZoneFrame current = ZoneFrame.new(has_bull, has_bear, has_bull ? bull_id : na, has_bear ? bear_id : na, has_bull ? bull_top : na, has_bull ? bull_bottom : na, has_bear ? bear_top : na, has_bear ? bear_bottom : na, bull_count, bear_count, bull_new, bear_new, bull_brk, bear_brk, ob_bias, state)
var ZoneFrame published = ZoneFrame.new(false, false, na, na, na, na, na, na, 0, 0, false, false, false, false, 0, 0)
if barstate.isconfirmed
published := current
published
// ── Exported builders ────────────────────────────────────────────────────────
// Build the structure frame from the engine's per-bar structure detector.
// `swing_len` is the engine pivot length; `fresh_max_bars` bounds the freshness
// window (bars since the last BOS/CHoCH that still count as "fresh").
export build_structure_frame(simple int swing_len = 50, simple int fresh_max_bars = 20) =>
// detect_structure carries `var` and `ta.*` state, so it must run on EVERY
// tick. Never move this call inside the barstate guard below: a conditional
// call desyncs the engine's internal swing/pivot history.
[trend, bull_bos, bull_choch, top_x, _top_y, _trail_up_x, trail_up, hh, bear_bos, bear_choch, btm_x, _btm_y, _trail_dn_x, trail_dn, swing_low, _hhll_x] = eng.detect_structure(swing_len)
// Pivot availability, taken from the engine's own swing output instead of
// inferred from sentinels. detect_pivot starts at `var x = 0` and
// `var trail_y = high`, so top_x/btm_x read 0 and trail_up/trail_dn read the
// running high/low extreme before any swing exists — neither is a pivot, and
// the frame contract says those are `na`. `x == 0` cannot be the tell: the
// earliest real pivot lands on bar_index 0 (detect_swings anchors a swing at
// `bar_index - swing_len`), so a bar-index test would discard a true pivot.
var bool have_up_pivot = false
var bool have_dn_pivot = false
var int bars_since = na
// The published frame. Built on confirmed bars only; between ticks the
// caller keeps the last confirmed frame. detect_structure raises its
// BOS/CHoCH alerts intrabar — the Suite wraps the very same alerts in
// resolve_confirmed_signal for this reason — so an ungated frame repaints:
// a cross that reverts before the close would surface as bos = true,
// dir_event = ±1, bars_since_event = 0, and then vanish at the close.
var StructureFrame published = StructureFrame.new(0, false, false, 0, na, na, na, na, na, false)
if barstate.isconfirmed
if not na(hh)
have_up_pivot := true
if not na(swing_low)
have_dn_pivot := true
bool bos_now = bull_bos or bear_bos
bool choch_now = bull_choch or bear_choch
bool event_now = bos_now or choch_now
int dir_event = (bull_bos or bull_choch) ? 1 : (bear_bos or bear_choch) ? -1 : 0
if event_now
bars_since := 0
else if not na(bars_since)
bars_since := bars_since + 1
bool fresh = not na(bars_since) and bars_since <= fresh_max_bars
// `var trend = 1` in the engine is a bootstrap literal, not a reading:
// it only ever moves on a CHoCH, so before the first confirmed event
// every symbol reports "bullish". Publish 0 (unknown) until an event has
// either confirmed the direction (BOS) or set it (CHoCH).
bool structure_available = not na(bars_since)
published := StructureFrame.new(structure_available ? trend : 0, bos_now, choch_now, dir_event, have_up_pivot ? trail_up : na, have_dn_pivot ? trail_dn : na, have_up_pivot ? top_x : na, have_dn_pivot ? btm_x : na, bars_since, fresh)
published
// Build the imbalance frame (FVG lifecycle + BPR + liquidity void) from the
// engine's object-based FVG tracker. Detection is the engine's; the rule layer
// (mitigation/BPR/void/zone-bias/state) matches the golden parity contract.
export build_imbalance_frame(float size_threshold = 0.0) =>
// The engine is asked for TERMINAL, WICK-measured fills. Both arguments are
// the parity contract, not preferences:
//
// fill_target_ratio = FULL_MIT_PCT. The engine retires an FVG once price
// reaches `top - ratio*size`, so the previous 0.5 default retired it at the
// gap MIDPOINT. Everything downstream read a truncated population:
// bull_fvg_partial was unreachable (past 50% the FVG had already left the
// active buffer), and count / BPR / liq_void / zone_bias / state were all
// computed from it. Deliberately NOT a parameter — a caller lowering it
// would silently break the golden contract while every test stayed green.
//
// fill_mode = HIGHLOW. The golden measures mitigation from the wick
// (`top - last_low` for bull, `last_high - bottom` for bear). The engine
// defaults to CLOSE, which reports ~0% for a bar that wicked deep into the
// gap and closed back above it. Referencing ct.* here is fine: only `pe` is
// a type-resolution-only import (see the allowlist in
// tests/test_pine_library_import_permissions.py).
[_bull_alert, _bear_alert, active_bull, active_bear, _new_bull, _new_bear, _bull_filled, _bear_filled, _bull_filled_new, _bear_filled_new, _bull_disc, _bear_disc] = eng.fvgs_objects(fill_mode = ct.LevelBreakMode.HIGHLOW, fill_target_ratio = FULL_MIT_PCT, size_threshold = size_threshold)
[has_bull, bull_top, bull_bottom, bull_mit] = _newest_active_fvg(active_bull)
[has_bear, bear_top, bear_bottom, bear_mit] = _newest_active_fvg(active_bear)
int bull_count = na(active_bull) ? 0 : array.size(active_bull)
int bear_count = na(active_bear) ? 0 : array.size(active_bear)
bool bull_partial = has_bull and not na(bull_mit) and bull_mit >= PARTIAL_MIT_PCT
bool bear_partial = has_bear and not na(bear_mit) and bear_mit >= PARTIAL_MIT_PCT
// BPR: overlapping active bull + bear FVG.
bool bpr_active = false
int bpr_dir = 0
float bpr_top = na
float bpr_bottom = na
if has_bull and has_bear
float ot = math.min(bull_top, bear_top)
float ob = math.max(bull_bottom, bear_bottom)
if ot > ob
bpr_active := true
bpr_top := ot
bpr_bottom := ob
bpr_dir := close > (ot + ob) / 2.0 ? 1 : -1
// Liquidity void: any active FVG >= 2% of mid-price (bull scanned first).
float mid = (high + low) / 2.0
[void_bull, void_bull_top, void_bull_bot] = _first_void(active_bull, mid, LIQ_VOID_MIN_SIZE_PCT)
[void_bear, void_bear_top, void_bear_bot] = _first_void(active_bear, mid, LIQ_VOID_MIN_SIZE_PCT)
bool void_active = void_bull or void_bear
float void_top = void_bull ? void_bull_top : void_bear ? void_bear_top : na
float void_bottom = void_bull ? void_bull_bot : void_bear ? void_bear_bot : na
// Zone bias from active FVG count delta.
int count_delta = bull_count - bear_count
int zone_bias = count_delta >= ZONE_BIAS_COUNT_DELTA ? 1 : count_delta <= -ZONE_BIAS_COUNT_DELTA ? -1 : 0
// Composite precedence: BPR > VOID > FVG_BULL > FVG_BEAR > NONE.
int state = bpr_active ? 3 : void_active ? 4 : has_bull ? 1 : has_bear ? 2 : 0
ImbalanceFrame current = ImbalanceFrame.new(has_bull, has_bear, has_bull ? bull_top : na, has_bull ? bull_bottom : na, has_bear ? bear_top : na, has_bear ? bear_bottom : na, has_bull ? bull_mit : na, has_bear ? bear_mit : na, bull_partial, bear_partial, bull_count, bear_count, bpr_active, bpr_dir, bpr_top, bpr_bottom, void_active, void_top, void_bottom, zone_bias, state)
var ImbalanceFrame published = ImbalanceFrame.new(false, false, na, na, na, na, na, na, false, false, 0, 0, false, 0, na, na, false, na, na, 0, 0)
if barstate.isconfirmed
published := current
published
// Build the zone frame (order-block lifecycle) from the engine's OB tracker.
// Standalone convenience facade: it detects structure once and feeds that same
// instance into track_obs, exactly as SMC_Long_Dip_Suite does (line 1740).
//
// NOTE for the aggregated build_context_frame(): do NOT call this next to
// build_structure_frame() — each public builder runs its own eng.detect_structure,
// so calling both spins up two independent structure state machines. The aggregator
// must detect once and call the private _build_zone seam directly.
//
// `swing_len` is the engine pivot length; `atr_len` / `min_mult` / `max_mult` are
// the ATR size filter. Their defaults MUST repeat ATR_LEN_MAIN /
// OB_FILTER_MIN_MULT / OB_FILTER_MAX_MULT as literals: Pine rejects a const as a
// parameter default — "The default value cannot be a function, variable or
// calculation" (CE10132) — so naming them here does not compile. The consts stay
// the documented source of these numbers; test_export_defaults_are_literals and
// test_zone_filter_defaults_match_their_consts keep the two in lockstep.
export build_zone_frame(simple int swing_len = 50, simple int atr_len = 50, float min_mult = 0.5, float max_mult = 2.5) =>
[_trend, bull_bos, bull_choch, _top_x, _top_y, _trail_up_x, _trail_up, hh, bear_bos, bear_choch, _btm_x, _btm_y, _trail_dn_x, _trail_dn, swing_low, _hhll_x] = eng.detect_structure(swing_len)
_build_zone(swing_len, hh, swing_low, bull_bos, bull_choch, bear_bos, bear_choch, atr_len, min_mult, max_mult)
// Add or reinforce one confirmed pivot cluster. Equal levels are compared by
// percentage so the tolerance is stable across instruments with different
// price scales. Arrays are bounded; oldest weak candidates fall off first.
_pool_add(array<float> levels, array<int> strengths, float candidate, float tolerance_pct, int max_levels) =>
int match_idx = na
float best_distance = na
int size = array.size(levels)
if size > 0
for i = 0 to size - 1
float existing = array.get(levels, i)
float denominator = math.max(math.abs(existing), syminfo.mintick)
float distance_pct = math.abs(candidate - existing) / denominator * 100.0
if distance_pct <= tolerance_pct and (na(best_distance) or distance_pct < best_distance)
match_idx := i
best_distance := distance_pct
if not na(match_idx)
int old_strength = array.get(strengths, match_idx)
int new_strength = math.min(5, old_strength + 1)
float old_level = array.get(levels, match_idx)
float merged_level = (old_level * old_strength + candidate) / (old_strength + 1)
array.set(levels, match_idx, merged_level)
array.set(strengths, match_idx, new_strength)
else
array.unshift(levels, candidate)
array.unshift(strengths, 1)
if array.size(levels) > max_levels
array.pop(levels)
array.pop(strengths)
true
// A pool stops being "untested" after price trades through it. Removal happens
// before newly confirmed pivots are added, so a pivot is not consumed by the
// same confirmation bar that makes it observable.
_pool_remove_taken(array<float> levels, array<int> strengths, bool buy_side, float bar_high, float bar_low) =>
int i = array.size(levels) - 1
while i >= 0
float level = array.get(levels, i)
bool taken = buy_side ? bar_high >= level : bar_low <= level
if taken
array.remove(levels, i)
array.remove(strengths, i)
i -= 1
true
_nearest_pool(array<float> levels, array<int> strengths, float price, bool buy_side) =>
float selected_level = na
int selected_strength = 0
float selected_distance = na
int size = array.size(levels)
if size > 0
for i = 0 to size - 1
float level = array.get(levels, i)
bool correct_side = buy_side ? level > price : level < price
float distance = math.abs(level - price)
if correct_side and (na(selected_distance) or distance < selected_distance)
selected_level := level
selected_strength := array.get(strengths, i)
selected_distance := distance
[selected_level, selected_strength]
// Stateful sweep seam shared by the live builder and the source-derived R3
// runtime fixture. Keeping injected bar/pivot inputs here lets TradingView
// execute deterministic vectors without shipping a test-only public API.
_build_sweep_from_inputs(float bar_high, float bar_low, float bar_close, float bar_volume, float average_volume, float pivot_high, float pivot_low, bool confirmed, simple int fresh_max_bars) =>
var float reference_high = na
var float reference_low = na
var bool event_bull = false
var bool event_bear = false
var int event_type = 0
var int event_direction = 0
var int event_taken_direction = 0
var float event_zone_top = na
var float event_zone_bottom = na
var float event_depth_pct = 0.0
var float event_volume_ratio = 0.0
var float event_reference_high = na
var float event_reference_low = na
var bool event_reclaimed = false
var int event_age = na
var SweepFrame published = SweepFrame.new(false, false, 0, 0, na, na, false, 0, 0.0, 0.0, 0, na, false)
if confirmed
bool bull_taken = not na(reference_low) and bar_low < reference_low and (na(bar_low[1]) or bar_low[1] >= reference_low)
bool bear_taken = not na(reference_high) and bar_high > reference_high and (na(bar_high[1]) or bar_high[1] <= reference_high)
if bull_taken or bear_taken
float bull_depth = bull_taken and reference_low != 0.0 ? (reference_low - bar_low) / math.abs(reference_low) * 100.0 : 0.0
float bear_depth = bear_taken and reference_high != 0.0 ? (bar_high - reference_high) / math.abs(reference_high) * 100.0 : 0.0
float depth = math.max(bull_depth, bear_depth)
float volume_ratio = not na(average_volume) and average_volume > 0 and not na(bar_volume) ? bar_volume / average_volume : 0.0
int sweep_type = depth >= SWEEP_DEPTH_STOP_HUNT_PCT and volume_ratio >= SWEEP_VOLUME_RATIO_MIN ? SWEEP_TYPE_STOP_HUNT :
volume_ratio >= SWEEP_VOLUME_RATIO_MIN ? SWEEP_TYPE_GRAB : SWEEP_TYPE_INDUCEMENT
event_bull := bull_taken
event_bear := bear_taken
event_type := sweep_type
event_direction := bull_taken and not bear_taken ? 1 : bear_taken and not bull_taken ? -1 : 0
event_taken_direction := bull_taken and not bear_taken ? -1 : bear_taken and not bull_taken ? 1 : 0
event_zone_top := bear_taken ? bar_high : reference_low
event_zone_bottom := bull_taken ? bar_low : reference_high
event_depth_pct := depth
event_volume_ratio := volume_ratio
event_reference_high := reference_high
event_reference_low := reference_low
event_age := 0
event_reclaimed := (not bull_taken or bar_close > reference_low) and (not bear_taken or bar_close < reference_high)
else if not na(event_age)
event_age += 1
if not event_reclaimed and event_age <= SWEEP_RECLAIM_MAX_BARS
event_reclaimed := (not event_bull or bar_close > event_reference_low) and (not event_bear or bar_close < event_reference_high)
bool fresh = not na(event_age) and event_age <= fresh_max_bars
int quality = 0
if fresh
quality += event_bull or event_bear ? 1 : 0
quality += event_type == SWEEP_TYPE_STOP_HUNT or event_type == SWEEP_TYPE_GRAB ? 1 : 0
quality += event_reclaimed ? 1 : 0
quality += event_depth_pct >= SWEEP_DEPTH_MIN_PCT ? 1 : 0
quality += event_volume_ratio >= SWEEP_VOLUME_RATIO_MIN ? 1 : 0
published := fresh ?
SweepFrame.new(event_bull, event_bear, event_type, event_direction, event_zone_top, event_zone_bottom, event_reclaimed, event_taken_direction, event_depth_pct, event_volume_ratio, math.min(quality, 5), event_age, true) :
SweepFrame.new(false, false, 0, 0, na, na, false, 0, 0.0, 0.0, 0, event_age, false)
// Install newly confirmed pivots only after testing the current bar
// against the prior reference.
if not na(pivot_high)
reference_high := pivot_high
if not na(pivot_low)
reference_low := pivot_low
published
// Detect and score recent sweep events from confirmed pivots and chart OHLCV.
// The scoring ladder is golden-parity; pivot/depth/reclaim detection is the
// explicitly documented Pine-only Column-B rule.
export build_sweep_frame(simple int pivot_len = 5, simple int volume_len = 20, simple int fresh_max_bars = 20) =>
float pivot_high = ta.pivothigh(high, pivot_len, pivot_len)
float pivot_low = ta.pivotlow(low, pivot_len, pivot_len)
float average_volume = ta.sma(volume, volume_len)
_build_sweep_from_inputs(high, low, close, volume, average_volume, pivot_high, pivot_low, barstate.isconfirmed, fresh_max_bars)
// Stateful pool seam shared by the live builder and the source-derived R3
// runtime fixture.
_build_pool_from_inputs(float bar_high, float bar_low, float bar_close, float pivot_high, float pivot_low, bool confirmed, float tolerance_pct, simple int max_levels) =>
var array<float> buy_levels = array.new<float>()
var array<int> buy_strengths = array.new<int>()
var array<float> sell_levels = array.new<float>()
var array<int> sell_strengths = array.new<int>()
var PoolFrame published = PoolFrame.new(na, na, 0, 0, 0.0, 0, 0, 0, 0.0, 0, 0)
if confirmed
_pool_remove_taken(buy_levels, buy_strengths, true, bar_high, bar_low)
_pool_remove_taken(sell_levels, sell_strengths, false, bar_high, bar_low)
if not na(pivot_high) and pivot_high > bar_close
_pool_add(buy_levels, buy_strengths, pivot_high, tolerance_pct, max_levels)
if not na(pivot_low) and pivot_low < bar_close
_pool_add(sell_levels, sell_strengths, pivot_low, tolerance_pct, max_levels)
[buy_level, buy_strength] = _nearest_pool(buy_levels, buy_strengths, bar_close, true)
[sell_level, sell_strength] = _nearest_pool(sell_levels, sell_strengths, bar_close, false)
int untested_buy = array.size(buy_levels)
int untested_sell = array.size(sell_levels)
int total_buy = buy_strength + untested_buy
int total_sell = sell_strength + untested_sell
int total = total_buy + total_sell
float raw_imbalance = total > 0 ? float(total_buy - total_sell) / total : 0.0
float pool_imbalance = math.round(raw_imbalance * 10000.0) / 10000.0
int magnet = pool_imbalance >= IMBALANCE_SIG_THRESHOLD ? 1 : pool_imbalance <= -IMBALANCE_SIG_THRESHOLD ? -1 : 0
float buy_proximity = not na(buy_level) and bar_close != 0.0 ? math.abs(buy_level - bar_close) / math.abs(bar_close) * 100.0 : na
float sell_proximity = not na(sell_level) and bar_close != 0.0 ? math.abs(sell_level - bar_close) / math.abs(bar_close) * 100.0 : na
float proximity = na(buy_proximity) ? nz(sell_proximity, 0.0) : na(sell_proximity) ? buy_proximity : math.min(buy_proximity, sell_proximity)
proximity := math.round(proximity * 10000.0) / 10000.0
int cluster_density = math.min(5, math.max(buy_strength, sell_strength))
int quality = 0
quality += not na(buy_level) or not na(sell_level) ? 1 : 0
quality += buy_strength >= CLUSTER_STRONG_COUNT or sell_strength >= CLUSTER_STRONG_COUNT ? 1 : 0
quality += proximity > 0 and proximity <= PROXIMITY_NEAR_PCT ? 1 : 0
quality += cluster_density >= CLUSTER_STRONG_COUNT ? 1 : 0
quality += math.abs(pool_imbalance) >= IMBALANCE_SIG_THRESHOLD ? 1 : 0
published := PoolFrame.new(buy_level, sell_level, buy_strength, sell_strength, proximity, cluster_density, untested_buy, untested_sell, pool_imbalance, magnet, math.min(quality, 5))
published
// Detect equal-pivot clusters and project the exact golden pool rule layer.
export build_pool_frame(simple int pivot_len = 5, float tolerance_pct = 0.1, simple int max_levels = 20) =>
float pivot_high = ta.pivothigh(high, pivot_len, pivot_len)
float pivot_low = ta.pivotlow(low, pivot_len, pivot_len)
_build_pool_from_inputs(high, low, close, pivot_high, pivot_low, barstate.isconfirmed, tolerance_pct, max_levels)
// Stateful session seam. The live wrapper owns IANA-zone classification; this
// helper owns the range, VWAP, scoring, and reset behavior replayed by the R3
// fixture.
_build_session_from_inputs(StructureFrame structure, ImbalanceFrame imbalance, int bar_time, float bar_high, float bar_low, float bar_close, float bar_hlc3, float bar_volume, int session_code, bool in_killzone, bool confirmed) =>
var int active_session = 0
var int session_started_at = na
var float session_top = na
var float session_bottom = na
// The finished extremes of the most recently COMPLETED session, stashed at
// both exit transitions (active -> outside, active -> other session)
// before the running range resets — mirroring SMC_Session_Context.pine.
var float previous_session_top = na
var float previous_session_bottom = na
var float opening_top = na
var float opening_bottom = na
var float cumulative_pv = 0.0
var float cumulative_volume = 0.0
var SessionFrame published = SessionFrame.new(0, false, false, false, 0, false, false, false, na, na, na, na, na, na, false, na, na, 0, 0, false, false)
if confirmed
bool session_changed = session_code != active_session
if session_code == 0
if active_session != 0 and not na(session_top)
previous_session_top := session_top
previous_session_bottom := session_bottom
active_session := 0
session_started_at := na
session_top := na
session_bottom := na
opening_top := na
opening_bottom := na
cumulative_pv := 0.0
cumulative_volume := 0.0
published := SessionFrame.new(0, false, false, false, 0, false, false, false, na, na, na, na, na, na, false, na, na, 0, 0, false, false)
else
if session_changed
if active_session != 0 and not na(session_top)
previous_session_top := session_top
previous_session_bottom := session_bottom
active_session := session_code
session_started_at := bar_time
session_top := bar_high
session_bottom := bar_low
opening_top := bar_high
opening_bottom := bar_low
cumulative_pv := 0.0
cumulative_volume := 0.0
else
session_top := math.max(session_top, bar_high)
session_bottom := math.min(session_bottom, bar_low)
bool opening_active = not na(session_started_at) and bar_time - session_started_at < OPENING_RANGE_MINUTES * 60000
if opening_active
opening_top := math.max(opening_top, bar_high)
opening_bottom := math.min(opening_bottom, bar_low)
if not na(bar_volume) and bar_volume > 0
cumulative_pv += bar_hlc3 * bar_volume
cumulative_volume += bar_volume
bool mss_bull = (structure.bos or structure.choch) and structure.dir_event == 1
bool mss_bear = (structure.bos or structure.choch) and structure.dir_event == -1
int bull_score = (mss_bull ? 2 : 0) + (imbalance.bull_fvg_active ? 1 : 0) + (not na(session_top) ? 1 : 0)
int bear_score = (mss_bear ? 2 : 0) + (imbalance.bear_fvg_active ? 1 : 0) + (not na(session_bottom) ? 1 : 0)
int direction_bias = bull_score > bear_score ? 1 : bear_score > bull_score ? -1 : 0
int context_score = 1
context_score += in_killzone ? 1 : 0
context_score += mss_bull or mss_bear ? 1 : 0
context_score += imbalance.bull_fvg_active or imbalance.bear_fvg_active ? 1 : 0
context_score += direction_bias != 0 ? 1 : 0
context_score += structure.trend != 0 ? 1 : 0
context_score += imbalance.bpr_active ? 1 : 0
// One-bar pulse, evaluated on the first bar of a session too —
// the previous session's extreme is broken most often right at
// the open, and SMC_Session_Context.pine fires there as well.
bool prev_break_bull = not na(previous_session_top) and bar_close > previous_session_top and bar_close[1] <= previous_session_top
bool prev_break_bear = not na(previous_session_bottom) and bar_close < previous_session_bottom and bar_close[1] >= previous_session_bottom
float session_mean = (session_top + session_bottom) / 2.0
float session_vwap = cumulative_volume > 0 ? cumulative_pv / cumulative_volume : na
published := SessionFrame.new(session_code, in_killzone, mss_bull, mss_bear, structure.trend, imbalance.bull_fvg_active, imbalance.bear_fvg_active, imbalance.bpr_active, session_top, session_bottom, session_mean, session_vwap, session_top, session_bottom, opening_active, opening_top, opening_bottom, direction_bias, math.min(context_score, 7), prev_break_bull, prev_break_bear)
published
// Compose session-local ranges and the Python-golden scoring layer. Trading
// windows are evaluated in their own zones; US and European DST are therefore
// independent by construction.
export build_session_frame(StructureFrame structure, ImbalanceFrame imbalance) =>
bool intraday = timeframe.isintraday
bool asia_active = intraday and not na(time(timeframe.period, "0900-1700:1234567", "Asia/Tokyo"))
bool london_active = intraday and not na(time(timeframe.period, "0800-1630:1234567", "Europe/London"))
bool ny_am_active = intraday and not na(time(timeframe.period, "0930-1300:1234567", "America/New_York"))
bool ny_pm_active = intraday and not na(time(timeframe.period, "1300-1600:1234567", "America/New_York"))
int session_code = ny_pm_active ? SESSION_NY_PM : ny_am_active ? SESSION_NY_AM : london_active ? SESSION_LONDON : asia_active ? SESSION_ASIA : 0
bool asia_kz = intraday and not na(time(timeframe.period, "0900-1300:1234567", "Asia/Tokyo"))
bool london_kz = intraday and not na(time(timeframe.period, "0800-1100:1234567", "Europe/London"))
bool ny_kz = intraday and not na(time(timeframe.period, "0930-1200:1234567", "America/New_York"))
bool in_killzone = asia_kz or london_kz or ny_kz
_build_session_from_inputs(structure, imbalance, time, high, low, close, hlc3, volume, session_code, in_killzone, barstate.isconfirmed)
// Aggregate seam shared by the live builder and the source-derived R3 runtime
// fixture. All voting and availability semantics live in exactly one place.
_aggregate_context(StructureFrame structure, ImbalanceFrame imbalance, ZoneFrame zone, SweepFrame sweep, PoolFrame pool, SessionFrame session) =>
int structure_vote = structure.trend
int imbalance_vote = imbalance.zone_bias
int zone_vote = zone.ob_bias
int sweep_vote = sweep.fresh ? sweep.direction : 0
int pool_vote = pool.magnet_direction
int session_vote = session.direction_bias
int directional_score = structure_vote + imbalance_vote + zone_vote + sweep_vote + pool_vote + session_vote
int bias = directional_score >= CONTEXT_BIAS_MIN_VOTES ? 1 : directional_score <= -CONTEXT_BIAS_MIN_VOTES ? -1 : 0
bool structure_available = structure.trend != 0 or not na(structure.support) or not na(structure.resistance)
bool imbalance_available = imbalance.state != 0
bool zone_available = zone.state != 0
bool sweep_available = sweep.fresh
bool pool_available = not na(pool.buy_side_level) or not na(pool.sell_side_level)
bool session_available = session.session_code != 0
int available_domains = (structure_available ? 1 : 0) + (imbalance_available ? 1 : 0) + (zone_available ? 1 : 0) + (sweep_available ? 1 : 0) + (pool_available ? 1 : 0) + (session_available ? 1 : 0)
float quality_sum = (structure_available ? (structure.fresh ? 100.0 : 50.0) : 0.0) +
(imbalance_available ? 100.0 : 0.0) + (zone_available ? 100.0 : 0.0) +
(sweep_available ? sweep.quality_score * 20.0 : 0.0) +
(pool_available ? pool.quality_score * 20.0 : 0.0) +
(session_available ? session.context_score / 7.0 * 100.0 : 0.0)
int quality_score = available_domains > 0 ? int(math.round(quality_sum / available_domains)) : 0
ContextFrame.new(structure, imbalance, zone, sweep, pool, session, bias, directional_score, available_domains, quality_score)
// Complete confirmed-bar aggregate. detect_structure runs exactly once here and
// the same tuple feeds both StructureFrame and ZoneFrame.
export build_context_frame(simple int swing_len = 50, simple int fresh_max_bars = 20, float fvg_size_threshold = 0.0, simple int atr_len = 50, float ob_min_mult = 0.5, float ob_max_mult = 2.5, simple int sweep_pivot_len = 5, simple int sweep_volume_len = 20, simple int pool_pivot_len = 5, float pool_tolerance_pct = 0.1, simple int pool_max_levels = 20) =>
[trend, bull_bos, bull_choch, top_x, _top_y, _trail_up_x, trail_up, hh, bear_bos, bear_choch, btm_x, _btm_y, _trail_dn_x, trail_dn, swing_low, _hhll_x] = eng.detect_structure(swing_len)
var bool have_up_pivot = false
var bool have_dn_pivot = false
var int bars_since = na
var StructureFrame structure = StructureFrame.new(0, false, false, 0, na, na, na, na, na, false)
if barstate.isconfirmed
if not na(hh)
have_up_pivot := true
if not na(swing_low)
have_dn_pivot := true
bool bos_now = bull_bos or bear_bos
bool choch_now = bull_choch or bear_choch
bool event_now = bos_now or choch_now
int dir_event = (bull_bos or bull_choch) ? 1 : (bear_bos or bear_choch) ? -1 : 0
if event_now
bars_since := 0
else if not na(bars_since)
bars_since += 1
bool fresh = not na(bars_since) and bars_since <= fresh_max_bars
structure := StructureFrame.new(not na(bars_since) ? trend : 0, bos_now, choch_now, dir_event, have_up_pivot ? trail_up : na, have_dn_pivot ? trail_dn : na, have_up_pivot ? top_x : na, have_dn_pivot ? btm_x : na, bars_since, fresh)
ZoneFrame zone = _build_zone(swing_len, hh, swing_low, bull_bos, bull_choch, bear_bos, bear_choch, atr_len, ob_min_mult, ob_max_mult)
ImbalanceFrame imbalance = build_imbalance_frame(fvg_size_threshold)
SweepFrame sweep = build_sweep_frame(sweep_pivot_len, sweep_volume_len, fresh_max_bars)
PoolFrame pool = build_pool_frame(pool_pivot_len, pool_tolerance_pct, pool_max_levels)
SessionFrame session_frame = build_session_frame(structure, imbalance)
_aggregate_context(structure, imbalance, zone, sweep, pool, session_frame)
Show all 833 lines