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.
Contents
- 01The one idea: liquidity
- 02The five-phase cycle
- 03What the evidence supports
- 04Market structure
- 05Liquidity and sweeps
- 06Displacement and fair value gaps
- 07Order blocks and breakers
- 08Premium, discount and OTE
- 09Timing and the session clock
- 10The full model, one trade
- 11Costs, sizing and losing streaks
- 12Testing the model
- 13Advanced terms
- 14Common mistakes
- 15Pre-trade checklist
- 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.
- 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 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 Sweep
Price runs one pool. The fired stops supply the other side of a large order.
Sweep, stop run, SMT divergence
WyckoffSpring
-
3 Displace
Price moves hard the other way and closes through a swing.
Displacement, CHoCH, CISD
WyckoffSign of strength
-
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 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.
| Claim | Status | Basis |
|---|---|---|
| Stop orders cluster just beyond round numbers and obvious levels. | Measured | In 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. | Measured | Across 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. | Measured | Exchange 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. | Measured | NYSE 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. | Convention | Precise ways to describe price. They organize the chart; on their own they do not forecast. |
| 62%, 70.5% and 79% retracement levels. | Convention | A definition of a deep pullback, not levels with proven special properties. |
| Fair value gaps and order blocks attract price and hold. | Untested | No widely cited peer-reviewed study shows an edge. Measure it on your own market with coded rules. |
| The chart shows what institutions are doing. | Narrative | US 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.
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.
When price trades through a pool, the close tells you what happened.
Sweepwick through, close back inside
Breakclose through, hold beyond
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.
| Bullish FVG | low of candle 3 > high of candle 1 | zone from high 1 to low 3 |
|---|---|---|
| Bearish FVG | high of candle 3 < low of candle 1 | zone from high 3 to low 1 |
| Pine Script | bullFvg = 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.
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.
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.
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.
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.
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.
- Build. Three equal lows at $496.70 to $496.75 (bars 15, 20 and 23) hold a pool of sell stops.
- Sweep. Bar 26 (11:35) runs the pool to $496.15 and closes back above it at $496.80.
- 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.
- 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.
- 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.
- 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.
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.
Position size
| Risk amount | account × risk per trade | $10,000 × 1% = $100 |
|---|---|---|
| Stop distance | entry to stop, plus costs | $1.02 per share |
| Shares from risk | risk amount ÷ stop distance | $100 ÷ $1.02 = 98 shares |
| Position value | shares × entry | 98 × $497.05 = $48,711 |
| Buying power cap | what the account can hold | 20 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
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.
| Concept | Rule | Starting parameter |
|---|---|---|
| Swing | Pivot: a bar with N lower highs (or higher lows) on each side | N = 2. Known only N bars later |
| Equal highs or lows | Two or more swings within a tolerance | 0.1 × ATR(14), about $0.05 here |
| Sweep | Wick beyond the level, close back inside on the same bar | Also test: back inside within 2 bars |
| Displacement | Range at least k × ATR(14), close in the outer quarter of the bar | k = 1.5 |
| Fair value gap | Bullish: low[0] > high[2]. Bearish: high[0] < low[2] | Minimum size 0.25 × ATR(14) |
| Order block | Last opposing candle before the displacement that broke structure | Zone from its high to its low |
| Invalidation | A candle closes beyond the far edge of the zone or the sweep extreme | Closes count, wicks do not |
- Code the rules, or at least write them so two people would mark the same chart the same way.
- Include spread, commission and slippage in every trade.
- 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.
- Design on one period and test on another that you have not looked at.
- Forward test at small size before trading full risk.
- 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.
| Term | What it is | Phase |
|---|---|---|
| Inducement | A minor swing that tempts early entries. Its stops become fuel before the real move. | 1 Build |
| SMT divergence | Two 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 void | A 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 block | Like a breaker, but the move that failed never made a new high or low first. | 4 Return |
| Propulsion block | A candle that trades into an earlier order block and then launches the move. A tighter continuation zone. | 4 Return |
| Breakaway gap | A gap that price never returns to. Strong earnings gaps in stocks often behave this way. | 5 Expand |
| Kill zone | A time window where displacement tends to start. In US stocks, the opening hour and the last hour. | Timing |
| Power of Three | Accumulation, 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.