//@version=6
// ============================================================================
// Broadening Wedge Scanner Pro [WavesUnchained]
// Version: 1.15
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Slides a fixed-size window of confirmed swings across the search depth and
// fits two boundary lines (upper from the window's outer highs, lower from
// its outer lows) to find EXPANDING wedges — descending broadening (both
// lines fall, lower faster — bullish bias), ascending broadening (both lines
// rise, upper faster — bearish bias), or symmetric broadening (upper rising,
// lower falling — direction-neutral "megaphone"). Every distinct,
// non-overlapping valid wedge is tracked independently until a confirmed
// breakout resolves it.
//
// Features:
// ✓ Sliding-window search across consecutive, alternating swing windows
// ✓ Tracks multiple simultaneously valid, non-overlapping wedges at once,
// each with its own drawings and breakout state
// ✓ Weighted 0-100 score: traversal, realized expansion, boundary
// reliability, line quality, duration, and current relevance
// ✓ Live breakout tracking (either direction resolves the pattern) + alert
// ✓ Near-miss candidate marker and score-breakdown debug table
//
// ============================================================================
indicator("Broadening Wedge Scanner Pro [WavesUnchained]", overlay=true, max_lines_count=500, max_labels_count=500)
// ============================================================================
// Inputs
// ============================================================================
pivotLen = input.int(6, "Swing-Stärke", minval=2, maxval=30)
maxSwings = input.int(80, "Max. gespeicherte Swings", minval=20, maxval=200)
searchDepth = input.int(24, "Suchbereich letzte Swings", minval=8, maxval=60)
wedgeWindow = input.int(8, "Swings pro Keil-Fenster", minval=6, maxval=20)
maxConcurrentPatterns = input.int(1, "Max. gleichzeitige Patterns", minval=1, maxval=15)
maxPatternAgeMult = input.float(1.5, "Max. Pattern-Alter (× Fensterbreite)", minval=1.0, step=0.5)
minScore = input.int(60, "Mindestqualität", minval=0, maxval=100)
atrLen = input.int(14, "ATR-Länge")
touchTolAtr = input.float(0.5, "Touch-Toleranz ATR", minval=0.05, step=0.05)
projectedExpansionTarget = input.float(1.3, "Ziel proj. Endbreite / Startbreite", minval=1.05, maxval=3.0, step=0.05)
minWidthAtr = input.float(1.0, "Mindest-Endbreite ATR", minval=0.2, step=0.1)
fasterRatioMin = input.float(1.2, "Min. Geschwindigkeits-Verhältnis (DBW/ABW)", minval=1.0, maxval=3.0, step=0.05)
minTraversalRatio = input.float(0.60, "Min. Traversal pro Leg", minval=0.40, maxval=0.90, step=0.05)
minRealizedExpansionRatio = input.float(1.15, "Min. realisierte Leg-Expansion", minval=1.0, maxval=3.0, step=0.05)
minAnchorSpanRatio = input.float(0.55, "Min. Anker-Span / Pattern-Dauer", minval=0.30, maxval=0.90, step=0.05)
maxBoundaryDriftAtr = input.float(8.0, "Max. Boundary-Drift ATR", minval=1.0, maxval=20.0, step=0.5)
showOldPatterns = input.bool(false, "Aufgelöste Patterns behalten")
showCandidate = input.bool(true, "Kandidaten anzeigen")
lineAheadBars = input.int(10, "Linien-Überstand (Bars)", minval=1, maxval=50)
showLabels = input.bool(true, "Score-Label anzeigen")
wedgeCol = input.color(color.purple, "Keil (offen)")
bullCol = input.color(color.lime, "Bullischer Breakout")
bearCol = input.color(color.red, "Bärischer Breakout")
candidateCol = input.color(color.new(color.orange, 0), "Kandidat")
groupDebug = "Debug"
showDebug = input.bool(false, "Score-Breakdown Debug-Tabelle", group=groupDebug)
candidateThreshold = input.int(25, "Kandidat-Mindestscore (Watch)", minval=0, maxval=100, group=groupDebug)
enableLogging = input.bool(false, "Pine-Logs aktivieren (Fehlersuche)", group=groupDebug)
groupDash = "Dashboard"
showDashboard = input.bool(true, "Dashboard anzeigen", group=groupDash)
groupAlert = "Alerts"
alertsOnBarClose = input.bool(true, "Alerts nur bei bestätigter Kerze (Bar Close)", group=groupAlert)
atr = ta.atr(atrLen)
// The bar-by-bar containment check below indexes high/low up to
// searchDepth × pivotLen bars back, which can exceed Pine's default
// auto-detected historical buffer for a variable (non-constant) offset.
max_bars_back(high, 2000)
max_bars_back(low, 2000)
max_bars_back(close, 2000)
// ============================================================================
// Types
// ============================================================================
type Swing
int x
float y
int dir // +1 = High, -1 = Low
type Pattern
bool valid
string ttype // "Descending Broadening" | "Ascending Broadening" | "Symmetric Broadening" | "None"
int score
int xU1
int xU2
float yU1
float yU2
int xL1
int xL2
float yL1
float yL2
int windowStartX
int windowEndX
bool structureOk
bool pathOk
bool traversalOk
bool realizedExpansionOk
bool boundaryOk
bool lineQualityOk
bool durationOk
bool barsInsideOk
bool boundariesOrderedOk
bool widthOk
bool anchorSpanOk
bool driftOk
float upperAnchorSpanRatio
float lowerAnchorSpanRatio
float upperDriftAtr
float lowerDriftAtr
float containmentRatio
int progressionMatches
int progressionTests
float realizedExpansionRatio
int legGrowthMatches
int legGrowthTests
float legGrowthRatio
float projectedExpansionRatio
int pathScore
int traversalScore
int rangeScore
int touchScore
int lineScore
int anchorScore
int durationScore
int relevanceScore
// A tracked, currently-live pattern owns its own drawing objects so it can
// be cleaned up independently of every other tracked pattern.
type TrackedPattern
Pattern p
line[] objLines
label[] objLabels
int createdBar
// ============================================================================
// Storage
// ============================================================================
var swings = array.new<Swing>()
var candidateLines = array.new_line()
var candidateLabels = array.new_label()
// Tracks the newest swing's bar index, not the array LENGTH — once `swings`
// hits its `maxSwings` cap, every push is paired with a shift, so the length
// never changes again and a length-based "did a new swing form" check would
// freeze forever right when the cap is first reached.
var int lastSwingX = -1
emptyPattern() =>
Pattern.new(false, "None", 0, na, na, na, na, na, na, na, na, na, na,
false, false, false, false, false, false, false, false,
false, false, false, false, na, na, na, na, na,
0, 0, na, 0, 0, na, na,
0, 0, 0, 0, 0, 0, 0, 0)
// Single best-in-window candidate, regardless of whether it could ever
// actually be tracked — kept only for the debug table (explains why the
// overall best-scoring shape still isn't firing) and the near-miss
// candidate marker. Independent of `trackedPatterns` below, which holds
// every distinct, non-overlapping VALID wedge found at once.
var Pattern best = emptyPattern()
var trackedPatterns = array.new<TrackedPattern>()
var table dash = table.new(position.top_right, 2, 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)
var table dbgTable = table.new(position.bottom_right, 2, 15,
bgcolor=color.new(color.white, 5),
border_color=color.new(color.gray, 60),
border_width=1,
frame_color=color.new(color.gray, 40),
frame_width=1)
color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
// ============================================================================
// Helpers
// ============================================================================
f_clear_candidate() =>
while array.size(candidateLines) > 0
line.delete(array.pop(candidateLines))
while array.size(candidateLabels) > 0
label.delete(array.pop(candidateLabels))
f_log(string msg) =>
if enableLogging
log.info("BWS " + msg)
f_line(line[] arr, int x1, float y1, int x2, float y2, color col, style, int width, ext = extend.none) =>
ln = line.new(x1, y1, x2, y2, color=col, style=style, width=width, extend=ext)
array.push(arr, ln)
ln
f_price(int x1, float y1, int x2, float y2, int x) =>
x2 == x1 ? y1 : y1 + (y2 - y1) * float(x - x1) / float(x2 - x1)
f_slope(int x1, float y1, int x2, float y2) =>
x2 == x1 ? na : (y2 - y1) / float(x2 - x1)
f_clamp01(float value) =>
math.min(math.max(value, 0.0), 1.0)
// Picks the two REAL touch points (not a computed average) that best act as
// a boundary: the pair with the fewest other points violating it (poking
// through by more than `tolPrice` on the wrong side — above for an upper/
// resistance line, below for a lower/support one), tie-broken by the most
// points touching it within tolerance and then by the widest anchor span.
// This is how a chartist actually draws
// a trendline — through real highs/lows that define the edge — not a
// least-squares fit, which pulls the line through the middle of the point
// cloud and can miss every real touch when a few points cluster together.
// O(n^3) over the window's own point count (n = nH or nL, capped by half of
// `wedgeWindow`, so at most a few dozen), trivial at this scale. Requires at
// least 2 points; callers already guarantee this (`array.size(highX) >= 2
// and array.size(lowX) >= 2`).
f_best_boundary(int[] xs, float[] ys, float tolPrice, bool isUpper) =>
n = array.size(xs)
bestI = 0
bestJ = n - 1
bestViolations = 1000000
bestTouches = -1
bestSpan = -1
for i = 0 to n - 2
xi = array.get(xs, i)
yi = array.get(ys, i)
for j = i + 1 to n - 1
xj = array.get(xs, j)
yj = array.get(ys, j)
touches = 0
violations = 0
for k = 0 to n - 1
xk = array.get(xs, k)
yk = array.get(ys, k)
dist = yk - f_price(xi, yi, xj, yj, xk)
if math.abs(dist) <= tolPrice
touches += 1
else if isUpper and dist > tolPrice
violations += 1
else if not isUpper and dist < -tolPrice
violations += 1
span = xj - xi
better = violations < bestViolations or (violations == bestViolations and (touches > bestTouches or (touches == bestTouches and span > bestSpan)))
if better
bestViolations := violations
bestTouches := touches
bestSpan := span
bestI := i
bestJ := j
[array.get(xs, bestI), array.get(ys, bestI), array.get(xs, bestJ), array.get(ys, bestJ)]
f_add_swing(int x, float y, int dir) =>
sz = array.size(swings)
if sz == 0
array.push(swings, Swing.new(x, y, dir))
else
last = array.get(swings, sz - 1)
if last.dir == dir
stronger = dir == 1 ? y > last.y : y < last.y
if stronger
array.set(swings, sz - 1, Swing.new(x, y, dir))
else
array.push(swings, Swing.new(x, y, dir))
while array.size(swings) > maxSwings
array.shift(swings)
// True interval overlap between two patterns' [windowStartX,windowEndX]
// ranges — keeps at most one tracked wedge per stretch of chart.
f_overlaps(Pattern a, Pattern b) =>
math.max(a.windowStartX, b.windowStartX) <= math.min(a.windowEndX, b.windowEndX)
f_find_overlap_index(Pattern p) =>
n = array.size(trackedPatterns)
idx = -1
if n > 0
for i = 0 to n - 1
tp = array.get(trackedPatterns, i)
if idx < 0 and f_overlaps(p, tp.p)
idx := i
idx
// Index of the lowest-scoring currently tracked pattern — used to make room
// for a new, non-overlapping, higher-scoring candidate once the cap is
// reached. Without this, the first `maxConcurrentPatterns` wedges that
// happen not to break out for a long stretch permanently fill every slot,
// silently discarding every new, better candidate found afterward (the same
// bug fixed in Wolfe Wave Scanner Pro's f_find_weakest_index).
f_find_weakest_index() =>
n = array.size(trackedPatterns)
idx = -1
minScoreSeen = 1000000
if n > 0
for i = 0 to n - 1
tp = array.get(trackedPatterns, i)
if tp.p.score < minScoreSeen
minScoreSeen := tp.p.score
idx := i
idx
// Index of the highest-scoring currently tracked pattern — used so the
// dashboard reflects an actual tracked, live pattern instead of the raw
// search candidate (same fix as Wolfe Wave Scanner Pro's
// f_find_best_active_index).
f_find_best_active_index() =>
n = array.size(trackedPatterns)
idx = -1
maxScoreSeen = -1
if n > 0
for i = 0 to n - 1
tp = array.get(trackedPatterns, i)
if tp.p.score > maxScoreSeen
maxScoreSeen := tp.p.score
idx := i
idx
// Deletes every drawing object owned by a tracked pattern. Arrays are
// reference types in Pine, so popping from tp.objLines/objLabels mutates
// the exact arrays the TrackedPattern struct already holds.
f_delete_tracked(TrackedPattern tp) =>
while array.size(tp.objLines) > 0
line.delete(array.pop(tp.objLines))
while array.size(tp.objLabels) > 0
label.delete(array.pop(tp.objLabels))
// Finalizes a still-open tracked pattern's boundary lines into a bounded
// segment ending at the current bar, instead of leaving them on
// extend.right forever. Needed whenever a pattern is displaced or resolved
// while `showOldPatterns` is on and its lines are kept on the chart — without
// this, every kept wedge boundary drifts as an infinite ray extrapolated off
// a 2-point fit, landing at nonsensical prices the longer the chart runs.
f_finalize_lines(TrackedPattern tp) =>
if array.size(tp.objLines) >= 2
upperLine = array.get(tp.objLines, 0)
lowerLine = array.get(tp.objLines, 1)
upperNow = f_price(tp.p.xU1, tp.p.yU1, tp.p.xU2, tp.p.yU2, bar_index)
lowerNow = f_price(tp.p.xL1, tp.p.yL1, tp.p.xL2, tp.p.yL2, bar_index)
line.set_extend(upperLine, extend.none)
line.set_extend(lowerLine, extend.none)
line.set_xy2(upperLine, bar_index, upperNow)
line.set_xy2(lowerLine, bar_index, lowerNow)
// Cleans up same-direction overlap chains the pairwise insertion check can
// miss: if A overlaps B and B overlaps C, but A and C don't directly overlap
// each other, all three could end up tracked at once even though they
// visually occupy the same stretch (same fix as Wolfe Wave Scanner Pro's
// f_dedupe_overlaps). Checks every pair, keeping only the higher-scoring one.
f_dedupe_overlaps() =>
n = array.size(trackedPatterns)
if n > 1
keep = array.new_bool(n, true)
for i = 0 to n - 2
for j = i + 1 to n - 1
tpI = array.get(trackedPatterns, i)
tpJ = array.get(trackedPatterns, j)
if array.get(keep, i) and array.get(keep, j) and f_overlaps(tpI.p, tpJ.p)
if tpI.p.score >= tpJ.p.score
array.set(keep, j, false)
else
array.set(keep, i, false)
for i = n - 1 to 0
if not array.get(keep, i)
tp = array.get(trackedPatterns, i)
if showOldPatterns
f_finalize_lines(tp)
else
f_delete_tracked(tp)
array.remove(trackedPatterns, i)
// ============================================================================
// Scoring Engine
// ============================================================================
f_eval_wedge(int[] pathX, float[] pathY, int[] pathDir, int[] highX, float[] highY, int[] lowX, float[] lowY, int windowStartX, int windowEndX) =>
nH = array.size(highX)
nL = array.size(lowX)
nPath = array.size(pathX)
touchTolPrice = touchTolAtr * atr
// Boundaries are the two real touch points that best act as a resistance
// (upper) / support (lower) line — fewest other points poking through it,
// most points actually touching it — not a least-squares average through
// the whole point cloud.
[xU1, yU1, xU2, yU2] = f_best_boundary(highX, highY, touchTolPrice, true)
[xL1, yL1, xL2, yL2] = f_best_boundary(lowX, lowY, touchTolPrice, false)
slopeU = f_slope(xU1, yU1, xU2, yU2)
slopeL = f_slope(xL1, yL1, xL2, yL2)
bothFalling = not na(slopeU) and not na(slopeL) and slopeU < 0 and slopeL < 0
bothRising = not na(slopeU) and not na(slopeL) and slopeU > 0 and slopeL > 0
diverging = not na(slopeU) and not na(slopeL) and slopeU > 0 and slopeL < 0
// Geometric family classification remains necessary, but is no longer
// sufficient. The swing path below must independently confirm the same
// higher-high/lower-low progression.
isDescendingBroadening = bothFalling and math.abs(slopeL) > math.abs(slopeU)
isAscendingBroadening = bothRising and math.abs(slopeU) > math.abs(slopeL)
isSymmetricBroadening = diverging
ttype = isDescendingBroadening ? "Descending Broadening" : isAscendingBroadening ? "Ascending Broadening" : isSymmetricBroadening ? "Symmetric Broadening" : "None"
structureOk = isDescendingBroadening or isAscendingBroadening or isSymmetricBroadening
midX = int(math.round((windowStartX + windowEndX) / 2.0))
upperAtStart = f_price(xU1, yU1, xU2, yU2, windowStartX)
lowerAtStart = f_price(xL1, yL1, xL2, yL2, windowStartX)
upperAtMid = f_price(xU1, yU1, xU2, yU2, midX)
lowerAtMid = f_price(xL1, yL1, xL2, yL2, midX)
upperAtEnd = f_price(xU1, yU1, xU2, yU2, windowEndX)
lowerAtEnd = f_price(xL1, yL1, xL2, yL2, windowEndX)
widthStart = math.abs(upperAtStart - lowerAtStart)
widthMid = math.abs(upperAtMid - lowerAtMid)
widthEnd = math.abs(upperAtEnd - lowerAtEnd)
boundariesOrdered = upperAtStart > lowerAtStart and upperAtMid > lowerAtMid and upperAtEnd > lowerAtEnd
projectedExpansionRatio = widthStart > 0 ? widthEnd / widthStart : 0.0
// The type-specific extremum sequence is a hard gate. Because `swings`
// is already de-duplicated to alternating highs/lows, the remaining
// question is whether every same-side step progresses in the direction
// required by the classified broadening family.
progressionTests = 0
progressionMatches = 0
alternatingPath = true
if nPath > 1
for i = 1 to nPath - 1
if array.get(pathDir, i) == array.get(pathDir, i - 1)
alternatingPath := false
if nH > 1
for i = 1 to nH - 1
previous = array.get(highY, i - 1)
current = array.get(highY, i)
expectedUp = isAscendingBroadening or isSymmetricBroadening
progressionTests += 1
if (expectedUp and current > previous) or (not expectedUp and current < previous)
progressionMatches += 1
if nL > 1
for i = 1 to nL - 1
previous = array.get(lowY, i - 1)
current = array.get(lowY, i)
expectedDown = isDescendingBroadening or isSymmetricBroadening
progressionTests += 1
if (expectedDown and current < previous) or (not expectedDown and current > previous)
progressionMatches += 1
progressionRatio = progressionTests > 0 ? float(progressionMatches) / float(progressionTests) : 0.0
pathOk = structureOk and alternatingPath and nPath >= 6 and nH >= 3 and nL >= 3 and progressionMatches == progressionTests
pathScore = structureOk and alternatingPath ? int(math.round(20.0 * progressionRatio)) : 0
// Measure each alternating leg as a fraction of the local channel width.
// A large extrapolated envelope with price trapped around its midpoint
// therefore scores poorly even when every bar technically remains inside.
traversalSum = 0.0
traversalCount = 0
qualifyingTraversals = 0
currentTraversalRun = 0
longestTraversalRun = 0
legAmplitudes = array.new_float()
if nPath > 1
for i = 1 to nPath - 1
x1 = array.get(pathX, i - 1)
y1 = array.get(pathY, i - 1)
x2 = array.get(pathX, i)
y2 = array.get(pathY, i)
upper1 = f_price(xU1, yU1, xU2, yU2, x1)
lower1 = f_price(xL1, yL1, xL2, yL2, x1)
upper2 = f_price(xU1, yU1, xU2, yU2, x2)
lower2 = f_price(xL1, yL1, xL2, yL2, x2)
width1 = upper1 - lower1
width2 = upper2 - lower2
pos1 = width1 > 0 ? (y1 - lower1) / width1 : 0.5
pos2 = width2 > 0 ? (y2 - lower2) / width2 : 0.5
traversal = f_clamp01(math.abs(pos2 - pos1))
traversalSum += traversal
traversalCount += 1
array.push(legAmplitudes, math.abs(y2 - y1))
if traversal >= minTraversalRatio
qualifyingTraversals += 1
currentTraversalRun += 1
longestTraversalRun := math.max(longestTraversalRun, currentTraversalRun)
else
currentTraversalRun := 0
averageTraversal = traversalCount > 0 ? traversalSum / float(traversalCount) : 0.0
qualifyingTraversalRatio = traversalCount > 0 ? float(qualifyingTraversals) / float(traversalCount) : 0.0
traversalOk = longestTraversalRun >= 3
traversalScore = int(math.round(20.0 * (0.65 * f_clamp01(averageTraversal / minTraversalRatio) + 0.35 * qualifyingTraversalRatio)))
// Realized expansion comes from actual alternating swing legs, not from
// the projected distance between two lines. Compare the first and last
// two legs and require most same-direction leg amplitudes to grow. The
// direction-aware comparison avoids penalizing the natural impulse/
// retracement size alternation in ascending and descending patterns.
legCount = array.size(legAmplitudes)
firstLegAverage = legCount >= 2 ? (array.get(legAmplitudes, 0) + array.get(legAmplitudes, 1)) / 2.0 : 0.0
lastLegAverage = legCount >= 2 ? (array.get(legAmplitudes, legCount - 2) + array.get(legAmplitudes, legCount - 1)) / 2.0 : 0.0
realizedExpansionRatio = firstLegAverage > 0 ? lastLegAverage / firstLegAverage : 0.0
legGrowthTests = 0
legGrowthMatches = 0
float previousUpLeg = na
float previousDownLeg = na
if legCount > 0
for i = 0 to legCount - 1
legAmplitude = array.get(legAmplitudes, i)
legEndsAtHigh = array.get(pathDir, i + 1) == 1
previousSameDirection = legEndsAtHigh ? previousUpLeg : previousDownLeg
if not na(previousSameDirection)
legGrowthTests += 1
if legAmplitude >= previousSameDirection * 0.95
legGrowthMatches += 1
if legEndsAtHigh
previousUpLeg := legAmplitude
else
previousDownLeg := legAmplitude
legGrowthRatio = legGrowthTests > 0 ? float(legGrowthMatches) / float(legGrowthTests) : 0.0
realizedExpansionOk = legCount >= 5 and realizedExpansionRatio >= minRealizedExpansionRatio and legGrowthRatio >= 0.60
realizedRatioQuality = minRealizedExpansionRatio > 1.0 ? f_clamp01((realizedExpansionRatio - 1.0) / (minRealizedExpansionRatio - 1.0)) : f_clamp01(realizedExpansionRatio)
projectedRatioQuality = f_clamp01((projectedExpansionRatio - 1.0) / (projectedExpansionTarget - 1.0))
rangeScore = int(math.round(15.0 * (0.70 * realizedRatioQuality + 0.20 * legGrowthRatio + 0.10 * projectedRatioQuality)))
// Both defining pairs must span most of the pattern. A tie in boundary
// selection already prefers the widest-spaced anchor pair, preventing a
// close pair from winning and then being extrapolated across the window.
windowBars = windowEndX - windowStartX
upperAnchorSpanRatio = windowBars > 0 ? float(xU2 - xU1) / float(windowBars) : 0.0
lowerAnchorSpanRatio = windowBars > 0 ? float(xL2 - xL1) / float(windowBars) : 0.0
upperDriftAtr = atr > 0 ? math.abs(slopeU) * float(windowBars) / atr : 1000000.0
lowerDriftAtr = atr > 0 ? math.abs(slopeL) * float(windowBars) / atr : 1000000.0
widthOk = widthEnd > minWidthAtr * atr
anchorSpanOk = upperAnchorSpanRatio >= minAnchorSpanRatio and lowerAnchorSpanRatio >= minAnchorSpanRatio
driftOk = upperDriftAtr <= maxBoundaryDriftAtr and lowerDriftAtr <= maxBoundaryDriftAtr
boundaryOk = boundariesOrdered and widthOk and anchorSpanOk and driftOk
anchorScore = int(math.round(10.0 * (f_clamp01(upperAnchorSpanRatio) + f_clamp01(lowerAnchorSpanRatio)) / 2.0))
// The two defining anchors are necessary, but do not earn touch-quality
// points. Only independent confirmations beyond those anchors contribute.
extraHighTouches = 0
highResidualSum = 0.0
highViolations = 0
if nH > 0
for i = 0 to nH - 1
hx = array.get(highX, i)
hy = array.get(highY, i)
lineY = f_price(xU1, yU1, xU2, yU2, hx)
dist = hy - lineY
highResidualSum += math.abs(dist)
if math.abs(hy - lineY) <= touchTolPrice
if hx != xU1 and hx != xU2
extraHighTouches += 1
else if dist > touchTolPrice
highViolations += 1
extraLowTouches = 0
lowResidualSum = 0.0
lowViolations = 0
if nL > 0
for i = 0 to nL - 1
lx = array.get(lowX, i)
ly = array.get(lowY, i)
lineY = f_price(xL1, yL1, xL2, yL2, lx)
dist = ly - lineY
lowResidualSum += math.abs(dist)
if math.abs(ly - lineY) <= touchTolPrice
if lx != xL1 and lx != xL2
extraLowTouches += 1
else if dist < -touchTolPrice
lowViolations += 1
extraHighQuality = f_clamp01(float(extraHighTouches) / 2.0)
extraLowQuality = f_clamp01(float(extraLowTouches) / 2.0)
touchScore = int(math.round(15.0 * (extraHighQuality + extraLowQuality) / 2.0))
// DBW/ABW: the faster side must be meaningfully faster (not just
// technically faster by a hair) to look like a real diverging wedge.
// Symmetric broadening: neither side should be wildly steeper than the
// other, same 3x-ratio reasoning as Triangle/Wolfe's symmetric case.
fasterRatio = isDescendingBroadening and math.abs(slopeU) > 0.0000001 ? math.abs(slopeL) / math.abs(slopeU) : isAscendingBroadening and math.abs(slopeL) > 0.0000001 ? math.abs(slopeU) / math.abs(slopeL) : na
symRatio = isSymmetricBroadening and math.abs(slopeL) > 0.0000001 ? math.abs(slopeU) / math.abs(slopeL) : na
slopeQualityOk = (isDescendingBroadening or isAscendingBroadening) ? (not na(fasterRatio) and fasterRatio >= fasterRatioMin) : isSymmetricBroadening ? (not na(symRatio) and symRatio >= 0.3 and symRatio <= 3.0) : false
durationOk = windowBars >= pivotLen * 3
durationScore = int(math.round(5.0 * f_clamp01(float(windowBars) / float(pivotLen * 6))))
// Containment is intentionally soft: count inside bars and middle-zone
// occupancy instead of allowing an oversized envelope to pass a binary
// veto. Traversal supplies the complementary boundary-utilization test.
barsInsideCount = 0
midZoneCount = 0
evaluatedBars = 0
for i = windowStartX to windowEndX
upperAt = f_price(xU1, yU1, xU2, yU2, i)
lowerAt = f_price(xL1, yL1, xL2, yL2, i)
barHigh = high[bar_index - i]
barLow = low[bar_index - i]
barClose = close[bar_index - i]
evaluatedBars += 1
if barHigh <= upperAt + touchTolPrice and barLow >= lowerAt - touchTolPrice
barsInsideCount += 1
localWidth = upperAt - lowerAt
if localWidth > 0
closePosition = (barClose - lowerAt) / localWidth
if closePosition >= 0.30 and closePosition <= 0.70
midZoneCount += 1
containmentRatio = evaluatedBars > 0 ? float(barsInsideCount) / float(evaluatedBars) : 0.0
midZoneRatio = evaluatedBars > 0 ? float(midZoneCount) / float(evaluatedBars) : 1.0
barsInsideOk = containmentRatio >= 0.90
pointCount = nH + nL
meanResidual = pointCount > 0 ? (highResidualSum + lowResidualSum) / float(pointCount) : touchTolPrice * 2.0
residualQuality = touchTolPrice > 0 ? 1.0 - f_clamp01(meanResidual / (touchTolPrice * 2.0)) : 0.0
violationRatio = pointCount > 0 ? float(highViolations + lowViolations) / float(pointCount) : 1.0
lineScore = int(math.round(10.0 * (0.40 * residualQuality + 0.35 * containmentRatio + 0.15 * (1.0 - violationRatio) + 0.10 * (1.0 - midZoneRatio))))
lineQualityOk = slopeQualityOk
maxRelevantAge = maxPatternAgeMult * float(math.max(windowBars, 1))
candidateAge = float(math.max(bar_index - windowEndX, 0))
relevanceScore = int(math.round(5.0 * (maxRelevantAge > 0 ? 1.0 - f_clamp01(candidateAge / maxRelevantAge) : 0.0)))
// The swing-path component is constant (20) for every pattern that passes
// the pathOk gate, so it is excluded from the score: it stays a gate and a
// debug readout only. The remaining components (80 points) are scaled to
// 0-100 so the score range and Mindestqualität keep their meaning.
rawScore = traversalScore + rangeScore + touchScore + lineScore + anchorScore + durationScore + relevanceScore
score = int(math.round(float(rawScore) * 100.0 / 80.0))
hardGatesOk = structureOk and pathOk and traversalOk and realizedExpansionOk and boundaryOk and lineQualityOk and durationOk
Pattern.new(
score >= minScore and hardGatesOk,
ttype,
score,
xU1, xU2, yU1, yU2,
xL1, xL2, yL1, yL2,
windowStartX, windowEndX,
structureOk, pathOk, traversalOk, realizedExpansionOk, boundaryOk, lineQualityOk, durationOk, barsInsideOk,
boundariesOrdered, widthOk, anchorSpanOk, driftOk,
upperAnchorSpanRatio, lowerAnchorSpanRatio, upperDriftAtr, lowerDriftAtr, containmentRatio,
progressionMatches, progressionTests, realizedExpansionRatio,
legGrowthMatches, legGrowthTests, legGrowthRatio, projectedExpansionRatio,
pathScore, traversalScore, rangeScore, touchScore, lineScore, anchorScore, durationScore, relevanceScore)
// ============================================================================
// Pattern drawing — builds one self-contained TrackedPattern
// ============================================================================
// A pattern's boundary is a straight line fitted on its own window; extending
// it forever as the chart advances means extrapolating that fixed slope over
// an ever-growing distance, which inevitably drifts into nonsensical prices
// the longer a pattern stays untouched (chart- and log-confirmed: with no
// cap, ~100% of tracked wedges ended via ATR-drift, essentially never via a
// real breakout). Capping the extension to one more window-width past the
// window's own end bounds how far the line can ever be extrapolated, by
// construction — instead of drawing it forever and then reactively expiring
// it once the damage is already visible.
f_line_cap_x(Pattern p) =>
p.windowEndX + (p.windowEndX - p.windowStartX)
f_draw_pattern(Pattern p) =>
objLines = array.new_line()
objLabels = array.new_label()
// Ends a little past the current bar instead of extend.right, which would
// draw the boundary all the way to the right edge of the chart. Advanced
// bar by bar in the breakout-tracking loop below while the pattern stays
// open (and short of the fixed cap), so it keeps pace with the chart
// without ever spanning it or extrapolating indefinitely.
aheadX = math.min(bar_index + lineAheadBars, f_line_cap_x(p))
f_line(objLines, p.xU1, p.yU1, aheadX, f_price(p.xU1, p.yU1, p.xU2, p.yU2, aheadX), wedgeCol, line.style_solid, 2)
f_line(objLines, p.xL1, p.yL1, aheadX, f_price(p.xL1, p.yL1, p.xL2, p.yL2, aheadX), wedgeCol, line.style_solid, 2)
if showLabels
txt = p.ttype + "\nScore " + str.tostring(p.score)
lb = label.new(p.windowEndX, p.yU2, txt, style=label.style_label_down, color=wedgeCol, textcolor=color.white, size=size.small)
array.push(objLabels, lb)
TrackedPattern.new(p, objLines, objLabels, bar_index)
// ============================================================================
// Swing Detection
// ============================================================================
ph = ta.pivothigh(high, pivotLen, pivotLen)
pl = ta.pivotlow(low, pivotLen, pivotLen)
if not na(ph)
f_add_swing(bar_index - pivotLen, ph, 1)
f_log("SWING dir=H x=" + str.tostring(bar_index - pivotLen) + " y=" + str.tostring(ph, format.mintick) + " count=" + str.tostring(array.size(swings)))
if not na(pl)
f_add_swing(bar_index - pivotLen, pl, -1)
f_log("SWING dir=L x=" + str.tostring(bar_index - pivotLen) + " y=" + str.tostring(pl, format.mintick) + " count=" + str.tostring(array.size(swings)))
// ============================================================================
// Search (only re-run when a new swing has actually formed)
// ============================================================================
// Slides a fixed-size window across the search depth; for each window end,
// the outer highs/lows within it define the two boundary lines. Every
// window end contributes one fixed-size candidate, so two genuinely separate
// wedges in the search depth can both surface.
sz = array.size(swings)
currentSwingX = sz > 0 ? array.get(swings, sz - 1).x : -1
newSwing = currentSwingX != lastSwingX
lastSwingX := currentSwingX
newWedgeFound = false
if newSwing and sz < wedgeWindow
f_log("SEARCH SKIPPED sz=" + str.tostring(sz) + " (<" + str.tostring(wedgeWindow) + ")")
if newSwing and sz >= wedgeWindow
start = math.max(0, sz - searchDepth)
candidatesPerWindowEnd = array.new<Pattern>()
localBest = emptyPattern()
// Guarded explicitly: Pine's `for i = A to B` silently reverses (and can
// step on an out-of-bounds index) whenever A > B — which happens here if
// `searchDepth` is set smaller than `wedgeWindow`, leaving no room for a
// single full window inside the search range.
firstEnd = math.max(start + wedgeWindow - 1, wedgeWindow - 1)
if firstEnd <= sz - 1
for end = firstEnd to sz - 1
winStart = end - wedgeWindow + 1
highX = array.new_int()
highY = array.new_float()
lowX = array.new_int()
lowY = array.new_float()
pathX = array.new_int()
pathY = array.new_float()
pathDir = array.new_int()
for k = winStart to end
s = array.get(swings, k)
array.push(pathX, s.x)
array.push(pathY, s.y)
array.push(pathDir, s.dir)
if s.dir == 1
array.push(highX, s.x)
array.push(highY, s.y)
else
array.push(lowX, s.x)
array.push(lowY, s.y)
candidate = emptyPattern()
if array.size(highX) >= 2 and array.size(lowX) >= 2
windowStartX = array.get(swings, winStart).x
windowEndX = array.get(swings, end).x
candidate := f_eval_wedge(pathX, pathY, pathDir, highX, highY, lowX, lowY, windowStartX, windowEndX)
if candidate.score > localBest.score
localBest := candidate
array.push(candidatesPerWindowEnd, candidate)
best := localBest
f_log("SEARCH sz=" + str.tostring(sz) + " best.score=" + str.tostring(best.score) + " type=" + best.ttype + " valid=" + str.tostring(best.valid)
+ " structure=" + str.tostring(best.structureOk) + " path=" + str.tostring(best.pathOk)
+ " progression=" + str.tostring(best.progressionMatches) + "/" + str.tostring(best.progressionTests)
+ " traversal=" + str.tostring(best.traversalOk) + " realizedExpansion=" + str.tostring(best.realizedExpansionOk)
+ " realizedRatio=" + str.tostring(best.realizedExpansionRatio, "#.###")
+ " legGrowth=" + str.tostring(best.legGrowthMatches) + "/" + str.tostring(best.legGrowthTests)
+ " projectedRatio=" + str.tostring(best.projectedExpansionRatio, "#.###")
+ " boundary=" + str.tostring(best.boundaryOk) + " ordered=" + str.tostring(best.boundariesOrderedOk)
+ " width=" + str.tostring(best.widthOk) + " anchorSpan=" + str.tostring(best.anchorSpanOk)
+ " anchorU=" + str.tostring(best.upperAnchorSpanRatio, "#.###") + " anchorL=" + str.tostring(best.lowerAnchorSpanRatio, "#.###")
+ " drift=" + str.tostring(best.driftOk) + " driftU=" + str.tostring(best.upperDriftAtr, "#.##")
+ " driftL=" + str.tostring(best.lowerDriftAtr, "#.##")
+ " lineQuality=" + str.tostring(best.lineQualityOk) + " duration=" + str.tostring(best.durationOk))
// Lightweight candidate wedge for the current best-in-window, redrawn on
// every search regardless of whether it clears minScore.
f_clear_candidate()
if showCandidate and best.structureOk and best.score >= candidateThreshold and not best.valid
candidateEndX = math.min(bar_index, f_line_cap_x(best))
candidateUpperStart = f_price(best.xU1, best.yU1, best.xU2, best.yU2, best.windowStartX)
candidateUpperEnd = f_price(best.xU1, best.yU1, best.xU2, best.yU2, candidateEndX)
candidateLowerStart = f_price(best.xL1, best.yL1, best.xL2, best.yL2, best.windowStartX)
candidateLowerEnd = f_price(best.xL1, best.yL1, best.xL2, best.yL2, candidateEndX)
candidateReason = not best.pathOk ? "Swing-Pfad" : not best.traversalOk ? "Traversal" : not best.realizedExpansionOk ? "Realisierte Expansion" : not best.anchorSpanOk ? "Anker-Span" : not best.driftOk ? "Boundary-Drift" : not best.boundariesOrderedOk ? "Linienordnung" : not best.widthOk ? "Mindestbreite" : not best.lineQualityOk ? "Linienqualität" : not best.durationOk ? "Dauer" : "Hard Gate"
candLn1 = line.new(best.windowStartX, candidateUpperStart, candidateEndX, candidateUpperEnd, color=candidateCol, style=line.style_dashed, width=2)
candLn2 = line.new(best.windowStartX, candidateLowerStart, candidateEndX, candidateLowerEnd, color=candidateCol, style=line.style_dashed, width=2)
array.push(candidateLines, candLn1)
array.push(candidateLines, candLn2)
candidateLabelY = math.min(math.max(close, math.min(candidateLowerEnd, candidateUpperEnd)), math.max(candidateLowerEnd, candidateUpperEnd))
candLabel = label.new(candidateEndX, candidateLabelY, "WATCH · " + best.ttype + " · " + str.tostring(best.score) + "\nBlocker: " + candidateReason, style=label.style_label_down, color=candidateCol, textcolor=color.white, size=size.small)
array.push(candidateLabels, candLabel)
// Every distinct, non-overlapping valid wedge gets its own tracked
// pattern. An overlapping candidate only replaces the existing one at
// that spot if it scores higher and isn't literally the same window.
if array.size(candidatesPerWindowEnd) > 0
for i = 0 to array.size(candidatesPerWindowEnd) - 1
c = array.get(candidatesPerWindowEnd, i)
if c.valid
// The newest swing needs pivotLen bars of opposite-direction
// movement to even confirm — and that's often the same move
// that already carries price through a boundary that, by
// the candidate's own end width, was already tight at
// windowEndX. Log-confirmed: 84% of all FIRED/REPLACED/EVICTED
// events had a BREAKOUT logged on the exact same bar — without
// this check, most detected wedges are stillborn: already
// broken out the instant they're found, never visibly "live".
// Same frozen-at-cap evaluation as the tracking loop: a
// window-end deep in `Suchbereich letzte Swings` can be old
// enough that extrapolating all the way to today's bar_index
// already overshoots its own capped extension — evaluate at
// the same bounded x so a merely-old-but-still-plausible
// candidate isn't rejected as "stillborn" for a reason that
// has nothing to do with whether it actually broke out.
stillOpenEvalX = math.min(bar_index, f_line_cap_x(c))
upperAtNow = f_price(c.xU1, c.yU1, c.xU2, c.yU2, stillOpenEvalX)
lowerAtNow = f_price(c.xL1, c.yL1, c.xL2, c.yL2, stillOpenEvalX)
stillOpen = close < upperAtNow and close > lowerAtNow
// `Suchbereich letzte Swings` can resurface a window-end that's
// already past its own age-expiry threshold by the time it's
// found (log-confirmed: fired, then expired on the very same
// bar with age=220 against an 81-bar window, 1.5×=121.5) —
// gate it here instead of firing it just to immediately expire
// it, which wastes a cycle and leaves the one tracking slot
// empty for a whole search event over a candidate that was
// never going to live anyway.
alreadyAged = bar_index - c.windowEndX > maxPatternAgeMult * math.max(c.windowEndX - c.windowStartX, 1)
if alreadyAged
f_log("PATTERN SKIPPED (already aged) type=" + c.ttype + " score=" + str.tostring(c.score) + " age=" + str.tostring(bar_index - c.windowEndX))
else if not stillOpen
f_log("PATTERN SKIPPED (stillborn) type=" + c.ttype + " score=" + str.tostring(c.score))
if stillOpen and not alreadyAged
overlapIdx = f_find_overlap_index(c)
if overlapIdx < 0
if array.size(trackedPatterns) < maxConcurrentPatterns
newTp = f_draw_pattern(c)
array.push(trackedPatterns, newTp)
newWedgeFound := true
f_log("PATTERN FIRED type=" + c.ttype + " score=" + str.tostring(c.score)
+ " start=" + str.tostring(c.windowStartX) + " end=" + str.tostring(c.windowEndX))
else
// Once every slot is full, a genuinely new, non-
// overlapping candidate must be able to evict the
// weakest tracked wedge if it outscores it — otherwise
// the first `maxConcurrentPatterns` wedges that happen
// not to break out for a while permanently occupy every
// slot and silently block every better candidate found
// afterward.
weakestIdx = f_find_weakest_index()
weakest = array.get(trackedPatterns, weakestIdx)
if c.score > weakest.p.score
if showOldPatterns
f_finalize_lines(weakest)
else
f_delete_tracked(weakest)
newTp = f_draw_pattern(c)
array.set(trackedPatterns, weakestIdx, newTp)
newWedgeFound := true
f_log("PATTERN EVICTED old_score=" + str.tostring(weakest.p.score) + " type=" + c.ttype + " score=" + str.tostring(c.score))
else
existing = array.get(trackedPatterns, overlapIdx)
if c.windowEndX != existing.p.windowEndX and c.score > existing.p.score
if showOldPatterns
f_finalize_lines(existing)
else
f_delete_tracked(existing)
newTp = f_draw_pattern(c)
array.set(trackedPatterns, overlapIdx, newTp)
newWedgeFound := true
f_log("PATTERN REPLACED type=" + c.ttype + " score=" + str.tostring(c.score))
// Cleans up any same-direction overlap chain the pairwise checks above
// can miss (A overlaps B, B overlaps C, but A and C don't directly
// overlap each other).
f_dedupe_overlaps()
// ============================================================================
// Breakout tracking (every bar — price can break either boundary any time)
// ============================================================================
breakoutBull = false
breakoutBear = false
if array.size(trackedPatterns) > 0
for i = array.size(trackedPatterns) - 1 to 0
tp = array.get(trackedPatterns, i)
// Evaluated at min(bar_index, capX) instead of bar_index directly: once
// a pattern has run past its fixed extension budget (one more
// window-width beyond its own formation, see f_line_cap_x), its
// boundary level FREEZES at whatever it was at the cap instead of
// continuing to extrapolate forever. A pattern that hasn't broken
// that frozen level within a reasonable time is stale (handled by
// `aged` below), not still meaningfully "live".
capX = f_line_cap_x(tp.p)
evalX = math.min(bar_index, capX)
upperNow = f_price(tp.p.xU1, tp.p.yU1, tp.p.xU2, tp.p.yU2, evalX)
lowerNow = f_price(tp.p.xL1, tp.p.yL1, tp.p.xL2, tp.p.yL2, evalX)
// One-bar grace period only, not a "fresh cross" requirement — same
// reasoning as Triangle Compression Scanner Pro: skips the check on
// the pattern's own creation bar (avoids an instant breakout label
// the same bar the wedge is drawn) and falls back to a plain
// containment check every bar after.
justCreated = bar_index <= tp.createdBar
bullHit = not justCreated and close > upperNow
bearHit = not justCreated and close < lowerNow
// Classic TA treats a pattern that hasn't resolved within a
// reasonable multiple of its own formation length as stale, not
// live — same principle applied here. This is the sole cleanup
// mechanism now (an ATR-based drift check was tried and removed:
// log-confirmed it was killing legitimate, already-capped patterns
// on arrival — ATR measures bar-to-bar noise, not the honest
// cumulative price range a wedge's own real slope implies over
// dozens-to-hundreds of bars, so it was never a calibrated way to
// judge this).
windowBars = tp.p.windowEndX - tp.p.windowStartX
aged = bar_index - tp.p.windowEndX > maxPatternAgeMult * math.max(windowBars, 1)
if bullHit or bearHit
breakoutBull := breakoutBull or bullHit
breakoutBear := breakoutBear or bearHit
col = bullHit ? bullCol : bearCol
txt = bullHit ? "Breakout ↑" : "Breakdown ↓"
lb = label.new(bar_index, bullHit ? upperNow : lowerNow, txt,
style=bullHit ? label.style_label_up : label.style_label_down,
color=col, textcolor=color.white, size=size.small)
array.push(tp.objLabels, lb)
f_log("BREAKOUT bull=" + str.tostring(bullHit) + " type=" + tp.p.ttype)
if showOldPatterns
f_finalize_lines(tp)
else
f_delete_tracked(tp)
array.remove(trackedPatterns, i)
else if aged
f_log("PATTERN EXPIRED type=" + tp.p.ttype + " score=" + str.tostring(tp.p.score) + " age=" + str.tostring(bar_index - tp.p.windowEndX))
if showOldPatterns
f_finalize_lines(tp)
else
f_delete_tracked(tp)
array.remove(trackedPatterns, i)
else if array.size(tp.objLines) >= 2
// Still open: keep advancing the boundary lines' far end to stay
// just `lineAheadBars` ahead of the current bar, capped at capX so
// it stops growing once the fixed extension budget is used up
// instead of drawing all the way across the chart.
aheadX = math.min(bar_index + lineAheadBars, capX)
line.set_xy2(array.get(tp.objLines, 0), aheadX, f_price(tp.p.xU1, tp.p.yU1, tp.p.xU2, tp.p.yU2, aheadX))
line.set_xy2(array.get(tp.objLines, 1), aheadX, f_price(tp.p.xL1, tp.p.yL1, tp.p.xL2, tp.p.yL2, aheadX))
// ============================================================================
// Near-miss candidate marker (Watch signal, visual only)
// ============================================================================
plotshape(newSwing and showCandidate and best.score >= candidateThreshold and not best.valid and best.structureOk, title="Wedge Candidate", style=shape.circle, location=location.abovebar, size=size.tiny, color=candidateCol)
// ============================================================================
// Dashboard
// ============================================================================
if showDashboard and barstate.islast
statusActive = array.size(trackedPatterns) > 0
// Reflects the actually tracked pattern when one exists. Without one, a
// valid candidate is never "ready": a valid, open, non-aged candidate is
// tracked the moment it is found, so an untracked valid `best` is always
// stillborn, aged out or already resolved. Only a near-miss (structure
// OK, not valid — the same rule as the chart's candidate marker) is shown
// as WATCH; otherwise SCAN with no pattern details.
dashWatch = not statusActive and best.structureOk and best.score >= candidateThreshold and not best.valid
Pattern dashPattern = emptyPattern()
if statusActive
dashPattern := array.get(trackedPatterns, f_find_best_active_index()).p
else if dashWatch
dashPattern := best
dashStatus = statusActive ? "ACTIVE" : dashWatch ? "WATCH" : "SCAN"
table.cell(dash, 0, 0, "Broadening Scanner", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(dash, 1, 0, dashStatus, text_color=color.white, bgcolor=statusActive ? color.new(wedgeCol, 0) : dashWatch ? candidateCol : color.new(color.gray, 80), text_size=size.small)
table.cell(dash, 0, 1, "Active Patterns", text_color=tc, text_size=size.small)
table.cell(dash, 1, 1, str.tostring(array.size(trackedPatterns)) + " / " + str.tostring(maxConcurrentPatterns), text_color=tc, text_size=size.small)
table.cell(dash, 0, 2, "Best Score", text_color=tc, text_size=size.small)
table.cell(dash, 1, 2, str.tostring(dashPattern.score) + " / " + str.tostring(minScore), text_color=tc, text_size=size.small)
table.cell(dash, 0, 3, "Type", text_color=tc, text_size=size.small)
table.cell(dash, 1, 3, dashPattern.structureOk ? dashPattern.ttype : "—", text_color=tc, text_size=size.small)
table.cell(dash, 0, 4, "Window", text_color=tc, text_size=size.small)
table.cell(dash, 1, 4, na(dashPattern.windowStartX) ? "—" : (str.tostring(dashPattern.windowEndX - dashPattern.windowStartX) + " Bars"), text_color=tc, text_size=size.small)
// ============================================================================
// Debug: score breakdown of the current best-in-depth candidate
// ============================================================================
if showDebug and barstate.islast
table.cell(dbgTable, 0, 0, "Score (Σ/80 → 100)", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(dbgTable, 1, 0, str.tostring(best.score) + " / " + str.tostring(minScore), text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(dbgTable, 0, 1, "Swing-Pfad (Gate)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 1, str.tostring(best.pathScore) + (best.pathOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 2, "Pfad-Fortschritt", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 2, str.tostring(best.progressionMatches) + "/" + str.tostring(best.progressionTests), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 3, "Traversal (20)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 3, str.tostring(best.traversalScore) + (best.traversalOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 4, "Real. Expansion (15)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 4, str.tostring(best.rangeScore) + (best.realizedExpansionOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 5, "Real Ratio / Wachstum", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 5, str.tostring(best.realizedExpansionRatio, "#.##") + " / " + str.tostring(best.legGrowthMatches) + "/" + str.tostring(best.legGrowthTests), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 6, "Proj. Expansion", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 6, str.tostring(best.projectedExpansionRatio, "#.##") + "×", text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 7, "Zusatzkontakte (15)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 7, str.tostring(best.touchScore), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 8, "Linienqualität (10)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 8, str.tostring(best.lineScore) + (best.lineQualityOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 9, "Anker-Span (10)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 9, str.tostring(best.anchorScore) + " · " + str.tostring(best.upperAnchorSpanRatio, "#.##") + "/" + str.tostring(best.lowerAnchorSpanRatio, "#.##") + (best.anchorSpanOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 10, "Boundary-Drift (ATR)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 10, str.tostring(best.upperDriftAtr, "#.##") + "/" + str.tostring(best.lowerDriftAtr, "#.##") + (best.driftOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 11, "Linienordnung / Breite", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 11, (best.boundariesOrderedOk ? "O" : "O GATE") + " / " + (best.widthOk ? "B" : "B GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 12, "Dauer (5)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 12, str.tostring(best.durationScore) + (best.durationOk ? " · OK" : " · GATE"), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 13, "Aktualität (5)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 13, str.tostring(best.relevanceScore), text_color=tc, text_size=size.small)
table.cell(dbgTable, 0, 14, "Containment (soft)", text_color=tc, text_size=size.small)
table.cell(dbgTable, 1, 14, str.tostring(best.containmentRatio * 100.0, "#.0") + " %" + (best.barsInsideOk ? " · OK" : ""), text_color=tc, text_size=size.small)
// ============================================================================
// Alerts
// ============================================================================
alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and newWedgeFound, "Neuer Broadening Wedge erkannt", "BWS · WEDGE FOUND · {{ticker}} {{interval}}")
alertcondition(alertOk and breakoutBull, "Bullischer Breakout", "BWS · BULL BREAKOUT · {{ticker}} {{interval}}")
alertcondition(alertOk and breakoutBear, "Bärischer Breakdown", "BWS · BEAR BREAKDOWN · {{ticker}} {{interval}}")
Overview
Broadening Wedge Scanner Pro [WavesUnchained] evaluates confirmed swing highs and lows within a sliding window, fitting upper and lower trendlines based on ATR expansion and traversal rules. The script signals bullish or bearish breakouts when price breaches the boundaries on bar close, optionally displaying quality scores, candidate patterns, and an information dashboard. Users can adjust pivot strength, search depth, window size, and minimum score thresholds. Because swings require confirmation, newly formed wedges appear with a lag.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Swing-Stärke
int
6
pivotLen
Max. gespeicherte Swings
int
80
maxSwings
Suchbereich letzte Swings
int
24
searchDepth
Swings pro Keil-Fenster
int
8
wedgeWindow
Max. gleichzeitige Patterns
int
1
maxConcurrentPatterns
Max. Pattern-Alter (× Fensterbreite)
float
1.5
maxPatternAgeMult
Mindestqualität
int
60
minScore
ATR-Länge
int
14
atrLen
Touch-Toleranz ATR
float
0.5
touchTolAtr
Ziel proj. Endbreite / Startbreite
float
1.3
projectedExpansionTarget
Mindest-Endbreite ATR
float
1.0
minWidthAtr
Min. Geschwindigkeits-Verhältnis (DBW/ABW)
float
1.2
fasterRatioMin
Show all 29 settings
Setting
Type
Default
Variable
Min. Traversal pro Leg
float
0.60
minTraversalRatio
Min. realisierte Leg-Expansion
float
1.15
minRealizedExpansionRatio
Min. Anker-Span / Pattern-Dauer
float
0.55
minAnchorSpanRatio
Max. Boundary-Drift ATR
float
8.0
maxBoundaryDriftAtr
Aufgelöste Patterns behalten
bool
false
showOldPatterns
Kandidaten anzeigen
bool
true
showCandidate
Linien-Überstand (Bars)
int
10
lineAheadBars
Score-Label anzeigen
bool
true
showLabels
Keil (offen)
color
color.purple
wedgeCol
Bullischer Breakout
color
color.lime
bullCol
Bärischer Breakout
color
color.red
bearCol
Kandidat
color
color.new(color.orange, 0)
candidateCol
Score-Breakdown Debug-Tabelle
bool
false
showDebug
Kandidat-Mindestscore (Watch)
int
25
candidateThreshold
Pine-Logs aktivieren (Fehlersuche)
bool
false
enableLogging
Dashboard anzeigen
bool
true
showDashboard
Alerts nur bei bestätigter Kerze (Bar Close)
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.
The CISD with Fib/Range Extensions and Stats indicator tracks Change in State of Delivery (CISD) level breakouts on the price chart. It detects pullback structures filtered by ATR and volume conditions, then projects future target levels using customizable Fib
The ORB Pro indicator calculates and plots Opening Range Breakout levels, stop-loss and take-profit zones, signal labels, and an status dashboard on the price chart. Built on academic research, it evaluates trades using an opening-candle direction filter, open
This indicator calculates and visualizes Opening Range Breakout (ORB) zones directly on the price chart. Breakout signals are identified when the close or an EMA crosses the opening range boundary, with entry sensitivity managed by retest requirements. It mode
This indicator screens Rob Smith's TheStrat setups across six configurable timeframes for TradingView's Pine Screener. It classifies bars and exposes inside reversals, 2-2 reversals and continuations, failing 2s, range expansions, and outside bars as numeric p
Calculates Darvas Box upper and lower channel boundaries based on configurable period length to identify trend breakouts. It plots top and bottom box levels directly on the price chart and plots buy (AL) and sell (SAT) signal labels when price crosses outside
Calculates the statistical likelihood of price reaching multiple percentage-spaced breakout levels above and below the current price. It renders horizontal lines with percentage probability labels directly on the price chart and maintains a summary dashboard i