Indicator · Pine Script v6 · On the chart

Modern Wyckoff State Machine Lite [WavesUnchained]

by casoon

This indicator implements a Wyckoff method state machine that tracks accumulation, distribution, markup, and markdown regimes across phases A through E.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code856
Settings35inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

856 lines of Pine Script v6 from indicators/market_structure/modern_wyckoff_state_machine_visual/modern_wyckoff_state_machine_visual.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

modern_wyckoff_state_machine_visual.pine
//@version=6
// ============================================================================
// Modern Wyckoff State Machine Lite [WavesUnchained]
// Version: 1.4.3
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Lightweight Wyckoff state machine for range context, A-E phase tracking,
// accumulation/distribution scoring, event labels and visual phase state.
//
// Features:
//   ✓ Accumulation, Distribution, Markup and Markdown state tracking
//   ✓ A-E phase model with minimum phase dwell time
//   ✓ Locked trading range after state start
//   ✓ Quality-graded Spring, UTAD, SOS, SOW, LPS and LPSY event detection
//   ✓ Stored Spring/UTAD retest logic
//   ✓ Pivot-confirmed LPS and LPSY detection
//   ✓ Event cooldowns for labels and alerts
//   ✓ Selling Climax / Buying Climax and absorption context
//   ✓ Accumulation vs Distribution score dashboard with Cause Score
//   ✓ Trading range, midpoint, phase zones and phase-track visualization
//   ✓ Alerts for key Wyckoff events and Phase E trend release
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   real trade volume only — volume-confirming conditions
//                         pass through, volume-constituting events (climax,
//                         absorption) are withheld and shown as unavailable
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
//
// ============================================================================

indicator("Modern Wyckoff State Machine Lite [WavesUnchained]",
     shorttitle="Wyckoff Lite",
     overlay=true,
     max_labels_count=500,
     max_lines_count=200,
     max_boxes_count=50)

// ============================================================================
// INPUTS
// ============================================================================

var string gRange = "Range Detection"
int rangeLen      = input.int(80, "Trading Range Length", minval=30, group=gRange)
int pivotLen      = input.int(5, "Swing Pivot Length", minval=2, group=gRange)
int adxLen        = input.int(14, "ADX Length", minval=2, group=gRange)
int atrLen        = input.int(14, "ATR Length", minval=1, group=gRange)
int volLen        = input.int(50, "Volume Z Length", minval=5, group=gRange)

float rangeAtrMax = input.float(8.0, "Max Range Width in ATR", minval=0.25, step=0.25, group=gRange)
float adxMax      = input.float(24.0, "Max ADX for Range", minval=1.0, step=0.5, group=gRange)
bool refreshCauseRange = input.bool(true, "Refresh Range During Cause (Phase A/B)", group=gRange)

var string gState = "State Machine"
int eventLookback = input.int(30, "Event Memory", minval=5, group=gState)
int minPhaseBars  = input.int(5, "Minimum Bars per Phase", minval=1, group=gState)
int eventCooldown = input.int(10, "Event Cooldown", minval=1, group=gState)
int breakoutConfirmBars = input.int(2, "Phase E Breakout Confirm Bars", minval=1, maxval=5, group=gState)
bool resetStuckRange = input.bool(true, "Reset Stuck Accumulation/Distribution", group=gState)
int  maxCauseBars    = input.int(120, "Max Bars in Phase Before Reset", minval=20, group=gState)
int  staleRangeBars  = input.int(40, "Reset After Bars Range Lost", minval=5, group=gState)

var string gEventQuality = "Event Quality"
float springAtrMin = input.float(0.25, "Spring / UTAD Min Depth ATR", minval=0.05, step=0.05, group=gEventQuality)
float springPctMin = input.float(0.75, "Spring / UTAD Min Depth %", minval=0.1, step=0.05, group=gEventQuality)
float minWickRatio = input.float(0.30, "Min Spring / UTAD Wick Ratio", minval=0.1, maxval=0.8, step=0.05, group=gEventQuality)
float minCandleAtr = input.float(0.50, "Min Spring / UTAD Candle ATR", minval=0.1, step=0.05, group=gEventQuality)
float strongWickRatio = input.float(0.50, "Strong Event Wick Ratio", minval=0.2, maxval=0.9, step=0.05, group=gEventQuality)
float strongCandleAtr = input.float(1.00, "Strong Event Candle ATR", minval=0.3, step=0.1, group=gEventQuality)
float strongVolRatio = input.float(1.40, "Strong Event Relative Volume", minval=0.5, step=0.1, group=gEventQuality)
float testAtrWindow = input.float(1.00, "Spring / UTAD Test Window ATR", minval=0.2, step=0.1, group=gEventQuality, tooltip="Half-width of the band around the Spring low / UTAD high. Price must first leave this band, then return into it, for a Test to count.")
float testVolFactor = input.float(0.85, "Test Max Volume vs Event", minval=0.2, maxval=1.2, step=0.05, group=gEventQuality)
float lpsVolFactor = input.float(0.85, "LPS / LPSY Max Pivot Volume", minval=0.2, maxval=1.2, step=0.05, group=gEventQuality)

var string gDisplay = "Display"
bool showRange     = input.bool(true, "Show Range", group=gDisplay)
bool showZones     = input.bool(true, "Show Phase Zones", group=gDisplay)
bool showLabels    = input.bool(true, "Show Event Labels", group=gDisplay)
bool showDash      = input.bool(false, "Show Dashboard", group=gDisplay)
bool showPhaseLine = input.bool(true, "Show Phase Track", group=gDisplay)
int maxPhaseZones  = input.int(20, "Max Phase Zones", minval=1, maxval=45, group=gDisplay)
string oldZoneMode = input.string("Fade", "Old Zone Mode", options=["Delete", "Fade", "Hide Labels", "Current Only"], group=gDisplay)
int oldZoneTransp  = input.int(96, "Old Zone Transparency", minval=80, maxval=100, group=gDisplay)
int maxEventLabels = input.int(120, "Max Event Labels", minval=20, maxval=450, group=gDisplay)

// ============================================================================
// TYPES / STATE
// ============================================================================

enum WState
    Neutral
    Accumulation
    Distribution
    Markup
    Markdown

enum WPhase
    None
    A
    B
    C
    D
    E

var WState wyState = WState.Neutral
var WPhase wyPhase = WPhase.None
var int phaseStartBar = na
var float lockedRangeHigh = na
var float lockedRangeLow = na
var int lockedRangeStartBar = na

// ============================================================================
// CORE MEASURES
// ============================================================================

float atr = ta.atr(atrLen)

float upMove   = high - high[1]
float downMove = low[1] - low

float plusDM  = upMove > downMove and upMove > 0 ? upMove : 0.0
float minusDM = downMove > upMove and downMove > 0 ? downMove : 0.0

float trur    = ta.rma(ta.tr(true), adxLen)
float plusDI  = 100 * ta.rma(plusDM, adxLen) / math.max(trur, syminfo.mintick)
float minusDI = 100 * ta.rma(minusDM, adxLen) / math.max(trur, syminfo.mintick)
float dx      = 100 * math.abs(plusDI - minusDI) / math.max(plusDI + minusDI, 0.0001)
float adx     = ta.rma(dx, adxLen)

float volMean = ta.sma(volume, volLen)
float volDev  = ta.stdev(volume, volLen)
float volZ    = volDev > 0 ? (volume - volMean) / volDev : 0.0
float relVol  = volMean > 0 ? volume / volMean : 1.0
// Volume validity. Wyckoff uses volume in two different roles, and they must be
// degraded differently. Confirming conditions (SOS/SOW, tests, LPS/LPSY) only
// qualify an event that price structure already defines — they pass through.
// Constituting events (Selling/Buying Climax, absorption) *are* volume events;
// without volume a climax is just a wide bar, so they are withheld rather than
// approximated. Both cases were previously silent: with volZ collapsing to 0
// and relVol to 1.0, tests and LPS/LPSY could never satisfy their 0.85 factors.
// See DATA_VALIDITY.md.
bool volumeIsReal =
     (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and
     not na(volume)

float spread  = high - low
float body    = math.abs(close - open)
float bodyPct = body / math.max(spread, syminfo.mintick)
float lowerWick = math.min(open, close) - low
float upperWick = high - math.max(open, close)
float lowerWickRatio = lowerWick / math.max(spread, syminfo.mintick)
float upperWickRatio = upperWick / math.max(spread, syminfo.mintick)
float candleAtr = spread / math.max(atr, syminfo.mintick)
float spreadMean = ta.sma(spread, volLen)
float spreadDev = ta.stdev(spread, volLen)
float spreadZ = spreadDev > 0 ? (spread - spreadMean) / spreadDev : 0.0

float rangeHigh = ta.highest(high, rangeLen)
float rangeLow  = ta.lowest(low, rangeLen)
float rangeHighPrev = rangeHigh[1]
float rangeLowPrev  = rangeLow[1]

bool lockActiveRange = wyState == WState.Accumulation or wyState == WState.Distribution or wyState == WState.Markup or wyState == WState.Markdown
if lockActiveRange and na(lockedRangeHigh)
    lockedRangeHigh := rangeHigh
    lockedRangeLow := rangeLow
    lockedRangeStartBar := bar_index
if wyState == WState.Neutral and not na(lockedRangeHigh)
    lockedRangeHigh := na
    lockedRangeLow := na
    lockedRangeStartBar := na

bool rangeLocked = not na(lockedRangeHigh) and not na(lockedRangeLow)
float activeRangeHigh = rangeLocked ? lockedRangeHigh : rangeHigh
float activeRangeLow = rangeLocked ? lockedRangeLow : rangeLow
float activeRangeHighRef = rangeLocked ? lockedRangeHigh : rangeHighPrev
float activeRangeLowRef = rangeLocked ? lockedRangeLow : rangeLowPrev
float rangeMid  = (activeRangeHigh + activeRangeLow) / 2.0
float rangeW    = activeRangeHigh - activeRangeLow
float rangeAtr  = rangeW / math.max(atr, syminfo.mintick)

float pos        = (close - activeRangeLow) / math.max(rangeW, syminfo.mintick)
float posClamped = math.max(0.0, math.min(1.0, pos))

bool nearLow  = posClamped < 0.28
bool nearHigh = posClamped > 0.72

bool rangeOk = adx < adxMax and rangeAtr < rangeAtrMax and bar_index > rangeLen

// ============================================================================
// STRUCTURE
// ============================================================================

float emaFast = ta.ema(close, 21)
float emaSlow = ta.ema(close, 55)
float emaTrend = ta.ema(close, 34)

bool trendUp   = emaFast > emaSlow
bool trendDown = emaFast < emaSlow

int priorRef = int(math.max(1, math.round(rangeLen / 2.0)))
bool priorTrendDown = close < emaTrend[priorRef]
bool priorTrendUp   = close > emaTrend[priorRef]

float ph = ta.pivothigh(high, pivotLen, pivotLen)
float pl = ta.pivotlow(low, pivotLen, pivotLen)

var float lastSwingHigh = na
var float lastSwingLow  = na

if not na(ph)
    lastSwingHigh := ph

if not na(pl)
    lastSwingLow := pl

bool bullBos = not na(lastSwingHigh) and close > lastSwingHigh
bool bearBos = not na(lastSwingLow) and close < lastSwingLow

// ============================================================================
// WYCKOFF EVENTS
// ============================================================================

eventQuality(bool triggered, float wickRatio, float candleAtrRatio, float volumeRatio, float sweepAtr) =>
    int q = 0
    if triggered
        int factors = 0
        if wickRatio >= strongWickRatio
            factors += 1
        if candleAtrRatio >= strongCandleAtr
            factors += 1
        if volumeRatio >= strongVolRatio
            factors += 1
        if sweepAtr >= springAtrMin * 2.0
            factors += 1
        q := factors >= 3 ? 3 : factors >= 1 ? 2 : 1
    q

qualityTag(int quality) =>
    quality == 3 ? "★ " : quality == 1 ? "~ " : ""

float springDepth = activeRangeLowRef - low
float springDepthPct = not na(activeRangeLowRef) and math.abs(activeRangeLowRef) > syminfo.mintick ? springDepth / activeRangeLowRef * 100.0 : 0.0
float springSweepAtr = springDepth / math.max(atr, syminfo.mintick)
bool springDepthOk = springDepth > atr * springAtrMin or springDepthPct >= springPctMin
bool springRaw = rangeOk and springDepth > 0 and close > activeRangeLowRef
bool spring    = springRaw and springDepthOk and nearLow and close > open and lowerWickRatio >= minWickRatio and candleAtr >= minCandleAtr
int springQuality = eventQuality(spring, lowerWickRatio, candleAtr, relVol, springSweepAtr)

float utadDepth = high - activeRangeHighRef
float utadDepthPct = not na(activeRangeHighRef) and math.abs(activeRangeHighRef) > syminfo.mintick ? utadDepth / activeRangeHighRef * 100.0 : 0.0
float utadSweepAtr = utadDepth / math.max(atr, syminfo.mintick)
bool utadDepthOk = utadDepth > atr * springAtrMin or utadDepthPct >= springPctMin
bool utadRaw = rangeOk and utadDepth > 0 and close < activeRangeHighRef
bool utad    = utadRaw and utadDepthOk and nearHigh and close < open and upperWickRatio >= minWickRatio and candleAtr >= minCandleAtr
int utadQuality = eventQuality(utad, upperWickRatio, candleAtr, relVol, utadSweepAtr)

var int lastSpringBar = na
var int lastUtadBar = na
var int lastSosBar = na
var int lastSowBar = na
var int lastTestSpringBar = na
var int lastTestUtadBar = na
var int lastLpsBar = na
var int lastLpsyBar = na
var int lastScBar = na
var int lastBcBar = na
var float lastSpringPrice = na
var float lastSpringVolRatio = na
var int lastSpringQuality = 0
var float lastUtadPrice = na
var float lastUtadVolRatio = na
var int lastUtadQuality = 0

bool springFire = spring and (na(lastSpringBar) or bar_index - lastSpringBar > eventCooldown)
if springFire
    lastSpringBar := bar_index
    lastSpringPrice := low
    lastSpringVolRatio := relVol
    lastSpringQuality := springQuality

bool utadFire = utad and (na(lastUtadBar) or bar_index - lastUtadBar > eventCooldown)
if utadFire
    lastUtadBar := bar_index
    lastUtadPrice := high
    lastUtadVolRatio := relVol
    lastUtadQuality := utadQuality

bool springMemory = ta.highest(springFire ? 1 : 0, eventLookback) > 0
bool utadMemory   = ta.highest(utadFire ? 1 : 0, eventLookback) > 0

bool sos = rangeOk and springMemory and close > rangeMid and close > high[1] and (not volumeIsReal or volZ > -0.5)
bool sow = rangeOk and utadMemory and close < rangeMid and close < low[1] and (not volumeIsReal or volZ > -0.5)

bool sosFire = sos and (na(lastSosBar) or bar_index - lastSosBar > eventCooldown)
if sosFire
    lastSosBar := bar_index

bool sowFire = sow and (na(lastSowBar) or bar_index - lastSowBar > eventCooldown)
if sowFire
    lastSowBar := bar_index

bool springTestInWindow = not na(lastSpringPrice) and not na(lastSpringBar) and bar_index > lastSpringBar and bar_index - lastSpringBar <= eventLookback
bool utadTestInWindow = not na(lastUtadPrice) and not na(lastUtadBar) and bar_index > lastUtadBar and bar_index - lastUtadBar <= eventLookback
// A Test is a RETURN to the Spring low / UTAD high — price must first leave the
// ±testAtrWindow band around it, so the bars right after the event cannot count.
var bool springLeftBand = false
var bool utadLeftBand = false
if springFire
    springLeftBand := false
else if not na(lastSpringPrice) and (low > lastSpringPrice + atr * testAtrWindow or high < lastSpringPrice - atr * testAtrWindow)
    springLeftBand := true
if utadFire
    utadLeftBand := false
else if not na(lastUtadPrice) and (high < lastUtadPrice - atr * testAtrWindow or low > lastUtadPrice + atr * testAtrWindow)
    utadLeftBand := true
bool springTestPrice = springTestInWindow and springLeftBand and low <= lastSpringPrice + atr * testAtrWindow and low >= lastSpringPrice - atr * testAtrWindow and close > lastSpringPrice
bool utadTestPrice = utadTestInWindow and utadLeftBand and high >= lastUtadPrice - atr * testAtrWindow and high <= lastUtadPrice + atr * testAtrWindow and close < lastUtadPrice
bool testSpring = rangeOk and springMemory and not spring and springTestPrice and (not volumeIsReal or relVol <= nz(lastSpringVolRatio, relVol) * testVolFactor)
bool testUtad   = rangeOk and utadMemory and not utad and utadTestPrice and (not volumeIsReal or relVol <= nz(lastUtadVolRatio, relVol) * testVolFactor)

bool testSpringFire = testSpring and (na(lastTestSpringBar) or bar_index - lastTestSpringBar > eventCooldown)
if testSpringFire
    lastTestSpringBar := bar_index

bool testUtadFire = testUtad and (na(lastTestUtadBar) or bar_index - lastTestUtadBar > eventCooldown)
if testUtadFire
    lastTestUtadBar := bar_index

bool sosMemory = ta.highest(sosFire ? 1 : 0, eventLookback) > 0
bool sowMemory = ta.highest(sowFire ? 1 : 0, eventLookback) > 0

bool pivotLowConfirmed = not na(pl)
bool pivotHighConfirmed = not na(ph)
float pivotLowVolRatio = volMean[pivotLen] > 0 ? volume[pivotLen] / volMean[pivotLen] : 1.0
float pivotHighVolRatio = volMean[pivotLen] > 0 ? volume[pivotLen] / volMean[pivotLen] : 1.0
bool pivotLowInLpsZone = low[pivotLen] > activeRangeLow and low[pivotLen] < activeRangeLow + rangeW * 0.45
bool pivotHighInLpsyZone = high[pivotLen] < activeRangeHigh and high[pivotLen] > activeRangeHigh - rangeW * 0.45
bool lps  = rangeOk and sosMemory and not sos and pivotLowConfirmed and pivotLowInLpsZone and (not volumeIsReal or pivotLowVolRatio <= lpsVolFactor) and close[pivotLen] > open[pivotLen]
bool lpsy = rangeOk and sowMemory and not sow and pivotHighConfirmed and pivotHighInLpsyZone and (not volumeIsReal or pivotHighVolRatio <= lpsVolFactor) and close[pivotLen] < open[pivotLen]

bool lpsFire = lps and (na(lastLpsBar) or bar_index - lastLpsBar > eventCooldown)
if lpsFire
    lastLpsBar := bar_index

bool lpsyFire = lpsy and (na(lastLpsyBar) or bar_index - lastLpsyBar > eventCooldown)
if lpsyFire
    lastLpsyBar := bar_index

bool sellingClimax = volumeIsReal and priorTrendDown and volZ > 1.5 and spreadZ > 1.0 and bodyPct > 0.45 and low <= activeRangeLow and close > low + spread * 0.35
bool buyingClimax  = volumeIsReal and priorTrendUp and volZ > 1.5 and spreadZ > 1.0 and bodyPct > 0.45 and high >= activeRangeHigh and close < high - spread * 0.35

bool sellingClimaxFire = sellingClimax and (na(lastScBar) or bar_index - lastScBar > eventCooldown)
if sellingClimaxFire
    lastScBar := bar_index

bool buyingClimaxFire = buyingClimax and (na(lastBcBar) or bar_index - lastBcBar > eventCooldown)
if buyingClimaxFire
    lastBcBar := bar_index

bool absorptionLow  = volumeIsReal and rangeOk and nearLow and volZ > 0.75 and close > low + spread * 0.5
bool absorptionHigh = volumeIsReal and rangeOk and nearHigh and volZ > 0.75 and close < high - spread * 0.5

// ============================================================================
// SCORES
// ============================================================================

float rangeScore = rangeOk ? 20.0 : 0.0

float accLocation  = nearLow ? 18.0 : posClamped < 0.5 ? 10.0 : 0.0
float distLocation = nearHigh ? 18.0 : posClamped > 0.5 ? 10.0 : 0.0

float accEvent  = springFire ? 15.0 + springQuality * 3.0 : testSpringFire ? 16.0 : sellingClimaxFire ? 12.0 : 0.0
float distEvent = utadFire ? 15.0 + utadQuality * 3.0 : testUtadFire ? 16.0 : buyingClimaxFire ? 12.0 : 0.0

float accAbsorb  = absorptionLow ? 18.0 : 0.0
float distAbsorb = absorptionHigh ? 18.0 : 0.0

float accReclaim = close > rangeMid ? 15.0 : 0.0
float distLoss   = close < rangeMid ? 15.0 : 0.0

float accStruct  = bullBos or trendUp ? 15.0 : 0.0
float distStruct = bearBos or trendDown ? 15.0 : 0.0

float accScore  = math.min(100.0, rangeScore + accLocation + accEvent + accAbsorb + accReclaim + accStruct)
float distScore = math.min(100.0, rangeScore + distLocation + distEvent + distAbsorb + distLoss + distStruct)

// rangeOk is exactly ADX + width, so each component is counted once.
float rangeQuality = math.min(100.0,
     (adx < adxMax ? 50.0 : 0.0) +
     (rangeAtr < rangeAtrMax ? 40.0 : 0.0) +
     (math.abs(posClamped - 0.5) < 0.35 ? 10.0 : 0.0))

float volMeanSlow = ta.sma(volume, volLen * 2)
float compressionScore = 100.0 - math.min(100.0, rangeAtr * 10.0)
float rangeDwellBars = rangeLocked and not na(lockedRangeStartBar) ? bar_index - lockedRangeStartBar : 0.0
float dwellScore = math.min(30.0, rangeDwellBars)
float swingScore = not na(lastSwingHigh) and not na(lastSwingLow) ? 20.0 : 0.0
float volumeDryScore = volumeIsReal and volMean < volMeanSlow ? 15.0 : 0.0
// Judged against the range before this bar (Ref) — the unlocked active range
// includes the current bar and would always be respected.
float respectScore = high <= activeRangeHighRef and low >= activeRangeLowRef ? 15.0 : 0.0
float causeScore = math.min(100.0, compressionScore * 0.35 + dwellScore + swingScore + volumeDryScore + respectScore)
bool causeBuilding = causeScore >= 45.0 and (causeScore >= causeScore[1] or causeScore >= 65.0)

// ============================================================================
// STATE MACHINE
// ============================================================================

int barsInPhase = na(phaseStartBar) ? 0 : bar_index - phaseStartBar
bool phaseCanAdvance = barsInPhase >= minPhaseBars

bool accBias    = accScore > distScore and accScore >= 45
bool distBias   = distScore > accScore and distScore >= 45
bool strongAcc  = accScore >= 65 and accScore > distScore + 15
bool strongDist = distScore >= 65 and distScore > accScore + 15

bool bullBreakoutRaw = close > activeRangeHighRef and accScore > distScore and (bullBos or spreadZ > 0.5 or close > activeRangeHighRef + atr * 0.1)
bool bearBreakoutRaw = close < activeRangeLowRef and distScore > accScore and (bearBos or spreadZ > 0.5 or close < activeRangeLowRef - atr * 0.1)
bool bullBreakoutConfirmed = ta.barssince(close <= activeRangeHighRef) >= breakoutConfirmBars and bullBreakoutRaw
bool bearBreakoutConfirmed = ta.barssince(close >= activeRangeLowRef) >= breakoutConfirmBars and bearBreakoutRaw

bool accPhaseA = sellingClimaxFire or (volumeIsReal and priorTrendDown and rangeOk and nearLow and volZ > 1.0)
bool accPhaseB = rangeOk and accBias and causeBuilding and not springFire and not sosFire
bool accPhaseC = springFire or testSpringFire
bool accPhaseD = sosFire or lpsFire or (close > rangeMid and close <= activeRangeHigh)
bool accPhaseE = bullBreakoutConfirmed

bool distPhaseA = buyingClimaxFire or (volumeIsReal and priorTrendUp and rangeOk and nearHigh and volZ > 1.0)
bool distPhaseB = rangeOk and distBias and causeBuilding and not utadFire and not sowFire
bool distPhaseC = utadFire or testUtadFire
bool distPhaseD = sowFire or lpsyFire or (close < rangeMid and close >= activeRangeLow)
bool distPhaseE = bearBreakoutConfirmed

if wyState == WState.Neutral
    if accPhaseA
        wyState := WState.Accumulation
        wyPhase := WPhase.A
        phaseStartBar := bar_index
        lockedRangeHigh := rangeHigh
        lockedRangeLow := rangeLow
        lockedRangeStartBar := bar_index
    else if distPhaseA
        wyState := WState.Distribution
        wyPhase := WPhase.A
        phaseStartBar := bar_index
        lockedRangeHigh := rangeHigh
        lockedRangeLow := rangeLow
        lockedRangeStartBar := bar_index
    else if strongAcc and rangeOk and causeBuilding
        wyState := WState.Accumulation
        wyPhase := WPhase.B
        phaseStartBar := bar_index
        lockedRangeHigh := rangeHigh
        lockedRangeLow := rangeLow
        lockedRangeStartBar := bar_index
    else if strongDist and rangeOk and causeBuilding
        wyState := WState.Distribution
        wyPhase := WPhase.B
        phaseStartBar := bar_index
        lockedRangeHigh := rangeHigh
        lockedRangeLow := rangeLow
        lockedRangeStartBar := bar_index

if wyState == WState.Accumulation and phaseCanAdvance
    if wyPhase == WPhase.A and accPhaseB
        wyPhase := WPhase.B
        phaseStartBar := bar_index
    else if wyPhase == WPhase.B and accPhaseC
        wyPhase := WPhase.C
        phaseStartBar := bar_index
    else if wyPhase == WPhase.C and accPhaseD
        wyPhase := WPhase.D
        phaseStartBar := bar_index
    else if wyPhase == WPhase.D and accPhaseE
        wyPhase := WPhase.E
        wyState := WState.Markup
        phaseStartBar := bar_index

if wyState == WState.Distribution and phaseCanAdvance
    if wyPhase == WPhase.A and distPhaseB
        wyPhase := WPhase.B
        phaseStartBar := bar_index
    else if wyPhase == WPhase.B and distPhaseC
        wyPhase := WPhase.C
        phaseStartBar := bar_index
    else if wyPhase == WPhase.C and distPhaseD
        wyPhase := WPhase.D
        phaseStartBar := bar_index
    else if wyPhase == WPhase.D and distPhaseE
        wyPhase := WPhase.E
        wyState := WState.Markdown
        phaseStartBar := bar_index

bool hardBullInvalidation = wyState == WState.Distribution and close > activeRangeHighRef and accScore > distScore + 25
bool hardBearInvalidation = wyState == WState.Accumulation and close < activeRangeLowRef and distScore > accScore + 25

if hardBullInvalidation
    wyState := WState.Accumulation
    wyPhase := WPhase.B
    phaseStartBar := bar_index
    lockedRangeHigh := rangeHigh
    lockedRangeLow := rangeLow
    lockedRangeStartBar := bar_index

if hardBearInvalidation
    wyState := WState.Distribution
    wyPhase := WPhase.B
    phaseStartBar := bar_index
    lockedRangeHigh := rangeHigh
    lockedRangeLow := rangeLow
    lockedRangeStartBar := bar_index

if wyState == WState.Markup and rangeOk and barsInPhase > 20 and close < activeRangeHighRef
    wyState := WState.Neutral
    wyPhase := WPhase.None
    phaseStartBar := na
    lockedRangeHigh := na
    lockedRangeLow := na
    lockedRangeStartBar := na

if wyState == WState.Markdown and rangeOk and barsInPhase > 20 and close > activeRangeLowRef
    wyState := WState.Neutral
    wyPhase := WPhase.None
    phaseStartBar := na
    lockedRangeHigh := na
    lockedRangeLow := na
    lockedRangeStartBar := na

// Lever 1 — reset a stuck Accumulation/Distribution back to Neutral when the
// trading-range context is lost (range conditions gone too long) or the phase
// has dwelled far past any realistic cause-building window.
bool inCause = wyState == WState.Accumulation or wyState == WState.Distribution
int  barsSinceRangeOk = ta.barssince(rangeOk)
bool causeStale = resetStuckRange and inCause and (barsInPhase >= maxCauseBars or nz(barsSinceRangeOk, 0) >= staleRangeBars)
if causeStale
    wyState := WState.Neutral
    wyPhase := WPhase.None
    phaseStartBar := na
    lockedRangeHigh := na
    lockedRangeLow := na
    lockedRangeStartBar := na

// Lever 2 — while still building cause (Phase A/B) and the range is valid, keep
// the locked range in sync with the live structure so the zone tracks current
// price; it freezes naturally once a Spring/UTAD advances the state to Phase C.
bool causeBuildingPhase = (wyState == WState.Accumulation or wyState == WState.Distribution) and (wyPhase == WPhase.A or wyPhase == WPhase.B)
if refreshCauseRange and causeBuildingPhase and rangeOk and not na(lockedRangeHigh)
    lockedRangeHigh := rangeHigh
    lockedRangeLow := rangeLow

rangeLocked := not na(lockedRangeHigh) and not na(lockedRangeLow)
activeRangeHigh := rangeLocked ? lockedRangeHigh : rangeHigh
activeRangeLow := rangeLocked ? lockedRangeLow : rangeLow
activeRangeHighRef := rangeLocked ? lockedRangeHigh : rangeHighPrev
activeRangeLowRef := rangeLocked ? lockedRangeLow : rangeLowPrev
rangeMid := (activeRangeHigh + activeRangeLow) / 2.0
rangeW := activeRangeHigh - activeRangeLow
rangeAtr := rangeW / math.max(atr, syminfo.mintick)

bool markupStarted = wyState == WState.Markup and wyPhase == WPhase.E and (wyState[1] != WState.Markup or wyPhase[1] != WPhase.E)
bool markdownStarted = wyState == WState.Markdown and wyPhase == WPhase.E and (wyState[1] != WState.Markdown or wyPhase[1] != WPhase.E)

// ============================================================================
// DISPLAY STRINGS
// ============================================================================

string stateText =
     wyState == WState.Accumulation ? "ACCUMULATION" :
     wyState == WState.Distribution ? "DISTRIBUTION" :
     wyState == WState.Markup ? "MARKUP" :
     wyState == WState.Markdown ? "MARKDOWN" :
     "NEUTRAL"

string phaseText =
     wyPhase == WPhase.A ? "A: Stopping Action" :
     wyPhase == WPhase.B ? "B: Building Cause" :
     wyPhase == WPhase.C ? wyState == WState.Distribution ? "C: UTAD / Test" : "C: Spring / Test" :
     wyPhase == WPhase.D ? wyState == WState.Distribution ? "D: Markdown Prep" : "D: Markup Prep" :
     wyPhase == WPhase.E ? "E: Trend Release" :
     "None"

float confidence =
     wyState == WState.Accumulation or wyState == WState.Markup ? accScore :
     wyState == WState.Distribution or wyState == WState.Markdown ? distScore :
     math.max(accScore, distScore)

// ============================================================================
// VISUALS
// ============================================================================

color longZoneColor = #00c853
color markupZoneColor = #009688
color shortZoneColor = #d50000
color markdownZoneColor = #ff6f00

color stateColor =
     wyState == WState.Accumulation ? longZoneColor :
     wyState == WState.Distribution ? shortZoneColor :
     wyState == WState.Markup ? markupZoneColor :
     wyState == WState.Markdown ? markdownZoneColor :
     color.gray

color zoneFillColor =
     wyState == WState.Accumulation ? color.new(longZoneColor, 80) :
     wyState == WState.Distribution ? color.new(shortZoneColor, 80) :
     wyState == WState.Markup ? color.new(markupZoneColor, 80) :
     wyState == WState.Markdown ? color.new(markdownZoneColor, 80) :
     color.new(color.gray, 92)

color zoneBorderColor =
     wyState == WState.Accumulation ? color.new(longZoneColor, 30) :
     wyState == WState.Distribution ? color.new(shortZoneColor, 30) :
     wyState == WState.Markup ? color.new(markupZoneColor, 30) :
     wyState == WState.Markdown ? color.new(markdownZoneColor, 30) :
     color.new(color.gray, 45)
color previousStateColor = stateColor[1]

var box activePhaseZone = na
var label activePhaseZoneLabel = na
var box[] phaseZoneBoxes = array.new_box()
var label[] phaseZoneLabels = array.new_label()

bool hasPhaseZone = showZones and wyState != WState.Neutral and wyPhase != WPhase.None and not na(activeRangeHigh) and not na(activeRangeLow) and rangeW > 0
bool phaseZoneChanged = wyState != wyState[1] or wyPhase != wyPhase[1]
string zoneStateShort =
     wyState == WState.Accumulation ? "ACC" :
     wyState == WState.Distribution ? "DIST" :
     wyState == WState.Markup ? "MARKUP" :
     wyState == WState.Markdown ? "MARKDOWN" :
     "NEUTRAL"
string phaseLetter =
     wyPhase == WPhase.A ? "A" :
     wyPhase == WPhase.B ? "B" :
     wyPhase == WPhase.C ? "C" :
     wyPhase == WPhase.D ? "D" :
     wyPhase == WPhase.E ? "E" :
     "-"
string phaseZoneText = zoneStateShort + "\nPhase " + phaseLetter
string phaseZoneTooltip =
     "Zone: " + zoneStateShort + " Phase " + phaseLetter +
     "\nState: " + stateText +
     "\nPhase: " + phaseText +
     "\nConfidence: " + str.tostring(confidence, "#") +
     "\nAccumulation: " + str.tostring(accScore, "#") +
     "\nDistribution: " + str.tostring(distScore, "#") +
     "\nCause: " + str.tostring(causeScore, "#") +
     "\nLocked Range: " + (rangeLocked ? "Yes" : "No") +
     "\nRange Quality: " + str.tostring(rangeQuality, "#") +
     "\nRange ATR: " + str.tostring(rangeAtr, "#.##") +
     "\nRange High: " + str.tostring(activeRangeHigh, format.mintick) +
     "\nRange Low: " + str.tostring(activeRangeLow, format.mintick)

if hasPhaseZone and (na(activePhaseZone) or phaseZoneChanged)
    if not na(activePhaseZone)
        box.set_right(activePhaseZone, bar_index)
        if oldZoneMode == "Fade"
            box.set_bgcolor(activePhaseZone, color.new(previousStateColor, oldZoneTransp))
            box.set_border_color(activePhaseZone, color.new(previousStateColor, 85))
        if oldZoneMode == "Hide Labels" and not na(activePhaseZoneLabel)
            label.delete(activePhaseZoneLabel)
            if array.size(phaseZoneLabels) > 0
                array.pop(phaseZoneLabels)
        if oldZoneMode == "Current Only"
            box.delete(activePhaseZone)
            if not na(activePhaseZoneLabel)
                label.delete(activePhaseZoneLabel)
            if array.size(phaseZoneBoxes) > 0
                array.pop(phaseZoneBoxes)
            if array.size(phaseZoneLabels) > 0
                array.pop(phaseZoneLabels)

    activePhaseZone := box.new(left=bar_index, top=activeRangeHigh, right=bar_index + 1, bottom=activeRangeLow,
         border_color=zoneBorderColor,
         bgcolor=zoneFillColor)
    activePhaseZoneLabel := label.new(bar_index, activeRangeHigh, phaseZoneText,
         style=label.style_label_down,
         textcolor=color.white,
         color=color.new(stateColor, 0),
         size=size.tiny,
         tooltip=phaseZoneTooltip)

    array.push(phaseZoneBoxes, activePhaseZone)
    array.push(phaseZoneLabels, activePhaseZoneLabel)

    if array.size(phaseZoneBoxes) > maxPhaseZones
        box oldBox = array.shift(phaseZoneBoxes)
        box.delete(oldBox)
    if array.size(phaseZoneLabels) > maxPhaseZones
        label oldLabel = array.shift(phaseZoneLabels)
        label.delete(oldLabel)
else if hasPhaseZone and not na(activePhaseZone)
    box.set_right(activePhaseZone, bar_index + 1)
    box.set_top(activePhaseZone, activeRangeHigh)
    box.set_bottom(activePhaseZone, activeRangeLow)
    box.set_bgcolor(activePhaseZone, zoneFillColor)
    box.set_border_color(activePhaseZone, zoneBorderColor)
    label.set_x(activePhaseZoneLabel, bar_index)
    label.set_y(activePhaseZoneLabel, activeRangeHigh)
    label.set_text(activePhaseZoneLabel, phaseZoneText)
    label.set_color(activePhaseZoneLabel, color.new(stateColor, 0))
    label.set_tooltip(activePhaseZoneLabel, phaseZoneTooltip)
else if not hasPhaseZone and not na(activePhaseZone)
    box.set_right(activePhaseZone, bar_index)
    activePhaseZone := na
    activePhaseZoneLabel := na

plot(showRange ? activeRangeHigh : na, "Range High", color=color.new(color.red, 10), linewidth=1)
plot(showRange ? rangeMid : na, "Range Mid", color=color.new(color.gray, 30), linewidth=1)
plot(showRange ? activeRangeLow : na, "Range Low", color=color.new(color.lime, 10), linewidth=1)

upperBand = plot(showRange ? activeRangeHigh : na, display=display.none)
lowerBand = plot(showRange ? activeRangeLow : na, display=display.none)
fill(upperBand, lowerBand, color=rangeOk ? color.new(color.gray, 94) : na)

barcolor(
     springFire or sosFire or lpsFire ? color.lime :
     utadFire or sowFire or lpsyFire ? color.red :
     testSpringFire ? color.new(color.lime, 35) :
     testUtadFire ? color.new(color.red, 35) :
     sellingClimaxFire ? color.new(color.green, 0) :
     buyingClimaxFire ? color.new(color.maroon, 0) :
     na)

float phaseValue =
     wyPhase == WPhase.A ? activeRangeLow :
     wyPhase == WPhase.B ? activeRangeLow + rangeW * 0.25 :
     wyPhase == WPhase.C ? activeRangeLow + rangeW * 0.50 :
     wyPhase == WPhase.D ? activeRangeLow + rangeW * 0.75 :
     wyPhase == WPhase.E ? activeRangeHigh :
     na

plot(showPhaseLine ? phaseValue : na, "Wyckoff Phase Track", color=color.new(stateColor, 0), linewidth=2, style=plot.style_linebr)

// ============================================================================
// EVENT LABELS
// ============================================================================

var label[] eventLabels = array.new_label()

storeEventLabel(label eventLabel) =>
    array.push(eventLabels, eventLabel)
    if array.size(eventLabels) > maxEventLabels
        label oldEventLabel = array.shift(eventLabels)
        label.delete(oldEventLabel)

if showLabels and sellingClimaxFire
    storeEventLabel(label.new(bar_index, low, "SC", style=label.style_label_up, textcolor=color.white, color=color.new(color.green, 0), size=size.tiny))

if showLabels and buyingClimaxFire
    storeEventLabel(label.new(bar_index, high, "BC", style=label.style_label_down, textcolor=color.white, color=color.new(color.red, 0), size=size.tiny))

if showLabels and springFire
    storeEventLabel(label.new(bar_index, low, qualityTag(springQuality) + "Spring", style=label.style_label_up, textcolor=color.white, color=color.new(color.green, 0), size=size.small,
         tooltip="Spring quality: " + str.tostring(springQuality) + " / 3\nSweep: " + str.tostring(springSweepAtr, "#.00") + " ATR\nWick: " + str.tostring(lowerWickRatio * 100, "#.0") + "%\nRelVol: " + str.tostring(relVol, "#.00") + "x"))

if showLabels and utadFire
    storeEventLabel(label.new(bar_index, high, qualityTag(utadQuality) + "UTAD", style=label.style_label_down, textcolor=color.white, color=color.new(color.maroon, 0), size=size.small,
         tooltip="UTAD quality: " + str.tostring(utadQuality) + " / 3\nSweep: " + str.tostring(utadSweepAtr, "#.00") + " ATR\nWick: " + str.tostring(upperWickRatio * 100, "#.0") + "%\nRelVol: " + str.tostring(relVol, "#.00") + "x"))

if showLabels and testSpringFire
    storeEventLabel(label.new(bar_index, low, "Test", style=label.style_label_up, textcolor=color.white, color=color.new(color.teal, 0), size=size.tiny,
         tooltip="Spring Test\nSpring: " + str.tostring(lastSpringPrice, format.mintick) + "\nEvent Quality: " + str.tostring(lastSpringQuality) + " / 3\nRelVol: " + str.tostring(relVol, "#.00") + "x vs event " + str.tostring(lastSpringVolRatio, "#.00") + "x"))

if showLabels and testUtadFire
    storeEventLabel(label.new(bar_index, high, "Test", style=label.style_label_down, textcolor=color.white, color=color.new(color.orange, 0), size=size.tiny,
         tooltip="UTAD Test\nUTAD: " + str.tostring(lastUtadPrice, format.mintick) + "\nEvent Quality: " + str.tostring(lastUtadQuality) + " / 3\nRelVol: " + str.tostring(relVol, "#.00") + "x vs event " + str.tostring(lastUtadVolRatio, "#.00") + "x"))

if showLabels and sosFire
    storeEventLabel(label.new(bar_index, high, "SOS", style=label.style_label_up, textcolor=color.white, color=color.new(color.blue, 0), size=size.tiny))

if showLabels and sowFire
    storeEventLabel(label.new(bar_index, low, "SOW", style=label.style_label_down, textcolor=color.white, color=color.new(color.purple, 0), size=size.tiny))

if showLabels and lpsFire
    storeEventLabel(label.new(bar_index - pivotLen, low[pivotLen], "LPS", style=label.style_label_up, textcolor=color.white, color=color.new(color.green, 25), size=size.tiny,
         tooltip="Pivot-confirmed LPS\nPivot volume: " + str.tostring(pivotLowVolRatio, "#.00") + "x\nConfirmed " + str.tostring(pivotLen) + " bars later"))

if showLabels and lpsyFire
    storeEventLabel(label.new(bar_index - pivotLen, high[pivotLen], "LPSY", style=label.style_label_down, textcolor=color.white, color=color.new(color.red, 25), size=size.tiny,
         tooltip="Pivot-confirmed LPSY\nPivot volume: " + str.tostring(pivotHighVolRatio, "#.00") + "x\nConfirmed " + str.tostring(pivotLen) + " bars later"))

// ============================================================================
// DASHBOARD
// ============================================================================

var table dash = table.new(position.top_right, 2, 11,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1,
     frame_color=color.new(color.gray, 40),
     frame_width=1)

color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
color bullAccent = #00c853
color bearAccent = #d50000
color warningAccent = #ff6f00
color stateTextColor =
     wyState == WState.Accumulation or wyState == WState.Markup ? bullAccent :
     wyState == WState.Distribution or wyState == WState.Markdown ? bearAccent :
     tc

if showDash and barstate.islast
    table.cell(dash, 0, 0, "Wyckoff", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(dash, 1, 0, stateText, text_color=stateTextColor, bgcolor=hd, text_size=size.small)

    table.cell(dash, 0, 1, "Phase", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 1, phaseText, text_color=tc, text_size=size.small)

    table.cell(dash, 0, 2, "Confidence", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 2, str.tostring(confidence, "#"), text_color=stateTextColor, text_size=size.small)

    table.cell(dash, 0, 3, "Accumulation", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 3, str.tostring(accScore, "#"), text_color=bullAccent, text_size=size.small)

    table.cell(dash, 0, 4, "Distribution", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 4, str.tostring(distScore, "#"), text_color=bearAccent, text_size=size.small)

    table.cell(dash, 0, 5, "Cause", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 5, str.tostring(causeScore, "#"), text_color=warningAccent, text_size=size.small)

    table.cell(dash, 0, 6, "Range Quality", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 6, str.tostring(rangeQuality, "#"), text_color=warningAccent, text_size=size.small)

    table.cell(dash, 0, 7, "Range ATR", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 7, str.tostring(rangeAtr, "#.##"), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 8, "Locked Range", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 8, rangeLocked ? "Yes" : "No", text_color=rangeLocked ? stateTextColor : tc, text_size=size.small)

    table.cell(dash, 0, 9, "Bars in Phase", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 9, str.tostring(barsInPhase), text_color=tc, text_size=size.small)

    string volCellText = "Real"
    if not volumeIsReal
        volCellText := syminfo.volumetype + " — climax/absorption off"
    table.cell(dash, 0, 10, "Volume", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 10, volCellText, text_color=volumeIsReal ? tc : warningAccent, text_size=size.small)

// ============================================================================
// ALERTS
// ============================================================================

bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed

alertcondition(alertOk and (springFire), "Wyckoff Spring", "MWSM · SPRING LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (utadFire), "Wyckoff UTAD", "MWSM · UTAD SHORT · {{ticker}} {{interval}}")
alertcondition(alertOk and (sosFire), "Wyckoff SOS", "MWSM · SIGN OF STRENGTH LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (sowFire), "Wyckoff SOW", "MWSM · SIGN OF WEAKNESS SHORT · {{ticker}} {{interval}}")
alertcondition(alertOk and (markupStarted), "Wyckoff Markup", "MWSM · MARKUP LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (markdownStarted), "Wyckoff Markdown", "MWSM · MARKDOWN SHORT · {{ticker}} {{interval}}")

Overview

Modern Wyckoff State Machine Lite [WavesUnchained] identifies structural events including springs, UTADs, SOS, SOW, LPS, LPSY, and climactic bars using ATR, ADX, and swing pivots. It overlays trading ranges, midpoints, phase zones, event labels, and an optional scoring dashboard directly on the price chart. Inputs configure range detection criteria, phase transition dwell times, breakout confirmation bars, and event quality filters. Volume-dependent events require authentic exchange volume and degrade on CFD data.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

SMC / ICT / price action, Support & resistance, Breakout, Volume

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Trading Range Lengthint80rangeLen
Swing Pivot Lengthint5pivotLen
ADX Lengthint14adxLen
ATR Lengthint14atrLen
Volume Z Lengthint50volLen
Max Range Width in ATRfloat8.0rangeAtrMax
Max ADX for Rangefloat24.0adxMax
Refresh Range During Cause (Phase A/B)booltruerefreshCauseRange
Event Memoryint30eventLookback
Minimum Bars per Phaseint5minPhaseBars
Event Cooldownint10eventCooldown
Phase E Breakout Confirm Barsint2breakoutConfirmBars
Show all 35 settings
SettingTypeDefaultVariable
Reset Stuck Accumulation/DistributionbooltrueresetStuckRange
Max Bars in Phase Before Resetint120maxCauseBars
Reset After Bars Range Lostint40staleRangeBars
Spring / UTAD Min Depth ATRfloat0.25springAtrMin
Spring / UTAD Min Depth %float0.75springPctMin
Min Spring / UTAD Wick Ratiofloat0.30minWickRatio
Min Spring / UTAD Candle ATRfloat0.50minCandleAtr
Strong Event Wick Ratiofloat0.50strongWickRatio
Strong Event Candle ATRfloat1.00strongCandleAtr
Strong Event Relative Volumefloat1.40strongVolRatio
Spring / UTAD Test Window ATRfloat1.00testAtrWindow
Test Max Volume vs Eventfloat0.85testVolFactor
LPS / LPSY Max Pivot Volumefloat0.85lpsVolFactor
Show RangebooltrueshowRange
Show Phase ZonesbooltrueshowZones
Show Event LabelsbooltrueshowLabels
Show DashboardboolfalseshowDash
Show Phase TrackbooltrueshowPhaseLine
Max Phase Zonesint20maxPhaseZones
Old Zone ModestringFadeoldZoneMode
Old Zone Transparencyint96oldZoneTransp
Max Event Labelsint120maxEventLabels
Alerts only on bar close (confirmed)booltruealertsOnBarClose
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

13 stars

This script identifies and visualizes bullish and bearish order blocks on the price chart. It detects low-volatility consolidation ranges followed by breakout moves accompanied by high trading volume. Users can select between Simple, Relative, or Weighted volu

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 6 months ago · MIT GitHub repository of Advanced Order Blocks with Volume

This indicator identifies dynamic support and resistance levels using pivot highs and lows. It plots resistance and support lines directly on the price chart once pivot points are confirmed. Additionally, it calculates a volume oscillator using 9-period and 26

Indicator v5 Support & resistanceSMC / ICT / price action
Updated 1 year ago · MPL-2.0 GitHub repository of Support & Resistance Levels with Break of Structure
2 stars

SMC + POC Signals identifies Smart Money Concepts structure elements including Break of Structure (BOS), Change of Character (CHoCH), Order Blocks, and Fair Value Gaps alongside a rolling Point of Control (POC) volume profile line. It marks potential long and

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 3 weeks ago · None stated GitHub repository of SMC + POC Signals [factor-lab]
1 stars

This indicator identifies bullish Change of Character (CHoCH) structure shifts directly on the price chart using pivot swing points. It tracks swing highs and lows governed by configurable left and right lengths and maximum swing age. The script highlights CHo

Indicator v6 SMC / ICT / price actionBreakout
Updated 2 days ago · None stated GitHub repository of REV Ladder (Best Bullish CHoCH)

SMC+OB+FVG_PH

by mydcc
1 stars

This indicator plots smart money concept structures directly on the price chart. It detects and visualizes market structure elements including change of character, break of structure, swing points, inducements, and liquidity sweeps. Additionally, it identifies

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated today · Custom licence GitHub repository of SMC+OB+FVG_PH

Moon Dev GEX Sweep & CVD

by moondevonyt
92 stars

The indicator detects liquidity sweeps across key price levels and calculates a cumulative volume delta proxy. It uses pivot high and low lookbacks to identify swing points, marking sweeps when price breaks beyond a level and quickly closes back inside. Featur

Indicator v5 Support & resistanceSMC / ICT / price action
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Sweep & CVD

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers