SMC Basics draws the freshest Order Blocks and Fair Value Gaps — confirmed, frozen, never redrawn — with the CE midline most free tools skip. This page is the complete manual: what the zones actually are, how professionals read them, and what the tool honestly cannot do.
//@version=6
// ============================================================================
// SMC Basics — ProEA Lab
// ----------------------------------------------------------------------------
// Free, open-source Smart-Money-Concepts starter: auto-detected Order Blocks
// and Fair Value Gaps with honest, non-repainting mechanics.
//
// What it draws
// · Order Blocks — on a confirmed swing break (BOS), the last opposite
// candle before the impulse. Geometry frozen at creation.
// · Fair Value Gaps — 3-candle imbalances larger than an ATR-scaled minimum.
// · Basic mitigation — zones fade once price trades into them and are
// removed (or ghosted) once price CLOSES through the far edge.
//
// Honest notes
// · Swing points confirm `pivotLen` bars late by construction — that is what
// makes them non-repainting. Nothing here predicts anything.
// · This is the free sibling of the SMC AI-Scored Toolkit (scoring, sweeps,
// regime, verdict, trade-plan ticket live there). This one stays simple
// on purpose: the freshest zones only, drawn cleanly.
// ============================================================================
indicator("SMC Basics — ProEA Lab", "SMC Basics", overlay = true, max_boxes_count = 60, max_labels_count = 30, max_lines_count = 40)
// ===== 1 · INPUTS ===========================================================
G1 = "Structure"
pivotLen = input.int(10, "Swing pivot length", minval = 3, maxval = 50, group = G1, tooltip = "Swing highs/lows confirm this many bars after they form — later, but honest.", display = display.none)
G2 = "Order Blocks"
showOB = input.bool(true, "Show Order Blocks", group = G2, display = display.none)
obUseWick = input.bool(false, "Use full wick (off = candle body)", group = G2, display = display.none)
obMax = input.int(3, "Keep last … per side", minval = 1, maxval = 5, group = G2, display = display.none)
G3 = "Fair Value Gaps"
showFVG = input.bool(true, "Show Fair Value Gaps", group = G3, display = display.none)
fvgMinAtr = input.float(0.3, "Min gap size (× ATR)", minval = 0.0, maxval = 2.0, step = 0.1, group = G3, display = display.none)
fvgMax = input.int(3, "Keep last … per side", minval = 1, maxval = 5, group = G3, display = display.none)
showCE = input.bool(true, "CE midline (50% of the gap)", group = G3, tooltip = "Consequent encroachment — the half-way line of an FVG. Pros judge a gap by how price behaves at this line.", display = display.none)
G4 = "Display"
showTags = input.bool(true, "Zone tags (OB / FVG)", group = G4, display = display.none)
keepGhost = input.bool(false, "Keep mitigated zones as ghost outlines", group = G4, display = display.none)
showChip = input.bool(true, "Header chip", group = G4, display = display.none)
// ===== 2 · PALETTE (fixed Aurora — matches the ProEA Lab family) ============
C_BULL = color.new(#22c68c, 0)
C_BEAR = color.new(#f06292, 0)
C_BULLF = color.new(#22c68c, 82)
C_BEARF = color.new(#f06292, 82)
C_BULLT = color.new(#22c68c, 90) // touched (faded) fill
C_BEART = color.new(#f06292, 90)
C_GHOST = color.new(#8a93a6, 78)
C_CHIP = color.new(#0e1430, 8)
C_TXT = color.new(#e8ecf8, 0)
// ===== 3 · SHARED SERIES (all ta.* global) ==================================
atr14 = ta.atr(14)
phPrice = ta.pivothigh(high, pivotLen, pivotLen)
plPrice = ta.pivotlow(low, pivotLen, pivotLen)
// the OB scan reads candles at a dynamic offset — size the buffers explicitly
max_bars_back(open, 60)
max_bars_back(high, 60)
max_bars_back(low, 60)
max_bars_back(close, 60)
// ===== 4 · TYPES + STATE ====================================================
// state: 0 = fresh · 1 = touched · 2 = mitigated
type Zone
box bx
label tg
line ce // FVG only: the 50% (consequent encroachment) midline
int kind = 0 // 0 = OB, 1 = FVG
int side = 1 // 1 = bull, -1 = bear
float top = na
float bottom = na
int state = 0
var array<Zone> zones = array.new<Zone>()
// swing structure memory (committed on confirmed bars only)
var float lastSwingHi = na
var float lastSwingLo = na
var int lastHiBar = na
var int lastLoBar = na
var bool brokeUpOnce = false
var bool brokeDnOnce = false
// ===== 5 · HELPERS ==========================================================
zoneFill(int side, int state) =>
state == 1 ? (side == 1 ? C_BULLT : C_BEART) : (side == 1 ? C_BULLF : C_BEARF)
// removes the oldest zones of one (kind, side) beyond the cap — keeps freshest
enforceCap(array<Zone> pool, int kind, int side, int cap) =>
if array.size(pool) > 0
count = 0
for i = array.size(pool) - 1 to 0
z = array.get(pool, i)
if z.kind == kind and z.side == side and z.state != 2
count += 1
if count > cap
box.delete(z.bx)
label.delete(z.tg)
line.delete(z.ce)
zDrop = array.remove(pool, i)
true
// hard ceiling on the whole pool (ghosts included) so nothing grows unbounded
prunePool(array<Zone> pool) =>
if array.size(pool) > 36
z = array.get(pool, 0)
box.delete(z.bx)
label.delete(z.tg)
line.delete(z.ce)
zDrop = array.remove(pool, 0)
true
registerZone(array<Zone> pool, int kind, int side, float zTop, float zBot, int leftBar, int cap) =>
kindTxt = kind == 0 ? "OB" : "FVG"
bx = box.new(leftBar, zTop, bar_index, zBot,
border_color = side == 1 ? C_BULL : C_BEAR, border_width = 1,
bgcolor = side == 1 ? C_BULLF : C_BEARF, extend = extend.right)
label tg = na
if showTags
tg := label.new(leftBar, zTop, kindTxt,
style = side == 1 ? label.style_label_lower_right : label.style_label_upper_right,
color = color.new(color.black, 100), textcolor = side == 1 ? C_BULL : C_BEAR,
size = size.tiny)
line ceLn = na
if kind == 1 and showCE
mid = (zTop + zBot) / 2
ceLn := line.new(leftBar, mid, bar_index, mid, extend = extend.right,
color = side == 1 ? color.new(#22c68c, 45) : color.new(#f06292, 45),
style = line.style_dashed, width = 1)
z = Zone.new(bx, tg, ceLn, kind, side, zTop, zBot, 0)
array.push(pool, z)
enforceCap(pool, kind, side, cap)
prunePool(pool)
// mitigation pass — runs on confirmed bars only; geometry never changes
updateZones(array<Zone> pool, float hi, float lo, float cl, bool ghost) =>
if array.size(pool) > 0
for i = array.size(pool) - 1 to 0
z = array.get(pool, i)
if z.state == 2
continue
mitigated = z.side == 1 ? cl < z.bottom : cl > z.top
touched = z.side == 1 ? lo <= z.top : hi >= z.bottom
if mitigated
z.state := 2
line.delete(z.ce)
if ghost
box.set_bgcolor(z.bx, color.new(color.gray, 100))
box.set_border_color(z.bx, C_GHOST)
box.set_extend(z.bx, extend.none)
box.set_right(z.bx, bar_index)
label.delete(z.tg)
true
else
box.delete(z.bx)
label.delete(z.tg)
zGone = array.remove(pool, i)
true
else if touched and z.state == 0
z.state := 1
box.set_bgcolor(z.bx, zoneFill(z.side, 1))
true
// ===== 6 · COMMIT (barstate.isconfirmed — nothing here repaints) ============
if barstate.isconfirmed
// -- remember confirmed swings ------------------------------------------
if not na(phPrice)
lastSwingHi := phPrice
lastHiBar := bar_index - pivotLen
brokeUpOnce := false
if not na(plPrice)
lastSwingLo := plPrice
lastLoBar := bar_index - pivotLen
brokeDnOnce := false
// -- Order Blocks: first close through a confirmed swing = the break ----
if showOB and not na(lastSwingHi) and not brokeUpOnce and close > lastSwingHi
brokeUpOnce := true
// last bearish candle before the impulse = bullish OB
obBar = -1
for k = 1 to 20
if close[k] < open[k]
obBar := k
break
if obBar > 0
zTop = obUseWick ? high[obBar] : math.max(open[obBar], close[obBar])
zBot = obUseWick ? low[obBar] : math.min(open[obBar], close[obBar])
registerZone(zones, 0, 1, zTop, zBot, bar_index - obBar, obMax)
if showOB and not na(lastSwingLo) and not brokeDnOnce and close < lastSwingLo
brokeDnOnce := true
obBar = -1
for k = 1 to 20
if close[k] > open[k]
obBar := k
break
if obBar > 0
zTop = obUseWick ? high[obBar] : math.max(open[obBar], close[obBar])
zBot = obUseWick ? low[obBar] : math.min(open[obBar], close[obBar])
registerZone(zones, 0, -1, zTop, zBot, bar_index - obBar, obMax)
// -- Fair Value Gaps: 3-candle imbalance ≥ ATR minimum ------------------
if showFVG
bullGap = low > high[2] and (low - high[2]) >= fvgMinAtr * atr14
bearGap = high < low[2] and (low[2] - high) >= fvgMinAtr * atr14
if bullGap
registerZone(zones, 1, 1, low, high[2], bar_index - 2, fvgMax)
if bearGap
registerZone(zones, 1, -1, low[2], high, bar_index - 2, fvgMax)
// -- basic mitigation ---------------------------------------------------
updateZones(zones, high, low, close, keepGhost)
// ===== 7 · RENDER (barstate.islast — display only) ==========================
var label chip = na
if barstate.islast
label.delete(chip)
chip := na
if showChip
chip := label.new(bar_index, high + atr14 * 3, "◆ SMC Basics | ProEA Lab",
style = label.style_label_down, color = C_CHIP, textcolor = C_TXT, size = size.small)
01 · WHAT IT DRAWS
Two mechanisms, frozen the moment they confirm
A break of structure creates the zones: the Order Block marks where the other side was absorbed before the impulse, the Fair Value Gap marks where price moved so fast that no two-way trade happened at all. Both are drawn once, on confirmed bars, and never recomputed — what you see on a closed chart is what you would have seen live.
Order Block — the last opposite-direction candle before a confirmed break of structure. Candle body by default, full wick optional.
Fair Value Gap — a 3-candle imbalance bigger than an ATR-scaled minimum, so noise gaps stay off your chart.
CE midline — a dashed line at every gap's exact 50%. This one line turns a box into a measurable behavior test.
Caps, not clutter — only the freshest zones per side are kept (up to 5 each). A chart with forty boxes is a chart with none.
02 · THE ORDER BLOCK
A market cannot break structure without absorbing someone
The last down-candle before an up-impulse is where that absorption is most visible: positions were built there, and when price returns, the same area often matters again. That is the entire OB thesis — a location where participation was proven, not a promise.
The swing must be confirmed — pivot logic, which appears bars later. That delay is what makes it honest.
The break must be a CLOSE — a wick through a swing is a poke, not a break.
Geometry is frozen at birth — the box never moves, never re-anchors, never repaints.
03 · THE FVG — AND THE CE READ
Half the information in every gap is at its midline
REJECTS AT THE CE
Price drifts back into the gap and turns at or before the 50% line. The gap is being defended — the impulse's story is intact, and the origin of the move stays the reference.
SLICES THROUGH THE CE
Price cuts the midline on momentum and keeps going. The gap is failing in real time — expect the far edge to be tested, and question the zone before it even breaks.
04 · A ZONE'S HONEST LIFE
Fresh → touched → mitigated. One direction, no arguing
FRESH — an untested claim. Nothing proven yet; size accordingly.
TOUCHED — price traded into it and the fill fades. What happened at that touch — rejection or slice-through — is real information.
MITIGATED — price CLOSED through the far edge. The indicator deletes the zone so you cannot negotiate with it (keep grey ghosts on if you want the history — broken zones often flip roles, and noticing that is where the skill grows).
05 · FIVE WAYS TRADERS USE THIS
Location first, behavior second, entry third
These are context structures, not signals — the tool never says buy or sell. Your strategy and your risk rules do.
PLAYBOOK 1 · CONTINUATION
The OB retest
Setup: structure breaks → an OB prints → you do nothing until price RETURNS to the box.
Frame: at the touch you want rejection behavior — wicks, slowing momentum — before acting in the break's direction.
Invalidated by: a close through the far edge — the indicator removes the zone and the idea with it.
PLAYBOOK 2 · REBALANCE
The FVG entry at the CE
Setup: an impulse leaves a gap → price drifts back into it → watch the CE line.
Frame: defense at or before the midline keeps the impulse's story intact; the origin of the move is the reference.
Invalidated by: slicing the CE — stand down before the far edge even breaks.
PLAYBOOK 3 · CONFLUENCE
The stack
Setup: an FVG sitting inside or immediately at an OB — absorption and one-sided delivery pointing at the same price.
Frame: when both are fresh and structure agrees, that is the A-grade location. It happens less often than you want — that is exactly why it means something.
PLAYBOOK 4 · THE FILTER
Structure outranks any zone
Setup: zones are half the sentence; the break that created them is the other half.
Frame: trade zones in the direction of the most recent break of structure. Fading a fresh opposite break because "there's a zone there" is how most SMC accounts die.
PLAYBOOK 5 · PATIENCE
Let the caps work
Setup: the tool deliberately shows only the freshest zones per side.
Frame: if your chart feels empty — good. The absence of a zone IS a signal: no location, no trade. The cap is the discipline most paid tools sell back to you as a "filter feature."
Session note: zones born during high-participation hours (London / New York for FX and metals) are backed by more real business than dead-session prints. There is no automated session filter here — check the clock; it is a one-second read.
07 · THE TWO-CHART WORKFLOW
Worth more than any setting on this page
Run this same indicator on your higher timeframe first — H4 for intraday, D for swing — then only take execution-timeframe zones that live inside the higher-timeframe ones. Each chart stays honest on its own timeframe; the pairing is the edge you build.
READ THIS BEFORE YOU TRADE WITH IT
What this tool does not do
It does not predict. It marks where structure broke and where delivery was one-sided — never what price will do next.
No zone scoring, no liquidity sweeps, no regime filter, no verdict, no alerts. Deliberately the clean minimal core — readable in one pass of the open source.
Swing points confirm bars late by construction. Anything faster is guessing, and guessing redraws.
When many zones print, YOU are the ranking engine — that is the skill this page teaches.
Trading risk is entirely yours. This is context for your decisions — not financial advice.