//@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.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Oscillator
string
RSI
oscillatorType
Source
source
close
src
RSI Length
int
14
rsiLength
RSI Signal Line
int
5
rsiSignalLength
WaveTrend Channel
int
10
wtChannelLength
WaveTrend Smoothing
int
21
wtAverageLength
WaveTrend Signal Line
int
4
wtSignalLength
Stoch RSI Length
int
14
stochRsiLength
Stochastic Length
int
14
stochLength
%K Smoothing
int
3
stochKSmoothing
%D Smoothing
int
3
stochDSmoothing
CCI Length
int
20
cciLength
Show all 40 settings
Setting
Type
Default
Variable
CCI Signal Line
int
9
cciSignalLength
Fisher Length
int
10
fisherLength
TSI Long Length
int
25
tsiLongLength
TSI Short Length
int
13
tsiShortLength
TSI Signal Line
int
7
tsiSignalLength
Williams %R Length
int
14
williamsLength
Williams %R Signal Line
int
9
williamsSignalLength
CMO Length
int
14
cmoLength
CMO Signal Line
int
9
cmoSignalLength
MFI Length
int
14
mfiLength
MFI Signal Line
int
9
mfiSignalLength
Structure Preset
string
Swing
structurePreset
Pivot Left (Custom)
int
7
pivotLeftCustom
Pivot Right (Custom)
int
7
pivotRightCustom
Minimum Pivot Distance (Custom)
int
10
minimumPivotDistanceCustom
Maximum Pivot Distance (Custom)
int
120
maximumPivotDistanceCustom
Min. Price Difference ATR (Custom)
float
0.10
minimumPriceDifferenceAtrCustom
Oscillator Anchor
string
Exact Pivot Bar
oscillatorAnchorMode
Local Extreme Search Radius
int
2
oscillatorAnchorRadius
Regular Divergence
bool
true
detectRegular
Hidden Divergence
bool
true
detectHidden
Momentum Alignment
bool
true
detectConfirmations
Require Extreme Zone
bool
false
requireExtremeZone
Trend Context Filter (Hidden Divergence)
bool
false
useTrendContextFilter
Trend EMA Length
int
50
trendEmaLength
Alerts only on bar close (confirmed)
bool
true
alertsOnBarClose
Enable Debug Logging (Pine Logs)
bool
false
enableDebugLogging
ATR Length
int
14
atrLength
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.
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
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
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
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
This indicator tracks intermediate market cycles inspired by Linda Raschke's approach using daily NYSE TICK statistics. It fetches daily high, low, and close values for the TICK index via security requests and computes moving averages of the highs and lows. It
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