SMC Toolkit detects core Smart Money Concepts on the price chart, including market structure breaks (BoS and CHoCH), Order Blocks, Fair Value Gaps (FVGs), and liquidity levels such as equal highs and lows.
// =============================================================================
// SMC Toolkit
// =============================================================================
// Author: Baris Tankut (@btankutt)
// Repository: https://github.com/btankutt/smc-pine-suite
// License: MPL 2.0 (see LICENSE-FREE)
// Created: 2026-05-15
// Version: 1.1.0
// -----------------------------------------------------------------------------
// Smart Money Concepts toolkit for TradingView:
// - Market structure detection (BoS / CHoCH)
// - Order Block identification with mitigation tracking
// - Fair Value Gap (FVG) detection
// - Liquidity level detection and sweep alerts
//
// Standard implementations of public-domain SMC concepts. Open source under
// MPL 2.0 - fork, study, adapt to your strategy.
//
// v1.1.0 changelog:
// - FIX: FVGs were marked filled on their creation bar (fill loop saw the
// just-created gap whose edge equals the creation bar's own low/high).
// Objects now store their creation bar and are never evaluated on it.
// - FIX: Order Blocks were mitigated on their own creation bar in gapless
// markets. Same creation-bar guard applies.
// - FIX: "Equal highs/lows lookback" input was never used; it now bounds
// how far back (in bars) a pivot may match a prior pivot.
// - FIX: all state updates, drawings and alert flags are gated on
// barstate.isconfirmed - no intrabar phantom signals; realtime output
// now always matches a historical recalculation.
// - NEW: dedicated "Max liquidity levels" input (was silently coupled to
// the OB + FVG display caps).
// - NEW: liquidity levels are invalidated when price CLOSES through them
// (a broken level is not resting liquidity anymore); swept/broken lines
// are grayed out and frozen, matching the OB/FVG convention.
// - NEW: mitigated OBs / filled FVGs stop extending right - only active
// zones project to the right edge.
// - REMOVED: dead "Show internal structure" input (was a no-op).
//
// Disclaimer: Educational tool, not financial advice. Past performance does
// not guarantee future results. Always backtest and use proper risk management.
// =============================================================================
//@version=6
indicator("SMC Toolkit [btankutt]", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)
// =============================================================================
// CONSTANTS
// =============================================================================
int ATR_LENGTH = 14
int VOLUME_SMA_LENGTH = 20
int OB_LOOKBACK_BARS = 30
int PIVOT_BUFFER_SIZE = 10
// Input group names
GRP_STRUCT = "Structure"
GRP_OB = "Order Blocks"
GRP_FVG = "Fair Value Gaps"
GRP_LIQ = "Liquidity"
GRP_DISP = "Display"
// =============================================================================
// USER INPUTS
// =============================================================================
// Group 1 - Structure
int swing_length = input.int(5, "Swing detection length", minval=2, maxval=50, group=GRP_STRUCT)
bool show_external_structure = input.bool(true, "Show structure labels", group=GRP_STRUCT)
// Group 2 - Order Blocks
bool ob_enabled = input.bool(true, "Enable Order Blocks", group=GRP_OB)
int ob_max_count = input.int(5, "Max OBs to display (total)", minval=1, maxval=50, group=GRP_OB)
bool ob_volume_filter = input.bool(true, "Filter by volume", group=GRP_OB)
color ob_bullish_color = input.color(color.new(color.green, 70), "Bullish OB color", group=GRP_OB)
color ob_bearish_color = input.color(color.new(color.red, 70), "Bearish OB color", group=GRP_OB)
// Group 3 - Fair Value Gaps
bool fvg_enabled = input.bool(true, "Enable FVG", group=GRP_FVG)
int fvg_max_count = input.int(5, "Max FVGs to display", minval=1, maxval=50, group=GRP_FVG)
float fvg_min_size_atr = input.float(0.5, "Min FVG size (ATR multiplier)", minval=0.1, maxval=5.0, step=0.1, group=GRP_FVG)
color fvg_bullish_color = input.color(color.new(color.aqua, 80), "Bullish FVG color", group=GRP_FVG)
color fvg_bearish_color = input.color(color.new(color.orange, 80), "Bearish FVG color", group=GRP_FVG)
// Group 4 - Liquidity
bool liq_enabled = input.bool(true, "Enable Liquidity", group=GRP_LIQ)
int liq_max_count = input.int(10, "Max liquidity levels", minval=1, maxval=50, group=GRP_LIQ)
int liq_lookback = input.int(50, "Equal highs/lows lookback (bars)", minval=10, maxval=200, group=GRP_LIQ, tooltip="A new pivot only forms a liquidity level with prior pivots that are at most this many bars old.")
float liq_tolerance_atr = input.float(0.2, "Tolerance (ATR multiplier)", minval=0.05, maxval=1.0, step=0.05, group=GRP_LIQ)
// Group 5 - Display
bool show_labels = input.bool(true, "Show event labels", group=GRP_DISP)
int box_transparency = input.int(70, "Box transparency", minval=0, maxval=100, group=GRP_DISP)
// =============================================================================
// USER-DEFINED TYPES
// =============================================================================
// created_bar guards maintenance loops: an object is never evaluated on the
// bar it was created (its edges are that bar's own high/low - a same-bar
// check would trivially "touch" it).
type OrderBlock
float top
float bottom
int created_bar
bool mitigated
bool bullish
type FVG
float top
float bottom
int created_bar
bool filled
bool bullish
type LiquidityLevel
float price
int created_bar
bool swept
bool is_bsl
// =============================================================================
// STATE
// =============================================================================
// Tracked SMC objects (logical state)
var array<OrderBlock> obs = array.new<OrderBlock>()
var array<FVG> fvgs = array.new<FVG>()
var array<LiquidityLevel> liqs = array.new<LiquidityLevel>()
// Parallel arrays for visual handles (kept in lockstep with the logical arrays).
// This avoids polluting the UDT spec with rendering concerns.
var array<box> ob_boxes = array.new<box>()
var array<box> fvg_boxes = array.new<box>()
var array<line> liq_lines = array.new<line>()
// Most recent confirmed swing levels
var float last_swing_high = na
var float last_swing_low = na
// "Has the last confirmed swing been broken?" - prevents re-firing BoS/CHoCH
// every bar while close remains beyond the swing.
var bool broke_last_high = false
var bool broke_last_low = false
// Current structural trend
// trend_initialized = false until the FIRST break (BoS/CHoCH detection only valid after)
// is_bullish = direction of the current trend (only meaningful when trend_initialized)
var bool trend_initialized = false
var bool is_bullish = false
// Recent pivot prices + the bar each formed on, used for equal-highs/lows
// liquidity detection. liq_lookback evicts entries older than N bars;
// PIVOT_BUFFER_SIZE caps the buffer length.
var array<float> recent_ph_prices = array.new<float>()
var array<int> recent_ph_bars = array.new<int>()
var array<float> recent_pl_prices = array.new<float>()
var array<int> recent_pl_bars = array.new<int>()
// Alert flags, reset each bar. Set only on confirmed bars, so any TradingView
// alert frequency is safe (recommended: "Once Per Bar Close").
bool bos_bullish = false
bool bos_bearish = false
bool choch_bullish = false
bool choch_bearish = false
bool bsl_swept = false
bool ssl_swept = false
// Volatility + volume context
float atr_value = ta.atr(ATR_LENGTH)
float vol_sma_value = ta.sma(volume, VOLUME_SMA_LENGTH)
// =============================================================================
// SECTION 1 - Swing detection
// =============================================================================
// Note: ta.pivothigh/pivotlow returns the pivot value `swing_length` bars
// AFTER it actually formed. This `swing_length` bar confirmation delay is
// required to avoid lookahead bias - adjust swing_length to balance
// responsiveness vs stability.
// ta.* calls stay unconditional at global scope; only STATE updates are
// gated on barstate.isconfirmed (true on every historical bar, and only on
// the final execution of the realtime bar - history is unchanged, live
// output can no longer flicker intrabar).
float ph = ta.pivothigh(swing_length, swing_length)
float pl = ta.pivotlow(swing_length, swing_length)
if barstate.isconfirmed
if not na(ph)
last_swing_high := ph
broke_last_high := false
if not na(pl)
last_swing_low := pl
broke_last_low := false
// =============================================================================
// SECTION 2 - Structure (BoS / CHoCH)
// =============================================================================
// A break only fires the FIRST time close exceeds the last swing. The
// per-swing "broke_last_*" flag is reset whenever a new pivot is confirmed.
// Note: on a single wide bar that closes beyond BOTH swings (rare), the
// upside break is processed first, then the downside - the bar emits both
// events and the trend ends bearish.
bool break_up = false
bool break_down = false
if barstate.isconfirmed
if not na(last_swing_high) and close > last_swing_high and not broke_last_high
break_up := true
broke_last_high := true
if not na(last_swing_low) and close < last_swing_low and not broke_last_low
break_down := true
broke_last_low := true
// First break establishes the initial trend direction.
if not trend_initialized and (break_up or break_down)
is_bullish := break_up
trend_initialized := true
if trend_initialized
if break_up
if is_bullish
bos_bullish := true
else
choch_bullish := true
is_bullish := true
if show_external_structure and show_labels
label.new(bar_index, high, choch_bullish ? "CHoCH" : "BoS",
yloc=yloc.abovebar, style=label.style_label_down,
color=color.new(color.green, 50), textcolor=color.white,
size=size.tiny)
if break_down
if is_bullish
choch_bearish := true
is_bullish := false
else
bos_bearish := true
if show_external_structure and show_labels
label.new(bar_index, low, choch_bearish ? "CHoCH" : "BoS",
yloc=yloc.belowbar, style=label.style_label_up,
color=color.new(color.red, 50), textcolor=color.white,
size=size.tiny)
// =============================================================================
// SECTION 3 - Order Block Detection
// =============================================================================
// When a break of structure occurs, the LAST opposite-color candle before
// the break is the order block. Optional volume filter excludes low-volume
// candles that historically produce weaker zones.
// Mitigation runs BEFORE creation and skips each OB's creation bar, so a
// fresh OB can only be mitigated by a later return into the zone - never by
// the breakout bar that defined it.
if barstate.isconfirmed and obs.size() > 0
for i = 0 to obs.size() - 1
OrderBlock ob = obs.get(i)
if not ob.mitigated and bar_index > ob.created_bar
bool hit = ob.bullish ? (low <= ob.top) : (high >= ob.bottom)
if hit
ob.mitigated := true
box ref = ob_boxes.get(i)
if not na(ref)
ref.set_bgcolor(color.new(color.gray, 90))
ref.set_border_color(color.gray)
ref.set_extend(extend.none)
ref.set_right(bar_index)
if barstate.isconfirmed and ob_enabled and break_up
int found_bar = -1
for i = 1 to OB_LOOKBACK_BARS
if close[i] < open[i]
found_bar := i
break
if found_bar > 0
if not ob_volume_filter or volume[found_bar] > vol_sma_value[found_bar]
color bg = color.new(ob_bullish_color, box_transparency)
box new_box = box.new(left=bar_index - found_bar, top=high[found_bar],
right=bar_index, bottom=low[found_bar],
border_color=ob_bullish_color, bgcolor=bg, extend=extend.right)
obs.push(OrderBlock.new(high[found_bar], low[found_bar], bar_index, false, true))
ob_boxes.push(new_box)
while obs.size() > ob_max_count
obs.shift()
box dropped = ob_boxes.shift()
if not na(dropped)
dropped.delete()
if barstate.isconfirmed and ob_enabled and break_down
int found_bar = -1
for i = 1 to OB_LOOKBACK_BARS
if close[i] > open[i]
found_bar := i
break
if found_bar > 0
if not ob_volume_filter or volume[found_bar] > vol_sma_value[found_bar]
color bg = color.new(ob_bearish_color, box_transparency)
box new_box = box.new(left=bar_index - found_bar, top=high[found_bar],
right=bar_index, bottom=low[found_bar],
border_color=ob_bearish_color, bgcolor=bg, extend=extend.right)
obs.push(OrderBlock.new(high[found_bar], low[found_bar], bar_index, false, false))
ob_boxes.push(new_box)
while obs.size() > ob_max_count
obs.shift()
box dropped = ob_boxes.shift()
if not na(dropped)
dropped.delete()
// =============================================================================
// SECTION 4 - Fair Value Gap
// =============================================================================
// A 3-candle gap pattern. Filter by ATR multiplier to skip noise-sized gaps.
// Fill check runs BEFORE creation and skips each FVG's creation bar: a
// bullish FVG's top IS the creation bar's low, so a same-bar check would
// mark every gap filled at birth (the v1.0.0 bug).
if barstate.isconfirmed and fvgs.size() > 0
for i = 0 to fvgs.size() - 1
FVG g = fvgs.get(i)
if not g.filled and bar_index > g.created_bar
bool hit = g.bullish ? (low <= g.top) : (high >= g.bottom)
if hit
g.filled := true
box ref = fvg_boxes.get(i)
if not na(ref)
ref.set_bgcolor(color.new(color.gray, 90))
ref.set_border_color(color.gray)
ref.set_extend(extend.none)
ref.set_right(bar_index)
if barstate.isconfirmed and fvg_enabled and bar_index > 2
float min_size = atr_value * fvg_min_size_atr
// Bullish FVG: current low is above the high two bars back.
if low > high[2] and (low - high[2]) >= min_size
color bg = color.new(fvg_bullish_color, box_transparency)
box bnew = box.new(left=bar_index - 1, top=low, right=bar_index, bottom=high[2],
border_color=fvg_bullish_color, bgcolor=bg, extend=extend.right)
fvgs.push(FVG.new(low, high[2], bar_index, false, true))
fvg_boxes.push(bnew)
while fvgs.size() > fvg_max_count
fvgs.shift()
box dropped = fvg_boxes.shift()
if not na(dropped)
dropped.delete()
// Bearish FVG: current high is below the low two bars back.
if high < low[2] and (low[2] - high) >= min_size
color bg = color.new(fvg_bearish_color, box_transparency)
box bnew = box.new(left=bar_index - 1, top=low[2], right=bar_index, bottom=high,
border_color=fvg_bearish_color, bgcolor=bg, extend=extend.right)
fvgs.push(FVG.new(low[2], high, bar_index, false, false))
fvg_boxes.push(bnew)
while fvgs.size() > fvg_max_count
fvgs.shift()
box dropped = fvg_boxes.shift()
if not na(dropped)
dropped.delete()
// =============================================================================
// SECTION 5 - Liquidity
// =============================================================================
// Equal-highs / equal-lows form liquidity pools.
// Buy-side liquidity (BSL): equal highs, where stop-losses from short
// positions cluster above price.
// Sell-side liquidity (SSL): equal lows, where stop-losses from long
// positions cluster below price.
// A new pivot within ATR-scaled tolerance of any prior pivot in the buffer
// counts as a liquidity level. The buffer holds at most PIVOT_BUFFER_SIZE
// pivots AND evicts pivots older than liq_lookback bars, so equality is
// only ever measured against recent history.
// Freeze a liquidity line once its level is consumed (swept or broken).
freezeLiqLine(line ln) =>
if not na(ln)
ln.set_color(color.new(color.gray, 60))
ln.set_extend(extend.none)
ln.set_x2(bar_index)
// Sweep / invalidation pass. Runs before creation; skips creation bars.
// Sweep = wick beyond the level, close back inside -> alert + label.
// Broken = close beyond the level (no sweep pattern) -> silent invalidation:
// a level price has closed through is no longer resting liquidity,
// and keeping it armed would fire bogus "swept" alerts on a later
// routine pullback.
if barstate.isconfirmed and liqs.size() > 0
for i = 0 to liqs.size() - 1
LiquidityLevel lvl = liqs.get(i)
if not lvl.swept and bar_index > lvl.created_bar
if lvl.is_bsl
if high > lvl.price and close < lvl.price
lvl.swept := true
bsl_swept := true
freezeLiqLine(liq_lines.get(i))
if show_labels
label.new(bar_index, high, "BSL Swept",
yloc=yloc.abovebar, style=label.style_label_down,
color=color.new(color.red, 30), textcolor=color.white,
size=size.tiny)
else if close > lvl.price
lvl.swept := true
freezeLiqLine(liq_lines.get(i))
else
if low < lvl.price and close > lvl.price
lvl.swept := true
ssl_swept := true
freezeLiqLine(liq_lines.get(i))
if show_labels
label.new(bar_index, low, "SSL Swept",
yloc=yloc.belowbar, style=label.style_label_up,
color=color.new(color.green, 30), textcolor=color.white,
size=size.tiny)
else if close < lvl.price
lvl.swept := true
freezeLiqLine(liq_lines.get(i))
if barstate.isconfirmed and liq_enabled and not na(ph)
int piv_bar = bar_index - swing_length
// Evict pivots older than the lookback window (buffer is chronological).
while recent_ph_bars.size() > 0 and piv_bar - recent_ph_bars.get(0) > liq_lookback
recent_ph_bars.shift()
recent_ph_prices.shift()
bool equal = false
if recent_ph_prices.size() > 0
for i = 0 to recent_ph_prices.size() - 1
if math.abs(ph - recent_ph_prices.get(i)) <= atr_value * liq_tolerance_atr
equal := true
break
if equal
line new_line = line.new(piv_bar, ph, bar_index, ph, extend=extend.right,
color=color.new(color.aqua, 30), style=line.style_dashed, width=1)
liqs.push(LiquidityLevel.new(ph, bar_index, false, true))
liq_lines.push(new_line)
while liqs.size() > liq_max_count
liqs.shift()
line dropped = liq_lines.shift()
if not na(dropped)
dropped.delete()
recent_ph_prices.push(ph)
recent_ph_bars.push(piv_bar)
if recent_ph_prices.size() > PIVOT_BUFFER_SIZE
recent_ph_prices.shift()
recent_ph_bars.shift()
if barstate.isconfirmed and liq_enabled and not na(pl)
int piv_bar = bar_index - swing_length
while recent_pl_bars.size() > 0 and piv_bar - recent_pl_bars.get(0) > liq_lookback
recent_pl_bars.shift()
recent_pl_prices.shift()
bool equal = false
if recent_pl_prices.size() > 0
for i = 0 to recent_pl_prices.size() - 1
if math.abs(pl - recent_pl_prices.get(i)) <= atr_value * liq_tolerance_atr
equal := true
break
if equal
line new_line = line.new(piv_bar, pl, bar_index, pl, extend=extend.right,
color=color.new(color.orange, 30), style=line.style_dashed, width=1)
liqs.push(LiquidityLevel.new(pl, bar_index, false, false))
liq_lines.push(new_line)
while liqs.size() > liq_max_count
liqs.shift()
line dropped = liq_lines.shift()
if not na(dropped)
dropped.delete()
recent_pl_prices.push(pl)
recent_pl_bars.push(piv_bar)
if recent_pl_prices.size() > PIVOT_BUFFER_SIZE
recent_pl_prices.shift()
recent_pl_bars.shift()
// =============================================================================
// ALERTS
// =============================================================================
// All flags are set on confirmed bars only. Recommended alert frequency:
// "Once Per Bar Close".
alertcondition(bos_bullish, "BoS Bullish", "Bullish break of structure detected")
alertcondition(bos_bearish, "BoS Bearish", "Bearish break of structure detected")
alertcondition(choch_bullish, "CHoCH Bullish", "Bullish change of character detected")
alertcondition(choch_bearish, "CHoCH Bearish", "Bearish change of character detected")
alertcondition(bsl_swept, "BSL Swept", "Buy-side liquidity swept")
alertcondition(ssl_swept, "SSL Swept", "Sell-side liquidity swept")
Overview
Active zones project forward and freeze or stop extending once mitigated or filled. Users can configure swing detection lengths, display limits, volume filters for order blocks, and minimum ATR thresholds for FVGs and liquidity tolerance. All state logic executes on confirmed bars to prevent real-time repainting, serving as an analytical charting tool without execution logic.
Market structure detection for BoS and CHoCH
Order blocks with mitigation tracking and volume filter
Fair Value Gap detection with ATR size threshold
Liquidity level detection for equal highs and lows
Confirmed bar execution to prevent phantom signals
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Swing detection length
int
5
swing_length
Show structure labels
bool
true
show_external_structure
Enable Order Blocks
bool
true
ob_enabled
Max OBs to display (total)
int
5
ob_max_count
Filter by volume
bool
true
ob_volume_filter
Bullish OB color
color
color.new(color.green, 70)
ob_bullish_color
Bearish OB color
color
color.new(color.red, 70)
ob_bearish_color
Enable FVG
bool
true
fvg_enabled
Max FVGs to display
int
5
fvg_max_count
Min FVG size (ATR multiplier)
float
0.5
fvg_min_size_atr
Bullish FVG color
color
color.new(color.aqua, 80)
fvg_bullish_color
Bearish FVG color
color
color.new(color.orange, 80)
fvg_bearish_color
Show all 18 settings
Setting
Type
Default
Variable
Enable Liquidity
bool
true
liq_enabled
Max liquidity levels
int
10
liq_max_count
Equal highs/lows lookback (bars)
int
50
liq_lookback
Tolerance (ATR multiplier)
float
0.2
liq_tolerance_atr
Show event labels
bool
true
show_labels
Box transparency
int
70
box_transparency
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This indicator tracks smart money market structure directly on the price chart. It calculates dual-period swing highs and swing lows to detect Changes of Character (CHoCH), Breaks of Structure (BOS), inducements, and liquidity sweeps. When structural levels ar
The CISD with Fib/Range Extensions and Stats indicator tracks Change in State of Delivery (CISD) level breakouts on the price chart. It detects pullback structures filtered by ATR and volume conditions, then projects future target levels using customizable Fib
This indicator maps Smart Money Concepts (SMC) market structure directly onto the price chart. It detects internal and swing structure breaks (BOS and CHoCH), tracks bullish and bearish order blocks, identifies fair value gaps (FVG), highlights equal highs and
This script identifies and visualizes bullish and bearish order blocks on the price chart. It detects low-volatility consolidation ranges followed by breakout moves accompanied by high trading volume. Users can select between Simple, Relative, or Weighted volu
This indicator implements a Wyckoff method state machine that tracks accumulation, distribution, markup, and markdown regimes across phases A through E. It identifies structural events including springs, UTADs, SOS, SOW, LPS, LPSY, and climactic bars using ATR
This multi-timeframe indicator detects and visualizes Smart Money Concepts (SMC) and ICT market structures on the price chart. It maps higher-timeframe dealing ranges, premium and discount zones, break of structure (BOS), change of character (CHoCH), order blo