Cengiz.ilerler.info

Smart Money Concepts from first principles

September 24, 2026

Price action field guide

Smart Money Concepts, from first principles

Strip away the vocabulary and Smart Money Concepts rest on one observable idea. Stop orders cluster just past obvious highs, lows and round numbers, and when price reaches them they fire together and push price hard. SMC is a vocabulary for how price reaches those pools, reacts, and travels to the next one. Use it as a model you test, not as a view into anyone's order book.

  • One chart
  • Five phases
  • Every term defined once
$495.75$496.25$496.75$497.25$497.75$498.25$498.75$499.25$499.75$500.25HLLHLLLHLLHHHLHHBOSCHoCHBOSBSL $500.00BSL $498.65SSL · equal lowsSweepFVGBullish OBPremiumDiscountOTE $496.59 to $496.95EQ 0.5 · $497.20TP $500.00 · +$2.95Entry $497.05SL $496.05 · −$1.00Opening selloffBuildSweepDisplaceReturnExpand510152025303540
The master chart: an illustrative MSFT 5-minute session from 9:30 to 12:55 ET. Prices are synthetic, drawn for teaching, and do not show real trading. Every figure below is a slice of this chart, and the numbers along the bottom are bar numbers. Toggle the layers to isolate each idea.
Contents
  1. 01The one idea: liquidity
  2. 02The five-phase cycle
  3. 03What the evidence supports
  4. 04Market structure
  5. 05Liquidity and sweeps
  6. 06Displacement and fair value gaps
  7. 07Order blocks and breakers
  8. 08Premium, discount and OTE
  9. 09Timing and the session clock
  10. 10The full model, one trade
  11. 11Costs, sizing and losing streaks
  12. 12Testing the model
  13. 13Advanced terms
  14. 14Common mistakes
  15. 15Pre-trade checklist
  16. 16Questions

011 Build

The one idea: liquidity

Any order can only fill against the liquidity that exists at that moment. An order larger than the resting interest on the other side has two choices: push price against itself, or wait for a moment when a crowd of opposite orders arrives at once.

Stop orders create those moments. Traders who are long place stop losses just below an obvious low, and breakout traders place sell orders in the same spot. When price trades there, those orders turn into market sells together. A buyer with size can fill against that burst of selling, and the fired stops often push price further in a quick cascade before it turns.

Above highs the picture is the mirror image: short sellers' stop losses and breakout buy orders.

Swing highSwing low Buy-side liquidity short stops, breakout buys Sell-side liquidity long stops, breakdown sells Large buy order needs sellers in size fills against the fired stops then price runs to the next pool
Stops cluster just beyond obvious highs and lows. A sweep into the sell stops supplies the sellers a large buyer needs, and the rally that follows runs toward the buy stops on the other side.
BSLBuy-side liquidity
Buy stops resting above highs. They supply the buyers a large seller needs.
SSLSell-side liquidity
Sell stops resting below lows. They supply the sellers a large buyer needs.

This part is measured, not assumed. Order records from a major currency dealing bank showed buy stops clustered just above round numbers and sell stops just below, and prices moved unusually fast once they reached those clusters. Chapter 03 separates what is measured from what is only convention or story.

Price tends to travel from one pool of liquidity to the next. The rest of SMC is vocabulary for the steps along the way.

02

The five-phase cycle

On every timeframe, the trip between pools runs through the same five phases. Each SMC term belongs to exactly one of them, so once the phases are clear, every new term has an obvious place to go. The small charts are slices of the master chart, with the phase's bars shaded.

  1. 1 Build

    Stops collect beyond highs and lows. Equal highs and equal lows collect the most.

    BSL, SSL, equal highs and lows, inducement

    WyckoffTrading range

  2. 2 Sweep

    Price runs one pool. The fired stops supply the other side of a large order.

    Sweep, stop run, SMT divergence

    WyckoffSpring

  3. 3 Displace

    Price moves hard the other way and closes through a swing.

    Displacement, CHoCH, CISD

    WyckoffSign of strength

  4. 4 Return

    Price pulls back into the zone the fast move left behind.

    FVG, order block, breaker, OTE, premium and discount

    WyckoffLast point of support

  5. 5 Expand

    Price travels to the opposite pool, which becomes fuel for the next cycle.

    BOS, liquidity targets, take profit

    WyckoffMarkup

The fifth phase feeds the first. The pool that price runs to becomes the edge of the next range, and the cycle starts again. Compressed into one session or one day, the sequence is often called the Power of Three: accumulation (build), manipulation (sweep) and distribution (displace, return, expand).

The sequence is much older than SMC. Richard Wyckoff described the same shape for stocks in the early twentieth century as accumulation: a trading range, a spring below its low, a sign of strength, a last point of support, then markup. The bottom line of each card gives his name for the phase.

03

What the evidence supports

SMC mixes three kinds of statements: things measured in market data, conventions that make charts readable, and stories about who is trading. They deserve different amounts of trust.

ClaimStatusBasis
Stop orders cluster just beyond round numbers and obvious levels.MeasuredIn order data from a major currency dealing bank, buy stops sat just above round numbers and sell stops just below them (Osler).
Order flow bunches around round numbers in stocks too.MeasuredAcross more than 100 million US stock trades, buying was excessive just below round numbers and selling just above them (Bhattacharya, Holden and Jacobsen, 2012).
Fired stops make price move faster.MeasuredExchange rates trended unusually fast once they reached documented stop clusters, and the effect showed at the hourly horizon (Osler, 2005).
Activity follows the session clock and spikes on scheduled news.MeasuredNYSE spreads are highest near the open and close (McInish and Wood, 1992). Scheduled releases cause sharp, short volatility bursts (Andersen and Bollerslev, 1998).
Swings, sweeps, BOS and CHoCH.ConventionPrecise ways to describe price. They organize the chart; on their own they do not forecast.
62%, 70.5% and 79% retracement levels.ConventionA definition of a deep pullback, not levels with proven special properties.
Fair value gaps and order blocks attract price and hold.UntestedNo widely cited peer-reviewed study shows an edge. Measure it on your own market with coded rules.
The chart shows what institutions are doing.NarrativeUS stocks trade across many exchanges and off-exchange venues, and much retail flow is filled by wholesalers. A chart shows where price went, not who moved it.

Most of the vocabulary was popularized by Michael J. Huddleston's Inner Circle Trader (ICT) material. The ideas underneath are older: Wyckoff's accumulation and distribution schematics and classic supply and demand zones. The practical rule is simple. Rely on the measured parts, use the conventions as definitions, and test everything else before risking money on it.

043 Displace5 Expand

Market structure

Structure is the record of swing points. A swing high is a bar whose high sits above the highs on either side of it, and a swing low is the mirror image. An uptrend prints higher highs (HH) and higher lows (HL). A downtrend prints lower highs (LH) and lower lows (LL).

Two events matter, and both require a candle close beyond a swing.

BOSBreak of structure
A close beyond a swing in the direction of the trend. The trend is still being delivered.
CHoCHChange of character
The first close beyond a swing against the trend. Control has changed hands, at least for now.
$496.25$496.75$497.25$497.75$498.25$498.75$499.25$499.75$500.25HLLHLLLHLLHHHLBOSCHoCHBOS5101520253035
Bars 1 to 35 · 9:30 to 12:20Bar 12 wicks below the $497.75 low and closes back above it. Bar 13 closes below and confirms the bearish BOS. The first close above a lower high comes at bar 28 (above $497.80): that is the CHoCH. Bar 34 closes above $498.25 and confirms the new uptrend with a BOS.

Only closes count. A wick beyond a swing means liquidity was taken, while a close beyond it means structure changed. That rule alone removes many false signals.

In code, a swing is usually a pivot: a bar with N lower highs on each side (N = 2 on the master chart). That definition has a cost. A pivot is only known N bars after it forms, so an indicator that draws it on the pivot bar is showing information that arrived later. Any backtest has to account for that lag.

Internal and external structure

External structure is the set of swings that define the current leg on your chosen timeframe, like the ones labeled above. Internal structure is the smaller swings inside a leg. Read direction from the external swings and use internal breaks only to time an entry. An internal CHoCH against the external trend is usually just a pullback.

051 Build2 Sweep

Liquidity and sweeps

Obvious levels attract the most stops, and equal highs or equal lows are the most obvious of all. Two touches at the same price look like strong support or resistance, which invites stops just beyond them. In stocks, traders watch the prior day's high and low, the premarket high and low, the opening range (the first 15 or 30 minutes), and round numbers such as $500.

"Equal" needs a tolerance. A fraction of the average true range scales across stocks and timeframes: 0.1 × ATR(14) is about $0.05 on the master chart, which is exactly how far apart its three lows sit.

$496.25$496.75$497.25$497.75$498.25$498.75BSL $498.65SSL · equal lowsSweep152025
Bars 13 to 29 · 10:30 to 11:50Three lows at $496.70 to $496.75 (bars 15, 20 and 23) form a shelf of sell-side liquidity. Bar 26 trades 55 cents through it to $496.15, then closes back above at $496.80. The buy-side pool at $498.65 is still untouched.

When price trades through a pool, the close tells you what happened.

Sweepwick through, close back inside

wick only

Breakclose through, hold beyond

close below
A sweep takes the stops and fails to hold beyond the level, which suggests the orders were absorbed. A break closes beyond the level and holds, and the level stops mattering. Until the candle closes, the two look identical.

063 Displace4 Return

Displacement and fair value gaps

After a sweep, price often moves one way in large-bodied candles that barely overlap. This is displacement. A usable definition is a candle whose range is at least 1.5 times the recent average true range and that closes near its extreme. Bar 27 on the master chart spans $1.10, 2.3 times the 14-bar ATR of $0.48, and closes 10 cents from its high.

Displacement leaves a footprint, a three-candle pattern called a fair value gap (FVG). Prices inside the gap traded only during one fast candle, almost entirely in one direction.

Candle 3 low $497.60CE midpoint $497.33Candle 1 high $497.05123gap: $0.55filled2530
Bars 25 to 32 · 11:30 to 12:05Bars 26 to 28. The high of candle 1 ($497.05) and the low of candle 3 ($497.60) leave a 55 cents gap that only candle 2 traded through. Price comes back and fills it at bar 32.
Bullish FVGlow of candle 3 > high of candle 1zone from high 1 to low 3
Bearish FVGhigh of candle 3 < low of candle 1zone from high 3 to low 1
Pine ScriptbullFvg = low > high[2]bearFvg = high < low[2]

The midpoint of the gap is called consequent encroachment (CE). The usual explanation for returns into a gap is that the fast move left little two-way trade there. Treat it as a tendency to measure, not a law: in strong trends many gaps are never revisited. In stocks, the overnight gap between sessions, especially after earnings, is the largest gap you will see.

$496.25$496.75$497.25$497.75$498.25$498.75$499.25$499.75$500.25510152025303540
Bars 1 to 41 · 9:30 to 12:50By the strict three-candle rule, the master chart holds 18 fair value gaps (gray). Only one (violet) formed straight out of a liquidity sweep and produced a change of character. That context is what makes a gap worth trading.

074 Return

Order blocks and breakers

An order block is the last opposing candle before displacement: the final down candle before a strong rally, or the final up candle before a strong drop. SMC reads it as the last point where the other side was in control. Its practical value is simpler. It is a small, well-defined zone that gives the entry and the stop a clear location.

$496.25$496.75$497.25$497.75$498.25$498.75FVGBullish OBDisplacementLast down candle253035
Bars 21 to 36 · 11:10 to 12:25Bar 26 is the last down candle before displacement. Its range, $496.15 to $497.05, is the bullish order block. The top of the block is exactly the bottom of the fair value gap, so the two zones share an edge.

A candle only earns the name when three things are true. It swept liquidity or sits right beside a sweep. The move away from it displaced and left a fair value gap. That move broke structure. Without all three, it is just the last red candle.

Breakers

When price closes through an order block instead of respecting it, the usual explanation is that traders who entered there are now losing, and many exit near breakeven when price returns to the block from the other side. Whatever the cause, the failed block is called a breaker, and it is traded in the opposite direction.

Bullish OBBreaker, now resistanceclose through the block
Schematic. A bullish order block fails when a candle closes below it. On the retest from below, the zone acts as resistance.

084 Return

Premium, discount and OTE

A leg with a clear swing low and swing high defines a dealing range. Its midpoint, equilibrium (EQ), splits it in two. Above equilibrium, price is at a premium, and below it, at a discount. The reason to buy in discount is arithmetic: the deeper the entry, the shorter the stop and the longer the distance to the target, so the same idea pays more R.

The optimal trade entry (OTE) narrows the discount to the 62% to 79% retracement band, with 70.5% as the traditional midpoint. Treat those ratios as a definition of a deep pullback rather than as precise levels. What matters is that the band overlaps a real zone.

PremiumDiscountOTE $496.59 to $496.95EQ 0.5 · $497.20Range high $498.25Range low $496.15$496.95253035
Bars 25 to 35 · 11:30 to 12:20The dealing range runs from the sweep low ($496.15) to the displacement high ($498.25). Equilibrium is $497.20 and OTE spans $496.59 to $496.95. Bar 32 dips to $496.95, the 62% level, just under the point where the fair value gap and the order block meet ($497.05). Three reasons to buy sit within 10 cents of each other.

09

Timing and the session clock

Liquidity builds while the market is quiet and gets taken when volume arrives. For US stocks, that rhythm follows the session clock and the calendar.

8:30 economic data14:00 FOMC16:00 earnings PremarketRegular sessionAfter hours OpenMidday lullPower hour sweeps, fast reversalsranges buildtrend or reverse master chart, 9:30 to 12:55 8:009:0010:0011:0012:0013:0014:0015:0016:0017:00
Times in ET. Spreads are widest near the open and the close, and volume and volatility usually peak in the first and last hour and fade at midday. Scheduled events move whole markets: economic data at 8:30, FOMC decisions at 14:00 on eight scheduled days a year, and company earnings. Microsoft reports after the close, so the reaction shows up as the next morning's gap.

A classic study of currency markets found that a major scheduled release, the US employment report, produced a jump of roughly 576% in five-minute volatility that faded within the day. The lesson carries over to stocks. The first minutes after scheduled news belong to whoever is fastest, and a limit order resting in a zone can fill at a much worse price than planned.

101 Build2 Sweep3 Displace4 Return5 Expand

The full model, one trade

Put the pieces in order and a complete setup appears. Assume MSFT's daily and 1 hour charts are in an uptrend, so the morning selloff is a pullback and only longs are considered. Each number on the chart matches a step below.

$495.75$496.25$496.75$497.25$497.75$498.25$498.75$499.25$499.75$500.25$500.75CHoCHBOSBSL $500.00BSL $498.65SSL · equal lowsTP $500.00 · +$2.95Entry $497.05SL $496.05 · −$1.00123456510152025303540
Bars 1 to 41 · 9:30 to 12:50Long from $497.05, stop $496.05, target $500.00. The red box is the risk and the green box is the reward.
  1. Build. Three equal lows at $496.70 to $496.75 (bars 15, 20 and 23) hold a pool of sell stops.
  2. Sweep. Bar 26 (11:35) runs the pool to $496.15 and closes back above it at $496.80.
  3. Displace. Bars 27 and 28 rally $1.35. Bar 28 closes at $498.15, above the $497.80 lower high (CHoCH), and leaves a gap from $497.05 to $497.60.
  4. Return. Bar 32 (12:05) pulls back to $496.95, into the gap, the order block and the 62% level. A limit buy at $497.05 fills, with the stop at $496.05, 10 cents under the sweep low.
  5. Expand. Bar 34 (12:15) closes at $498.60, above the $498.25 swing high (BOS), and takes the first pool of buy stops at $498.65.
  6. Target. Bar 40 (12:45) trades to $500.10, through the $500.00 high and the buy stops above it. The trade closes at the target.
Risk$1.00/sh
Reward$2.95/sh
Multiple2.95R
Break-even win rate25.3%

At almost 3R per winner, a trader who is right one time in three still comes out ahead, before costs. Chapter 11 adds costs and shows what that edge feels like over 100 trades.

Two ways to enter

Limit at the zoneused here
Rest a limit order where the gap, the order block and the OTE band overlap. Best R and no chasing, but some trades never fill, and you buy while price is still falling.
Confirmationlower timeframe
Wait for a 1-minute CHoCH inside the zone, then enter. Fewer losers and a clearer invalidation, but a worse price and some missed moves.

What cancels the setup

  • A candle closes below the sweep low at $496.15. The idea is wrong, whether or not the limit filled.
  • Price reaches $498.65 before returning to the zone. The move happened without you, so do not chase it.
  • Earnings, 8:30 data or an FOMC decision is due before the trade can reasonably play out.

11

Costs, sizing and losing streaks

A correct read of the chart is worth nothing without the arithmetic around it. Three numbers decide whether the model survives: costs, position size and losing streaks.

Costs

Spread, commission and slippage come out of every trade. MSFT is one of the most liquid stocks in the world, but a one-cent spread and a cent of slippage still add up to $0.02 a share round trip. Costs rise at the open and around news, exactly when these setups tend to trigger.

Net risk$1.02/sh
Net reward$2.93/sh
Net multiple2.87R
Break-even win rate25.8%

Position size

Risk amountaccount × risk per trade$10,000 × 1% = $100
Stop distanceentry to stop, plus costs$1.02 per share
Shares from riskrisk amount ÷ stop distance$100 ÷ $1.02 = 98 shares
Position valueshares × entry98 × $497.05 = $48,711
Buying power capwhat the account can hold20 shares in cash, 40 at 2:1 margin

Size follows the stop, never the other way round. With a tight intraday stop, buying power usually caps the position before the risk budget does. 20 shares in a cash account risk about $20, well under 1% of the account, and that is fine. Widening risk per trade to make a small account feel busy is not.

Losing streaks

0%5%10%15%20%3≤58614716815913109117125133142151161718119+longest losing streak in 100 trades
20,000 simulated runs of 100 trades, each winner paying 2.87R after costs and each loser costing 1R, at a one in three win rate. Bars show how often each longest losing streak occurred; the dark bar is the median.
Median worst streak9 losses
1 run in 1013+ losses
Median drawdown11%
Negative after 1004.6%

This model has a real edge of +0.29R per trade after costs, and it still produces long runs of losses. At 1% risk per trade, the typical run of 100 trades draws down about 11% from a peak, and one run in ten draws down 18% or more. Plan for it before it happens: risk 0.5% to 1% per trade, set a daily and a weekly loss limit, and never raise size to win back a drawdown. Margin magnifies all of this, since at 2:1 or 4:1 buying power the same move against a full position costs two or four times as much of the account.

Managing the trade

The example holds to the final target. Two common alternatives change the numbers. Taking half off at the first pool ($498.65, +$1.60, 1.6R) lowers the payoff per winner but books profit earlier. Moving the stop to breakeven after the BOS protects capital but turns some eventual winners into scratches. Pick one rule and test it rather than deciding trade by trade.

12

Testing the model

SMC is taught as discretionary chart reading, which makes it easy to fool yourself: looking back, every sweep is obvious. The fix is to write each rule precisely enough that code could apply it. These starting definitions match the master chart.

ConceptRuleStarting parameter
SwingPivot: a bar with N lower highs (or higher lows) on each sideN = 2. Known only N bars later
Equal highs or lowsTwo or more swings within a tolerance0.1 × ATR(14), about $0.05 here
SweepWick beyond the level, close back inside on the same barAlso test: back inside within 2 bars
DisplacementRange at least k × ATR(14), close in the outer quarter of the bark = 1.5
Fair value gapBullish: low[0] > high[2]. Bearish: high[0] < low[2]Minimum size 0.25 × ATR(14)
Order blockLast opposing candle before the displacement that broke structureZone from its high to its low
InvalidationA candle closes beyond the far edge of the zone or the sweep extremeCloses count, wicks do not
  1. Code the rules, or at least write them so two people would mark the same chart the same way.
  2. Include spread, commission and slippage in every trade.
  3. Collect at least 100 trades per variant before trusting a win rate. With a true 33% win rate, 100 trades still produce a measured rate anywhere from about 24% to 43% one time in twenty.
  4. Design on one period and test on another that you have not looked at.
  5. Forward test at small size before trading full risk.
  6. Track expectancy, profit factor, maximum drawdown and the longest losing streak, not just the win rate.
TradingView Pine Script — Smart Money Concepts
// Smart Money Concepts, independent implementation
// Copyright (c) 2026 Cengiz Ilerler. All rights reserved.
// Proprietary and confidential. This source code is licensed, not sold. Access is granted to paying users through
// TradingView invite-only publishing for their personal use only. Copying, redistribution, modification, reverse
// engineering or resale of this code, in whole or in part, is prohibited without written permission from the author.
//
// How it works
//   Swings      symmetric pivots: a bar is a swing high when it is the highest of `length` bars on both sides, so a swing
//               is confirmed `length` bars after it prints. Highs and lows alternate; a same-side pivot only replaces the
//               current one when it is more extreme and the current level is still unbroken.
//   Structure   a level breaks on the first close beyond it (optionally by a margin, optionally on a displacement candle).
//               BOS continues the bias, CHoCH flips it, every level breaks once.
//   Blocks      the last opposite-color candle before the impulse that broke structure (or the extreme candle of the leg),
//               mitigated by wick, close or 50% level, dropped after a maximum age.
//   Gaps        three-candle imbalances at least X ATR wide, shrink as they fill, higher timeframe gaps on confirmed bars only.
//   Levels      previous day / week / month high and low from completed bars only.
//   Nothing here uses lookahead: every element appears live at the same bar it appears on history.
//
//@version=6
indicator('Smart Money Concepts', 'SMC', overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500)

//======================================================================================================================
// constants
//======================================================================================================================
BULL       = 1
BEAR       = -1
RING       = 5000          // bars of OHLC kept for block detection
MAX_SEARCH = 1000          // how far back a block candle is searched

G_GENERAL  = 'General'
G_SWING    = 'Swing structure'
G_INTERNAL = 'Internal structure'
G_BREAKS   = 'Structure breaks'
G_BLOCKS   = 'Order blocks'
G_EQUAL    = 'Equal highs and lows'
G_GAPS     = 'Fair value gaps'
G_ZONES    = 'Premium and discount'
G_LEVELS   = 'Previous highs and lows'
G_TABLE    = 'Info table'
G_ALERTS   = 'Alerts'

//======================================================================================================================
// inputs
//======================================================================================================================
styleInput               = input.string('Colored', 'Style',                              group = G_GENERAL, options = ['Colored', 'Monochrome'], tooltip = 'Monochrome ignores every color input below and draws everything in two shades of gray')
showHistoryInput         = input.bool(true,    'Show history',                          group = G_GENERAL, tooltip = 'Off keeps only the latest structure break per side and per engine on the chart')
bullColorInput           = input.color(#26a69a, 'Bullish',                               group = G_GENERAL, inline = 'colors')
bearColorInput           = input.color(#ef5350, 'Bearish',                               group = G_GENERAL, inline = 'colors')
colorCandlesInput        = input.bool(false,   'Color candles by internal bias',        group = G_GENERAL)

showSwingInput           = input.bool(true,    'Show swing structure',                  group = G_SWING)
swingLengthInput         = input.int(20,       'Pivot length',                          group = G_SWING, minval = 3, tooltip = 'Bars on each side of a swing high or low. A swing is confirmed this many bars after it prints')
swingLabelsInput         = input.string('All', 'Labels',                                group = G_SWING, inline = 'swl', options = ['All', 'BOS', 'CHoCH'])
swingLabelSizeInput      = input.string('Small', '',                                    group = G_SWING, inline = 'swl', options = ['Tiny', 'Small', 'Normal'])
showSwingPointsInput     = input.bool(false,   'Swing point labels (HH, HL, LH, LL)',   group = G_SWING)
showStrongWeakInput      = input.bool(true,    'Strong and weak high / low',            group = G_SWING)

showInternalInput        = input.bool(true,    'Show internal structure',               group = G_INTERNAL)
internalLengthInput      = input.int(5,        'Pivot length',                          group = G_INTERNAL, minval = 2)
internalLabelsInput      = input.string('CHoCH', 'Labels',                              group = G_INTERNAL, inline = 'inl', options = ['All', 'BOS', 'CHoCH'])
internalLabelSizeInput   = input.string('Tiny', '',                                     group = G_INTERNAL, inline = 'inl', options = ['Tiny', 'Small', 'Normal'])
hideInternalAtSwingInput = input.bool(true,    'Hide internal breaks at swing levels',  group = G_INTERNAL)

breakMarginInput         = input.float(0.1,    'Break margin (ATR x)',                  group = G_BREAKS, minval = 0, step = 0.05, tooltip = 'A close has to clear the level by at least this much before it counts as a break. 0 accepts any close beyond the level')
displacementInput        = input.float(0,      'Displacement body (ATR x)',             group = G_BREAKS, minval = 0, step = 0.1, tooltip = 'The break candle body has to be at least this large. 0 disables the check. Both filters apply to swing and internal structure alike')

showInternalBlocksInput  = input.bool(true,    'Internal order blocks',                 group = G_BLOCKS, inline = 'iob')
internalBlockCountInput  = input.int(3,        '',                                      group = G_BLOCKS, inline = 'iob', minval = 1, maxval = 20)
showSwingBlocksInput     = input.bool(false,   'Swing order blocks',                    group = G_BLOCKS, inline = 'sob')
swingBlockCountInput     = input.int(3,        '',                                      group = G_BLOCKS, inline = 'sob', minval = 1, maxval = 20)
blockSourceInput         = input.string('Last opposite candle', 'Block candle',         group = G_BLOCKS, options = ['Last opposite candle', 'Extreme candle of the leg'], tooltip = 'Last opposite candle: the last down candle before the up move that broke structure (and vice versa). Extreme candle: the lowest / highest candle of the leg')
blockBodyInput           = input.bool(false,   'Body only',                             group = G_BLOCKS, tooltip = 'Use the candle body instead of its full range')
blockMitigationInput     = input.string('Close', 'Mitigation',                          group = G_BLOCKS, options = ['Wick', 'Close', 'Mid'], tooltip = 'Wick: price trades through the far edge. Close: a close beyond the far edge. Mid: price trades through the 50% level')
blockMaxAgeInput         = input.int(300,      'Max age (bars)',                        group = G_BLOCKS, minval = 10)
blockVolatilityInput     = input.float(2.0,    'Ignore candles wider than (ATR x)',     group = G_BLOCKS, minval = 0, step = 0.5, tooltip = '0 disables the filter')
internalBullBlockColor   = input.color(color.new(#2962ff, 82), 'Internal bullish',      group = G_BLOCKS, inline = 'ibc')
internalBearBlockColor   = input.color(color.new(#ff5252, 82), 'bearish',               group = G_BLOCKS, inline = 'ibc')
swingBullBlockColor      = input.color(color.new(#00bcd4, 80), 'Swing bullish',         group = G_BLOCKS, inline = 'sbc')
swingBearBlockColor      = input.color(color.new(#ff9800, 80), 'bearish',               group = G_BLOCKS, inline = 'sbc')

showEqualInput           = input.bool(true,    'Show equal highs and lows',             group = G_EQUAL)
equalLengthInput         = input.int(3,        'Pivot length',                          group = G_EQUAL, minval = 1)
equalThresholdInput      = input.float(0.1,    'Tolerance (ATR x)',                     group = G_EQUAL, minval = 0, step = 0.05)
equalLabelSizeInput      = input.string('Tiny', 'Label size',                           group = G_EQUAL, options = ['Tiny', 'Small', 'Normal'])

showGapsInput            = input.bool(true,    'Show fair value gaps',                  group = G_GAPS)
gapTimeframeInput        = input.timeframe('', 'Timeframe',                             group = G_GAPS, tooltip = 'Empty uses the chart timeframe. Higher timeframe gaps are detected when their third candle closes, so history and live behave the same')
gapMinSizeInput          = input.float(0.3,    'Min size (ATR x)',                      group = G_GAPS, minval = 0, step = 0.1)
gapFillInput             = input.string('Wick', 'Fill by',                              group = G_GAPS, options = ['Wick', 'Close'])
gapShrinkInput           = input.bool(true,    'Shrink on partial fill',                group = G_GAPS)
gapMaxAgeInput           = input.int(300,      'Max age (bars)',                        group = G_GAPS, minval = 10)
gapMaxCountInput         = input.int(20,       'Max shown',                             group = G_GAPS, minval = 1, maxval = 100)
bullGapColor             = input.color(color.new(#00e676, 85), 'Bullish',               group = G_GAPS, inline = 'gc')
bearGapColor             = input.color(color.new(#ff1744, 85), 'Bearish',               group = G_GAPS, inline = 'gc')

showZonesInput           = input.bool(true,    'Show premium and discount zones',        group = G_ZONES)
premiumColorInput        = input.color(color.new(#ef5350, 90), 'Premium',               group = G_ZONES, inline = 'zc')
discountColorInput       = input.color(color.new(#26a69a, 90), 'Discount',              group = G_ZONES, inline = 'zc')
equilibriumColorInput    = input.color(#9598a1, 'Equilibrium',                          group = G_ZONES, inline = 'zc')

showDailyInput           = input.bool(false,   'Daily',                                 group = G_LEVELS, inline = 'd')
dailyStyleInput          = input.string('Solid', '',                                    group = G_LEVELS, inline = 'd', options = ['Solid', 'Dashed', 'Dotted'])
dailyColorInput          = input.color(#2962ff, '',                                     group = G_LEVELS, inline = 'd')
showWeeklyInput          = input.bool(true,    'Weekly',                                group = G_LEVELS, inline = 'w')
weeklyStyleInput         = input.string('Solid', '',                                    group = G_LEVELS, inline = 'w', options = ['Solid', 'Dashed', 'Dotted'])
weeklyColorInput         = input.color(#2962ff, '',                                     group = G_LEVELS, inline = 'w')
showMonthlyInput         = input.bool(true,    'Monthly',                               group = G_LEVELS, inline = 'm')
monthlyStyleInput        = input.string('Solid', '',                                    group = G_LEVELS, inline = 'm', options = ['Solid', 'Dashed', 'Dotted'])
monthlyColorInput        = input.color(#2962ff, '',                                     group = G_LEVELS, inline = 'm')

showTableInput           = input.bool(true,    'Show info table',                       group = G_TABLE)
tablePositionInput       = input.string('Top right', 'Position',                        group = G_TABLE, options = ['Top right', 'Top left', 'Bottom right', 'Bottom left'])
tableSizeInput           = input.string('Small', 'Text size',                           group = G_TABLE, options = ['Tiny', 'Small', 'Normal'])

webhookAlertInput        = input.bool(true,    'JSON alert() on any event',             group = G_ALERTS, tooltip = 'Create one alert on this indicator with the condition "Any alert() function call". It sends a JSON payload with every event of the bar, once per bar close. The individual conditions in the alert dialog work as usual; set them to "Once per bar close"')

//======================================================================================================================
// palette: every drawing reads these, so one input switches the whole script to grayscale
//======================================================================================================================
MONO_LIGHT = #d1d4dc
MONO_DARK  = #787b86
mono       = styleInput == 'Monochrome'

bullColor             = mono ? MONO_LIGHT : bullColorInput
bearColor             = mono ? MONO_DARK  : bearColorInput
internalBullBlockFill = mono ? color.new(MONO_LIGHT, 85) : internalBullBlockColor
internalBearBlockFill = mono ? color.new(MONO_DARK, 78)  : internalBearBlockColor
swingBullBlockFill    = mono ? color.new(MONO_LIGHT, 80) : swingBullBlockColor
swingBearBlockFill    = mono ? color.new(MONO_DARK, 72)  : swingBearBlockColor
bullGapFill           = mono ? color.new(MONO_LIGHT, 88) : bullGapColor
bearGapFill           = mono ? color.new(MONO_DARK, 84)  : bearGapColor
premiumFill           = mono ? color.new(MONO_DARK, 86)  : premiumColorInput
discountFill          = mono ? color.new(MONO_LIGHT, 92) : discountColorInput
equilibriumColor      = mono ? MONO_DARK  : equilibriumColorInput
dailyColor            = mono ? MONO_LIGHT : dailyColorInput
weeklyColor           = mono ? MONO_LIGHT : weeklyColorInput
monthlyColor          = mono ? MONO_LIGHT : monthlyColorInput

//======================================================================================================================
// types
//======================================================================================================================
type swingPoint
    float price  = na
    int   bar    = na
    bool  broken = false
    label tag    = na

type structureEngine
    swingPoint high
    swingPoint low
    swingPoint prevHigh
    swingPoint prevLow
    int    lastSide  = 0
    int    bias      = 0
    string lastTag   = ''
    int    lastDir   = 0
    float  lastLevel = na
    int    lastBar   = na
    line   bullLine  = na
    label  bullLabel = na
    line   bearLine  = na
    label  bearLabel = na

type orderBlock
    float top
    float bottom
    int   bar
    int   dir
    bool  internal
    int   createdBar
    bool  touched = false
    box   bx

type fairValueGap
    float top
    float bottom
    int   dir
    int   createdBar
    box   bx

type events
    bool swingBullBos       = false
    bool swingBullChoch     = false
    bool swingBearBos       = false
    bool swingBearChoch     = false
    bool internalBullBos    = false
    bool internalBullChoch  = false
    bool internalBearBos    = false
    bool internalBearChoch  = false
    bool bullBlockTouched   = false
    bool bearBlockTouched   = false
    bool bullBlockMitigated = false
    bool bearBlockMitigated = false
    bool equalHighs         = false
    bool equalLows          = false
    bool bullGap            = false
    bool bearGap            = false
    string payload          = ''

//======================================================================================================================
// series, requests and state
//======================================================================================================================
atrValue   = nz(ta.atr(200), math.max(high - low, syminfo.mintick))
swingPh    = ta.pivothigh(swingLengthInput, swingLengthInput)
swingPl    = ta.pivotlow(swingLengthInput, swingLengthInput)
internalPh = ta.pivothigh(internalLengthInput, internalLengthInput)
internalPl = ta.pivotlow(internalLengthInput, internalLengthInput)
equalPh    = ta.pivothigh(equalLengthInput, equalLengthInput)
equalPl    = ta.pivotlow(equalLengthInput, equalLengthInput)
high2      = high[2]
low2       = low[2]
time1      = time[1]

gapTimeframe = gapTimeframeInput == '' ? timeframe.period : gapTimeframeInput
gapOnChartTf = timeframe.in_seconds(gapTimeframe) <= timeframe.in_seconds()
htfNewBar    = timeframe.change(gapTimeframe)
// completed higher timeframe candles: index 0 is the candle that just closed (no lookahead anywhere)
[htfHigh0, htfLow0, htfTime1, htfHigh2, htfLow2] = request.security(syminfo.tickerid, gapTimeframe, [high, low, time[1], high[2], low[2]], lookahead = barmerge.lookahead_off)
[prevDayHigh, prevDayLow]     = request.security(syminfo.tickerid, 'D', [high, low], lookahead = barmerge.lookahead_off)
[prevWeekHigh, prevWeekLow]   = request.security(syminfo.tickerid, 'W', [high, low], lookahead = barmerge.lookahead_off)
[prevMonthHigh, prevMonthLow] = request.security(syminfo.tickerid, 'M', [high, low], lookahead = barmerge.lookahead_off)

var structureEngine swingEngine     = structureEngine.new(swingPoint.new(), swingPoint.new(), swingPoint.new(), swingPoint.new())
var structureEngine internalEngine  = structureEngine.new(swingPoint.new(), swingPoint.new(), swingPoint.new(), swingPoint.new())
var array<orderBlock> internalBlocks = array.new<orderBlock>()
var array<orderBlock> swingBlocks    = array.new<orderBlock>()
var array<fairValueGap> gaps         = array.new<fairValueGap>()
var array<float> ringOpen  = array.new<float>(RING)
var array<float> ringHigh  = array.new<float>(RING)
var array<float> ringLow   = array.new<float>(RING)
var array<float> ringClose = array.new<float>(RING)

//======================================================================================================================
// helpers
//======================================================================================================================
labelSize(string s) => s == 'Tiny' ? size.tiny : s == 'Small' ? size.small : size.normal
lineStyle(string s) => s == 'Solid' ? line.style_solid : s == 'Dashed' ? line.style_dashed : line.style_dotted
tablePosition(string s) => s == 'Top left' ? position.top_left : s == 'Bottom right' ? position.bottom_right : s == 'Bottom left' ? position.bottom_left : position.top_right
labelAllowed(string filter, string tag) => filter == 'All' or filter == tag
biasText(int b) => b == BULL ? 'Bullish' : b == BEAR ? 'Bearish' : 'None'
biasColor(int b) => b == BULL ? bullColor : b == BEAR ? bearColor : color.gray

method record(events ev, string kind, float price, float level) =>
    item = '{"event":"' + kind + '","price":' + str.tostring(price, format.mintick) + ',"level":' + str.tostring(level, format.mintick) + '}'
    ev.payload := ev.payload == '' ? item : ev.payload + ',' + item

// a confirmed pivot high enters the engine; highs and lows alternate, a same-side pivot only replaces the current one
// when it is more extreme and the current level has not been broken yet
method registerHigh(structureEngine e, float price, int pivotBar, bool drawPoint) =>
    if e.lastSide != 1 or e.high.broken or price > e.high.price
        extendSwing = e.lastSide == 1 and not e.high.broken
        if extendSwing and not na(e.high.tag)
            label.delete(e.high.tag)
        if not extendSwing
            e.prevHigh := e.high
        e.high     := swingPoint.new(price, pivotBar)
        e.lastSide := 1
        if drawPoint
            txt = na(e.prevHigh.price) ? 'H' : price > e.prevHigh.price ? 'HH' : 'LH'
            e.high.tag := label.new(pivotBar, price, txt, color = color(na), textcolor = bearColor, style = label.style_label_down, size = size.tiny)

method registerLow(structureEngine e, float price, int pivotBar, bool drawPoint) =>
    if e.lastSide != -1 or e.low.broken or price < e.low.price
        extendSwing = e.lastSide == -1 and not e.low.broken
        if extendSwing and not na(e.low.tag)
            label.delete(e.low.tag)
        if not extendSwing
            e.prevLow := e.low
        e.low      := swingPoint.new(price, pivotBar)
        e.lastSide := -1
        if drawPoint
            txt = na(e.prevLow.price) ? 'L' : price < e.prevLow.price ? 'LL' : 'HL'
            e.low.tag := label.new(pivotBar, price, txt, color = color(na), textcolor = bullColor, style = label.style_label_up, size = size.tiny)

// block candle for a break on the current bar whose leg started at originBar; returns [top, bottom, bar]
findBlock(int dir, int originBar) =>
    float top    = na
    float bottom = na
    int   found  = na
    int   searchFrom = bar_index - 1
    int   searchTo   = math.max(originBar, bar_index - MAX_SEARCH, bar_index - RING + 1, 0)
    lastOpposite = blockSourceInput == 'Last opposite candle'
    if searchFrom >= searchTo and lastOpposite
        for i = searchFrom to searchTo
            o = ringOpen.get(i % RING)
            h = ringHigh.get(i % RING)
            l = ringLow.get(i % RING)
            c = ringClose.get(i % RING)
            if blockVolatilityInput > 0 and h - l > blockVolatilityInput * atrValue
                continue
            if (dir == BULL and c < o) or (dir == BEAR and c > o)
                found := i
                break
    if searchFrom >= searchTo and not lastOpposite
        float extreme = na
        for i = searchTo to searchFrom
            h = ringHigh.get(i % RING)
            l = ringLow.get(i % RING)
            if blockVolatilityInput > 0 and h - l > blockVolatilityInput * atrValue
                continue
            if dir == BULL and (na(extreme) or l < extreme)
                extreme := l
                found   := i
            if dir == BEAR and (na(extreme) or h > extreme)
                extreme := h
                found   := i
    if not na(found)
        bo = ringOpen.get(found % RING)
        bh = ringHigh.get(found % RING)
        bl = ringLow.get(found % RING)
        bc = ringClose.get(found % RING)
        top    := blockBodyInput ? math.max(bo, bc) : bh
        bottom := blockBodyInput ? math.min(bo, bc) : bl
    [top, bottom, found]

addBlock(array<orderBlock> blocks, int dir, bool isInternal, float top, float bottom, int blockBar, int maxCount) =>
    fill   = dir == BULL ? (isInternal ? internalBullBlockFill : swingBullBlockFill) : (isInternal ? internalBearBlockFill : swingBearBlockFill)
    border = isInternal ? color(na) : color.new(fill, 40)
    bx     = box.new(blockBar, top, bar_index, bottom, border_color = border, bgcolor = fill, extend = extend.right)
    blocks.push(orderBlock.new(top, bottom, blockBar, dir, isInternal, bar_index, false, bx))
    while blocks.size() > maxCount
        old = blocks.shift()
        box.delete(old.bx)

// mitigation, first touch and ageing of stored blocks
updateBlocks(array<orderBlock> blocks, events ev) =>
    if blocks.size() > 0
        for i = blocks.size() - 1 to 0
            ob = blocks.get(i)
            if ob.createdBar == bar_index
                continue
            mid  = math.avg(ob.top, ob.bottom)
            bool gone = false
            if ob.dir == BULL
                gone := blockMitigationInput == 'Close' ? close < ob.bottom : blockMitigationInput == 'Mid' ? low < mid : low < ob.bottom
            else
                gone := blockMitigationInput == 'Close' ? close > ob.top : blockMitigationInput == 'Mid' ? high > mid : high > ob.top
            if gone or bar_index - ob.createdBar > blockMaxAgeInput
                if gone
                    if ob.dir == BULL
                        ev.bullBlockMitigated := true
                    else
                        ev.bearBlockMitigated := true
                    ev.record((ob.dir == BULL ? 'bull' : 'bear') + '_block_mitigated', close, ob.dir == BULL ? ob.bottom : ob.top)
                blocks.remove(i)
                box.delete(ob.bx)
                continue
            if not ob.touched and (ob.dir == BULL ? low <= ob.top : high >= ob.bottom)
                ob.touched := true
                if ob.dir == BULL
                    ev.bullBlockTouched := true
                else
                    ev.bearBlockTouched := true
                ev.record((ob.dir == BULL ? 'bull' : 'bear') + '_block_touched', close, ob.dir == BULL ? ob.top : ob.bottom)

drawBreak(structureEngine e, int dir, string tag, int fromBar, float level, bool isInternal, string sz) =>
    col = dir == BULL ? bullColor : bearColor
    if not showHistoryInput
        oldLine  = dir == BULL ? e.bullLine : e.bearLine
        oldLabel = dir == BULL ? e.bullLabel : e.bearLabel
        if not na(oldLine)
            line.delete(oldLine)
        if not na(oldLabel)
            label.delete(oldLabel)
    ln = line.new(fromBar, level, bar_index, level, color = col, style = isInternal ? line.style_dashed : line.style_solid)
    lb = label.new(math.round(math.avg(fromBar, bar_index)), level, tag, color = color(na), textcolor = col, style = dir == BULL ? label.style_label_down : label.style_label_up, size = sz)
    if dir == BULL
        e.bullLine  := ln
        e.bullLabel := lb
    else
        e.bearLine  := ln
        e.bearLabel := lb

// structure breaks for one engine: state, drawing, block creation and events
processBreaks(structureEngine e, bool isInternal, structureEngine swing, events ev, array<orderBlock> blocks) =>
    showStructure = isInternal ? showInternalInput : showSwingInput
    labelFilter   = isInternal ? internalLabelsInput : swingLabelsInput
    sz            = labelSize(isInternal ? internalLabelSizeInput : swingLabelSizeInput)
    blocksOn      = isInternal ? showInternalBlocksInput : showSwingBlocksInput
    blockCount    = isInternal ? internalBlockCountInput : swingBlockCountInput
    margin        = breakMarginInput * atrValue
    displaced     = displacementInput <= 0 or math.abs(close - open) >= displacementInput * atrValue

    if not na(e.high.price) and not e.high.broken and close > e.high.price + margin
        if displaced
            tag     = e.bias == BEAR ? 'CHoCH' : 'BOS'
            level   = e.high.price
            fromBar = e.high.bar
            e.high.broken := true
            e.bias      := BULL
            e.lastTag   := tag
            e.lastDir   := BULL
            e.lastLevel := level
            e.lastBar   := bar_index
            duplicate = isInternal and hideInternalAtSwingInput and nz(swing.high.bar, -1) == fromBar
            if not duplicate
                if isInternal
                    if tag == 'BOS'
                        ev.internalBullBos := true
                    else
                        ev.internalBullChoch := true
                else
                    if tag == 'BOS'
                        ev.swingBullBos := true
                    else
                        ev.swingBullChoch := true
                ev.record((isInternal ? 'internal_' : 'swing_') + (tag == 'BOS' ? 'bos_bull' : 'choch_bull'), close, level)
                if showStructure and labelAllowed(labelFilter, tag)
                    drawBreak(e, BULL, tag, fromBar, level, isInternal, sz)
                if blocksOn
                    origin = na(e.low.bar) ? bar_index - MAX_SEARCH : e.low.bar
                    [top, bottom, blockBar] = findBlock(BULL, origin)
                    if not na(blockBar)
                        addBlock(blocks, BULL, isInternal, top, bottom, blockBar, blockCount)

    if not na(e.low.price) and not e.low.broken and close < e.low.price - margin
        if displaced
            tag     = e.bias == BULL ? 'CHoCH' : 'BOS'
            level   = e.low.price
            fromBar = e.low.bar
            e.low.broken := true
            e.bias      := BEAR
            e.lastTag   := tag
            e.lastDir   := BEAR
            e.lastLevel := level
            e.lastBar   := bar_index
            duplicate = isInternal and hideInternalAtSwingInput and nz(swing.low.bar, -1) == fromBar
            if not duplicate
                if isInternal
                    if tag == 'BOS'
                        ev.internalBearBos := true
                    else
                        ev.internalBearChoch := true
                else
                    if tag == 'BOS'
                        ev.swingBearBos := true
                    else
                        ev.swingBearChoch := true
                ev.record((isInternal ? 'internal_' : 'swing_') + (tag == 'BOS' ? 'bos_bear' : 'choch_bear'), close, level)
                if showStructure and labelAllowed(labelFilter, tag)
                    drawBreak(e, BEAR, tag, fromBar, level, isInternal, sz)
                if blocksOn
                    origin = na(e.high.bar) ? bar_index - MAX_SEARCH : e.high.bar
                    [top, bottom, blockBar] = findBlock(BEAR, origin)
                    if not na(blockBar)
                        addBlock(blocks, BEAR, isInternal, top, bottom, blockBar, blockCount)

addGap(int dir, float top, float bottom, int leftTime) =>
    fill = dir == BULL ? bullGapFill : bearGapFill
    bx   = box.new(leftTime, top, time, bottom, xloc = xloc.bar_time, border_color = color(na), bgcolor = fill, extend = extend.right)
    gaps.push(fairValueGap.new(top, bottom, dir, bar_index, bx))
    while gaps.size() > gapMaxCountInput
        old = gaps.shift()
        box.delete(old.bx)

// fills (full or partial) and ageing of stored gaps
updateGaps() =>
    if gaps.size() > 0
        for i = gaps.size() - 1 to 0
            g = gaps.get(i)
            if g.createdBar == bar_index
                continue
            src    = g.dir == BULL ? (gapFillInput == 'Close' ? close : low) : (gapFillInput == 'Close' ? close : high)
            filled = g.dir == BULL ? src <= g.bottom : src >= g.top
            if filled or bar_index - g.createdBar > gapMaxAgeInput
                gaps.remove(i)
                box.delete(g.bx)
                continue
            if gapShrinkInput
                if g.dir == BULL and src < g.top
                    g.top := src
                    box.set_top(g.bx, src)
                if g.dir == BEAR and src > g.bottom
                    g.bottom := src
                    box.set_bottom(g.bx, src)

// nearest stored block of one direction: distance from close (0 when inside), top, bottom
scanBlocks(array<orderBlock> blocks, int dir, float curDist, float curTop, float curBottom) =>
    float bestDist   = curDist
    float bestTop    = curTop
    float bestBottom = curBottom
    for ob in blocks
        if ob.dir != dir
            continue
        dist = dir == BULL ? math.max(close - ob.top, 0) : math.max(ob.bottom - close, 0)
        if na(bestDist) or dist < bestDist
            bestDist   := dist
            bestTop    := ob.top
            bestBottom := ob.bottom
    [bestDist, bestTop, bestBottom]

zoneText(float dist, float top, float bottom) =>
    na(dist) ? '-' : str.tostring(bottom, format.mintick) + ' to ' + str.tostring(top, format.mintick) + ' (' + str.tostring(dist / close * 100, '#.##') + '%)'

breakText(structureEngine e) =>
    e.lastTag == '' ? '-' : e.lastTag + (e.lastDir == BULL ? ' up ' : ' down ') + str.tostring(e.lastLevel, format.mintick) + ', ' + str.tostring(bar_index - e.lastBar) + ' bars ago'

tableRow(table t, int row, string name, string value, color valueColor, string sz) =>
    table.cell(t, 0, row, name, text_color = color.gray, text_halign = text.align_left, text_size = sz)
    table.cell(t, 1, row, value, text_color = valueColor, text_halign = text.align_right, text_size = sz)

updateLevel(line hi, line lo, label hl, label ll, float levelHigh, float levelLow, int startBar) =>
    fromBar = nz(startBar, 0)
    line.set_xy1(hi, fromBar, levelHigh)
    line.set_xy2(hi, bar_index + 20, levelHigh)
    label.set_xy(hl, bar_index + 20, levelHigh)
    line.set_xy1(lo, fromBar, levelLow)
    line.set_xy2(lo, bar_index + 20, levelLow)
    label.set_xy(ll, bar_index + 20, levelLow)

//======================================================================================================================
// per-bar flow
//======================================================================================================================
events ev = events.new()

ringSlot = bar_index % RING
ringOpen.set(ringSlot, open)
ringHigh.set(ringSlot, high)
ringLow.set(ringSlot, low)
ringClose.set(ringSlot, close)

// 1. existing objects first: mitigation, touches, fills, ageing
updateBlocks(internalBlocks, ev)
updateBlocks(swingBlocks, ev)
if showGapsInput
    updateGaps()

// 2. confirmed swings enter the engines
if not na(swingPh)
    swingEngine.registerHigh(swingPh, bar_index - swingLengthInput, showSwingPointsInput)
if not na(swingPl)
    swingEngine.registerLow(swingPl, bar_index - swingLengthInput, showSwingPointsInput)
if not na(internalPh)
    internalEngine.registerHigh(internalPh, bar_index - internalLengthInput, false)
if not na(internalPl)
    internalEngine.registerLow(internalPl, bar_index - internalLengthInput, false)

// 3. structure breaks, swing first so internal duplicates can be recognised
processBreaks(swingEngine, false, swingEngine, ev, swingBlocks)
processBreaks(internalEngine, true, swingEngine, ev, internalBlocks)

// 4. equal highs and lows: consecutive raw pivots within the tolerance
var float lastEqHigh    = na
var int   lastEqHighBar = na
var float lastEqLow     = na
var int   lastEqLowBar  = na
var line  eqhLine       = na
var label eqhLabel      = na
var line  eqlLine       = na
var label eqlLabel      = na
if showEqualInput
    if not na(equalPh)
        pBar = bar_index - equalLengthInput
        if not na(lastEqHigh) and math.abs(equalPh - lastEqHigh) <= equalThresholdInput * atrValue
            if not showHistoryInput and not na(eqhLine)
                line.delete(eqhLine)
                label.delete(eqhLabel)
            eqhLine  := line.new(lastEqHighBar, lastEqHigh, pBar, equalPh, color = bearColor, style = line.style_dotted)
            eqhLabel := label.new(math.round(math.avg(lastEqHighBar, pBar)), math.max(lastEqHigh, equalPh), 'EQH', color = color(na), textcolor = bearColor, style = label.style_label_down, size = labelSize(equalLabelSizeInput))
            ev.equalHighs := true
            ev.record('equal_highs', equalPh, lastEqHigh)
        lastEqHigh    := equalPh
        lastEqHighBar := pBar
    if not na(equalPl)
        pBar = bar_index - equalLengthInput
        if not na(lastEqLow) and math.abs(equalPl - lastEqLow) <= equalThresholdInput * atrValue
            if not showHistoryInput and not na(eqlLine)
                line.delete(eqlLine)
                label.delete(eqlLabel)
            eqlLine  := line.new(lastEqLowBar, lastEqLow, pBar, equalPl, color = bullColor, style = line.style_dotted)
            eqlLabel := label.new(math.round(math.avg(lastEqLowBar, pBar)), math.min(lastEqLow, equalPl), 'EQL', color = color(na), textcolor = bullColor, style = label.style_label_up, size = labelSize(equalLabelSizeInput))
            ev.equalLows := true
            ev.record('equal_lows', equalPl, lastEqLow)
        lastEqLow    := equalPl
        lastEqLowBar := pBar

// 5. fair value gaps: chart timeframe on bar close, higher timeframe when its third candle has closed
if showGapsInput
    ready = gapOnChartTf ? barstate.isconfirmed : htfNewBar
    if ready
        c1High  = gapOnChartTf ? high2 : htfHigh2
        c1Low   = gapOnChartTf ? low2 : htfLow2
        c3High  = gapOnChartTf ? high : htfHigh0
        c3Low   = gapOnChartTf ? low : htfLow0
        leftT   = gapOnChartTf ? time1 : htfTime1
        minSize = gapMinSizeInput * atrValue
        if c3Low > c1High and c3Low - c1High >= minSize
            addGap(BULL, c3Low, c1High, leftT)
            ev.bullGap := true
            ev.record('bull_gap', close, c3Low)
        if c3High < c1Low and c1Low - c3High >= minSize
            addGap(BEAR, c1Low, c3High, leftT)
            ev.bearGap := true
            ev.record('bear_gap', close, c3High)

// 6. trailing extremes of the swing structure: highest high since the last swing high, lowest low since the last swing low
var float trailTop       = na
var int   trailTopBar    = na
var float trailBottom    = na
var int   trailBottomBar = na
var int   seenHighBar    = na
var int   seenLowBar     = na
if nz(swingEngine.high.bar, -1) != nz(seenHighBar, -1)
    seenHighBar := swingEngine.high.bar
    trailTop    := swingEngine.high.price
    trailTopBar := swingEngine.high.bar
if nz(swingEngine.low.bar, -1) != nz(seenLowBar, -1)
    seenLowBar     := swingEngine.low.bar
    trailBottom    := swingEngine.low.price
    trailBottomBar := swingEngine.low.bar
if not na(trailTop) and high >= trailTop
    trailTop    := high
    trailTopBar := bar_index
if not na(trailBottom) and low <= trailBottom
    trailBottom    := low
    trailBottomBar := bar_index

// period starts for the previous high / low levels
var int dayStart   = na
var int weekStart  = na
var int monthStart = na
if timeframe.change('D')
    dayStart := bar_index
if timeframe.change('W')
    weekStart := bar_index
if timeframe.change('M')
    monthStart := bar_index

//======================================================================================================================
// last-bar drawings: created lazily on the last bar, updated on every tick
//======================================================================================================================
var line  strongHighLine  = na
var label strongHighLabel = na
var line  strongLowLine   = na
var label strongLowLabel  = na
if barstate.islast and showStrongWeakInput and not na(trailTop) and not na(trailBottom)
    if na(strongHighLine)
        strongHighLine  := line.new(na, na, na, na, color = bearColor)
        strongHighLabel := label.new(na, na, '', color = color(na), textcolor = bearColor, style = label.style_label_down, size = size.tiny)
        strongLowLine   := line.new(na, na, na, na, color = bullColor)
        strongLowLabel  := label.new(na, na, '', color = color(na), textcolor = bullColor, style = label.style_label_up, size = size.tiny)
    line.set_xy1(strongHighLine, trailTopBar, trailTop)
    line.set_xy2(strongHighLine, bar_index + 20, trailTop)
    label.set_xy(strongHighLabel, bar_index + 20, trailTop)
    label.set_text(strongHighLabel, swingEngine.bias == BEAR ? 'Strong high' : 'Weak high')
    line.set_xy1(strongLowLine, trailBottomBar, trailBottom)
    line.set_xy2(strongLowLine, bar_index + 20, trailBottom)
    label.set_xy(strongLowLabel, bar_index + 20, trailBottom)
    label.set_text(strongLowLabel, swingEngine.bias == BULL ? 'Strong low' : 'Weak low')

var box   premiumBox       = na
var box   discountBox      = na
var line  equilibriumLine  = na
var label premiumLabel     = na
var label discountLabel    = na
var label equilibriumLabel = na
if barstate.islast and showZonesInput and not na(trailTop) and not na(trailBottom)
    if na(premiumBox)
        premiumBox       := box.new(na, na, na, na, border_color = color(na), bgcolor = premiumFill)
        discountBox      := box.new(na, na, na, na, border_color = color(na), bgcolor = discountFill)
        equilibriumLine  := line.new(na, na, na, na, color = equilibriumColor, style = line.style_dotted)
        premiumLabel     := label.new(na, na, 'Premium', color = color(na), textcolor = premiumFill, style = label.style_label_left, size = size.small)
        discountLabel    := label.new(na, na, 'Discount', color = color(na), textcolor = discountFill, style = label.style_label_left, size = size.small)
        equilibriumLabel := label.new(na, na, 'Equilibrium', color = color(na), textcolor = equilibriumColor, style = label.style_label_left, size = size.small)
    mid  = math.avg(trailTop, trailBottom)
    left = math.min(trailTopBar, trailBottomBar)
    box.set_lefttop(premiumBox, left, trailTop)
    box.set_rightbottom(premiumBox, bar_index + 20, mid)
    box.set_lefttop(discountBox, left, mid)
    box.set_rightbottom(discountBox, bar_index + 20, trailBottom)
    line.set_xy1(equilibriumLine, left, mid)
    line.set_xy2(equilibriumLine, bar_index + 20, mid)
    label.set_xy(premiumLabel, bar_index + 20, math.avg(trailTop, mid))
    label.set_xy(discountLabel, bar_index + 20, math.avg(mid, trailBottom))
    label.set_xy(equilibriumLabel, bar_index + 20, mid)

var line  pdhLine  = na
var line  pdlLine  = na
var label pdhLabel = na
var label pdlLabel = na
if barstate.islast and showDailyInput and timeframe.in_seconds() < timeframe.in_seconds('D')
    if na(pdhLine)
        pdhLine  := line.new(na, na, na, na, color = dailyColor, style = lineStyle(dailyStyleInput))
        pdlLine  := line.new(na, na, na, na, color = dailyColor, style = lineStyle(dailyStyleInput))
        pdhLabel := label.new(na, na, 'PDH', color = color(na), textcolor = dailyColor, style = label.style_label_left, size = size.small)
        pdlLabel := label.new(na, na, 'PDL', color = color(na), textcolor = dailyColor, style = label.style_label_left, size = size.small)
    updateLevel(pdhLine, pdlLine, pdhLabel, pdlLabel, prevDayHigh, prevDayLow, dayStart)

var line  pwhLine  = na
var line  pwlLine  = na
var label pwhLabel = na
var label pwlLabel = na
if barstate.islast and showWeeklyInput and timeframe.in_seconds() < timeframe.in_seconds('W')
    if na(pwhLine)
        pwhLine  := line.new(na, na, na, na, color = weeklyColor, style = lineStyle(weeklyStyleInput))
        pwlLine  := line.new(na, na, na, na, color = weeklyColor, style = lineStyle(weeklyStyleInput))
        pwhLabel := label.new(na, na, 'PWH', color = color(na), textcolor = weeklyColor, style = label.style_label_left, size = size.small)
        pwlLabel := label.new(na, na, 'PWL', color = color(na), textcolor = weeklyColor, style = label.style_label_left, size = size.small)
    updateLevel(pwhLine, pwlLine, pwhLabel, pwlLabel, prevWeekHigh, prevWeekLow, weekStart)

var line  pmhLine  = na
var line  pmlLine  = na
var label pmhLabel = na
var label pmlLabel = na
if barstate.islast and showMonthlyInput and timeframe.in_seconds() < timeframe.in_seconds('M')
    if na(pmhLine)
        pmhLine  := line.new(na, na, na, na, color = monthlyColor, style = lineStyle(monthlyStyleInput))
        pmlLine  := line.new(na, na, na, na, color = monthlyColor, style = lineStyle(monthlyStyleInput))
        pmhLabel := label.new(na, na, 'PMH', color = color(na), textcolor = monthlyColor, style = label.style_label_left, size = size.small)
        pmlLabel := label.new(na, na, 'PML', color = color(na), textcolor = monthlyColor, style = label.style_label_left, size = size.small)
    updateLevel(pmhLine, pmlLine, pmhLabel, pmlLabel, prevMonthHigh, prevMonthLow, monthStart)

var table infoTable = na
if barstate.islast and showTableInput
    if na(infoTable)
        infoTable := table.new(tablePosition(tablePositionInput), 2, 8, bgcolor = color.new(color.black, 85), border_color = color.new(color.gray, 70), border_width = 1)
    txtSize = labelSize(tableSizeInput)
    [bullDist0, bullTop0, bullBottom0] = scanBlocks(internalBlocks, BULL, float(na), float(na), float(na))
    [bullDist, bullTop, bullBottom]    = scanBlocks(swingBlocks, BULL, bullDist0, bullTop0, bullBottom0)
    [bearDist0, bearTop0, bearBottom0] = scanBlocks(internalBlocks, BEAR, float(na), float(na), float(na))
    [bearDist, bearTop, bearBottom]    = scanBlocks(swingBlocks, BEAR, bearDist0, bearTop0, bearBottom0)
    float gapDist   = na
    float gapTop    = na
    float gapBottom = na
    int   gapDir    = 0
    for g in gaps
        d = close > g.top ? close - g.top : close < g.bottom ? g.bottom - close : 0
        if na(gapDist) or d < gapDist
            gapDist   := d
            gapTop    := g.top
            gapBottom := g.bottom
            gapDir    := g.dir
    gapText   = na(gapDist) ? '-' : (gapDir == BULL ? 'Bullish ' : 'Bearish ') + zoneText(gapDist, gapTop, gapBottom)
    rangeText = na(trailTop) or na(trailBottom) or trailTop == trailBottom ? '-' : str.tostring((close - trailBottom) / (trailTop - trailBottom) * 100, '#') + '% (' + (close >= math.avg(trailTop, trailBottom) ? 'premium' : 'discount') + ')'
    tableRow(infoTable, 0, 'Swing bias', biasText(swingEngine.bias), biasColor(swingEngine.bias), txtSize)
    tableRow(infoTable, 1, 'Internal bias', biasText(internalEngine.bias), biasColor(internalEngine.bias), txtSize)
    tableRow(infoTable, 2, 'Last swing break', breakText(swingEngine), biasColor(swingEngine.lastDir), txtSize)
    tableRow(infoTable, 3, 'Last internal break', breakText(internalEngine), biasColor(internalEngine.lastDir), txtSize)
    tableRow(infoTable, 4, 'Nearest bullish block', zoneText(bullDist, bullTop, bullBottom), bullColor, txtSize)
    tableRow(infoTable, 5, 'Nearest bearish block', zoneText(bearDist, bearTop, bearBottom), bearColor, txtSize)
    tableRow(infoTable, 6, 'Nearest gap', gapText, gapDir == BULL ? bullColor : gapDir == BEAR ? bearColor : color.gray, txtSize)
    tableRow(infoTable, 7, 'Range position', rangeText, color.white, txtSize)

candleColor = internalEngine.bias == BEAR ? bearColor : bullColor
plotcandle(colorCandlesInput ? open : na, high, low, close, title = 'Bias candles', color = candleColor, wickcolor = candleColor, bordercolor = candleColor)

//======================================================================================================================
// alerts
//======================================================================================================================
alertcondition(ev.swingBullBos,       'Swing bullish BOS',        'Swing bullish BOS')
alertcondition(ev.swingBullChoch,     'Swing bullish CHoCH',      'Swing bullish CHoCH')
alertcondition(ev.swingBearBos,       'Swing bearish BOS',        'Swing bearish BOS')
alertcondition(ev.swingBearChoch,     'Swing bearish CHoCH',      'Swing bearish CHoCH')
alertcondition(ev.internalBullBos,    'Internal bullish BOS',     'Internal bullish BOS')
alertcondition(ev.internalBullChoch,  'Internal bullish CHoCH',   'Internal bullish CHoCH')
alertcondition(ev.internalBearBos,    'Internal bearish BOS',     'Internal bearish BOS')
alertcondition(ev.internalBearChoch,  'Internal bearish CHoCH',   'Internal bearish CHoCH')
alertcondition(ev.bullBlockTouched,   'Bullish block touched',    'Price entered a bullish order block')
alertcondition(ev.bearBlockTouched,   'Bearish block touched',    'Price entered a bearish order block')
alertcondition(ev.bullBlockMitigated, 'Bullish block mitigated',  'A bullish order block was mitigated')
alertcondition(ev.bearBlockMitigated, 'Bearish block mitigated',  'A bearish order block was mitigated')
alertcondition(ev.equalHighs,         'Equal highs',              'Equal highs')
alertcondition(ev.equalLows,          'Equal lows',               'Equal lows')
alertcondition(ev.bullGap,            'Bullish fair value gap',   'Bullish fair value gap')
alertcondition(ev.bearGap,            'Bearish fair value gap',   'Bearish fair value gap')

if webhookAlertInput and ev.payload != ''
    alert('{"symbol":"' + syminfo.ticker + '","timeframe":"' + timeframe.period + '","time":' + str.tostring(time) + ',"close":' + str.tostring(close, format.mintick) + ',"events":[' + ev.payload + ']}', alert.freq_once_per_bar_close)

Note

Independent TradingView Pine Script v6 implementation with swing structure, internal structure, order blocks, fair value gaps, premium/discount zones, and previous highs/lows.

13

Advanced terms

Most of the remaining vocabulary refines one phase of the cycle. Each term below is placed where it belongs.

TermWhat it isPhase
InducementA minor swing that tempts early entries. Its stops become fuel before the real move.1 Build
SMT divergenceTwo correlated markets disagree at a swing, for example MSFT sweeps its low while QQQ holds. A hint that the sweep is a trap.2 Sweep
Change in state of delivery (CISD)A close back beyond the opening price of the candles that made the last push. A faster, lighter signal than CHoCH.3 Displace
Liquidity voidA long one-sided run with almost no overlap between candles. A large version of a fair value gap.3 Displace
Balanced price range (BPR)The overlap of a bullish and a bearish fair value gap. Price was one-sided there in both directions.4 Return
Mitigation blockLike a breaker, but the move that failed never made a new high or low first.4 Return
Propulsion blockA candle that trades into an earlier order block and then launches the move. A tighter continuation zone.4 Return
Breakaway gapA gap that price never returns to. Strong earnings gaps in stocks often behave this way.5 Expand
Kill zoneA time window where displacement tends to start. In US stocks, the opening hour and the last hour.Timing
Power of ThreeAccumulation, manipulation, distribution. The whole cycle inside one session or day.All phases

14

Common mistakes

Nearly every one of these comes from using a term outside its place in the cycle, or from skipping the arithmetic.

  • AvoidMarking every gap and block on the chart.

    InsteadKeep only zones created by a sweep, displacement and a structure break.

  • AvoidTreating a wick as a break.

    InsteadWait for a candle to close beyond the level.

  • AvoidTrading a 5-minute CHoCH against the daily trend.

    InsteadTake direction from the higher timeframe and use the lower one only to enter.

  • AvoidBuying in premium because the pattern looks clean.

    InsteadBuy only in the discount of the current dealing range, and sell only in its premium.

  • AvoidPlacing the stop inside the sweep.

    InsteadPut it beyond the swept extreme, where the idea is proven wrong.

  • AvoidChoosing targets by R multiple.

    InsteadTarget the next liquidity pool, then decide whether that R is worth taking.

  • AvoidTreating the first minutes after 9:30 like any other time.

    InsteadSpreads are widest at the open. Let the opening range form, or size down.

  • AvoidHolding through earnings or an FOMC decision by accident.

    InsteadCheck the calendar. Scheduled news can gap price through any level.

  • AvoidSizing by feel, or adding size after losses.

    InsteadSize every trade from the stop at a fixed risk, and stop for the day after a set loss.

  • AvoidBacktesting by eye.

    InsteadCode the rules, include costs, and test on data you did not use to design them.

15

Pre-trade checklist

Pre-filled with the example trade from chapter 10. Uncheck anything your own setup lacks.

All 9 met. Size the position from the stop distance.

16

Questions

Is SMC just supply and demand, or Wyckoff, with new names?

Largely, yes. SMC adds an explicit reason (stop liquidity) and an order of events (the cycle), which helps decide which zones to ignore. Wyckoff's spring, sign of strength and last point of support map almost one to one onto sweep, displacement and return.

Does it work beyond stocks?

The logic applies anywhere with stop orders and visible swings: index futures, forex, crypto and commodities. Futures add real volume and a central order book, which lets you check whether a sweep actually traded size. Spot forex has no central book, so retail volume there is only a tick count.

Which timeframes should I use?

Use a pair of them. A higher one sets direction and a lower one times the entry: daily with 1 hour, or 1 hour with 5 minute, as on the master chart.

Does SMC show what institutions are actually doing?

No. A chart shows where price went, not who moved it. Treat SMC as a probabilistic model of how stops and fast moves behave, with defined risk.

Why do most retail day traders lose money even with a method?

Costs, position size and persistence matter more than any pattern. In one study of Brazilian equity futures, 97% of individuals who day traded for more than 300 days lost money, and about 1% earned more than the minimum wage. Small risk per trade and tested rules are what separate a method from a gamble.

← All trading notes