Strategy · Pine Script v6 · Own pane

Oscillator Divergence Zones [WavesUnchained] — Strategy

by casoon

This strategy calculates regular and hidden divergences across selectable momentum oscillators, including RSI, CCI, and MFI, and plots volatility-based divergence zones.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code471
Settings40inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new strategy, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart. The trades and results appear in the Strategy Tester tab; adjust the settings with the gear icon.

Source Code

471 lines of Pine Script v6 from strategies/oscillator_divergence_zones/oscillator_divergence_zones_strategy.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

oscillator_divergence_zones_strategy.pine
//@version=6
// ============================================================================
// Oscillator Divergence Zones [WavesUnchained] — Strategy (standalone)
// Version: 1.2.4
// Author: WavesUnchained
// Build: 2026-10-12 00:00:00
// ============================================================================
// Detects regular and hidden divergences for RSI, CCI, or MFI.
//
// Features:
//   ✓ Oscillator selector: RSI (default), CCI, MFI
//   ✓ Regular divergence: price/oscillator structural opposition at pivots
//   ✓ Hidden divergence: trend-continuation counterpart (toggle separately)
//   ✓ Three-mode level filter: Off / Fixed OB-OS / Dynamic Zones (percentile-based)
//   ✓ StdDev filter: optional statistical overextension gate for regular divergences
//   ✓ ATR-based zone width: band height locked to creation-time volatility
//   ✓ Configurable break trigger: full-candle (default) or close-based
//   ✓ Zone retest counter: touch count displayed at zone right edge
//   ✓ Dual-pane visualization: oscillator pane markers and chart overlay
//   ✓ Alert conditions for all four divergence types
//
// ============================================================================

strategy("Oscillator Divergence Zones [WavesUnchained] — Strategy", overlay=false,
     format=format.price,
     precision=2,
     max_lines_count=500,
     max_labels_count=500,
     max_boxes_count=500,
     default_qty_type=strategy.percent_of_equity, default_qty_value=10,
     commission_type=strategy.commission.percent, commission_value=0.02,
     slippage=1, max_bars_back=500)

// Synthetische Bar-Preise (Heikin Ashi, Renko …): Orders füllen zu Marktpreisen,
// der Backtest gilt dort nicht. Kurzer Hinweis unten rechts statt auf der Kerze.
var table chartTypeWarn = table.new(position.bottom_right, 1, 1)
if barstate.islast and not chart.is_standard
    table.cell(chartTypeWarn, 0, 0, "ODZ Strategy · backtest invalid on this chart type",
         bgcolor = color.new(color.red, 10), text_color = color.white, text_size = size.small)

// ─── Inputs ──────────────────────────────────────────────────────────────────
g_osc    = "Oscillator"
oscType  = input.string("RSI", "Type", options=["RSI", "CCI", "MFI"], group=g_osc)
oscSrc   = input.source(close, "Source",                     group=g_osc)
oscLen   = input.int(14,       "Length",       minval=2,     group=g_osc)
autoLevels = input.bool(true,  "Auto OB/OS per Oscillator", group=g_osc,
     tooltip="Automatically applies standard OB/OS levels per oscillator: RSI 70/30 · CCI 100/−100 · MFI 80/20. Disable to set custom values below.")
obLevel  = input.int(70,       "Overbought",  minval=-500, maxval=500, group=g_osc,
     tooltip="Used only when Auto OB/OS is disabled. RSI: 70 · CCI: 100 · MFI: 80")
osLevel  = input.int(30,       "Oversold",    minval=-500, maxval=500, group=g_osc,
     tooltip="Used only when Auto OB/OS is disabled. RSI: 30 · CCI: −100 · MFI: 20")

g_div         = "Divergence"
pivLeft       = input.int(7,   "Pivot Left Bars",           minval=1, group=g_div)
pivRight      = input.int(7,   "Pivot Right Bars",          minval=1, group=g_div)
minRange      = input.int(10,  "Min Bars Between Pivots",   minval=1, group=g_div)
maxRange      = input.int(100, "Max Bars Between Pivots",   minval=5, group=g_div)
showHidden    = input.bool(false, "Show Hidden Divergence",            group=g_div)
filterMode    = input.string("Fixed", "Level Filter", options=["Off", "Fixed", "Dynamic Zones"], group=g_div,
     tooltip="Off: no level filter. Fixed: suppress signals outside a fixed % of the OB–OS range. Dynamic Zones: adaptive percentile-based thresholds computed from recent oscillator history.")
bullFilterPct  = input.int(75,  "Max for Bull (%)", minval=0, maxval=100, group=g_div,
     tooltip="Fixed mode only. Upper limit for bullish signals as % of the OB–OS range. 75% ≈ RSI 60 · CCI 50 · MFI 65")
bearFilterPct  = input.int(25,  "Min for Bear (%)", minval=0, maxval=100, group=g_div,
     tooltip="Fixed mode only. Lower limit for bearish signals as % of the OB–OS range. 25% ≈ RSI 40 · CCI −50 · MFI 35")
dynSampleLen  = input.int(100,  "DZ Sample Length", minval=10, group=g_div,
     tooltip="Dynamic Zones mode only. Number of bars used to compute the percentile distribution of the oscillator.")
dynPcntile    = input.float(90.0, "DZ Threshold (%)", minval=50.0, maxval=99.0, step=1.0, group=g_div,
     tooltip="Dynamic Zones mode only. Symmetric threshold: bull filter uses the (100−X)th percentile as upper limit; bear filter uses the Xth percentile as lower limit. E.g. 90 → only fire when oscillator is in the bottom/top 10% of its recent history.")

g_std        = "StdDev Filter"
useStdFilter = input.bool(false,  "Statistical Overextension Filter", group=g_std,
     tooltip="Only fire regular divergences when the oscillator is outside its rolling mean ± N×StdDev band at the pivot. Reduces false signals in flat/choppy markets. Does not apply to hidden divergences.")
stdPeriod    = input.int(100,  "Lookback Period", minval=10,               group=g_std)
stdMult      = input.float(1.5, "StdDev Multiplier", minval=0.5, step=0.1, group=g_std)

g_zone       = "Zones"
showZones    = input.bool(true,  "Show Zones on Chart",             group=g_zone)
maxZones     = input.int(10,     "Max Active Zones", minval=1, maxval=50, group=g_zone)
zoneExtend   = input.int(15,     "Zone Lookahead (bars)", minval=1,   group=g_zone)
atrLen       = input.int(14,     "ATR Length (zone width)", minval=1, group=g_zone)
zoneFactor   = input.float(0.25, "Zone Width (ATR×)", minval=0.05, maxval=3.0, step=0.05, group=g_zone)
breakOnClose = input.bool(false, "Break on Close",                   group=g_zone)
showTouches  = input.bool(true,  "Show Retest Count",                group=g_zone)

g_col   = "Colors"
bullCol = input.color(#26a69a, "Bullish", group=g_col)
bearCol = input.color(#ef5350, "Bearish", group=g_col)

g_debug    = "Debug"
showPivots = input.bool(false, "Show All Pivots", group=g_debug,
     tooltip="Marks every detected price pivot as a small circle — helps diagnose why a divergence was not triggered. Filled = passed all filters. Semi-transparent = pivot detected but no divergence signal.")

// ─── Types ───────────────────────────────────────────────────────────────────
type Zone
    line  edge
    box   area
    int   touches
    label info
    float halfW
    bool  bull

// ─── State ───────────────────────────────────────────────────────────────────
var array<Zone> bullZones    = array.new<Zone>()
var array<Zone> bearZones    = array.new<Zone>()
var array<Zone> hidBullZones = array.new<Zone>()
var array<Zone> hidBearZones = array.new<Zone>()

var int   prevBullBar    = na
var float prevBullOsc    = na
var int   prevBearBar    = na
var float prevBearOsc    = na
var int   prevHidBullBar = na
var float prevHidBullOsc = na
var int   prevHidBearBar = na
var float prevHidBearOsc = na

// ─── Oscillator ──────────────────────────────────────────────────────────────
atr         = ta.atr(atrLen)
rsiSeries = ta.rsi(oscSrc, oscLen)
cciSeries = ta.cci(oscSrc, oscLen)
mfiSeries = ta.mfi(oscSrc, oscLen)

oscVal = oscType == "CCI" ? cciSeries :
         oscType == "MFI" ? mfiSeries : rsiSeries

// CCI is centered on 0; all others on 50
midLevel   = oscType == "CCI" ? 0 : 50
oscAtPiv   = oscVal[pivRight]

effectiveOB = autoLevels ? (oscType == "CCI" ? 100 : oscType == "MFI" ? 80 : 70)  : obLevel
effectiveOS = autoLevels ? (oscType == "CCI" ? -100 : oscType == "MFI" ? 20 : 30) : osLevel

// ─── Pivots on price ─────────────────────────────────────────────────────────
foundPL = not na(ta.pivotlow(low,   pivLeft, pivRight))
foundPH = not na(ta.pivothigh(high, pivLeft, pivRight))

pivBarIdx = bar_index - pivRight

bullFilterLvl = effectiveOS + bullFilterPct / 100.0 * (effectiveOB - effectiveOS)
bearFilterLvl = effectiveOS + bearFilterPct / 100.0 * (effectiveOB - effectiveOS)

dZoneAbove = ta.percentile_nearest_rank(oscVal, dynSampleLen, dynPcntile)
dZoneBelow = ta.percentile_nearest_rank(oscVal, dynSampleLen, 100.0 - dynPcntile)

bullOk = switch filterMode
    "Fixed"         => oscAtPiv < bullFilterLvl
    "Dynamic Zones" => oscAtPiv < dZoneBelow[pivRight]
    => true

bearOk = switch filterMode
    "Fixed"         => oscAtPiv > bearFilterLvl
    "Dynamic Zones" => oscAtPiv > dZoneAbove[pivRight]
    => true

// StdDev overextension filter (regular divergences only)
_oscMean  = ta.sma(oscVal, stdPeriod)
_oscStd   = ta.stdev(oscVal, stdPeriod)
_oscUpper = _oscMean + stdMult * _oscStd
_oscLower = _oscMean - stdMult * _oscStd
stdBullOk = not useStdFilter or oscAtPiv < _oscLower[pivRight]
stdBearOk = not useStdFilter or oscAtPiv > _oscUpper[pivRight]

// Check divergence against one of the last 3 preceding pivots so that
// intermediate pivots do not prevent detection of the structurally relevant pair.
// ta.valuewhen must run on every bar: inside the short-circuited `and`/`or` chains below
// it would only see the bars where it happened to be evaluated.
plP1 = ta.valuewhen(foundPL, low[pivRight], 1)
plO1 = ta.valuewhen(foundPL, oscAtPiv, 1)
plB1 = ta.valuewhen(foundPL, pivBarIdx, 1)
plP2 = ta.valuewhen(foundPL, low[pivRight], 2)
plO2 = ta.valuewhen(foundPL, oscAtPiv, 2)
plB2 = ta.valuewhen(foundPL, pivBarIdx, 2)
plP3 = ta.valuewhen(foundPL, low[pivRight], 3)
plO3 = ta.valuewhen(foundPL, oscAtPiv, 3)
plB3 = ta.valuewhen(foundPL, pivBarIdx, 3)
phP1 = ta.valuewhen(foundPH, high[pivRight], 1)
phO1 = ta.valuewhen(foundPH, oscAtPiv, 1)
phB1 = ta.valuewhen(foundPH, pivBarIdx, 1)
phP2 = ta.valuewhen(foundPH, high[pivRight], 2)
phO2 = ta.valuewhen(foundPH, oscAtPiv, 2)
phB2 = ta.valuewhen(foundPH, pivBarIdx, 2)
phP3 = ta.valuewhen(foundPH, high[pivRight], 3)
phO3 = ta.valuewhen(foundPH, oscAtPiv, 3)
phB3 = ta.valuewhen(foundPH, pivBarIdx, 3)

_distOk(prevB) =>
    dist = pivBarIdx - prevB
    not na(prevB) and dist >= minRange and dist <= maxRange

_bullDivWith(prevP, prevO, prevB) => _distOk(prevB) and low[pivRight]  < prevP and oscAtPiv > prevO
_bearDivWith(prevP, prevO, prevB) => _distOk(prevB) and high[pivRight] > prevP and oscAtPiv < prevO
_bullHidWith(prevP, prevO, prevB) => _distOk(prevB) and low[pivRight]  > prevP and oscAtPiv < prevO
_bearHidWith(prevP, prevO, prevB) => _distOk(prevB) and high[pivRight] < prevP and oscAtPiv > prevO

// Regular: price/oscillator oppose → reversal signal
bullDiv    = foundPL and bullOk and stdBullOk and (_bullDivWith(plP1, plO1, plB1) or _bullDivWith(plP2, plO2, plB2) or _bullDivWith(plP3, plO3, plB3))
bearDiv    = foundPH and bearOk and stdBearOk and (_bearDivWith(phP1, phO1, phB1) or _bearDivWith(phP2, phO2, phB2) or _bearDivWith(phP3, phO3, phB3))

// Hidden: price/oscillator diverge in trend direction → continuation signal
// Always calculated so strategy/alerts can use them; display gated by showHidden
bullHidDiv = foundPL and bullOk and (_bullHidWith(plP1, plO1, plB1) or _bullHidWith(plP2, plO2, plB2) or _bullHidWith(plP3, plO3, plB3))
bearHidDiv = foundPH and bearOk and (_bearHidWith(phP1, phO1, phB1) or _bearHidWith(phP2, phO2, phB2) or _bearHidWith(phP3, phO3, phB3))

// ─── Zone functions ───────────────────────────────────────────────────────────
_addZone(arr, col, priceLevel, isHidden, isBull) =>
    halfW    = atr * zoneFactor / 2
    x1       = bar_index - pivRight
    x2       = bar_index + zoneExtend
    // Label sits outside the zone box — below for bull, above for bear
    labelY   = isBull ? priceLevel - halfW : priceLevel + halfW
    edgeLine = line.new(x1, priceLevel, x2, priceLevel,
         color=col, width=1,
         style=isHidden ? line.style_dashed : line.style_solid,
         force_overlay=true)
    areaBox  = box.new(x1, priceLevel + halfW, x2, priceLevel - halfW,
         border_color=color.new(col, 60),
         bgcolor=isHidden ? color.new(col, 100) : color.new(col, 90),
         force_overlay=true)
    infoLbl  = label.new(x2, labelY, "",
         xloc=xloc.bar_index, style=label.style_label_left,
         color=color.new(col, 20), textcolor=color.white, size=size.small,
         tooltip="Retest count: how many times price re-entered this zone without breaking it.",
         force_overlay=true)
    arr.push(Zone.new(edgeLine, areaBox, 0, infoLbl, halfW, isBull))
    while arr.size() > maxZones
        old = arr.shift()
        old.edge.delete()
        old.area.delete()
        old.info.delete()

_updateZones(arr, isBull) =>
    if arr.size() > 0
        toRemove = array.new<int>()
        for i = 0 to arr.size() - 1
            z       = arr.get(i)
            lvl     = z.edge.get_y1()
            broken  = breakOnClose ? (isBull ? close < lvl : close > lvl) : (isBull ? high <= lvl : low >= lvl)
            touched = not broken and (isBull ? low <= lvl + z.halfW : high >= lvl - z.halfW)
            if broken
                z.edge.set_x2(bar_index)
                z.edge.set_style(line.style_dotted)
                z.area.set_right(bar_index)
                z.area.set_bgcolor(color.new(color.gray, 94))
                z.area.set_border_color(color.new(color.gray, 88))
                z.info.delete()
                toRemove.unshift(i)
            else
                z.edge.set_x2(bar_index + zoneExtend)
                z.area.set_right(bar_index + zoneExtend)
                if touched
                    z.touches += 1
                if showTouches and z.touches > 0
                    z.info.set_x(bar_index + zoneExtend)
                    z.info.set_text("×" + str.tostring(z.touches))
        if toRemove.size() > 0
            for j = 0 to toRemove.size() - 1
                arr.remove(toRemove.get(j))

// ─── Execute ─────────────────────────────────────────────────────────────────
_updateZones(bullZones,    true)
_updateZones(bearZones,    false)
_updateZones(hidBullZones, true)
_updateZones(hidBearZones, false)

if showZones
    if bullDiv
        _addZone(bullZones,    bullCol, low[pivRight],  false, true)
    if bearDiv
        _addZone(bearZones,    bearCol, high[pivRight], false, false)
    if showHidden and bullHidDiv
        _addZone(hidBullZones, bullCol, low[pivRight],  true,  true)
    if showHidden and bearHidDiv
        _addZone(hidBearZones, bearCol, high[pivRight], true,  false)

// Regular connector lines: dotted, full opacity
if bullDiv
    currBar = bar_index - pivRight
    if not na(prevBullBar)
        line.new(prevBullBar, prevBullOsc, currBar, oscAtPiv,
             color=color.new(bullCol, 20), style=line.style_dotted, width=2)
    prevBullBar := currBar
    prevBullOsc := oscAtPiv

if bearDiv
    currBar = bar_index - pivRight
    if not na(prevBearBar)
        line.new(prevBearBar, prevBearOsc, currBar, oscAtPiv,
             color=color.new(bearCol, 20), style=line.style_dotted, width=2)
    prevBearBar := currBar
    prevBearOsc := oscAtPiv

// Hidden connector lines: dashed, reduced opacity
if showHidden and bullHidDiv
    currBar = bar_index - pivRight
    if not na(prevHidBullBar)
        line.new(prevHidBullBar, prevHidBullOsc, currBar, oscAtPiv,
             color=color.new(bullCol, 55), style=line.style_dashed, width=1)
    prevHidBullBar := currBar
    prevHidBullOsc := oscAtPiv

if showHidden and bearHidDiv
    currBar = bar_index - pivRight
    if not na(prevHidBearBar)
        line.new(prevHidBearBar, prevHidBearOsc, currBar, oscAtPiv,
             color=color.new(bearCol, 55), style=line.style_dashed, width=1)
    prevHidBearBar := currBar
    prevHidBearOsc := oscAtPiv

// ─── Oscillator pane plots ───────────────────────────────────────────────────
noneCol = color.new(color.white, 100)

p_ob  = plot(effectiveOB, "OB",  color=color.new(bearCol, 50))
p_os  = plot(effectiveOS, "OS",  color=color.new(bullCol, 50))
p_mid = plot(midLevel, "Mid", color=color.new(color.gray, 70))
plot(filterMode == "Dynamic Zones" ? dZoneAbove : na, "DZ Upper",
     color=color.new(bearCol, 20), linewidth=1, style=plot.style_circles)
plot(filterMode == "Dynamic Zones" ? dZoneBelow : na, "DZ Lower",
     color=color.new(bullCol, 20), linewidth=1, style=plot.style_circles)
p_osc = plot(oscVal, "Oscillator",
     color=color.from_gradient(oscVal, effectiveOS, effectiveOB, bullCol, bearCol),
     linewidth=2)

fill(p_osc, p_mid, effectiveOB, effectiveOS, color.new(bearCol, 85), na)
fill(p_osc, p_mid, effectiveOB, effectiveOS, na, color.new(bullCol, 85))

plotshape(bullDiv ? oscAtPiv : na, offset=-pivRight,
     title="Bull (pane)", style=shape.labelup, location=location.absolute,
     color=bullCol, textcolor=color.white, text="▲", size=size.small)

plotshape(bearDiv ? oscAtPiv : na, offset=-pivRight,
     title="Bear (pane)", style=shape.labeldown, location=location.absolute,
     color=bearCol, textcolor=color.white, text="▼", size=size.small)

_hidDist = (effectiveOB - effectiveOS) * 0.08
plotshape(showHidden and bullHidDiv ? oscAtPiv - _hidDist : na, offset=-pivRight,
     title="Bull hidden (pane)", style=shape.triangleup, location=location.absolute,
     color=color.new(bullCol, 40), size=size.small)

plotshape(showHidden and bearHidDiv ? oscAtPiv + _hidDist : na, offset=-pivRight,
     title="Bear hidden (pane)", style=shape.triangledown, location=location.absolute,
     color=color.new(bearCol, 40), size=size.small)

// ─── Chart overlay shapes ────────────────────────────────────────────────────
plotshape(bullDiv ? low[pivRight] : na, offset=-pivRight,
     title="Bull reversal (chart)", style=shape.labelup, location=location.absolute,
     color=noneCol, textcolor=bullCol, text="▲\nReversal", force_overlay=true)

plotshape(bearDiv ? high[pivRight] : na, offset=-pivRight,
     title="Bear reversal (chart)", style=shape.labeldown, location=location.absolute,
     color=noneCol, textcolor=bearCol, text="Reversal\n▼", force_overlay=true)

plotshape(showHidden and bullHidDiv ? low[pivRight] : na, offset=-pivRight,
     title="Bull continuation (chart)", style=shape.labelup, location=location.absolute,
     color=noneCol, textcolor=color.new(bullCol, 35), text="▲\nCont.", force_overlay=true)

plotshape(showHidden and bearHidDiv ? high[pivRight] : na, offset=-pivRight,
     title="Bear continuation (chart)", style=shape.labeldown, location=location.absolute,
     color=noneCol, textcolor=color.new(bearCol, 35), text="Cont.\n▼", force_overlay=true)

// ─── Debug: all detected pivots ──────────────────────────────────────────────
// Semi-transparent circles on every pivot — bull pivots teal, bear pivots red.
// A circle without a divergence label means a filter blocked the signal.
plotshape(showPivots and foundPL ? oscAtPiv : na, offset=-pivRight,
     title="Debug: Pivot Low (pane)", style=shape.circle, location=location.absolute,
     color=color.new(bullCol, 60), size=size.tiny)
plotshape(showPivots and foundPH ? oscAtPiv : na, offset=-pivRight,
     title="Debug: Pivot High (pane)", style=shape.circle, location=location.absolute,
     color=color.new(bearCol, 60), size=size.tiny)
plotshape(showPivots and foundPL ? low[pivRight] : na, offset=-pivRight,
     title="Debug: Pivot Low (chart)", style=shape.circle, location=location.absolute,
     color=color.new(bullCol, 60), size=size.tiny, force_overlay=true)
plotshape(showPivots and foundPH ? high[pivRight] : na, offset=-pivRight,
     title="Debug: Pivot High (chart)", style=shape.circle, location=location.absolute,
     color=color.new(bearCol, 60), size=size.tiny, force_overlay=true)

// ─── Alerts ──────────────────────────────────────────────────────────────────
// alertcondition: named alerts, require one setup per condition in the Alert dialog
// alert(): all signals through a single "any alert() call" setup on the indicator
_ticker = syminfo.tickerid + " (" + timeframe.period + ")"
if bullDiv
    alert("Bullish Divergence — " + _ticker + " | " + oscType, alert.freq_once_per_bar)
if bearDiv
    alert("Bearish Divergence — " + _ticker + " | " + oscType, alert.freq_once_per_bar)
if bullHidDiv
    alert("Bullish Hidden Divergence — " + _ticker + " | " + oscType, alert.freq_once_per_bar)
if bearHidDiv
    alert("Bearish Hidden Divergence — " + _ticker + " | " + oscType, alert.freq_once_per_bar)
// ─── Strategy inputs ─────────────────────────────────────────────
g_strat       = "Strategy"
tradeDirInput = input.string("Both", "Trade Direction",
     options=["Both", "Long Only", "Short Only"], group=g_strat,
     tooltip="Filter entries by direction. Useful for asymmetric instruments.")
confirmClose  = input.bool(true,  "Entries on Confirmed Bar Only", group=g_strat,
     tooltip="Only trigger entries on closed bars. Prevents repainting in live trading.")
cooldownBars  = input.int(0,     "Cooldown Bars After Exit", minval=0, group=g_strat,
     tooltip="Block new entries for N bars after a position closes. Set 0 to disable.")
enableBE      = input.bool(false, "Break-Even Stop", group=g_strat,
     tooltip="Move SL to entry price once the trade is N×ATR in profit.")
beATRInput    = input.float(1.0,  "Break-Even Trigger (ATR×)", minval=0.1, step=0.1, group=g_strat,
     tooltip="ATR distance from entry that triggers the break-even move.")

g_filters     = "Filters"
useSession    = input.bool(false, "Session Filter", group=g_filters)
sessionInput  = input.session("0000-2345", "Session", group=g_filters)
useDateRange  = input.bool(false, "Date Range", group=g_filters)
dateFrom      = input.time(timestamp("01 Jan 2020 00:00 +0000"), "From", group=g_filters)
dateTo        = input.time(timestamp("31 Dec 2030 23:59 +0000"), "To",   group=g_filters)
useMaxDD      = input.bool(false, "Max Drawdown", group=g_filters)
maxDD         = input.float(20.0, "Max Drawdown (%)", minval=1, step=1, group=g_filters)
useMaxIntraDD = input.bool(false, "Max Intraday Loss", group=g_filters)
maxIntraDD    = input.float(5.0,  "Max Intraday Loss (%)", minval=1, step=1, group=g_filters)
useLossStreak = input.bool(false, "Losing Streak Limit", group=g_filters)
maxLossStreak = input.int(5,      "Max Consecutive Losses", minval=1, group=g_filters)

strategy.risk.max_drawdown(useMaxDD ? maxDD : 100, strategy.percent_of_equity)
strategy.risk.max_intraday_loss(useMaxIntraDD ? maxIntraDD : 100, strategy.percent_of_equity)

var int _genLossStreak = 0
if strategy.losstrades > strategy.losstrades[1]
    _genLossStreak := _genLossStreak + 1
if strategy.wintrades > strategy.wintrades[1]
    _genLossStreak := 0
_genFilterOk = (not useSession or not na(time(timeframe.period, sessionInput))) and
     (not useDateRange or (time >= dateFrom and time <= dateTo)) and
     (not useLossStreak or _genLossStreak < maxLossStreak)

var int _lastExitBar = na
_justClosed = strategy.closedtrades > 0 and strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1) == bar_index
if _justClosed
    _lastExitBar := bar_index
_inCooldown = cooldownBars > 0 and not na(_lastExitBar) and (bar_index - _lastExitBar) < cooldownBars
_canEntry   = not _inCooldown and (not confirmClose or barstate.isconfirmed) and _genFilterOk
slBufInput = input.float(0.5, "SL ATR Buffer (×)", minval=0.1, step=0.1, group=g_strat,
     tooltip="ATR distance beyond the divergence pivot for the stop loss.")
tpRRInput  = input.float(2.0, "TP R:R Ratio",      minval=0.5, step=0.5, group=g_strat,
     tooltip="Take profit at this R:R multiple of the SL distance from entry.")

// ─── Strategy execution ──────────────────────────────────────────
_canLong  = tradeDirInput != "Short Only"
_canShort = tradeDirInput != "Long Only"

var float _sl = na
var float _tp = na

// Computed every bar — inside the entry `if` it would only average signal bars
_atr = ta.atr(14)

if (bullDiv) and _canLong and _canEntry
    _sl    := low[pivRight] - _atr * slBufInput
    _slDist = math.max(close - _sl, syminfo.mintick)
    _tp    := close + _slDist * tpRRInput
    strategy.entry("Long", strategy.long)

if (bearHidDiv) and _canShort and _canEntry
    _sl    := high[pivRight] + _atr * slBufInput
    _slDist = math.max(_sl - close, syminfo.mintick)
    _tp    := close - _slDist * tpRRInput
    strategy.entry("Short", strategy.short)

// Break-even integrated into the same exit (single stop level — no exit races)
_beAtr   = _atr * beATRInput
_beLong  = enableBE and strategy.position_size > 0 and close >= strategy.position_avg_price + _beAtr
_beShort = enableBE and strategy.position_size < 0 and close <= strategy.position_avg_price - _beAtr

_finalLongStop  = _beLong  ? math.max(_sl, strategy.position_avg_price) : _sl
_finalShortStop = _beShort ? math.min(_sl, strategy.position_avg_price) : _sl

if strategy.position_size > 0
    strategy.exit("Long Exit", "Long", stop=_finalLongStop, limit=_tp)
if strategy.position_size < 0
    strategy.exit("Short Exit", "Short", stop=_finalShortStop, limit=_tp)

Overview

Oscillator Divergence Zones [WavesUnchained] — Strategy displays oscillator dynamics in a separate pane while projecting support and resistance zones onto the main chart. Trades trigger on confirmed oscillator divergences filtered by overbought, oversold, or percentile-based dynamic levels, using a default position size of ten percent of equity. Key user settings configure oscillator parameters, pivot detection lengths, statistical deviation filters, and ATR-derived zone widths. Because pivot detection requires right-side confirmation bars, entry signals lag market turning points.

Details

Published by

casoon on GitHub

Type

Strategy, Pine Script v6

Draws

Separate pane below the chart

Category

Divergence, Oscillators & momentum, Support & resistance

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
TypestringRSIoscType
SourcesourcecloseoscSrc
Lengthint14oscLen
Auto OB/OS per OscillatorbooltrueautoLevels
Overboughtint70obLevel
Oversoldint30osLevel
Pivot Left Barsint7pivLeft
Pivot Right Barsint7pivRight
Min Bars Between Pivotsint10minRange
Max Bars Between Pivotsint100maxRange
Show Hidden DivergenceboolfalseshowHidden
Level FilterstringFixedfilterMode
Show all 40 settings
SettingTypeDefaultVariable
Max for Bull (%)int75bullFilterPct
Min for Bear (%)int25bearFilterPct
DZ Sample Lengthint100dynSampleLen
DZ Threshold (%)float90.0dynPcntile
Statistical Overextension FilterboolfalseuseStdFilter
Lookback Periodint100stdPeriod
StdDev Multiplierfloat1.5stdMult
Show Zones on ChartbooltrueshowZones
Max Active Zonesint10maxZones
Zone Lookahead (bars)int15zoneExtend
ATR Length (zone width)int14atrLen
Zone Width (ATR×)float0.25zoneFactor
Break on CloseboolfalsebreakOnClose
Show Retest CountbooltrueshowTouches
Bullishcolor#26a69abullCol
Bearishcolor#ef5350bearCol
Show All PivotsboolfalseshowPivots
Trade DirectionstringBothtradeDirInput
Entries on Confirmed Bar OnlybooltrueconfirmClose
Cooldown Bars After Exitint0cooldownBars
Break-Even StopboolfalseenableBE
Break-Even Trigger (ATR×)float1.0beATRInput
Session FilterboolfalseuseSession
Sessionsession0000-2345sessionInput
Date RangeboolfalseuseDateRange
Fromtimetimestamp("01 Jan 2020 00:00 +0000")dateFrom
Totimetimestamp("31 Dec 2030 23:59 +0000")dateTo
Max DrawdownboolfalseuseMaxDD
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

330 stars

MACD + Divergences [Alorse] is a momentum strategy that calculates the MACD oscillator and identifies regular and hidden divergences using pivot high and low points. It filters trade signals with dual Exponential Moving Averages (EMA 150 and 600) to ensure tra

Strategy v4 Oscillators & momentumDivergence Repaints
Updated 2 months ago · None stated GitHub repository of MACD + Divergences [Alorse]
106 stars

This Pine Script strategy uses the Commodity Channel Index (CCI) with a 20-period SMA to generate long-only signals. It plots the CCI line and dashed bands at 150 and -150 on the chart, filling the area between bands. Entries occur when CCI crosses above the l

Strategy v5 Oscillators & momentumSupport & resistance
Updated 2 years ago · GPL-3.0 GitHub repository of CCI High Performance Long Only
11 stars

The Stochastic Divergence Strategy calculates the Stochastic oscillator (%K and %D) to identify regular bullish and bearish divergence patterns against price pivots. It plots the oscillator and divergence lines in a separate indicator pane. Entries trigger on

Strategy v6 Oscillators & momentumDivergence
Updated 5 months ago · Apache-2.0 GitHub repository of Stochastic Divergence Strategy

Awesome Oscillator - Eterna MCP

by EternaHybridExchange
9 stars

This strategy implements Bill Williams' Awesome Oscillator (AO) calculated as the difference between two simple moving averages of the median price. It generates long and short entries using three classic signal triggers: zero-line crossovers, twin peaks diver

Strategy v5 Oscillators & momentumMoving averages
Updated 7 months ago · MIT GitHub repository of Awesome Oscillator - Eterna MCP

This strategy plots the previous daily high and low on the price chart and calculates the Andean oscillator bull and bear components with an exponential moving average signal. It looks for a bull or bear cross of that signal, then confirms the setup against re

Strategy v5 Oscillators & momentumSupport & resistance Repaints
Updated 3 years ago · MIT GitHub repository of Daily High/Low and Andean Oscillator Strategy

This strategy trades 1-minute futures contracts on Binance. It plots pivot high and low resistance and support levels on the price chart, along with an EMA and a custom Cto indicator. Long and short entries trigger on EMA and Cto crossovers combined with price

Strategy v5 TrendMoving averages Repaints
Updated 2 years ago · None stated GitHub repository of Binancash Futures Strategy 1M Candles

More strategys

Trade Your Charts With a Regulated Broker

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

Compare brokers