//@version=6
// ============================================================================
// Oscillator Topology [WavesUnchained]
// Version: 1.1.4
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Analyzes the SHAPE of oscillator movements rather than their magnitude or
// direction. Curvature, spike width, symmetry and flatness encode information
// not visible in the raw oscillator value.
//
// Key shapes:
// V-Spike — sharp, narrow peak/trough: aggressive reversal, high conviction
// Round Top — broad, gradual peak: exhaustion building, reversal likely but slow
// Flat Zone — oscillator stuck near extreme: trend death / distribution phase
// Asymmetric — fast rise, slow fall (or vice versa): indicates hidden flow
//
// Features:
// ✓ Curvature at pivot extremes (2nd derivative normalized by recent WT range)
// ✓ Width of last WT peak/trough (bars spent near extreme)
// ✓ Asymmetry index: rise speed vs fall speed at each pivot
// ✓ Shape classification per pivot: V-Spike / Round / Flat / Asymmetric
// ✓ Curvature plot and shape label at each classified pivot
// ✓ Dashboard with current dominant shape and topology summary
// ✓ Alerts: V-Spike at peak / trough
//
// ============================================================================
indicator("Oscillator Topology [WavesUnchained]", "Osc Topology [WU]", format=format.price, precision=2, overlay=false,
max_labels_count=100)
// ─── Inputs ───────────────────────────────────────────────────────────────────
grWT = "WaveTrend"
wtChLen = input.int(10, "Channel Length", minval=1, group=grWT)
wtAvLen = input.int(21, "Average Length", minval=1, group=grWT)
grTopo = "Topology Settings"
pivLbL = input.int(5, "Pivot Left Bars", minval=2, maxval=20, group=grTopo)
pivLbR = input.int(5, "Pivot Right Bars", minval=2, maxval=20, group=grTopo)
spikeThresh = input.float(3.0, "V-Spike Curvature Threshold", minval=0.5, maxval=10.0, step=0.25, group=grTopo,
tooltip="Normalized curvature above this threshold classifies the pivot as a V-Spike (sharp reversal). Lower = more sensitive.")
flatBars = input.int(4, "Flat Zone Min Bars", minval=2, maxval=15, group=grTopo,
tooltip="Minimum consecutive bars near the extreme to classify as a Flat Zone (trend death).")
flatTolerance = input.float(0.15, "Flat Zone Tolerance (fraction of pivot height)", minval=0.05, maxval=0.5, step=0.05, group=grTopo)
grD = "Display"
showCurv = input.bool(true, "Show Curvature Line", group=grD)
showLbls = input.bool(true, "Show Shape Labels", group=grD)
showDash = input.bool(true, "Show Dashboard", group=grD)
// ─── WaveTrend ────────────────────────────────────────────────────────────────
_wtEsa = ta.ema(hlc3, wtChLen)
_wtD = ta.ema(math.abs(hlc3 - _wtEsa), wtChLen)
_wtCi = (hlc3 - _wtEsa) / (0.015 * math.max(_wtD, syminfo.mintick))
wtOsc = ta.ema(_wtCi, wtAvLen)
// ─── Curvature ────────────────────────────────────────────────────────────────
// Second derivative of WT, normalized by recent WT range (makes it scale-free)
wtVel = wtOsc - wtOsc[1]
wtAcc = wtVel - wtVel[1]
wtRange = ta.highest(wtOsc, 20) - ta.lowest(wtOsc, 20)
wtCurv = wtRange > 0 ? wtAcc / wtRange * 100 : 0.0 // -100..+100 scaled curvature
// ─── Pivot detection ──────────────────────────────────────────────────────────
pivHigh = ta.pivothigh(wtOsc, pivLbL, pivLbR)
pivLow = ta.pivotlow(wtOsc, pivLbL, pivLbR)
// At a confirmed pivot, classify the shape by looking at bars around it
f_shape(isPivHigh) =>
// Both valuewhen calls execute unconditionally so the ta.* series stay consistent
pivValH = ta.valuewhen(not na(ta.pivothigh(wtOsc, pivLbL, pivLbR)), wtOsc[pivLbR], 0)
pivValL = ta.valuewhen(not na(ta.pivotlow(wtOsc, pivLbL, pivLbR)), wtOsc[pivLbR], 0)
pivVal = isPivHigh ? pivValH : pivValL
curvAtPiv = wtCurv[pivLbR]
absCurv = math.abs(curvAtPiv)
// Width = consecutive bars near the extreme (within flatTolerance × pivot height
// above baseline): the pivot bar plus the unbroken run on each side of it
baseline = isPivHigh ? ta.lowest(wtOsc, 30) : ta.highest(wtOsc, 30)
pivHeight = math.abs(pivVal - baseline)
tolerance = pivHeight * flatTolerance
int flatCount = 1
// right side: bars after the pivot (offsets pivLbR-1 .. 0)
for j = 1 to pivLbR
if math.abs(wtOsc[pivLbR - j] - pivVal) < tolerance
flatCount += 1
else
break
// left side: bars before the pivot
for j = 1 to math.min(pivLbL + pivLbR - 1, 20)
if math.abs(wtOsc[pivLbR + j] - pivVal) < tolerance
flatCount += 1
else
break
// Asymmetry: compare rise speed vs fall speed (approach bars vs departure bars)
riseSpeed = pivLbL > 0 ? (pivVal - wtOsc[pivLbR + pivLbL]) / pivLbL : 0.0
fallSpeed = pivLbR > 0 ? (pivVal - wtOsc) / pivLbR : 0.0
asymRatio = (riseSpeed + fallSpeed) != 0 ? math.abs(riseSpeed - fallSpeed) / math.max(math.abs(riseSpeed + fallSpeed), 0.01) : 0.0
shape = if absCurv >= spikeThresh
"V-Spike"
else if flatCount >= flatBars
"Flat"
else if asymRatio > 0.5
"Asymm"
else
"Round"
[shape, absCurv, flatCount]
[shapeH, curvH, flatH] = f_shape(true)
[shapeL, curvL, flatL] = f_shape(false)
// ─── Shape label colors ───────────────────────────────────────────────────────
f_shapeColor(s) =>
color c = color.new(color.gray, 20) // Round
if s == "V-Spike"
c := color.new(#d50000, 0)
else if s == "Flat"
c := color.new(#ff6f00, 0)
else if s == "Asymm"
c := color.new(#9c27b0, 0)
c
// ─── Shape labels on chart ────────────────────────────────────────────────────
if showLbls
if not na(pivHigh)
shapeC = f_shapeColor(shapeH)
label.new(bar_index - pivLbR, wtOsc[pivLbR] + 5, shapeH,
color=color(na), style=label.style_none,
textcolor=shapeC, size=size.small)
if not na(pivLow)
shapeC = f_shapeColor(shapeL)
label.new(bar_index - pivLbR, wtOsc[pivLbR] - 5, shapeL,
color=color(na), style=label.style_none,
textcolor=shapeC, size=size.small)
// ─── Plots ────────────────────────────────────────────────────────────────────
hline(0, "Zero", color=color.new(color.gray, 50))
hline(spikeThresh, "Spike+", color=color.new(color.red, 60), linestyle=hline.style_dotted)
hline(-spikeThresh, "Spike-", color=color.new(color.red, 60), linestyle=hline.style_dotted)
plot(wtOsc, "WaveTrend", color=color.new(#26a69a, 30), linewidth=2)
plot(showCurv ? wtCurv : na, "Curvature", color=color.new(#ffd600, 30), linewidth=1)
// Pivot markers sized by curvature
plotshape(not na(pivHigh), "Peak", shape.triangledown, location.top,
color=f_shapeColor(shapeH), size=size.tiny)
plotshape(not na(pivLow), "Trough", shape.triangleup, location.bottom,
color=f_shapeColor(shapeL), size=size.tiny)
// ─── Dashboard ────────────────────────────────────────────────────────────────
// Track last two shapes for summary
var string lastHighShape = "—"
var string lastLowShape = "—"
var float lastHighCurv = 0.0
var float lastLowCurv = 0.0
if not na(pivHigh)
lastHighShape := shapeH
lastHighCurv := curvH
if not na(pivLow)
lastLowShape := shapeL
lastLowCurv := curvL
color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
var table t = table.new(position.top_right, 3, 5,
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 showDash and barstate.islast
table.cell(t, 0, 0, "Osc Topology", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 1, 0, "Shape", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 2, 0, "|Curv|", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 0, 1, "Last Peak", text_color=tc, text_size=size.small)
table.cell(t, 1, 1, lastHighShape, text_color=f_shapeColor(lastHighShape), text_size=size.small)
table.cell(t, 2, 1, str.tostring(lastHighCurv, "#.#"), text_color=tc, text_size=size.small)
table.cell(t, 0, 2, "Last Trough", text_color=tc, text_size=size.small)
table.cell(t, 1, 2, lastLowShape, text_color=f_shapeColor(lastLowShape), text_size=size.small)
table.cell(t, 2, 2, str.tostring(lastLowCurv, "#.#"), text_color=tc, text_size=size.small)
// Current curvature
table.cell(t, 0, 3, "", bgcolor=color.new(color.gray, 80))
table.cell(t, 1, 3, "", bgcolor=color.new(color.gray, 80))
table.cell(t, 2, 3, "", bgcolor=color.new(color.gray, 80))
string curvState = "Flat"
color curvC = tc
if math.abs(wtCurv) > spikeThresh
curvState := "Spike forming"
curvC := color.new(#d50000, 0)
else if wtCurv > 0.5
curvState := "Accelerating ↑"
curvC := color.new(#00c853, 0)
else if wtCurv < -0.5
curvState := "Decelerating ↓"
curvC := color.new(#ff6f00, 0)
table.cell(t, 0, 4, "Now", text_color=tc, text_size=size.small)
table.cell(t, 1, 4, curvState, text_color=curvC, text_size=size.small)
table.cell(t, 2, 4, str.tostring(wtCurv, "#.#"), text_color=curvC, text_size=size.small)
// ─── Alerts ───────────────────────────────────────────────────────────────────
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (not na(pivHigh) and shapeH == "V-Spike"), "V-Spike Peak",
"OTOP · V-SPIKE PEAK SHORT · {{ticker}} {{interval}}")
alertcondition(alertOk and (not na(pivLow) and shapeL == "V-Spike"), "V-Spike Trough",
"OTOP · V-SPIKE TROUGH LONG · {{ticker}} {{interval}}")
Overview
Oscillator Topology [WavesUnchained] evaluates curvature, peak width, asymmetry, and flatness around pivot extremes to categorize oscillator movements into V-Spikes, Round Tops, Flat Zones, or Asymmetric shapes. Drawn in a separate pane, the indicator plots a curvature line, marks identified shapes with chart labels at pivot points, and displays a summary dashboard.
WaveTrend oscillator shape analysis
Curvature calculation at pivot extremes
Classification into V-Spikes, Round Tops, and Flat Zones
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Channel Length
int
10
wtChLen
Average Length
int
21
wtAvLen
Pivot Left Bars
int
5
pivLbL
Pivot Right Bars
int
5
pivLbR
V-Spike Curvature Threshold
float
3.0
spikeThresh
Flat Zone Min Bars
int
4
flatBars
Flat Zone Tolerance (fraction of pivot height)
float
0.15
flatTolerance
Show Curvature Line
bool
true
showCurv
Show Shape Labels
bool
true
showLbls
Show Dashboard
bool
true
showDash
Alerts only on bar close (confirmed)
bool
true
alertsOnBarClose
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 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 plots the RSI oscillator on the main chart and displays its value distribution in a table positioned at the top right. The script calculates the relative strength index using the input source and length, then plots horizontal lines at 0, 50, and
Market Speedometer plots a 0-100 market speed reading in a separate pane. It measures movement by bar-to-bar change, true range, or a short average of recent bars, then ranks that movement against ATR and a percentile lookback. An EMA smooths the reading, and
KamaFama is an indicator that translates a Freqtrade trading strategy into Pine Script for visualization. It combines Kaufman Adaptive Moving Average (KAMA), MAMA/FAMA calculations derived via Hilbert Transform, Williams %R, and correlation trend filters to ev
Calculates normalized reversal metrics across up to ten Forex currency pairs using user-selected moving average methods, including HarmonicMA, EMA, SMA, or WMA. The indicator identifies overbought, oversold, and mean-crossing conditions using a configurable th
The 8-Factor Composite Screener evaluates equities across fundamental and technical criteria, including valuation ratios, earnings and revenue growth, 12-month return momentum, proximity to 52-week highs, Stage 2 moving average alignment, benchmark relative st