SMC_Breakout_Overlay.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
// Product role: companion_operator_only.
//
// SMC Breakout Overlay — LonesomeTheBlue-style breakout/breakdown boxes.
//
// THREE-TIER structure source (pick via "Structure source", default Auto):
//
// 1. LIBRARY (standalone, default): imports smc_engine_private and calls the
// SAME order-block / structure detector the engine uses — real BOS/CHoCH
// and swing levels — WITHOUT needing SMC Core on the chart. This is the
// standalone default: proper SMC structure, not a naive approximation.
//
// 2. ENGINE BUS (live): bind the four "BUS …" inputs to SMC Core's hidden
// per-bar plots (SchemaVersion / ZoneActive / Trigger / Invalidation). When
// the schema reads 7001 the live, fully-gated engine setup drives the boxes
// (a close crossing its Trigger = breakout, its Invalidation = breakdown).
// Auto uses this whenever the engine is bound+on-chart, else falls to (1).
// Optional enrichment channels (ZoneOb*/ZoneFvg*/StopLevel/Target1-2/
// QualityScore) draw the real active OB/FVG zone as the box, plot the
// engine's real Stop/Target and feed them into the win/loss sim, and
// filter breakouts below a chosen quality score. Bind only what you want.
//
// 3. LOCAL PIVOTS (fallback): naive ta.pivot swing levels + crossover, no
// dependency at all. For charts where you deliberately want zero library.
//
// It does NOT import smc_micro_profiles — that is a per-symbol SNAPSHOT (baked
// constants, self-attesting LIBRARY_STRUCTURE_MISSING); the real per-bar
// structure lives in smc_engine_private (tier 1) or on the engine BUS (tier 2).
//
// BOS = a break that CONTINUES the prevailing trend.
// CHoCH = a break that REVERSES it (change of character).
// (BOS/CHoCH come from the library in tier 1, are computed locally in tier 3,
// and are shown as LONG TRIGGER / INVALIDATION in the engine-gated tier 2.)
//
// Box height = threshold% of the most-recent N-bar range (LonesomeTheBlue idea).
indicator("SMC Breakout Overlay", overlay = true,
max_lines_count = 100, max_labels_count = 200, max_boxes_count = 500, max_bars_back = 500)
import preuss_steffen/smc_engine_private/5 as eng
// ── Inputs ──────────────────────────────────────────────────────
// Note: input.*() and group-name literals are evaluated once and are const by
// construction — no `var` needed (matches the SMC dashboard's input style and
// keeps the per-file var-declaration budget for genuine cross-bar state only).
g_src = "Structure source"
string structSource = input.string("Auto", "Structure source",
options = ["Auto", "Engine BUS (live)", "Library (standalone)", "Local pivots"], group = g_src,
tooltip = "Auto: the linked Suite feed when SMC Long-Dip Suite is connected + on chart, else the SMC library. " +
"Engine BUS (live): the live Suite's own gated setup (needs SMC Long-Dip Suite on the chart). " +
"Library (standalone): real SMC engine-library structure detection, no Suite needed. " +
"Local pivots: naive swing-pivot fallback, zero dependency.")
int structLen = input.int(50, "Library swing length", minval = 5, maxval = 200, group = g_src,
tooltip = "Swing lookback for the SMC engine-library structure detector (library tier).")
g_bus = "Chart Link - SMC Long-Dip Suite"
// Declared in the Suite's BUS plot() order. TradingView lists a source study's
// outputs in that plot order, so matching it here means each row's dropdown pick
// moves strictly downwards while binding — no scrolling back up.
// Required for the engine tier: SchemaVersion / ZoneActive / Trigger / Invalidation.
// Optional enrichment (real zones / risk plan / quality): QualityScore, StopLevel,
// Target1/2, ZoneObTop/Bottom, ZoneFvgTop/Bottom.
src_schema = input.source(close, "BUS SchemaVersion", group = g_bus, display = display.none,
tooltip = "Bind to the SMC Long-Dip Suite 'BUS SchemaVersion' plot. Must read 7001 for the engine tier to activate. The rows below are listed in the same order as the Suite outputs in the dropdown.")
src_zone_active = input.source(close, "BUS ZoneActive", group = g_bus, display = display.none)
src_trigger = input.source(close, "BUS Trigger", group = g_bus, display = display.none)
src_invalidation = input.source(close, "BUS Invalidation", group = g_bus, display = display.none)
src_quality = input.source(close, "BUS QualityScore", group = g_bus, display = display.none)
src_stop = input.source(close, "BUS StopLevel", group = g_bus, display = display.none)
src_target1 = input.source(close, "BUS Target1", group = g_bus, display = display.none)
src_target2 = input.source(close, "BUS Target2", group = g_bus, display = display.none)
src_ob_top = input.source(close, "BUS ZoneObTop", group = g_bus, display = display.none)
src_ob_bottom = input.source(close, "BUS ZoneObBottom", group = g_bus, display = display.none)
src_fvg_top = input.source(close, "BUS ZoneFvgTop", group = g_bus, display = display.none)
src_fvg_bottom = input.source(close, "BUS ZoneFvgBottom", group = g_bus, display = display.none)
int expected_bus_version = 7001 // matches SMC Core's published 'BUS SchemaVersion'
bool engine_bound = math.round(src_schema) == expected_bus_version
// Effective tier this bar — exactly one is true. Auto/Engine-BUS fall back to
// the library when the engine is not bound on the chart. The option-value
// comparison lives in its own bool: the literal is a settings dropdown VALUE
// (kept stable so saved charts keep their selection), not a rendered string,
// and the sim table below consumes mode_bus directly.
bool wants_engine_tier = structSource == "Auto" or structSource == "Engine BUS (live)"
bool mode_bus = wants_engine_tier and engine_bound
bool mode_piv = structSource == "Local pivots"
bool mode_lib = not mode_bus and not mode_piv
g_enrich = "Engine enrichments (tier 2 only)"
bool use_real_zone = input.bool(true, "Box = real OB/FVG zone", group = g_enrich,
tooltip = "Bull breakouts: draw the box on the engine's active Order-Block (or FVG) zone " +
"instead of the threshold band. Needs the ZoneOb*/ZoneFvg* rows linked; falls back to the band.")
bool use_real_plan = input.bool(true, "Draw + simulate real Stop/Target", group = g_enrich,
tooltip = "Plot the engine's actual Stop/Target1/Target2 and feed the win/loss tracker with the " +
"real Stop/Target (long side) instead of the synthetic ATR-RR. Needs the StopLevel/Target1 rows linked.")
float qualityMin = input.float(0, "Min quality score (0 = off)", minval = 0, maxval = 100, group = g_enrich,
tooltip = "Suppress engine-tier breakouts whose linked QualityScore is below this. 0 disables the filter. " +
"Needs the QualityScore row linked to be meaningful.")
// Resolved engine zone (bull side; prefer OB, else FVG). na → fall back to band.
bool _ob_valid = mode_bus and not na(src_ob_top) and not na(src_ob_bottom) and src_ob_top > src_ob_bottom
bool _fvg_valid = mode_bus and not na(src_fvg_top) and not na(src_fvg_bottom) and src_fvg_top > src_fvg_bottom
float ezone_top = _ob_valid ? src_ob_top : _fvg_valid ? src_fvg_top : na
float ezone_bot = _ob_valid ? src_ob_bottom : _fvg_valid ? src_fvg_bottom : na
// Quality gate. Treat QualityScore as "bound" only when it differs from the
// unbound `close` default and reads as a plausible 0..100 score — so a user who
// sets qualityMin but forgets to bind (on a symbol priced in 0..100) can NEVER
// be wrongly blocked; the filter simply stays inert until actually bound.
bool _quality_bound = mode_bus and src_quality != close and src_quality >= 0 and src_quality <= 100
bool quality_ok = qualityMin <= 0 or not _quality_bound or src_quality >= qualityMin
g_trig = "BOS / CHoCH filter (library + pivot tiers)"
bool use_bos = input.bool(true, "Trigger on BOS", group = g_trig)
bool use_choch = input.bool(true, "Trigger on CHoCH", group = g_trig)
int pivLen = input.int(5, "Pivot swing strength (pivot tier)", minval = 2, maxval = 50, group = g_trig,
tooltip = "Left/right bars for the local swing pivots in the pivot tier.")
int minHold = input.int(5, "Min level age (pivot tier)", minval = 1, maxval = 200, group = g_trig,
tooltip = "Skip pivot-tier boxes where the broken swing is younger than N bars. " +
"Not applied in library/engine tiers (structure there is already confirmed/gated).")
g_box = "Box geometry"
int rangeLen = input.int(300, "Range lookback (bars)", minval = 30, maxval = 500, group = g_box,
tooltip = "Window used to compute the threshold band height (LonesomeTheBlue uses 300).")
float thrPct = input.float(3.0, "Threshold band %", minval = 0.5, maxval = 10.0, step = 0.5, group = g_box,
tooltip = "Box height as % of the lookback-window range.") / 100.0
g_vis = "Visuals"
color colUp = input.color(color.new(color.blue, 0), "Breakout (bull) color", group = g_vis)
color colDn = input.color(color.new(color.red, 0), "Breakdown (bear) color", group = g_vis)
int lineW = input.int(1, "Line width", minval = 1, maxval = 4, group = g_vis)
string lstyle = input.string(line.style_solid, "Line style",
options = [line.style_solid, line.style_dashed, line.style_dotted], group = g_vis)
bool show_arrows = input.bool(true, "Show ▲/▼ markers", group = g_vis)
bool show_volemoji = input.bool(true, "Show volume sentiment emoji", group = g_vis,
tooltip = "🟢 if bull-volume >55% between level establishment and breakout, 🔴 if bear-volume >55%, 🟡 mixed.")
g_sim = "Win/Loss tracker (ATR-RR sim)"
bool sim_on = input.bool(true, "Enable tracker", group = g_sim)
float rr = input.float(1.5, "RR ratio", minval = 0.5, maxval = 5.0, step = 0.1, group = g_sim, active = sim_on)
int atrLen = input.int(14, "ATR length", minval = 5, maxval = 50, group = g_sim, active = sim_on)
float atrMult = input.float(1.5, "ATR × mult → SL", minval = 0.5, maxval = 5.0, step = 0.1, group = g_sim, active = sim_on)
bool sl_on_close = input.bool(true, "SL on close (else high/low)", group = g_sim, active = sim_on)
bool show_table = input.bool(true, "Show stats table", group = g_sim, active = sim_on)
// ── Tier 3 source: local swing pivots (ALWAYS computed) ──
// ta.* must be evaluated every bar for a correct series, so all three sources
// are computed unconditionally; the tier-select below picks which one drives
// the boxes. A pivot confirmed now was established `pivLen` bars ago (that bar
// becomes the box's left edge).
float ph = ta.pivothigh(pivLen, pivLen)
float pl = ta.pivotlow(pivLen, pivLen)
var float piv_resistance = na
var int piv_res_anchor = na
var float piv_support = na
var int piv_sup_anchor = na
if not na(ph)
piv_resistance := ph
piv_res_anchor := bar_index - pivLen
if not na(pl)
piv_support := pl
piv_sup_anchor := bar_index - pivLen
// ── Tier 1 source: smc_engine_private structure detector (ALWAYS computed) ──
// Same detector the engine uses. Returns real BOS/CHoCH alerts on the break
// bar plus the confirmed swing high/low levels and their bar indices. `top_x`/
// `btm_x` are absolute bar_index values (the library draws line.new(x, y, …)),
// so they anchor the box left edge directly.
[_lib_trend, lib_bull_bos, lib_bull_choch, lib_top_x, lib_top_y, _lib_tux, _lib_tu, _lib_hh,
lib_bear_bos, lib_bear_choch, lib_btm_x, lib_btm_y, _lib_tdx, _lib_td, _lib_ll, _lib_hx] =
eng.detect_structure(structLen)
// ── Tier 2 source: SMC engine BUS (optional) ──
// `zone_start_bar` = the bar the current setup zone became active; it anchors
// the box's left edge in engine mode.
bool zone_on = src_zone_active > 0.5
var int zone_start_bar = na
if zone_on and na(zone_start_bar)
zone_start_bar := bar_index
if not zone_on
zone_start_bar := na
// ── Tier-select: which source drives the boxes this bar ──
float resistance = mode_bus ? (zone_on ? src_trigger : na) : mode_lib ? lib_top_y : piv_resistance
float support = mode_bus ? (zone_on ? src_invalidation : na) : mode_lib ? lib_btm_y : piv_support
int res_anchor_bar = mode_bus ? zone_start_bar : mode_lib ? lib_top_x : piv_res_anchor
int sup_anchor_bar = mode_bus ? zone_start_bar : mode_lib ? lib_btm_x : piv_sup_anchor
// ── Break detection + BOS/CHoCH classification ──
// Pivot/BUS tiers: a break is a close crossing the level. Library tier: the
// detector emits explicit BOS/CHoCH alerts on the break bar — use them directly.
// `trend` is the pivot-tier classifier (+1 after a bull break, -1 after bear).
bool cross_up = not na(resistance) and ta.crossover(close, resistance)
bool cross_dn = not na(support) and ta.crossunder(close, support)
var int trend = 0
bool piv_bos_bull = cross_up and trend >= 0
bool piv_choch_bull = cross_up and trend < 0
bool piv_bos_bear = cross_dn and trend <= 0
bool piv_choch_bear = cross_dn and trend > 0
if cross_up
trend := 1
if cross_dn
trend := -1
bool broke_up = mode_lib ? (lib_bull_bos or lib_bull_choch) : cross_up
bool broke_dn = mode_lib ? (lib_bear_bos or lib_bear_choch) : cross_dn
bool bos_bull = mode_lib ? lib_bull_bos : piv_bos_bull
bool choch_bull = mode_lib ? lib_bull_choch : piv_choch_bull
bool bos_bear = mode_lib ? lib_bear_bos : piv_bos_bear
bool choch_bear = mode_lib ? lib_bear_choch : piv_choch_bear
// ── Threshold band height (LonesomeTheBlue formula) ────────────
// `rangeLen` is a simple int (input), so ta.highest/ta.lowest accept it; on
// early bars Pine simply uses the fewer bars available.
float chwidth = (ta.highest(high, rangeLen) - ta.lowest(low, rangeLen)) * thrPct
// ── Volume-sentiment emoji (ChartPrime-style) ──────────────────
// Sums green vs. red bar volume between `from_bar` and the current bar.
// Returns 🟢 / 🔴 / 🟡.
vol_sentiment(int from_bar) =>
// Cap span to a safe lookback so we never index beyond `max_bars_back`
// and never run an unbounded loop when an anchor persists for thousands
// of bars (would cause runtime errors / severe slowdowns).
int VOL_LOOKBACK_MAX = 500
int raw_span = math.max(bar_index - from_bar, 1)
int span = math.min(raw_span, VOL_LOOKBACK_MAX)
float green_sum = 0.0
float red_sum = 0.0
float total_sum = 0.0
for i = 0 to span - 1
float v = nz(volume[i])
total_sum += v
if close[i] > open[i]
green_sum += v
else if close[i] < open[i]
red_sum += v
float gr = total_sum > 0 ? green_sum / total_sum * 100.0 : 0.0
float rr_ = total_sum > 0 ? red_sum / total_sum * 100.0 : 0.0
gr > 55.0 ? "🟢" : rr_ > 55.0 ? "🔴" : "🟡"
// ── Trigger evaluation ─────────────────────────────────────────
// H-1 follow-up (PR #1940 review): the trigger booleans drive both the
// alertcondition gate AND plotshape / sim. If we let them flicker intrabar,
// a mid-bar wick can fire a box without the alert ever confirming. Anchor the
// truth to `barstate.isconfirmed` once at the source so every downstream
// consumer (alerts, plotshape, sim) stays aligned.
bool _trig_confirmed = barstate.isconfirmed
// Structure filter: the engine tier accepts every level break (the engine
// already validated the setup); library + pivot tiers apply the BOS/CHoCH
// toggles.
bool struct_bull_ok = mode_bus ? true : ((use_bos and bos_bull) or (use_choch and choch_bull))
bool struct_bear_ok = mode_bus ? true : ((use_bos and bos_bear) or (use_choch and choch_bear))
// Level-age filter applies only to locally-detected pivots; library/engine
// levels are already confirmed/gated, so age is not re-checked there.
bool age_bull_ok = mode_piv ? (not na(res_anchor_bar) and (bar_index - res_anchor_bar) >= minHold) : true
bool age_bear_ok = mode_piv ? (not na(sup_anchor_bar) and (bar_index - sup_anchor_bar) >= minHold) : true
bool trig_bull = _trig_confirmed and broke_up and struct_bull_ok and age_bull_ok and quality_ok
bool trig_bear = _trig_confirmed and broke_dn and struct_bear_ok and age_bear_ok and quality_ok
// ── Draw box (single box.new — proper object lifecycle, no max_lines spam) ─
draw_box(int x1, float y_top, float y_bot, int x2, color col) =>
// Use box.new so each breakout produces ONE managed object instead of 4
// permanently allocated `line` objects (would silently hit max_lines_count
// and stop drawing after enough triggers).
box.new(left = x1, top = y_top, right = x2, bottom = y_bot,
border_color = col, border_width = lineW, border_style = lstyle,
bgcolor = color.new(col, 90))
// Label text helper: "<base> 🟢" — base tag already resolved for the tier.
breakout_label(string base_tag, int from_bar) =>
string emo = show_volemoji ? " " + vol_sentiment(from_bar) : ""
base_tag + emo
if trig_bull
int x1 = na(res_anchor_bar) ? math.max(bar_index - 1, 0) : res_anchor_bar
// Engine tier: draw the real active OB/FVG zone when bound; else the band.
bool _use_zone = mode_bus and use_real_zone and not na(ezone_top) and not na(ezone_bot)
float y_top = _use_zone ? ezone_top : resistance
float y_bot = _use_zone ? ezone_bot : resistance - chwidth
draw_box(x1, y_top, y_bot, bar_index, colUp)
string base = mode_bus ? (_use_zone ? "LONG ZONE" : "LONG TRIGGER") : (choch_bull ? "CHoCH" : "BOS")
label.new(int(math.avg(x1, bar_index)), y_top, breakout_label(base, x1),
color = color.new(colUp, 70), textcolor = color.white,
style = label.style_label_down, size = size.tiny)
if trig_bear
int x1 = na(sup_anchor_bar) ? math.max(bar_index - 1, 0) : sup_anchor_bar
float y_bot = support
float y_top = support + chwidth
draw_box(x1, y_top, y_bot, bar_index, colDn)
string base = mode_bus ? "INVALIDATION" : (choch_bear ? "CHoCH" : "BOS")
label.new(int(math.avg(x1, bar_index)), y_bot, breakout_label(base, x1),
color = color.new(colDn, 70), textcolor = color.white,
style = label.style_label_up, size = size.tiny)
// ── Markers (▲ below / ▼ above the breakout bar) ───────────────
plotshape(show_arrows and trig_bull, "SMC Breakout", shape.triangleup,
location.belowbar, colUp, size = size.small)
plotshape(show_arrows and trig_bear, "SMC Breakdown", shape.triangledown,
location.abovebar, colDn, size = size.small)
// ── Engine risk plan (tier 2): plot the engine's real Stop / Target levels ──
// Only when bound; plot.style_linebr breaks the line across na gaps so it shows
// only while the engine publishes a live plan.
bool _plan_on = mode_bus and use_real_plan
plot(_plan_on and not na(src_stop) ? src_stop : na, "Engine Stop", color = color.new(color.red, 0), linewidth = 1, style = plot.style_linebr)
plot(_plan_on and not na(src_target1) ? src_target1 : na, "Engine Target 1", color = color.new(color.green, 0), linewidth = 1, style = plot.style_linebr)
plot(_plan_on and not na(src_target2) ? src_target2 : na, "Engine Target 2", color = color.new(color.teal, 0), linewidth = 1, style = plot.style_linebr)
// ── Alerts ─────────────────────────────────────────────────────
// H-1 (system review 2026-04-24): every alertcondition() must be gated
// against intra-bar firing. _alertGate is the canonical token the audit
// pin (tests/test_pine_alert_bar_close_gate.py) recognises.
_alertGate = barstate.isconfirmed
alertcondition(_alertGate and trig_bull, title = "SMC Breakout",
message = "SMC Breakout — close broke the resistance / engine long-trigger.")
alertcondition(_alertGate and trig_bear, title = "SMC Breakdown",
message = "SMC Breakdown — close broke the support / engine invalidation.")
alertcondition(_alertGate and (trig_bull or trig_bear), title = "SMC Breakout or Breakdown",
message = "SMC Breakout or Breakdown")
// ── Win/Loss tracker (ATR-RR simulation) ───────────────────────
// Mirrors ChartPrime's idea: when a breakout/breakdown fires, open a
// virtual trade with SL = ATR×mult, TP = SL×RR, and tally hit/miss.
// Pure rendering — not connected to the real strategy engine.
float atr_v = ta.atr(atrLen)
float sl_dist = atr_v * atrMult
var bool sim_active = false
var bool sim_long = false
var float sim_tp = na
var float sim_sl = na
var int sim_wins = 0
var int sim_losses = 0
if sim_on and not sim_active and (trig_bull or trig_bear)
sim_active := true
sim_long := trig_bull
// Engine tier + real-plan: the long side uses the engine's actual Stop /
// Target1 instead of the synthetic ATR-RR band. Short/bear side (rare on
// this long-only engine) always uses ATR-RR — no engine plan applies.
bool _real_plan = mode_bus and use_real_plan and sim_long and not na(src_stop) and not na(src_target1)
if sim_long
sim_sl := _real_plan ? src_stop : close - sl_dist
sim_tp := _real_plan ? src_target1 : close + sl_dist * rr
else
sim_sl := close + sl_dist
sim_tp := close - sl_dist * rr
if sim_on and sim_active
float sl_probe = sl_on_close ? close : (sim_long ? low : high)
float tp_probe = (sim_long ? high : low )
bool tp_hit = sim_long ? (tp_probe >= sim_tp) : (tp_probe <= sim_tp)
bool sl_hit = sim_long ? (sl_probe <= sim_sl) : (sl_probe >= sim_sl)
if tp_hit
sim_wins += 1
sim_active := false
else if sl_hit
sim_losses += 1
sim_active := false
int sim_total = sim_wins + sim_losses
float sim_wr = sim_total > 0 ? sim_wins / float(sim_total) * 100.0 : 0.0
string src_label = mode_bus ? "Chart Link" : mode_lib ? "Library" : "Pivots"
var table tb = table.new(position.top_right, 2, 5, bgcolor = color.new(color.gray, 80),
border_width = 1, border_color = color.new(color.gray, 60))
if show_table and sim_on and barstate.islast
table.cell(tb, 0, 0, "SMC BO Sim (RR " + str.tostring(rr, "#.0") + ")",
text_color = color.new(color.white, 0), bgcolor = color.new(color.gray, 60))
table.cell(tb, 1, 0, "", bgcolor = color.new(color.gray, 60))
table.cell(tb, 0, 1, "Source", text_color = color.new(color.silver, 0))
table.cell(tb, 1, 1, src_label,
text_color = mode_bus ? color.new(#08c09b, 0) : mode_lib ? color.new(color.aqua, 0) : color.new(color.silver, 0))
table.cell(tb, 0, 2, "Wins", text_color = color.new(color.silver, 0))
table.cell(tb, 1, 2, str.tostring(sim_wins), text_color = color.new(#08c09b, 0))
table.cell(tb, 0, 3, "Losses", text_color = color.new(color.silver, 0))
table.cell(tb, 1, 3, str.tostring(sim_losses), text_color = color.new(#cd0202, 0))
table.cell(tb, 0, 4, "Win %", text_color = color.new(color.silver, 0))
table.cell(tb, 1, 4, str.tostring(sim_wr, "#.0") + "%",
text_color = sim_wr >= 50.0 ? color.new(#08c09b, 0) : color.new(#cd0202, 0))
Show all 401 lines