ProEA Lab · free & open-source indicator

Order Blocks & FVGs, explained properly.

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.

Get the indicator — free ↓
SMC Basics running live on a gold (XAUUSD) chart: bullish and bearish Order Blocks and Fair Value Gaps with dashed CE midlines drawn on real candles

The exact indicator this page teaches — on a real chart, zones drawn the moment they confirmed.

00 · GET IT — FREE & OPEN-SOURCE

Paste it into TradingView — about thirty seconds

  1. On any TradingView chart, open Pine Editor — the Pine icon in the right-hand rail. A free account works.
  2. Delete the sample code and paste the full source below.
  3. Press Add to chart. Done — save the script to keep it.
⤓ Or download the .pine file
smc-basics.pine · Pine v6 · open-source
//@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.

SMC Basics anatomy: a confirmed swing break creates an Order Block box and a Fair Value Gap box with its dashed CE midline
  • 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.

Three steps: a swing high confirms, a candle closes through it, and the last opposite candle before the impulse becomes the Order Block
  • 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

A 3-candle imbalance leaves a gap with a CE midline; rejection at the CE means the gap is defended, slicing through it means the gap is failing

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

Three zone states: fresh with a solid fill, touched with a faded fill after price tested it, mitigated and removed after a close through the far edge
  • 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.

The stack: a Fair Value Gap overlapping an Order Block while price returns into the combined zone
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.

Two panels: trading a zone with the break of structure versus fading against a fresh break, which is the mistake
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."

06 · SETTINGS RECIPES

Set it up for your style

STYLECHARTPIVOT LENGTHCAPS · FVG MIN
ScalpingM1–M56–8 — fast structure2–3 per side · 0.2 ATR
Day tradingM15–H110 — the default tuning3 per side · 0.3 ATR
SwingH4 / D14–20 — slower, cleaner2 per side · 0.4–0.5 ATR · wick OBs on
  • Lower pivot length = more zones, faster, noisier. Higher = fewer, slower, cleaner.
  • 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

Six-step workflow: mark the higher timeframe first, drop to the execution timeframe, act only where zones overlap, read the touch, let structure decide direction, move on when mitigated

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.

CONTEXT, NOT A SIGNAL · IT MARKS WHERE STRUCTURE BROKE, NEVER WHAT PRICE WILL DO · FREE & OPEN-SOURCE · RESEARCH TOOLING, NOT FINANCIAL ADVICE

© 2026 ProEA · All rights reservedBuilt for MT5 · Not affiliated with MetaQuotes

Past performance ≠ future. Backtest is broker-specific.