Indicator · Pine Script v6 · Own pane

Pivot Momentum Structure [WavesUnchained]

by casoon

Pivot Momentum Structure plots a selectable oscillator in a separate pane and scores momentum against confirmed price pivots.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code1,752
Settings40inputs
LicenceMIT
Last update3 days ago27 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

1,752 lines of Pine Script v6 from indicators/momentum/pivot_momentum_structure/pivot_momentum_structure.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

pivot_momentum_structure.pine
//@version=6
// ============================================================================
// Pivot Momentum Structure [WavesUnchained]
// Version: 2.9.1
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Structure-oriented oscillator:
//
//   ✓ Selectable RSI, WaveTrend, Stochastic RSI, CCI, Fisher Transform, TSI,
//     Williams %R, CMO, or MFI (the only volume-based source)
//   ✓ Price pivots are the authoritative structural anchors
//   ✓ HH / HL / LH / LL classification from confirmed price pivots (ATR-tolerant on price)
//   ✓ Divergence/confirmation anchor is scored (recency, oscillator rate-of-change,
//     extreme-zone context, price distinctness) across a window of recent pivots,
//     not just the nearest qualifying one
//   ✓ Optional local-oscillator-extreme anchor mode (reduces price/oscillator pivot mistiming)
//   ✓ Regular bullish / bearish divergence
//   ✓ Hidden bullish / bearish divergence
//   ✓ Momentum alignment (structure agreement, distinct from divergence)
//   ✓ Structure presets (Scalping / Intraday / Swing / Position / Major Structure / Custom)
//   ✓ Oscillator-difference filter: reference-range %, standard deviation, or off
//   ✓ Optional oscillator and price divergence lines
//   ✓ Optional price-pivot labels on the main chart
//   ✓ Dashboard with pivot type and event age
//   ✓ Drawing-object retention limits
//   ✓ Alert conditions
//
// Important:
// Pivots require right-side confirmation bars. Signals are therefore confirmed
// only after `Pivot right bars`, but are drawn back on the original pivot bar.
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   only for the MFI source — inactive without real trade volume
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
// ============================================================================
indicator(
     "Pivot Momentum Structure [WavesUnchained]",
     shorttitle = "PMS",
     overlay = false,
     format = format.price,
     precision = 2,
     max_bars_back = 5000,
     max_lines_count = 500,
     max_labels_count = 500
)
// ─────────────────────────────────────────────────────────────────────────────
// INPUT GROUPS
// ─────────────────────────────────────────────────────────────────────────────
string GROUP_OSC       = "Oscillator"
string GROUP_PIVOT     = "Pivot Structure"
string GROUP_DIV       = "Divergence"
string GROUP_FILTER    = "Quality Filters"
string GROUP_VISUAL    = "Display"
string GROUP_COLORS    = "Colors"
string GROUP_ALERT     = "Alerts"
string GROUP_DEBUG     = "Debug"
// ─────────────────────────────────────────────────────────────────────────────
// OSCILLATOR INPUTS
// ─────────────────────────────────────────────────────────────────────────────
string oscillatorType = input.string(
     "RSI",
     "Oscillator",
     options = ["RSI", "WaveTrend", "Stochastic RSI", "CCI", "Fisher", "TSI", "Williams %R", "CMO", "MFI"],
     group = GROUP_OSC
)
src = input.source(
     close,
     "Source",
     tooltip = "Used by RSI, Stochastic RSI, TSI and CMO. WaveTrend, CCI, Fisher, Williams %R and MFI use their own conventional price inputs (hlc3, hl2, or high/low/close) regardless of this setting.",
     group = GROUP_OSC
)
// RSI
int rsiLength = input.int(
     14,
     "RSI Length",
     minval = 2,
     group = GROUP_OSC
)
int rsiSignalLength = input.int(
     5,
     "RSI Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// WaveTrend
int wtChannelLength = input.int(
     10,
     "WaveTrend Channel",
     minval = 2,
     group = GROUP_OSC
)
int wtAverageLength = input.int(
     21,
     "WaveTrend Smoothing",
     minval = 2,
     group = GROUP_OSC
)
int wtSignalLength = input.int(
     4,
     "WaveTrend Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// Stochastic RSI
int stochRsiLength = input.int(
     14,
     "Stoch RSI Length",
     minval = 2,
     group = GROUP_OSC
)
int stochLength = input.int(
     14,
     "Stochastic Length",
     minval = 2,
     group = GROUP_OSC
)
int stochKSmoothing = input.int(
     3,
     "%K Smoothing",
     minval = 1,
     group = GROUP_OSC
)
int stochDSmoothing = input.int(
     3,
     "%D Smoothing",
     minval = 1,
     group = GROUP_OSC
)
// CCI
int cciLength = input.int(
     20,
     "CCI Length",
     minval = 2,
     group = GROUP_OSC
)
int cciSignalLength = input.int(
     9,
     "CCI Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// Fisher Transform
int fisherLength = input.int(
     10,
     "Fisher Length",
     minval = 2,
     group = GROUP_OSC
)
// TSI
int tsiLongLength = input.int(
     25,
     "TSI Long Length",
     minval = 1,
     group = GROUP_OSC
)
int tsiShortLength = input.int(
     13,
     "TSI Short Length",
     minval = 1,
     group = GROUP_OSC
)
int tsiSignalLength = input.int(
     7,
     "TSI Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// Williams %R
int williamsLength = input.int(
     14,
     "Williams %R Length",
     minval = 2,
     group = GROUP_OSC
)
int williamsSignalLength = input.int(
     9,
     "Williams %R Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// CMO
int cmoLength = input.int(
     14,
     "CMO Length",
     minval = 2,
     group = GROUP_OSC
)
int cmoSignalLength = input.int(
     9,
     "CMO Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// MFI
int mfiLength = input.int(
     14,
     "MFI Length",
     minval = 2,
     group = GROUP_OSC
)
int mfiSignalLength = input.int(
     9,
     "MFI Signal Line",
     minval = 1,
     group = GROUP_OSC
)
// ─────────────────────────────────────────────────────────────────────────────
// PIVOT INPUTS
// ─────────────────────────────────────────────────────────────────────────────
string structurePreset = input.string(
     "Swing",
     "Structure Preset",
     options = ["Scalping", "Intraday", "Swing", "Position", "Major Structure", "Custom"],
     tooltip = "Presets set Pivot Left/Right, Min/Max Pivot Distance, and Min. Price Difference together, based on the swing size you want to detect (not the chart timeframe). Choose Custom to control them manually below.",
     group = GROUP_PIVOT
)
int pivotLeftCustom = input.int(
     7,
     "Pivot Left (Custom)",
     minval = 1,
     maxval = 50,
     tooltip = "Used only when Structure Preset is Custom.",
     group = GROUP_PIVOT
)
int pivotRightCustom = input.int(
     7,
     "Pivot Right (Custom)",
     minval = 1,
     maxval = 50,
     tooltip = "Used only when Structure Preset is Custom.",
     group = GROUP_PIVOT
)
int minimumPivotDistanceCustom = input.int(
     10,
     "Minimum Pivot Distance (Custom)",
     minval = 1,
     maxval = 500,
     tooltip = "Used only when Structure Preset is Custom. Minimum distance between two high pivots or two low pivots.",
     group = GROUP_PIVOT
)
int maximumPivotDistanceCustom = input.int(
     120,
     "Maximum Pivot Distance (Custom)",
     minval = 5,
     maxval = 2000,
     tooltip = "Used only when Structure Preset is Custom. Divergences between pivots farther apart than this are ignored.",
     group = GROUP_PIVOT
)
float minimumPriceDifferenceAtrCustom = input.float(
     0.10,
     "Min. Price Difference ATR (Custom)",
     minval = 0.0,
     step = 0.05,
     tooltip = "Used only when Structure Preset is Custom. Prevents divergences between nearly identical price pivots.",
     group = GROUP_FILTER
)
int pivotLeft = pivotLeftCustom
int pivotRight = pivotRightCustom
int minimumPivotDistance = minimumPivotDistanceCustom
int maximumPivotDistance = maximumPivotDistanceCustom
float minimumPriceDifferenceAtr = minimumPriceDifferenceAtrCustom
if structurePreset == "Scalping"
    pivotLeft := 5
    pivotRight := 5
    minimumPivotDistance := 8
    maximumPivotDistance := 60
    minimumPriceDifferenceAtr := 0.15
else if structurePreset == "Intraday"
    pivotLeft := 7
    pivotRight := 7
    minimumPivotDistance := 10
    maximumPivotDistance := 100
    minimumPriceDifferenceAtr := 0.15
else if structurePreset == "Swing"
    pivotLeft := 8
    pivotRight := 8
    minimumPivotDistance := 12
    maximumPivotDistance := 120
    minimumPriceDifferenceAtr := 0.10
else if structurePreset == "Position"
    pivotLeft := 10
    pivotRight := 10
    minimumPivotDistance := 15
    maximumPivotDistance := 160
    minimumPriceDifferenceAtr := 0.10
else if structurePreset == "Major Structure"
    pivotLeft := 14
    pivotRight := 14
    minimumPivotDistance := 20
    maximumPivotDistance := 250
    minimumPriceDifferenceAtr := 0.15
string oscillatorAnchorMode = input.string(
     "Exact Pivot Bar",
     "Oscillator Anchor",
     options = ["Exact Pivot Bar", "Local Extreme"],
     tooltip = "Exact Pivot Bar (default): oscillator value read exactly on the price-pivot bar — the minimal-transformation reading, no search/selection involved. Local Extreme: uses the oscillator's own high/low within a small window around the price-pivot bar so divergence lines visually land on a peak/trough — but this changes which values feed the divergence math, not just how it's drawn, so treat it as an experimental alternative until verified against Exact Pivot Bar via the debug log.",
     group = GROUP_PIVOT
)
int oscillatorAnchorRadius = input.int(
     2,
     "Local Extreme Search Radius",
     minval = 1,
     maxval = 10,
     tooltip = "Bars searched on each side of the pivot bar when Oscillator Anchor is Local Extreme. Clamped to Pivot Right so only confirmed data is used.",
     group = GROUP_PIVOT
)
// ─────────────────────────────────────────────────────────────────────────────
// DIVERGENCE INPUTS
// ─────────────────────────────────────────────────────────────────────────────
bool detectRegular = input.bool(
     true,
     "Regular Divergence",
     group = GROUP_DIV
)
bool detectHidden = input.bool(
     true,
     "Hidden Divergence",
     group = GROUP_DIV
)
bool detectConfirmations = input.bool(
     true,
     "Momentum Alignment",
     tooltip = "Marks price and momentum structure that agree on a higher or lower extreme. This is structure agreement, not a trade trigger.",
     group = GROUP_DIV
)
bool requireExtremeZone = input.bool(
     false,
     "Require Extreme Zone",
     tooltip = "Regular divergences are only accepted if at least one of the two oscillator pivots is in an extreme zone.",
     group = GROUP_DIV
)
bool useTrendContextFilter = input.bool(
     false,
     "Trend Context Filter (Hidden Divergence)",
     tooltip = "Hidden bullish only fires when the pivot low is above the Trend EMA; hidden bearish only when the pivot high is below it. Hidden divergences are continuation signals — this keeps them aligned with the prevailing trend instead of firing during chop or against the trend.",
     group = GROUP_DIV
)
int trendEmaLength = input.int(
     50,
     "Trend EMA Length",
     minval = 1,
     tooltip = "Used only when Trend Context Filter is enabled.",
     group = GROUP_DIV
)
// ─────────────────────────────────────────────────────────────────────────────
// ALERT INPUTS
// ─────────────────────────────────────────────────────────────────────────────
bool alertsOnBarClose = input.bool(
     true,
     "Alerts only on bar close (confirmed)",
     tooltip = "Pivot confirmation (ta.pivothigh/ta.pivotlow) still depends on the currently forming bar until it closes, so a signal can flip intrabar. When on, alerts only fire once that bar is confirmed.",
     group = GROUP_ALERT
)
// ─────────────────────────────────────────────────────────────────────────────
// DEBUG INPUTS
// ─────────────────────────────────────────────────────────────────────────────
bool enableDebugLogging = input.bool(
     false,
     "Enable Debug Logging (Pine Logs)",
     tooltip = "Logs every confirmed pivot to the Pine Logs panel: price/oscillator values, structure classification, the anchor found (if any) and its values/distance, every raw boolean the divergence/confirmation math is built from, and the final signal flags. Export via the Logs panel (three-dot menu → Export logs) for offline verification. Off by default — this is a diagnostic tool, not something end users need running.",
     group = GROUP_DEBUG
)
// ─────────────────────────────────────────────────────────────────────────────
// QUALITY FILTERS
// ─────────────────────────────────────────────────────────────────────────────
int atrLength = input.int(
     14,
     "ATR Length",
     minval = 1,
     group = GROUP_FILTER
)
string oscillatorDifferenceMode = input.string(
     "Reference Range %",
     "Oscillator Difference Mode",
     options = ["Reference Range %", "Standard Deviation", "Off"],
     tooltip = "Reference Range %: minimum difference as a percentage of this oscillator's fixed OB-OS reference range. Standard Deviation: minimum difference relative to the oscillator's own rolling volatility, adapting to market and timeframe. Off: no minimum oscillator difference filter.",
     group = GROUP_FILTER
)
float minimumOscillatorDifferencePct = input.float(
     5.0,
     "Min. Oscillator Difference (% of reference range)",
     minval = 0.0,
     maxval = 100.0,
     step = 0.5,
     tooltip = "Used when Oscillator Difference Mode is Reference Range %.",
     group = GROUP_FILTER
)
float minimumOscillatorDeviationMultiple = input.float(
     0.20,
     "Min. Oscillator Difference (x StDev)",
     minval = 0.0,
     step = 0.05,
     tooltip = "Used when Oscillator Difference Mode is Standard Deviation.",
     group = GROUP_FILTER
)
int oscillatorDeviationLength = input.int(
     100,
     "StDev Lookback",
     minval = 10,
     tooltip = "Bars used to compute the oscillator's rolling standard deviation. Used when Oscillator Difference Mode is Standard Deviation.",
     group = GROUP_FILTER
)
bool allowEqualPricePivots = input.bool(
     false,
     "Allow Near-Equal Price Pivots",
     tooltip = "Treats pivots within the price tolerance as a double top or double bottom. Only ever produces a regular divergence (paired with an opposing oscillator move) — never a hidden divergence or a momentum alignment, since those require the price to have genuinely moved beyond the tolerance.",
     group = GROUP_FILTER
)
// ─────────────────────────────────────────────────────────────────────────────
// VISUAL INPUTS
// ─────────────────────────────────────────────────────────────────────────────
bool showOscillatorSignal = input.bool(
     true,
     "Show Signal Line",
     group = GROUP_VISUAL
)
bool showPriceStructureLabels = input.bool(
     true,
     "Price Structure in Oscillator",
     tooltip = "Shows HH, HL, LH and LL at the oscillator value of each price pivot bar.",
     group = GROUP_VISUAL
)
bool showOscillatorStructure = input.bool(
     false,
     "Also Show Oscillator Structure",
     tooltip = "Shows the oscillator structure in the pivot marker tooltip.",
     group = GROUP_VISUAL
)
bool showDivergenceLabels = input.bool(
     true,
     "Divergence Labels",
     group = GROUP_VISUAL
)
bool showQualityScore = input.bool(
     true,
     "Show Divergence Quality Score",
     tooltip = "0-100, informational only — never gates or filters a signal. Averages pivot spacing, oscillator-difference magnitude (relative to its own rolling StDev), extremity past the OB/OS boundary, and trend context (overextension for regular divergence, alignment for hidden divergence).",
     group = GROUP_VISUAL
)
bool showOscillatorDivergenceLines = input.bool(
     true,
     "Divergence Lines in Oscillator",
     group = GROUP_VISUAL
)
bool showPriceDivergenceLines = input.bool(
     true,
     "Divergence Lines on Main Chart",
     group = GROUP_VISUAL
)
bool showChartPivotLabels = input.bool(
     true,
     "Pivot Markers on Main Chart",
     group = GROUP_VISUAL
)
bool showPivotGuideLines = input.bool(
     false,
     "Connect Oscillator Pivots",
     tooltip = "Connects consecutive high pivots and low pivots in the oscillator.",
     group = GROUP_VISUAL
)
bool showStatusTable = input.bool(
     true,
     "Status Dashboard",
     group = GROUP_VISUAL
)
int maximumStoredDrawings = input.int(
     150,
     "Max Drawings (per Type)",
     minval = 20,
     maxval = 240,
     tooltip = "Applied separately to lines and labels — up to this many of each are kept, oldest removed first. Total drawing objects on the chart can be up to double this number.",
     group = GROUP_VISUAL
)
// ─────────────────────────────────────────────────────────────────────────────
// COLORS
// ─────────────────────────────────────────────────────────────────────────────
color oscillatorColor = input.color(
     color.rgb(116, 78, 194),
     "Oscillator",
     group = GROUP_COLORS
)
color signalColor = input.color(
     color.new(color.gray, 15),
     "Signal Line",
     group = GROUP_COLORS
)
color hhColor = input.color(
     color.rgb(22, 163, 74),
     "Higher High",
     group = GROUP_COLORS
)
color hlColor = input.color(
     color.rgb(2, 119, 189),
     "Higher Low",
     group = GROUP_COLORS
)
color lhColor = input.color(
     color.rgb(230, 145, 0),
     "Lower High",
     group = GROUP_COLORS
)
color llColor = input.color(
     color.rgb(211, 47, 47),
     "Lower Low",
     group = GROUP_COLORS
)
color bullishDivergenceColor = input.color(
     color.rgb(46, 170, 80),
     "Bullish Divergence",
     group = GROUP_COLORS
)
color bearishDivergenceColor = input.color(
     color.rgb(239, 83, 80),
     "Bearish Divergence",
     group = GROUP_COLORS
)
color hiddenBullishColor = input.color(
     color.rgb(0, 150, 136),
     "Hidden Bullish",
     group = GROUP_COLORS
)
color hiddenBearishColor = input.color(
     color.rgb(255, 152, 0),
     "Hidden Bearish",
     group = GROUP_COLORS
)
color confirmationColor = input.color(
     color.new(color.gray, 25),
     "Momentum Alignment",
     group = GROUP_COLORS
)
// ─────────────────────────────────────────────────────────────────────────────
// OSCILLATOR SCALE (depends only on the oscillatorType input)
// ─────────────────────────────────────────────────────────────────────────────
float upperExtreme = switch oscillatorType
    "RSI"            => 70.0
    "WaveTrend"      => 60.0
    "Stochastic RSI" => 80.0
    "CCI"            => 100.0
    "Fisher"         => 1.5
    "TSI"            => 25.0
    "Williams %R"    => -20.0
    "CMO"            => 50.0
    "MFI"            => 80.0
    => 70.0
float lowerExtreme = switch oscillatorType
    "RSI"            => 30.0
    "WaveTrend"      => -60.0
    "Stochastic RSI" => 20.0
    "CCI"            => -100.0
    "Fisher"         => -1.5
    "TSI"            => -25.0
    "Williams %R"    => -80.0
    "CMO"            => -50.0
    "MFI"            => 20.0
    => 30.0
float centerLevel = switch oscillatorType
    "WaveTrend"   => 0.0
    "CCI"         => 0.0
    "Fisher"      => 0.0
    "TSI"         => 0.0
    "Williams %R" => -50.0
    "CMO"         => 0.0
    => 50.0
f_safeDivide(float numerator, float denominator) =>
    denominator != 0.0 ? numerator / denominator : 0.0
// ─────────────────────────────────────────────────────────────────────────────
// OSCILLATOR CALCULATIONS
// ─────────────────────────────────────────────────────────────────────────────
// RSI
float rsiValue = ta.rsi(src, rsiLength)
float rsiSignal = ta.ema(rsiValue, rsiSignalLength)
// WaveTrend
float wtSource = hlc3
float wtEsa = ta.ema(wtSource, wtChannelLength)
float wtDeviation = ta.ema(math.abs(wtSource - wtEsa), wtChannelLength)
float wtCompositeIndex = wtDeviation != 0.0 ? (wtSource - wtEsa) / (0.015 * wtDeviation) : 0.0
float wtMain = ta.ema(wtCompositeIndex, wtAverageLength)
float wtSignal = ta.sma(wtMain, wtSignalLength)
// Stochastic RSI
float stochBaseRsi = ta.rsi(src, stochRsiLength)
float stochLowest = ta.lowest(stochBaseRsi, stochLength)
float stochHighest = ta.highest(stochBaseRsi, stochLength)
float rawStochRsi = 100.0 * f_safeDivide(
     stochBaseRsi - stochLowest,
     stochHighest - stochLowest
)
float stochK = ta.sma(rawStochRsi, stochKSmoothing)
float stochD = ta.sma(stochK, stochDSmoothing)
// CCI
float cciValue = ta.cci(hlc3, cciLength)
float cciSignal = ta.sma(cciValue, cciSignalLength)
// Fisher Transform (Ehlers) — normalized hl2 into [-1,1], then transformed; centered on 0
float fisherHigh = ta.highest(hl2, fisherLength)
float fisherLow = ta.lowest(hl2, fisherLength)
float fisherRange = fisherHigh - fisherLow
float fisherNorm = fisherRange > 0.0 ? (hl2 - fisherLow) / fisherRange - 0.5 : 0.0
var float fisherRaw = 0.0
fisherRaw := 0.66 * fisherNorm + 0.67 * nz(fisherRaw[1])
fisherRaw := math.max(-0.999, math.min(0.999, fisherRaw))
var float fisherValue = 0.0
fisherValue := 0.5 * math.log((1.0 + fisherRaw) / (1.0 - fisherRaw)) + 0.5 * nz(fisherValue[1])
float fisherSignal = fisherValue[1]
// TSI — double-smoothed momentum, ×100, centered on 0
float tsiChange = ta.change(src)
float tsiDoubleSmoothed = ta.ema(ta.ema(tsiChange, tsiLongLength), tsiShortLength)
float tsiAbsSmoothed = ta.ema(ta.ema(math.abs(tsiChange), tsiLongLength), tsiShortLength)
float tsiValue = tsiAbsSmoothed != 0.0 ? 100.0 * tsiDoubleSmoothed / tsiAbsSmoothed : 0.0
float tsiSignal = ta.ema(tsiValue, tsiSignalLength)
// Williams %R
float williamsValue = ta.wpr(williamsLength)
float williamsSignal = ta.sma(williamsValue, williamsSignalLength)
// CMO — (SumUp - SumDown) / (SumUp + SumDown) * 100, bounded -100..100
float cmoChange = ta.change(src)
float cmoUp = cmoChange > 0.0 ? cmoChange : 0.0
float cmoDown = cmoChange < 0.0 ? -cmoChange : 0.0
float cmoSumUp = ta.sma(cmoUp, cmoLength) * cmoLength
float cmoSumDown = ta.sma(cmoDown, cmoLength) * cmoLength
float cmoValue = (cmoSumUp + cmoSumDown) != 0.0 ? 100.0 * (cmoSumUp - cmoSumDown) / (cmoSumUp + cmoSumDown) : 0.0
float cmoSignal = ta.sma(cmoValue, cmoSignalLength)
// MFI — the only volume-based source. Validity is decided per symbol by
// syminfo.volumetype: "tick" is a number of transactions, not a quantity; "n/a" is used by most
// non-crypto feeds, so real volume = feed delivers volume and type is not "tick"
// and n/a means none. Without it the series is na, so no oscillator pivot is
// read and no structure, divergence or alert is produced — see the notice label
// below. ta.mfi is called unconditionally so its internal state stays intact.
bool volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)
float mfiRaw = ta.mfi(hlc3, mfiLength)
float mfiValue = volumeIsReal ? mfiRaw : na
float mfiSignal = ta.sma(mfiValue, mfiSignalLength)
// Selected series
float oscillator = switch oscillatorType
    "RSI"            => rsiValue
    "WaveTrend"      => wtMain
    "Stochastic RSI" => stochK
    "CCI"            => cciValue
    "Fisher"         => fisherValue
    "TSI"            => tsiValue
    "Williams %R"    => williamsValue
    "CMO"            => cmoValue
    "MFI"            => mfiValue
    => rsiValue
float oscillatorSignal = switch oscillatorType
    "RSI"            => rsiSignal
    "WaveTrend"      => wtSignal
    "Stochastic RSI" => stochD
    "CCI"            => cciSignal
    "Fisher"         => fisherSignal
    "TSI"            => tsiSignal
    "Williams %R"    => williamsSignal
    "CMO"            => cmoSignal
    "MFI"            => mfiSignal
    => rsiSignal
// ─────────────────────────────────────────────────────────────────────────────
// OSCILLATOR DIFFERENCE THRESHOLD
// ─────────────────────────────────────────────────────────────────────────────
float oscillatorStDev = ta.stdev(oscillator, oscillatorDeviationLength)
float minimumOscillatorDifference = switch oscillatorDifferenceMode
    "Reference Range %"  => (upperExtreme - lowerExtreme) * (minimumOscillatorDifferencePct / 100.0)
    "Standard Deviation" => oscillatorStDev * minimumOscillatorDeviationMultiple
    "Off"                => 0.0
    => (upperExtreme - lowerExtreme) * (minimumOscillatorDifferencePct / 100.0)
// ─────────────────────────────────────────────────────────────────────────────
// HELPER FUNCTIONS
// ─────────────────────────────────────────────────────────────────────────────
f_structureHigh(float currentValue, float previousValue, float tolerance) =>
    string structureResult = "H"
    if not na(previousValue)
        if currentValue > previousValue + tolerance
            structureResult := "HH"
        else if currentValue < previousValue - tolerance
            structureResult := "LH"
        else
            structureResult := "EH"
    structureResult
f_structureLow(float currentValue, float previousValue, float tolerance) =>
    string structureResult = "L"
    if not na(previousValue)
        if currentValue > previousValue + tolerance
            structureResult := "HL"
        else if currentValue < previousValue - tolerance
            structureResult := "LL"
        else
            structureResult := "EL"
    structureResult
f_structureColor(string structure) =>
    switch structure
        "HH" => hhColor
        "HL" => hlColor
        "LH" => lhColor
        "LL" => llColor
        => color.gray
f_boolStr(bool value) =>
    value ? "true" : "false"
f_arrayIntToStr(array<int> arr) =>
    string result = ""
    if array.size(arr) > 0
        for i = 0 to array.size(arr) - 1
            result := result + (i > 0 ? "|" : "") + str.tostring(array.get(arr, i))
    result
f_arrayFloatToStr(array<float> arr) =>
    string result = ""
    if array.size(arr) > 0
        for i = 0 to array.size(arr) - 1
            result := result + (i > 0 ? "|" : "") + str.tostring(array.get(arr, i), "#.####")
    result
f_storeLine(array<line> storage, line drawing, int maximumCount) =>
    array.push(storage, drawing)
    if array.size(storage) > maximumCount
        line oldest = array.shift(storage)
        line.delete(oldest)
f_storeLabel(array<label> storage, label drawing, int maximumCount) =>
    array.push(storage, drawing)
    if array.size(storage) > maximumCount
        label oldest = array.shift(storage)
        label.delete(oldest)
f_validDistance(int currentIndex, int previousIndex) =>
    int distance = currentIndex - previousIndex
    distance >= minimumPivotDistance and distance <= maximumPivotDistance
f_priceDifferenceValid(
     float currentPrice,
     float previousPrice,
     float pivotAtr
 ) =>
    float absoluteDifference = math.abs(currentPrice - previousPrice)
    absoluteDifference >= pivotAtr * minimumPriceDifferenceAtr
f_oscillatorDifferenceValid(
     float currentOscillator,
     float previousOscillator
 ) =>
    math.abs(currentOscillator - previousOscillator) >= minimumOscillatorDifference
f_priceHigher(
     float currentPrice,
     float previousPrice,
     float tolerance
 ) =>
    currentPrice > previousPrice + tolerance
f_priceLower(
     float currentPrice,
     float previousPrice,
     float tolerance
 ) =>
    currentPrice < previousPrice - tolerance
f_priceApproximatelyEqual(
     float currentPrice,
     float previousPrice,
     float tolerance
 ) =>
    math.abs(currentPrice - previousPrice) <= tolerance
f_anchorScore(
     float currentPrice,
     float candPrice,
     float priceTolerance,
     float currentOscillator,
     float candOsc,
     int currentIndex,
     int candIndex,
     bool isHighPivot
 ) =>
    // Recency dominates (40%): mirrors the prior "nearest qualifying" behavior as the
    // baseline, so a distant candidate can only win by being clearly better elsewhere —
    // this is a deliberate guard against the v2.4.0 regression (raw oscillator-difference
    // magnitude systematically favored distant pivots, since oscillators mechanically have
    // more room to diverge the more bars separate two points).
    int distance = currentIndex - candIndex
    float distanceRange = maximumPivotDistance - minimumPivotDistance
    float recencyScore = 50.0
    if distanceRange > 0.0
        recencyScore := 100.0 - math.max(0.0, math.min(100.0, (distance - minimumPivotDistance) / distanceRange * 100.0))
    // Oscillator rate-of-change (30%): difference per bar, not raw magnitude — a sharp
    // divergence over few bars scores higher than the same magnitude stretched over many,
    // so this doesn't reintroduce the distance bias the way raw magnitude did.
    float oscDiff = math.abs(currentOscillator - candOsc)
    float oscRate = distance > 0 ? oscDiff / distance : oscDiff
    float rateScore = oscillatorStDev > 0.0 ? math.max(0.0, math.min(100.0, oscRate / (oscillatorStDev * 0.5) * 100.0)) : 50.0
    // Extreme-zone context (20%): candidate pivot's own oscillator value was in the OB/OS
    // zone — a structurally significant point regardless of how far back it sits.
    float extremeScore = 0.0
    if isHighPivot
        extremeScore := candOsc >= upperExtreme ? 100.0 : 0.0
    else
        extremeScore := candOsc <= lowerExtreme ? 100.0 : 0.0
    // Price distinctness (10%): minor tie-breaker only.
    float priceDiff = math.abs(currentPrice - candPrice)
    float priceScore = priceTolerance > 0.0 ? math.max(0.0, math.min(100.0, priceDiff / (priceTolerance * 3.0) * 100.0)) : 50.0
    recencyScore * 0.40 + rateScore * 0.30 + extremeScore * 0.20 + priceScore * 0.10
f_findBestAnchor(
     array<int> idxArr,
     array<float> priceArr,
     array<float> oscArr,
     int currentIndex,
     float currentPrice,
     float currentOscillator,
     float candidateAtr,
     bool isHighPivot
 ) =>
    int bestI = -1
    float bestScore = -1.0
    if array.size(idxArr) > 0
        for i = array.size(idxArr) - 1 to 0
            int candIndex = array.get(idxArr, i)
            if f_validDistance(currentIndex, candIndex)
                float candPrice = array.get(priceArr, i)
                float candOsc = array.get(oscArr, i)
                float candTolerance = candidateAtr * minimumPriceDifferenceAtr
                bool priceOk = f_priceDifferenceValid(currentPrice, candPrice, candidateAtr)
                     or (allowEqualPricePivots and f_priceApproximatelyEqual(currentPrice, candPrice, candTolerance))
                if priceOk and f_oscillatorDifferenceValid(currentOscillator, candOsc)
                    float score = f_anchorScore(
                         currentPrice,
                         candPrice,
                         candTolerance,
                         currentOscillator,
                         candOsc,
                         currentIndex,
                         candIndex,
                         isHighPivot
                    )
                    if score > bestScore
                        bestScore := score
                        bestI := i
    [bestI, bestScore]
f_localOscillatorExtreme(bool findHighest, int centerOffset, int radius) =>
    float best = oscillator[centerOffset]
    int bestOffset = 0
    for i = -radius to radius
        float candidate = oscillator[centerOffset + i]
        if findHighest and candidate > best
            best := candidate
            bestOffset := i
        if not findHighest and candidate < best
            best := candidate
            bestOffset := i
    [best, bestOffset]
f_divergenceQualityScore(
     bool isBearish,
     bool isRegular,
     int pivotDistance,
     float oscDiff,
     float currentOsc,
     float pivotPrice,
     float trendEmaAtPivot,
     float atrAtPivot
 ) =>
    float spacingRange = maximumPivotDistance - minimumPivotDistance
    float spacingScore = 50.0
    if spacingRange > 0.0
        spacingScore := math.max(0.0, math.min(100.0, (pivotDistance - minimumPivotDistance) / spacingRange * 100.0))
    float magnitudeScore = 50.0
    if oscillatorStDev > 0.0
        magnitudeScore := math.min(100.0, oscDiff / (oscillatorStDev * 2.0) * 100.0)
    float rangeSpan = upperExtreme - lowerExtreme
    float rawExtremity = isBearish ? (currentOsc - upperExtreme) : (lowerExtreme - currentOsc)
    float extremityScore = 50.0
    if rangeSpan > 0.0
        extremityScore := math.max(0.0, math.min(100.0, rawExtremity / rangeSpan * 100.0))
    float trendDistanceAtr = atrAtPivot > 0.0 ? (pivotPrice - trendEmaAtPivot) / atrAtPivot : 0.0
    float signedTrendDistance = isBearish ? trendDistanceAtr : -trendDistanceAtr
    float trendRaw = isRegular ? signedTrendDistance : -signedTrendDistance
    float trendScore = math.max(0.0, math.min(100.0, 50.0 + trendRaw * 25.0))
    (spacingScore + magnitudeScore + extremityScore + trendScore) / 4.0
// ─────────────────────────────────────────────────────────────────────────────
// BASE PLOTS
// ─────────────────────────────────────────────────────────────────────────────
plot(
     oscillator,
     "Oscillator",
     color = oscillatorColor,
     linewidth = 2
)
plot(
     showOscillatorSignal ? oscillatorSignal : na,
     "Signal Line",
     color = signalColor,
     linewidth = 1
)
upperPlot = plot(
     upperExtreme,
     "Upper Extreme Zone",
     color = color.new(bearishDivergenceColor, 20),
     linewidth = 1
)
lowerPlot = plot(
     lowerExtreme,
     "Lower Extreme Zone",
     color = color.new(bullishDivergenceColor, 20),
     linewidth = 1
)
plot(
     centerLevel,
     "Midline",
     color = color.new(color.gray, 60),
     linewidth = 1
)
fill(
     upperPlot,
     lowerPlot,
     color = color.new(oscillatorColor, 94),
     title = "Oscillator Zone"
)
// MFI selected on a feed without real trade volume: the oscillator is na, so the
// script produces nothing at all. Say so instead of showing an empty pane.
if barstate.islast and oscillatorType == "MFI" and not volumeIsReal
    label.new(
         bar_index,
         centerLevel,
         "MFI needs real trade volume — syminfo.volumetype is \"" + syminfo.volumetype + "\"\nNo pivots, structure or divergences are produced. Pick a price-based oscillator.",
         style = label.style_label_left,
         color = color.new(color.orange, 20),
         textcolor = color.white,
         size = size.small
    )
// ─────────────────────────────────────────────────────────────────────────────
// PRICE PIVOTS
// ─────────────────────────────────────────────────────────────────────────────
float pricePivotHigh = ta.pivothigh(high, pivotLeft, pivotRight)
float pricePivotLow = ta.pivotlow(low, pivotLeft, pivotRight)
bool newHighPivot = not na(pricePivotHigh)
bool newLowPivot = not na(pricePivotLow)
int pivotIndex = bar_index - pivotRight
float pivotAtr = ta.atr(atrLength)[pivotRight]
float trendEma = ta.ema(close, trendEmaLength)
int oscillatorAnchorEffectiveRadius = math.min(oscillatorAnchorRadius, pivotRight)
float pivotOscillatorExact = oscillator[pivotRight]
float pivotOscillatorHigh = pivotOscillatorExact
float pivotOscillatorLow = pivotOscillatorExact
int pivotOscillatorHighIndex = pivotIndex
int pivotOscillatorLowIndex = pivotIndex
if oscillatorAnchorMode == "Local Extreme"
    [extremeHighValue, extremeHighOffset] = f_localOscillatorExtreme(true, pivotRight, oscillatorAnchorEffectiveRadius)
    pivotOscillatorHigh := extremeHighValue
    pivotOscillatorHighIndex := pivotIndex - extremeHighOffset
    [extremeLowValue, extremeLowOffset] = f_localOscillatorExtreme(false, pivotRight, oscillatorAnchorEffectiveRadius)
    pivotOscillatorLow := extremeLowValue
    pivotOscillatorLowIndex := pivotIndex - extremeLowOffset
// ─────────────────────────────────────────────────────────────────────────────
// PERSISTENT PIVOT STATE
// ─────────────────────────────────────────────────────────────────────────────
int MAX_STORED_PIVOTS = 8
// Immediately preceding pivot — used only for the HH/HL/LH/LL structure label
var int previousHighIndex = na
var int previousHighOscIndex = na
var float previousHighPrice = na
var float previousHighOscillator = na
var int previousLowIndex = na
var int previousLowOscIndex = na
var float previousLowPrice = na
var float previousLowOscillator = na
// Recent-pivot history — searched for the best divergence/confirmation anchor
// highIndices/lowIndices are the price-pivot bar (used for distance checks);
// highOscIndices/lowOscIndices are where the oscillator value was actually read
// (same as highIndices/lowIndices unless Oscillator Anchor is Local Extreme).
var array<int>   highIndices      = array.new_int()
var array<int>   highOscIndices   = array.new_int()
var array<float> highPrices       = array.new_float()
var array<float> highOscillators  = array.new_float()
var array<int>   lowIndices       = array.new_int()
var array<int>   lowOscIndices    = array.new_int()
var array<float> lowPrices        = array.new_float()
var array<float> lowOscillators   = array.new_float()
// Latest detected structure
var string latestPriceStructure = "—"
var string latestOscillatorStructure = "—"
var string latestPivotType = "—"
var int latestPivotBarIndex = na
var string latestEvent = "No confirmed structure"
var color latestEventColor = color.gray
var int latestEventBarIndex = na
var int latestEventAnchorDistance = na
// Drawing storage
var array<line> storedLines = array.new<line>()
var array<label> storedLabels = array.new<label>()
// ─────────────────────────────────────────────────────────────────────────────
// EVENT FLAGS
// Reset on every calculation bar.
// ─────────────────────────────────────────────────────────────────────────────
bool regularBullishDivergence = false
bool regularBearishDivergence = false
bool hiddenBullishDivergence = false
bool hiddenBearishDivergence = false
bool bullishConfirmation = false
bool bearishConfirmation = false
// ─────────────────────────────────────────────────────────────────────────────
// HIGH-PIVOT PROCESSING
// ─────────────────────────────────────────────────────────────────────────────
if newHighPivot and not na(pivotOscillatorHigh)
    float currentPrice = pricePivotHigh
    float currentOscillator = pivotOscillatorHigh
    int currentIndex = pivotIndex
    int currentOscIndex = pivotOscillatorHighIndex
    float priceTolerance = pivotAtr * minimumPriceDifferenceAtr
    string priceStructure = f_structureHigh(currentPrice, previousHighPrice, priceTolerance)
    string oscillatorStructure = f_structureHigh(currentOscillator, previousHighOscillator, 0.0)
    float logPrevPrice = previousHighPrice
    float logPrevOsc = previousHighOscillator
    string logHistIdx = f_arrayIntToStr(highIndices)
    string logHistPrice = f_arrayFloatToStr(highPrices)
    string logHistOsc = f_arrayFloatToStr(highOscillators)
    [anchorI, anchorScore] = f_findBestAnchor(
         highIndices,
         highPrices,
         highOscillators,
         currentIndex,
         currentPrice,
         currentOscillator,
         pivotAtr,
         true
    )
    bool hasAnchor = anchorI >= 0
    float anchorPrice = na
    float anchorOscillator = na
    int anchorIndex = na
    int anchorOscIndex = na
    if hasAnchor
        anchorPrice := array.get(highPrices, anchorI)
        anchorOscillator := array.get(highOscillators, anchorI)
        anchorIndex := array.get(highIndices, anchorI)
        anchorOscIndex := array.get(highOscIndices, anchorI)
    bool priceMadeEqualHigh = hasAnchor
         and f_priceApproximatelyEqual(currentPrice, anchorPrice, priceTolerance)
    bool priceMadeHigherHigh = hasAnchor
         and f_priceHigher(currentPrice, anchorPrice, priceTolerance)
    bool priceMadeLowerHigh = hasAnchor
         and f_priceLower(currentPrice, anchorPrice, priceTolerance)
    bool regularBearishPriceCondition = priceMadeHigherHigh
         or (allowEqualPricePivots and priceMadeEqualHigh)
    bool oscillatorMadeHigherHigh = hasAnchor and currentOscillator > anchorOscillator
    bool oscillatorMadeLowerHigh = hasAnchor and currentOscillator < anchorOscillator
    bool highExtremeValid = not requireExtremeZone
         or currentOscillator >= upperExtreme
         or (hasAnchor and anchorOscillator >= upperExtreme)
    bool trendContextBearishOk = not useTrendContextFilter or currentPrice < trendEma[pivotRight]
    regularBearishDivergence := detectRegular
         and hasAnchor
         and regularBearishPriceCondition
         and oscillatorMadeLowerHigh
         and highExtremeValid
    hiddenBearishDivergence := detectHidden
         and hasAnchor
         and priceMadeLowerHigh
         and oscillatorMadeHigherHigh
         and trendContextBearishOk
    bool highBearishConfirmation = detectConfirmations
         and hasAnchor
         and priceMadeLowerHigh
         and oscillatorMadeLowerHigh
    bool highBullishConfirmation = detectConfirmations
         and hasAnchor
         and priceMadeHigherHigh
         and oscillatorMadeHigherHigh
    bearishConfirmation := bearishConfirmation or highBearishConfirmation
    bullishConfirmation := bullishConfirmation or highBullishConfirmation
    color pivotColor = f_structureColor(priceStructure)
    string anchorNote = "\nAnchor: none (no qualifying prior pivot)"
    if hasAnchor
        anchorNote := "\nAnchor: " + str.tostring(currentIndex - anchorIndex) + " bars back"
        if anchorIndex != previousHighIndex
            anchorNote := anchorNote + " (not the immediately preceding pivot)"
    string tooltipText =
         "Price Structure: " + priceStructure +
         "\nMomentum Structure: " + oscillatorStructure +
         "\nPrice: " + str.tostring(currentPrice, format.mintick) +
         "\nOscillator: " + str.tostring(currentOscillator, "#.##") +
         anchorNote
    if showPriceStructureLabels
        string labelText = showOscillatorStructure ? priceStructure + "\n" + oscillatorStructure : priceStructure
        label structureLabel = label.new(
             currentOscIndex,
             currentOscillator,
             labelText,
             xloc = xloc.bar_index,
             yloc = yloc.price,
             color = pivotColor,
             textcolor = color.white,
             style = label.style_label_down,
             size = size.tiny,
             tooltip = tooltipText
        )
        f_storeLabel(
             storedLabels,
             structureLabel,
             maximumStoredDrawings
        )
    if showChartPivotLabels
        label chartLabel = label.new(
             currentIndex,
             currentPrice,
             priceStructure,
             xloc = xloc.bar_index,
             yloc = yloc.price,
             color = pivotColor,
             textcolor = color.white,
             style = label.style_label_down,
             size = size.tiny,
             tooltip = tooltipText,
             force_overlay = true
        )
        f_storeLabel(
             storedLabels,
             chartLabel,
             maximumStoredDrawings
        )
    if showPivotGuideLines and not na(previousHighOscIndex)
        line guideLine = line.new(
             previousHighOscIndex,
             previousHighOscillator,
             currentOscIndex,
             currentOscillator,
             xloc = xloc.bar_index,
             color = color.new(pivotColor, 55),
             style = line.style_solid,
             width = 1
        )
        f_storeLine(
             storedLines,
             guideLine,
             maximumStoredDrawings
        )
    if regularBearishDivergence or hiddenBearishDivergence
        color divergenceColor = regularBearishDivergence ? bearishDivergenceColor : hiddenBearishColor
        string divergenceText = regularBearishDivergence ? "Bearish Divergence" : "Hidden Bearish"
        divergenceStyle = regularBearishDivergence ? line.style_solid : line.style_dashed
        float qualityScore = f_divergenceQualityScore(
             true,
             regularBearishDivergence,
             currentIndex - anchorIndex,
             math.abs(currentOscillator - anchorOscillator),
             currentOscillator,
             currentPrice,
             trendEma[pivotRight],
             pivotAtr
        )
        if showQualityScore
            divergenceText := divergenceText + " " + str.tostring(qualityScore, "#")
        if showOscillatorDivergenceLines
            line oscillatorDivergenceLine = line.new(
                 anchorOscIndex,
                 anchorOscillator,
                 currentOscIndex,
                 currentOscillator,
                 xloc = xloc.bar_index,
                 color = divergenceColor,
                 style = divergenceStyle,
                 width = 2
            )
            f_storeLine(
                 storedLines,
                 oscillatorDivergenceLine,
                 maximumStoredDrawings
            )
        if showPriceDivergenceLines
            line priceDivergenceLine = line.new(
                 anchorIndex,
                 anchorPrice,
                 currentIndex,
                 currentPrice,
                 xloc = xloc.bar_index,
                 color = divergenceColor,
                 style = divergenceStyle,
                 width = 2,
                 force_overlay = true
            )
            f_storeLine(
                 storedLines,
                 priceDivergenceLine,
                 maximumStoredDrawings
            )
        if showDivergenceLabels
            label divergenceLabel = label.new(
                 currentOscIndex,
                 currentOscillator,
                 divergenceText,
                 xloc = xloc.bar_index,
                 yloc = yloc.price,
                 color = color.new(divergenceColor, 100),
                 textcolor = divergenceColor,
                 style = label.style_label_up,
                 size = size.small,
                 tooltip = tooltipText
            )
            f_storeLabel(
                 storedLabels,
                 divergenceLabel,
                 maximumStoredDrawings
            )
        latestEvent := divergenceText
        latestEventColor := divergenceColor
        latestEventBarIndex := bar_index
        latestEventAnchorDistance := currentIndex - anchorIndex
    else if highBullishConfirmation
        latestEvent := "Bullish Momentum Alignment"
        latestEventColor := confirmationColor
        latestEventBarIndex := bar_index
        latestEventAnchorDistance := currentIndex - anchorIndex
    else if highBearishConfirmation
        latestEvent := "Bearish Momentum Alignment"
        latestEventColor := confirmationColor
        latestEventBarIndex := bar_index
        latestEventAnchorDistance := currentIndex - anchorIndex
    latestPriceStructure := priceStructure
    latestOscillatorStructure := oscillatorStructure
    latestPivotType := "High"
    latestPivotBarIndex := bar_index
    array.push(highIndices, currentIndex)
    array.push(highOscIndices, currentOscIndex)
    array.push(highPrices, currentPrice)
    array.push(highOscillators, currentOscillator)
    if array.size(highIndices) > MAX_STORED_PIVOTS
        array.shift(highIndices)
        array.shift(highOscIndices)
        array.shift(highPrices)
        array.shift(highOscillators)
    previousHighIndex := currentIndex
    previousHighOscIndex := currentOscIndex
    previousHighPrice := currentPrice
    previousHighOscillator := currentOscillator
    if enableDebugLogging
        log.info(
             "PMS PIVOT type=High idx=" + str.tostring(currentIndex) +
             " osc=" + oscillatorType + " mode=" + oscillatorAnchorMode +
             " price=" + str.tostring(currentPrice, format.mintick) +
             " priceStruct=" + priceStructure +
             " oscVal=" + str.tostring(currentOscillator, "#.####") +
             " oscIdx=" + str.tostring(currentOscIndex) +
             " oscStruct=" + oscillatorStructure +
             " hasAnchor=" + f_boolStr(hasAnchor) +
             " anchorIdx=" + str.tostring(anchorIndex) +
             " anchorDist=" + str.tostring(currentIndex - anchorIndex) +
             " anchorPrice=" + str.tostring(anchorPrice, format.mintick) +
             " anchorOsc=" + str.tostring(anchorOscillator, "#.####") +
             " anchorScore=" + (hasAnchor ? str.tostring(anchorScore, "#.##") : "na") +
             " oscStDev=" + str.tostring(oscillatorStDev, "#.####") +
             " priceMadeHigherHigh=" + f_boolStr(priceMadeHigherHigh) +
             " priceMadeLowerHigh=" + f_boolStr(priceMadeLowerHigh) +
             " priceMadeEqualHigh=" + f_boolStr(priceMadeEqualHigh) +
             " oscMadeHigherHigh=" + f_boolStr(oscillatorMadeHigherHigh) +
             " oscMadeLowerHigh=" + f_boolStr(oscillatorMadeLowerHigh) +
             " highExtremeValid=" + f_boolStr(highExtremeValid) +
             " trendContextBearishOk=" + f_boolStr(trendContextBearishOk) +
             " regularBearishDiv=" + f_boolStr(regularBearishDivergence) +
             " hiddenBearishDiv=" + f_boolStr(hiddenBearishDivergence) +
             " bullConfirm=" + f_boolStr(highBullishConfirmation) +
             " bearConfirm=" + f_boolStr(highBearishConfirmation) +
             " priceTolerance=" + str.tostring(priceTolerance, "#.########") +
             " prevPrice=" + str.tostring(logPrevPrice, format.mintick) +
             " prevOsc=" + str.tostring(logPrevOsc, "#.####") +
             " allowEqual=" + f_boolStr(allowEqualPricePivots) +
             " minOscDiff=" + str.tostring(minimumOscillatorDifference, "#.####") +
             " minPivotDist=" + str.tostring(minimumPivotDistance) +
             " maxPivotDist=" + str.tostring(maximumPivotDistance) +
             " histIdx=" + logHistIdx +
             " histPrice=" + logHistPrice +
             " histOsc=" + logHistOsc
        )
// ─────────────────────────────────────────────────────────────────────────────
// LOW-PIVOT PROCESSING
// ─────────────────────────────────────────────────────────────────────────────
if newLowPivot and not na(pivotOscillatorLow)
    float currentPrice = pricePivotLow
    float currentOscillator = pivotOscillatorLow
    int currentIndex = pivotIndex
    int currentOscIndex = pivotOscillatorLowIndex
    float priceTolerance = pivotAtr * minimumPriceDifferenceAtr
    string priceStructure = f_structureLow(currentPrice, previousLowPrice, priceTolerance)
    string oscillatorStructure = f_structureLow(currentOscillator, previousLowOscillator, 0.0)
    float logPrevPrice = previousLowPrice
    float logPrevOsc = previousLowOscillator
    string logHistIdx = f_arrayIntToStr(lowIndices)
    string logHistPrice = f_arrayFloatToStr(lowPrices)
    string logHistOsc = f_arrayFloatToStr(lowOscillators)
    [anchorI, anchorScore] = f_findBestAnchor(
         lowIndices,
         lowPrices,
         lowOscillators,
         currentIndex,
         currentPrice,
         currentOscillator,
         pivotAtr,
         false
    )
    bool hasAnchor = anchorI >= 0
    float anchorPrice = na
    float anchorOscillator = na
    int anchorIndex = na
    int anchorOscIndex = na
    if hasAnchor
        anchorPrice := array.get(lowPrices, anchorI)
        anchorOscillator := array.get(lowOscillators, anchorI)
        anchorIndex := array.get(lowIndices, anchorI)
        anchorOscIndex := array.get(lowOscIndices, anchorI)
    bool priceMadeEqualLow = hasAnchor
         and f_priceApproximatelyEqual(currentPrice, anchorPrice, priceTolerance)
    bool priceMadeHigherLow = hasAnchor
         and f_priceHigher(currentPrice, anchorPrice, priceTolerance)
    bool priceMadeLowerLow = hasAnchor
         and f_priceLower(currentPrice, anchorPrice, priceTolerance)
    bool regularBullishPriceCondition = priceMadeLowerLow
         or (allowEqualPricePivots and priceMadeEqualLow)
    bool oscillatorMadeHigherLow = hasAnchor and currentOscillator > anchorOscillator
    bool oscillatorMadeLowerLow = hasAnchor and currentOscillator < anchorOscillator
    bool lowExtremeValid = not requireExtremeZone
         or currentOscillator <= lowerExtreme
         or (hasAnchor and anchorOscillator <= lowerExtreme)
    bool trendContextBullishOk = not useTrendContextFilter or currentPrice > trendEma[pivotRight]
    regularBullishDivergence := detectRegular
         and hasAnchor
         and regularBullishPriceCondition
         and oscillatorMadeHigherLow
         and lowExtremeValid
    hiddenBullishDivergence := detectHidden
         and hasAnchor
         and priceMadeHigherLow
         and oscillatorMadeLowerLow
         and trendContextBullishOk
    bool lowBullishConfirmation = detectConfirmations
         and hasAnchor
         and priceMadeHigherLow
         and oscillatorMadeHigherLow
    bool lowBearishConfirmation = detectConfirmations
         and hasAnchor
         and priceMadeLowerLow
         and oscillatorMadeLowerLow
    bullishConfirmation := bullishConfirmation or lowBullishConfirmation
    bearishConfirmation := bearishConfirmation or lowBearishConfirmation
    color pivotColor = f_structureColor(priceStructure)
    string anchorNote = "\nAnchor: none (no qualifying prior pivot)"
    if hasAnchor
        anchorNote := "\nAnchor: " + str.tostring(currentIndex - anchorIndex) + " bars back"
        if anchorIndex != previousLowIndex
            anchorNote := anchorNote + " (not the immediately preceding pivot)"
    string tooltipText =
         "Price Structure: " + priceStructure +
         "\nMomentum Structure: " + oscillatorStructure +
         "\nPrice: " + str.tostring(currentPrice, format.mintick) +
         "\nOscillator: " + str.tostring(currentOscillator, "#.##") +
         anchorNote
    if showPriceStructureLabels
        string labelText = showOscillatorStructure ? priceStructure + "\n" + oscillatorStructure : priceStructure
        label structureLabel = label.new(
             currentOscIndex,
             currentOscillator,
             labelText,
             xloc = xloc.bar_index,
             yloc = yloc.price,
             color = pivotColor,
             textcolor = color.white,
             style = label.style_label_up,
             size = size.tiny,
             tooltip = tooltipText
        )
        f_storeLabel(
             storedLabels,
             structureLabel,
             maximumStoredDrawings
        )
    if showChartPivotLabels
        label chartLabel = label.new(
             currentIndex,
             currentPrice,
             priceStructure,
             xloc = xloc.bar_index,
             yloc = yloc.price,
             color = pivotColor,
             textcolor = color.white,
             style = label.style_label_up,
             size = size.tiny,
             tooltip = tooltipText,
             force_overlay = true
        )
        f_storeLabel(
             storedLabels,
             chartLabel,
             maximumStoredDrawings
        )
    if showPivotGuideLines and not na(previousLowOscIndex)
        line guideLine = line.new(
             previousLowOscIndex,
             previousLowOscillator,
             currentOscIndex,
             currentOscillator,
             xloc = xloc.bar_index,
             color = color.new(pivotColor, 55),
             style = line.style_solid,
             width = 1
        )
        f_storeLine(
             storedLines,
             guideLine,
             maximumStoredDrawings
        )
    if regularBullishDivergence or hiddenBullishDivergence
        color divergenceColor = regularBullishDivergence ? bullishDivergenceColor : hiddenBullishColor
        string divergenceText = regularBullishDivergence ? "Bullish Divergence" : "Hidden Bullish"
        divergenceStyle = regularBullishDivergence ? line.style_solid : line.style_dashed
        float qualityScore = f_divergenceQualityScore(
             false,
             regularBullishDivergence,
             currentIndex - anchorIndex,
             math.abs(currentOscillator - anchorOscillator),
             currentOscillator,
             currentPrice,
             trendEma[pivotRight],
             pivotAtr
        )
        if showQualityScore
            divergenceText := divergenceText + " " + str.tostring(qualityScore, "#")
        if showOscillatorDivergenceLines
            line oscillatorDivergenceLine = line.new(
                 anchorOscIndex,
                 anchorOscillator,
                 currentOscIndex,
                 currentOscillator,
                 xloc = xloc.bar_index,
                 color = divergenceColor,
                 style = divergenceStyle,
                 width = 2
            )
            f_storeLine(
                 storedLines,
                 oscillatorDivergenceLine,
                 maximumStoredDrawings
            )
        if showPriceDivergenceLines
            line priceDivergenceLine = line.new(
                 anchorIndex,
                 anchorPrice,
                 currentIndex,
                 currentPrice,
                 xloc = xloc.bar_index,
                 color = divergenceColor,
                 style = divergenceStyle,
                 width = 2,
                 force_overlay = true
            )
            f_storeLine(
                 storedLines,
                 priceDivergenceLine,
                 maximumStoredDrawings
            )
        if showDivergenceLabels
            label divergenceLabel = label.new(
                 currentOscIndex,
                 currentOscillator,
                 divergenceText,
                 xloc = xloc.bar_index,
                 yloc = yloc.price,
                 color = color.new(divergenceColor, 100),
                 textcolor = divergenceColor,
                 style = label.style_label_down,
                 size = size.small,
                 tooltip = tooltipText
            )
            f_storeLabel(
                 storedLabels,
                 divergenceLabel,
                 maximumStoredDrawings
            )
        latestEvent := divergenceText
        latestEventColor := divergenceColor
        latestEventBarIndex := bar_index
        latestEventAnchorDistance := currentIndex - anchorIndex
    else if lowBullishConfirmation
        latestEvent := "Bullish Momentum Alignment"
        latestEventColor := confirmationColor
        latestEventBarIndex := bar_index
        latestEventAnchorDistance := currentIndex - anchorIndex
    else if lowBearishConfirmation
        latestEvent := "Bearish Momentum Alignment"
        latestEventColor := confirmationColor
        latestEventBarIndex := bar_index
        latestEventAnchorDistance := currentIndex - anchorIndex
    latestPriceStructure := priceStructure
    latestOscillatorStructure := oscillatorStructure
    latestPivotType := "Low"
    latestPivotBarIndex := bar_index
    array.push(lowIndices, currentIndex)
    array.push(lowOscIndices, currentOscIndex)
    array.push(lowPrices, currentPrice)
    array.push(lowOscillators, currentOscillator)
    if array.size(lowIndices) > MAX_STORED_PIVOTS
        array.shift(lowIndices)
        array.shift(lowOscIndices)
        array.shift(lowPrices)
        array.shift(lowOscillators)
    previousLowIndex := currentIndex
    previousLowOscIndex := currentOscIndex
    previousLowPrice := currentPrice
    previousLowOscillator := currentOscillator
    if enableDebugLogging
        log.info(
             "PMS PIVOT type=Low idx=" + str.tostring(currentIndex) +
             " osc=" + oscillatorType + " mode=" + oscillatorAnchorMode +
             " price=" + str.tostring(currentPrice, format.mintick) +
             " priceStruct=" + priceStructure +
             " oscVal=" + str.tostring(currentOscillator, "#.####") +
             " oscIdx=" + str.tostring(currentOscIndex) +
             " oscStruct=" + oscillatorStructure +
             " hasAnchor=" + f_boolStr(hasAnchor) +
             " anchorIdx=" + str.tostring(anchorIndex) +
             " anchorDist=" + str.tostring(currentIndex - anchorIndex) +
             " anchorPrice=" + str.tostring(anchorPrice, format.mintick) +
             " anchorOsc=" + str.tostring(anchorOscillator, "#.####") +
             " anchorScore=" + (hasAnchor ? str.tostring(anchorScore, "#.##") : "na") +
             " oscStDev=" + str.tostring(oscillatorStDev, "#.####") +
             " priceMadeHigherLow=" + f_boolStr(priceMadeHigherLow) +
             " priceMadeLowerLow=" + f_boolStr(priceMadeLowerLow) +
             " priceMadeEqualLow=" + f_boolStr(priceMadeEqualLow) +
             " oscMadeHigherLow=" + f_boolStr(oscillatorMadeHigherLow) +
             " oscMadeLowerLow=" + f_boolStr(oscillatorMadeLowerLow) +
             " lowExtremeValid=" + f_boolStr(lowExtremeValid) +
             " trendContextBullishOk=" + f_boolStr(trendContextBullishOk) +
             " regularBullishDiv=" + f_boolStr(regularBullishDivergence) +
             " hiddenBullishDiv=" + f_boolStr(hiddenBullishDivergence) +
             " bullConfirm=" + f_boolStr(lowBullishConfirmation) +
             " bearConfirm=" + f_boolStr(lowBearishConfirmation) +
             " priceTolerance=" + str.tostring(priceTolerance, "#.########") +
             " prevPrice=" + str.tostring(logPrevPrice, format.mintick) +
             " prevOsc=" + str.tostring(logPrevOsc, "#.####") +
             " allowEqual=" + f_boolStr(allowEqualPricePivots) +
             " minOscDiff=" + str.tostring(minimumOscillatorDifference, "#.####") +
             " minPivotDist=" + str.tostring(minimumPivotDistance) +
             " maxPivotDist=" + str.tostring(maximumPivotDistance) +
             " histIdx=" + logHistIdx +
             " histPrice=" + logHistPrice +
             " histOsc=" + logHistOsc
        )
// ─────────────────────────────────────────────────────────────────────────────
// STATUS TABLE
// ─────────────────────────────────────────────────────────────────────────────
color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
var table statusTable = table.new(
     position.top_right,
     2,
     9,
     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
)
if barstate.islast
    if showStatusTable
        // "Latest Pivot" block (rows 1-4): reflects the most recently confirmed pivot,
        // regardless of whether it produced a divergence/alignment event.
        // "Latest Event" block (rows 6-8): reflects the most recently fired event, which
        // can be anchored to an older pivot — these two blocks can legitimately show
        // different timestamps (see README "Anchor Selection").
        string pivotAgeText = na(latestPivotBarIndex) ? "—" : str.tostring(bar_index - latestPivotBarIndex) + " bars"
        string ageText = na(latestEventBarIndex) ? "—" : str.tostring(bar_index - latestEventBarIndex) + " bars"
        string anchorText = na(latestEventAnchorDistance) ? "—" : str.tostring(latestEventAnchorDistance) + " bars back"
        table.cell(
             statusTable,
             0,
             0,
             "Momentum Structure",
             text_color = tc,
             bgcolor = hd,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             0,
             oscillatorType,
             text_color = tc,
             bgcolor = hd,
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             1,
             "Pivot",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             1,
             latestPivotType,
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             2,
             "Price",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             2,
             latestPriceStructure,
             text_color = f_structureColor(latestPriceStructure),
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             3,
             "Momentum",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             3,
             latestOscillatorStructure,
             text_color = f_structureColor(latestOscillatorStructure),
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             4,
             "Pivot Age",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             4,
             pivotAgeText,
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             5,
             "Latest Event",
             text_color = tc,
             bgcolor = hd,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             5,
             "",
             text_color = tc,
             bgcolor = hd,
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             6,
             "Status",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             6,
             latestEvent,
             text_color = latestEventColor,
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             7,
             "Event Age",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             7,
             ageText,
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             0,
             8,
             "Anchor",
             text_color = tc,
             text_size = size.small
        )
        table.cell(
             statusTable,
             1,
             8,
             anchorText,
             text_color = tc,
             text_size = size.small
        )
    else
        table.clear(statusTable, 0, 0, 1, 8)
// ─────────────────────────────────────────────────────────────────────────────
// ALERTS
// Alerts fire on the confirmation bar, not retrospectively on the pivot bar.
// Pivot confirmation still depends on the forming bar until it closes, so
// alertOk gates every condition below on bar close (see Alerts input group).
// ─────────────────────────────────────────────────────────────────────────────
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(
     alertOk and regularBullishDivergence,
     "Regular Bullish Divergence",
     "PMS · REGULAR BULLISH DIVERGENCE · {{ticker}} {{interval}}"
)
alertcondition(
     alertOk and regularBearishDivergence,
     "Regular Bearish Divergence",
     "PMS · REGULAR BEARISH DIVERGENCE · {{ticker}} {{interval}}"
)
alertcondition(
     alertOk and hiddenBullishDivergence,
     "Hidden Bullish Divergence",
     "PMS · HIDDEN BULLISH DIVERGENCE · {{ticker}} {{interval}}"
)
alertcondition(
     alertOk and hiddenBearishDivergence,
     "Hidden Bearish Divergence",
     "PMS · HIDDEN BEARISH DIVERGENCE · {{ticker}} {{interval}}"
)
alertcondition(
     alertOk and bullishConfirmation,
     "Bullish Momentum Alignment",
     "PMS · BULLISH ALIGNMENT · {{ticker}} {{interval}}"
)
alertcondition(
     alertOk and bearishConfirmation,
     "Bearish Momentum Alignment",
     "PMS · BEARISH ALIGNMENT · {{ticker}} {{interval}}"
)
alertcondition(
     alertOk and (
         regularBullishDivergence
             or regularBearishDivergence
             or hiddenBullishDivergence
             or hiddenBearishDivergence
     ),
     "New Divergence",
     "PMS · NEW DIVERGENCE · {{ticker}} {{interval}}"
)

Overview

Pivot Momentum Structure [WavesUnchained] classifies higher highs, higher lows, lower highs and lower lows, then marks regular and hidden bullish or bearish divergences plus structure alignment. Users choose among RSI, WaveTrend, Stochastic RSI, CCI, Fisher, TSI, Williams %R, CMO or MFI, and structure presets from scalping through major structure. Optional filters, divergence lines, price-pivot labels, a dashboard and alerts are included. Signals appear only after the right-side pivot confirmation bars.

Details

Published by

casoon on GitHub

Draws

Separate pane below the chart

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
OscillatorstringRSIoscillatorType
Sourcesourceclosesrc
RSI Lengthint14rsiLength
RSI Signal Lineint5rsiSignalLength
WaveTrend Channelint10wtChannelLength
WaveTrend Smoothingint21wtAverageLength
WaveTrend Signal Lineint4wtSignalLength
Stoch RSI Lengthint14stochRsiLength
Stochastic Lengthint14stochLength
%K Smoothingint3stochKSmoothing
%D Smoothingint3stochDSmoothing
CCI Lengthint20cciLength
Show all 40 settings
SettingTypeDefaultVariable
CCI Signal Lineint9cciSignalLength
Fisher Lengthint10fisherLength
TSI Long Lengthint25tsiLongLength
TSI Short Lengthint13tsiShortLength
TSI Signal Lineint7tsiSignalLength
Williams %R Lengthint14williamsLength
Williams %R Signal Lineint9williamsSignalLength
CMO Lengthint14cmoLength
CMO Signal Lineint9cmoSignalLength
MFI Lengthint14mfiLength
MFI Signal Lineint9mfiSignalLength
Structure PresetstringSwingstructurePreset
Pivot Left (Custom)int7pivotLeftCustom
Pivot Right (Custom)int7pivotRightCustom
Minimum Pivot Distance (Custom)int10minimumPivotDistanceCustom
Maximum Pivot Distance (Custom)int120maximumPivotDistanceCustom
Min. Price Difference ATR (Custom)float0.10minimumPriceDifferenceAtrCustom
Oscillator AnchorstringExact Pivot BaroscillatorAnchorMode
Local Extreme Search Radiusint2oscillatorAnchorRadius
Regular DivergencebooltruedetectRegular
Hidden DivergencebooltruedetectHidden
Momentum AlignmentbooltruedetectConfirmations
Require Extreme ZoneboolfalserequireExtremeZone
Trend Context Filter (Hidden Divergence)boolfalseuseTrendContextFilter
Trend EMA Lengthint50trendEmaLength
Alerts only on bar close (confirmed)booltruealertsOnBarClose
Enable Debug Logging (Pine Logs)boolfalseenableDebugLogging
ATR Lengthint14atrLength
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

This indicator implements a full Chanlun pipeline on the price chart. It merges contained bars, marks top and bottom fractals, and connects alternating fractals into strokes. Overlapping strokes form pivot boxes, and MACD area ratios flag trend or consolidatio

Indicator v6 Support & resistanceDivergence
Updated 2 months ago · GPL-3.0 GitHub repository of 缠论 - 分型与笔 [Chanlun Core]

This indicator scans a pasted list of symbols for RSI directional momentum and shows matching long and short signals in a table in a separate pane. The user chooses continuous break, continuous retracement, flip break, flip retracement, divergence, or hidden d

Indicator v6 Oscillators & momentumDivergence
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup - RSI Directional Momentum Scanner
228 stars

This indicator calculates the Relative Strength Index (RSI) across four user-selected timeframes and plots the results in a separate pane. It identifies regular bullish and bearish divergences between price pivot points and the primary timeframe RSI value. Add

Indicator v6 Oscillators & momentumDivergence
Updated 3 months ago · None stated GitHub repository of Multi-Timeframe RSI with Divergence

Oscillator Suite

by TamaraKGreen
46 stars

Oscillator Suite plots a normalised price oscillator in a separate pane and overlays a signal line that can use SMA, EMA, WMA, HMA, RMA, VWMA, T3, ZEMA, KAMA, or GKEMA. It marks bullish and bearish divergences, a smoothed money-flow series, confluence areas an

Indicator v5 Oscillators & momentumDivergence
Updated 8 weeks ago · None stated GitHub repository of Oscillator Suite

WaveTrend 3D

by nersent
29 stars

WaveTrend 3D is a redesigned oscillator that implements the classic WaveTrend algorithm with kernel-based regression for trend reversal confirmation and multi-timeframe support. It calculates a smoothed oscillator line and displays a symmetrical three-dimensio

Indicator v5 Oscillators & momentumDivergence
Updated 11 months ago · Custom licence GitHub repository of WaveTrend 3D

More Oscillators & momentum scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

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

Compare brokers