Accumulation
Could selling be getting absorbed, building a case for a later rise?
Wyckoff Evidence Desk v1.0.0 · Free Pine source
Compare evidence for a rise and a fall. Follow what changes before calling it a setup.
Get the free indicator ↓Start with Base · optional Footprint and Lab

Wyckoff Evidence Desk compares accumulation and distribution hypotheses on the same chart. It shows each side’s range, confirmed events and reasons to wait or invalidate it. Contested means both paths remain plausible. The scores are independent rules, not calibrated probabilities. Start with Base; Footprint and the Lab are separate editions.
No. ACC and DIST collect separate rule-based evidence; their scores need not add to 100. Contested means two active hypotheses are too close to choose a clear leader. A qualifying backup needs acceptance, a later return and the configured evidence, lead, volume and context checks. Read the evidence map
Base and Lab make no Footprint request. Only the separate Footprint edition needs an eligible TradingView account and feed. Base uses standard candle prices and available volume; missing volume still permits price-only hypotheses but blocks qualified setups by default. Choose the edition to install
The producer reports native synthetic engine checks and Base chart runs on micro Nasdaq and EURUSD. Those checks do not prove trading outcomes. Its small development Lab sample lost money; Footprint native execution and held-out market validation remain unverified. Read the evidence and limits
Wyckoff studied price and volume together. This desk turns selected ideas into explicit, revisable rules.
Could selling be getting absorbed, building a case for a later rise?
Could demand be weakening, building a case for a later fall?
The tool tests both explanations. “Contested” means two active hypotheses have similar scores.
These are hypotheses about price behavior. Candles do not reveal anyone’s hidden intentions.
The public Wyckoff Method overview relates price, volume and the development of trading ranges. Its examples include paths without a Spring or UTAD. This desk formalizes a selected subset with its own numerical rules; it is not an official Wyckoff Analytics product or a complete implementation of the method.
The first improvement is keeping competing explanations visible. The next is recording events only when their rules can be confirmed, including failed breaks. Read the reason beside the label, rather than fitting the chart to a perfect historical drawing.
The letters describe what has been observed. They do not promise what comes next.
Wait for the opposite boundary and a test. The first candidate is provisional.
Price works inside a known range. A balanced-range start can begin here.
A Spring or UTAD can recover a breached edge. This phase is not mandatory.
A decisive break still needs two consecutive closes beyond the boundary allowance.
A later backup can become a setup if its checks pass. Structural failure can still end it.
There is more than one route. A valid range can move directly from B to D. A failed idea may return to range-building or reset to scanning.
It gives you a hypothesis to test, the missing evidence and the point where that hypothesis fails. Start in Bar Replay: follow events from their confirmation candles, including failures. Then test a specific entry and risk rule on different data with realistic costs.
The indicator does not place orders. A qualified setup is not proof of an advantage. The supplied Strategy Lab’s small development sample lost money; the tool has not established profitability.
A higher score means more supporting rules, not a greater proven chance of winning.

ACC and DIST have independent scores. They do not need to total 100.
Hover for the full reason. A question mark marks a candidate. An INVALID label records a failed structure.
WAIT means no newly qualified setup on this bar. It is not a pending broker order.
Three separate Pine v6 files. Each works as a complete script.
Use standard candles with available volume and enough history.
Create a new indicator, save, then Add to chart.
Read both columns. SCAN can simply mean the tool is still waiting.
START HERE
Uses candle prices and volume. No Footprint subscription is required.
Copy the whole file. No coding needed.
//@version=6
// Wyckoff Evidence Desk 1.0.0. Original heuristic interpretation, not a probability or profit claim.
indicator("Wyckoff Evidence Desk", "WED", overlay = true, behind_chart = false, max_labels_count = 120, max_boxes_count = 4, max_lines_count = 20, max_bars_back = 2000)
const bool footprintBuild = false
float fpDelta = na
// Causal, direction-normalized engine. The same code runs both hypotheses.
// All episode state changes occur only for committed frames. No future bars are read.
type WConfig
int minB = 8
int reclaimBars = 3
int maxAge = 240
float tolerance = 0.25
float maxSweep = 1.5
float minWidth = 2.5
float trailAtr = 3.0
bool allowBootstrap = true
type WBar
int i = 0
bool closed = true
float o = na
float h = na
float l = na
float c = na
float atr = na
float rv = na
float vol = na
bool volumeOk = false
bool climax = false
bool quiet = false
float rangeLo = na
float rangeHi = na
float ph = na
float pl = na
float pivotVolume = na
float pivotRv = na
int pivotIndex = 0
float spreadRatio = na
bool contraction = false
int context = 0
float delta = na
type WEngine
int dir = 1
int phase = 0
int stage = 0
float lo = na
float hi = na
int born = na
int climaxBar = na
float climaxVolume = na
int arBar = na
int bStarted = na
bool bootstrap = false
int pending = 0
int sweepBar = na
float sweepExtreme = na
float sweepRv = na
int sweepKind = 0
bool testPassed = false
bool testObserved = false
int strengthBar = na
int breakout = na
int outside = 0
int invalidCloses = 0
bool backedUp = false
float trail = na
float lastSwingHigh = na
float pvSum = 0.0
float vSum = 0.0
float vwap = na
float score = 0.0
string event = ""
string lastEvent = "Waiting"
string why = "Waiting for exhaustion or a balanced range"
int resetAt = -10000
int lastTest = -10000
int lastProcessed = -1
method tag(WEngine e, string name, string reason) =>
e.event := name
e.lastEvent := name
e.why := reason
method clearEpisode(WEngine e, int i, string reason) =>
e.phase := 0
e.stage := 0
e.lo := na
e.hi := na
e.born := na
e.climaxBar := na
e.climaxVolume := na
e.arBar := na
e.bStarted := na
e.bootstrap := false
e.pending := 0
e.sweepBar := na
e.sweepExtreme := na
e.sweepRv := na
e.sweepKind := 0
e.testPassed := false
e.testObserved := false
e.strengthBar := na
e.breakout := na
e.outside := 0
e.invalidCloses := 0
e.backedUp := false
e.trail := na
e.lastSwingHigh := na
e.pvSum := 0.0
e.vSum := 0.0
e.vwap := na
e.score := 0.0
e.resetAt := i
e.lastTest := -10000
e.tag("INVALID", reason)
method startEpisode(WEngine e, WBar b, bool bootstrap) =>
e.born := b.i
e.bootstrap := bootstrap
e.climaxBar := b.i
e.climaxVolume := b.volumeOk ? b.vol : na
e.lo := bootstrap ? b.rangeLo : b.l
e.hi := bootstrap ? b.rangeHi : na
e.phase := bootstrap ? 2 : 1
e.stage := bootstrap ? 3 : 1
e.bStarted := bootstrap ? b.i : na
e.tag(bootstrap ? "TR?" : e.dir == 1 ? "SC?" : "BC?", bootstrap ? "Balanced range bootstrap; Phase A was not observed" : "Exhaustion candidate; waiting for a confirmed automatic reaction")
method step(WEngine e, WBar b, WConfig cfg) =>
if b.closed and b.i > e.lastProcessed
e.lastProcessed := b.i
e.event := ""
float tol = b.atr * cfg.tolerance
float width = e.hi - e.lo
int enteredPhase = e.phase
bool inside = not na(e.hi) and b.c >= e.lo and b.c <= e.hi
float closeLocation = b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5
bool pivotLowEffort = not na(b.pivotRv) and b.pivotRv < 1.15
bool demand = b.c > b.o and closeLocation >= 0.65 and b.spreadRatio >= 1.05 and (not b.volumeOk or b.rv >= 1.1)
bool expired = e.phase > 0 and e.phase < 5 and b.i - e.born > cfg.maxAge
if e.phase > 0 and not na(e.hi)
e.invalidCloses := b.c < e.lo - tol ? e.invalidCloses + 1 : 0
bool invalid = e.phase > 0 and e.invalidCloses > cfg.reclaimBars
bool deepAdverse = e.phase > 0 and not na(e.hi) and b.l < e.lo - cfg.maxSweep * b.atr
bool bothEdges = e.phase > 0 and not na(e.hi) and b.l < e.lo - tol and b.h > e.hi + tol
bool trendFailed = e.phase == 5 and (b.c < e.trail or b.c < e.hi - tol)
if expired or invalid or deepAdverse
e.clearEpisode(b.i, expired ? "Range expired before acceptance" : deepAdverse ? "Adverse excursion exceeded maximum structural depth" : "Sustained acceptance beyond the invalidation boundary")
else if trendFailed
e.clearEpisode(b.i, b.c < e.trail ? "Confirmed close crossed the one-way structural trail" : "Backup failed: close crossed the breakout boundary")
else if bothEdges
e.pending := 0
e.testPassed := false
e.testObserved := false
if e.phase >= 3 and e.phase < 5
e.phase := 2
e.bStarted := b.i
e.breakout := na
e.outside := 0
e.tag("AMBIGUOUS", "This bar crossed both range edges; OHLC cannot establish the sequence")
else if e.phase == 0
if b.i - e.resetAt > 3 and not na(b.atr) and b.atr > 0
if b.climax
e.startEpisode(b, false)
else if cfg.allowBootstrap and b.quiet and b.rangeHi - b.rangeLo >= cfg.minWidth * b.atr and b.c >= b.rangeLo and b.c <= b.rangeHi
e.startEpisode(b, true)
else if b.volumeOk and b.rv >= 1.5 and closeLocation >= 0.65 and b.i - e.lastTest > 8
e.lastTest := b.i
e.tag(e.dir == 1 ? "PS?" : "PSY?", "Preliminary stopping activity; no range has been established")
else if e.phase == 1
if e.stage == 1
// SC/BC is provisional until AR establishes the second boundary.
if b.l < e.lo
e.lo := b.l
e.climaxBar := b.i
e.climaxVolume := b.volumeOk ? b.vol : na
if not na(b.ph) and b.pivotIndex > e.climaxBar and b.ph - e.lo >= cfg.minWidth * b.atr
e.hi := b.ph
e.arBar := b.pivotIndex
e.stage := 2
e.tag("AR", "Confirmed swing establishes the other boundary; range is now frozen")
else if e.stage == 2
bool test = not na(b.pl) and b.pivotIndex > e.arBar and b.pl >= e.lo - tol and b.pl <= e.lo + width * 0.3 and inside
bool comparableVolume = not na(b.pivotVolume) and b.pivotVolume > 0 and not na(e.climaxVolume) and e.climaxVolume > 0
bool lessVolume = comparableVolume ? b.pivotVolume < e.climaxVolume : not b.volumeOk
if test and lessVolume
e.phase := 2
e.stage := 3
e.bStarted := b.i
e.lastTest := b.i
e.tag("ST", b.volumeOk ? "Secondary test on lower volume; cause-building hypothesis begins" : "Price-only secondary test; volume unavailable")
else if e.phase == 2 or e.phase == 3
bool mature = b.i - e.bStarted >= cfg.minB
if e.pending == 1
e.sweepExtreme := math.min(e.sweepExtreme, b.l)
e.sweepRv := b.volumeOk ? math.max(nz(e.sweepRv, b.rv), b.rv) : e.sweepRv
bool tooDeep = e.lo - e.sweepExtreme > cfg.maxSweep * b.atr
if b.i - e.sweepBar > cfg.reclaimBars or tooDeep
e.pending := 0
e.phase := 2
e.testPassed := false
e.tag("SWEEP FAILED", "Reclaim window expired or excursion exceeded the allowed depth")
else if b.c >= e.lo and closeLocation >= 0.55
e.pending := 0
e.phase := 3
e.sweepKind := na(e.sweepRv) ? 0 : e.sweepRv >= 2 ? 1 : e.sweepRv >= 1 ? 2 : 3
e.testPassed := e.sweepKind == 3
e.tag(e.dir == 1 ? "SPRING" : "UTAD", b.c > e.hi ? "Recovered beyond the opposite edge; test and strength rules still apply" : e.testPassed ? "Low-volume reclaim confirmed; subsequent strength still required" : "Reclaim confirmed; waiting for a later lower-effort test")
else if mature and b.l < e.lo - tol and b.c <= e.hi and e.lo - b.l <= cfg.maxSweep * b.atr
e.pending := 1
e.sweepBar := b.i
e.sweepExtreme := b.l
e.sweepRv := b.rv
e.testPassed := false
e.testObserved := false
e.tag("SWEEP?", "Boundary breach observed; waiting for an actual close back inside")
if inside and closeLocation >= 0.55
e.pending := 0
e.phase := 3
e.sweepKind := not b.volumeOk ? 0 : b.rv >= 2 ? 1 : b.rv >= 1 ? 2 : 3
e.testPassed := e.sweepKind == 3
e.tag(e.dir == 1 ? "SPRING" : "UTAD", e.testPassed ? "Same-bar low-volume reclaim confirmed; waiting for strength" : "Same-bar reclaim confirmed; later test required")
else if e.phase == 3 and not e.testPassed
bool higherTest = not na(b.pl) and b.pivotIndex > e.sweepBar and b.pl > e.sweepExtreme + tol and b.pl <= e.lo + width * 0.4 and inside
bool dryTest = not na(b.pivotRv) and b.pivotRv < math.min(nz(e.sweepRv, 1), 1.15)
bool priceOnlyTest = e.sweepKind == 0 and not b.volumeOk and na(b.pivotRv)
if higherTest and (dryTest or priceOnlyTest)
e.testPassed := true
e.testObserved := true
e.lastTest := b.i
e.tag("TEST", priceOnlyTest ? "Structural test only; lower-effort volume evidence is unavailable" : "Confirmed higher-low / lower-high test on reduced relative volume")
else if mature and demand and b.c > math.max(nz(e.lastSwingHigh, e.lo + width * 0.65), e.lo + width * 0.5) + tol and (e.phase == 2 or e.testPassed)
e.phase := 4
e.strengthBar := b.i
e.tag(e.dir == 1 ? "SOS" : "SOW", enteredPhase == 2 ? "Directional dominance; no terminal sweep required" : "Directional dominance on expanded effort and result")
if b.c > e.hi + tol
e.breakout := b.i
e.outside := 1
e.tag(e.dir == 1 ? "JAC" : "FTI", "First decisive close beyond the frozen range; acceptance is pending")
else if e.phase == 2 and inside and b.i - e.lastTest > 5
if not na(b.pl) and b.pivotIndex > e.bStarted and b.pl <= e.lo + width * 0.3 and b.pl >= e.lo - tol and pivotLowEffort
e.lastTest := b.i
e.tag("ST-B", "Range-edge test during cause building")
else if b.h > e.hi + tol
e.lastTest := b.i
e.tag(e.dir == 1 ? "UTA" : "mSOW", "Opposite edge rejected back into the range; direction remains unresolved")
else if e.phase == 4
if na(e.breakout)
if demand and b.c > e.hi + tol
e.breakout := b.i
e.outside := 1
e.tag(e.dir == 1 ? "JAC" : "FTI", "Breakout close observed; a second outside close is required")
else if not na(b.pl) and b.pivotIndex > e.strengthBar and b.pl > nz(e.sweepExtreme, e.lo) + tol and inside and pivotLowEffort and b.i - e.lastTest > 5
e.lastTest := b.i
e.tag(e.dir == 1 ? "LPS" : "LPSY", "Confirmed pullback holds above structural origin on reduced effort")
else
e.outside := b.c > e.hi + tol ? e.outside + 1 : 0
if e.outside >= 2
e.phase := 5
e.trail := math.max(e.lo, b.c - cfg.trailAtr * b.atr)
e.tag("ACCEPT", "Two consecutive closes accepted outside the range; awaiting backup")
else if b.c < e.hi - tol or b.i - e.breakout > cfg.reclaimBars
e.breakout := na
e.outside := 0
e.phase := 2
e.bStarted := b.i
e.testPassed := false
e.tag("BREAKOUT FAILED", "Outside acceptance failed; cause-building hypothesis restarted")
else if e.phase == 5
if b.c < e.trail
e.clearEpisode(b.i, "Confirmed close crossed the one-way structural trail")
else if b.c < e.hi - tol
e.clearEpisode(b.i, "Backup failed: price closed back through the breakout boundary")
else
e.trail := math.max(e.trail, b.c - cfg.trailAtr * b.atr)
bool retest = b.i > e.breakout and b.l <= e.hi + tol and b.l >= e.hi - cfg.maxSweep * b.atr and b.c >= e.hi and b.c > b.o
if retest and not e.backedUp and (not b.volumeOk or b.rv <= 1.3)
e.backedUp := true
e.tag("BU", b.volumeOk ? "Accepted breakout retested on controlled effort; one setup per episode" : "Price-only backup; volume confirmation unavailable")
// AVWAP anchors at the observed episode start; missing volume is never invented.
if e.phase > 0
if b.volumeOk
e.pvSum += (b.h + b.l + b.c) / 3 * b.vol
e.vSum += b.vol
e.vwap := e.pvSum / e.vSum
if not na(b.ph) and b.pivotIndex >= e.born and b.ph < nz(e.hi, b.ph + 1)
e.lastSwingHigh := b.ph
float base = e.phase == 1 ? 12 : e.phase == 2 ? 25 : e.phase == 3 ? 40 : e.phase == 4 ? 55 : 65
float support = (e.testPassed ? 8 : 0) + (b.contraction ? 5 : 0) + (not na(e.vwap) and b.c > e.vwap ? 5 : 0) + (b.context == 1 ? 7 : b.context == -1 ? -8 : 0) + (closeLocation >= 0.65 ? 5 : 0) + (not na(b.delta) and b.delta > 0.1 ? 5 : 0)
e.score := math.max(0, math.min(100, base + support - (e.bootstrap ? 5 : 0) - (b.volumeOk ? 0 : 15) - (e.pending == 1 ? 8 : 0)))
e.phase
const string G_ENGINE = "01 · Detection"
int swingBars = input.int(3, "Confirmed swing bars (latency)", minval = 2, maxval = 10, group = G_ENGINE)
int baseline = input.int(50, "Adaptive baseline", minval = 20, maxval = 200, group = G_ENGINE)
int rangeBars = input.int(30, "Balanced-range lookback", minval = 15, maxval = 100, group = G_ENGINE)
bool allowBootstrap = input.bool(true, "Allow balanced-range bootstrap", tooltip = "Recognizes continuation ranges without inventing historical SC/BC. Both directions may remain plausible.", group = G_ENGINE)
int minCause = input.int(8, "Minimum cause-building bars", minval = 3, maxval = 80, group = G_ENGINE)
int reclaimWindow = input.int(3, "Reclaim deadline (closed bars)", minval = 1, maxval = 5, group = G_ENGINE)
int maxAge = input.int(240, "Expire range after bars", minval = 40, maxval = 1500, group = G_ENGINE)
float tolerance = input.float(0.25, "Boundary tolerance / ATR", minval = 0.05, maxval = 1, step = 0.05, group = G_ENGINE)
float maxSweep = input.float(1.5, "Maximum sweep depth / ATR", minval = 0.5, maxval = 4, step = 0.1, group = G_ENGINE)
float minWidth = input.float(2.5, "Minimum range width / ATR", minval = 1, maxval = 8, step = 0.25, group = G_ENGINE)
float trailMultiple = input.float(3, "One-way trail / ATR", minval = 1, maxval = 8, step = 0.25, group = G_ENGINE)
const string G_CONTEXT = "02 · Evidence and setup gates"
bool useContext = input.bool(true, "Use confirmed higher-timeframe context", group = G_CONTEXT)
string contextInput = input.timeframe("", "Context timeframe (blank = automatic 4×)", group = G_CONTEXT)
bool requireVolume = input.bool(true, "Require volume for qualified backup", tooltip = "Price-only hypotheses remain visible. Missing/zero volume cannot qualify a setup when enabled.", group = G_CONTEXT)
float minEvidence = input.float(65, "Minimum backup evidence score", minval = 40, maxval = 95, group = G_CONTEXT)
float leadNeeded = input.float(10, "Minimum lead over opposing hypothesis", minval = 0, maxval = 40, group = G_CONTEXT)
bool enableJson = input.bool(false, "Emit JSON event alerts", group = G_CONTEXT)
float atr = ta.atr(14)
float volMedian = ta.median(volume, baseline)[1]
bool hasVolume = not na(volume) and volume > 0 and not na(volMedian) and volMedian > 0
float rvol = hasVolume ? volume / volMedian : na
float spread = high - low
float spreadMedian = ta.median(spread, baseline)[1]
float spreadRatio = spreadMedian > 0 ? spread / spreadMedian : 0
float volumeExtreme = ta.percentile_nearest_rank(volume, baseline, 90)[1]
float spreadExtreme = ta.percentile_nearest_rank(spread, baseline, 90)[1]
float ph = ta.pivothigh(high, swingBars, swingBars)
float pl = ta.pivotlow(low, swingBars, swingBars)
float recentHi = ta.highest(high, rangeBars)[1]
float recentLo = ta.lowest(low, rangeBars)[1]
float pathLength = math.sum(math.abs(ta.change(close)), rangeBars)[1]
float efficiency = pathLength > 0 ? math.abs(close[1] - close[rangeBars + 1]) / pathLength : 1
float fastVolume = ta.sma(volume, 8)
float slowVolume = ta.sma(volume, 30)
bool contraction = hasVolume and fastVolume < slowVolume * 0.9
float trend = ta.ema(close, 34)
bool priorDown = close[1] < trend[1] and trend[1] < trend[6]
bool priorUp = close[1] > trend[1] and trend[1] > trend[6]
bool quiet = efficiency < 0.3 and recentHi - recentLo <= atr * 10
bool extreme = hasVolume and volume >= volumeExtreme and rvol >= 1.8 and spread >= spreadExtreme and spreadRatio >= 1.3
float closeLocation = spread > 0 ? (close - low) / spread : 0.5
bool warm = bar_index > math.max(baseline, rangeBars) + 35 and not na(atr)
string contextTf = contextInput == "" ? timeframe.from_seconds(int(timeframe.in_seconds() * 4)) : contextInput
if barstate.isfirst and not chart.is_standard
runtime.error("Use standard time-based candles. Synthetic prices are unsuitable for this analysis.")
if barstate.isfirst and useContext and timeframe.in_seconds(contextTf) <= timeframe.in_seconds()
runtime.error("Context timeframe must be higher than the chart timeframe.")
[htfClose, htfMean, htfOldMean] = request.security(syminfo.tickerid, contextTf, [close[1], ta.ema(close, 34)[1], ta.ema(close, 34)[2]], lookahead = barmerge.lookahead_on)
int htfBias = not useContext ? 0 : htfClose > htfMean and htfMean > htfOldMean ? 1 : htfClose < htfMean and htfMean < htfOldMean ? -1 : 0
f_frame(int direction) =>
WBar b = WBar.new()
b.i := bar_index
b.closed := barstate.isconfirmed and warm
b.o := direction * open
b.h := direction == 1 ? high : -low
b.l := direction == 1 ? low : -high
b.c := direction * close
b.atr := atr
b.rv := rvol
b.vol := volume
b.volumeOk := hasVolume
b.climax := extreme and (direction == 1 ? priorDown and low < recentLo and closeLocation >= 0.4 : priorUp and high > recentHi and closeLocation <= 0.6)
b.quiet := quiet
b.rangeLo := direction == 1 ? recentLo : -recentHi
b.rangeHi := direction == 1 ? recentHi : -recentLo
b.ph := direction == 1 ? ph : -pl
b.pl := direction == 1 ? pl : -ph
b.pivotVolume := volume[swingBars]
b.pivotRv := rvol[swingBars]
b.pivotIndex := bar_index - swingBars
b.spreadRatio := spreadRatio
b.contraction := contraction
b.context := direction * htfBias
b.delta := direction * fpDelta
b
var WEngine bull = WEngine.new(1)
var WEngine bear = WEngine.new(-1)
WConfig config = WConfig.new(minCause, reclaimWindow, maxAge, tolerance, maxSweep, minWidth, trailMultiple, allowBootstrap)
bull.step(f_frame(1), config)
bear.step(f_frame(-1), config)
bool eligibleLong = barstate.isconfirmed and warm and bull.event == "BU" and bull.score >= minEvidence and bull.score - bear.score >= leadNeeded and (not requireVolume or hasVolume) and (not useContext or htfBias >= 0)
bool eligibleShort = barstate.isconfirmed and warm and bear.event == "BU" and bear.score >= minEvidence and bear.score - bull.score >= leadNeeded and (not requireVolume or hasVolume) and (not useContext or htfBias <= 0)
alertcondition(eligibleLong, "Qualified bullish backup", "Wyckoff Evidence Desk: qualified bullish backup on {{ticker}} {{interval}}. Evidence is not a probability.")
alertcondition(eligibleShort, "Qualified bearish backup", "Wyckoff Evidence Desk: qualified bearish backup on {{ticker}} {{interval}}. Evidence is not a probability.")
alertcondition(barstate.isconfirmed and (bull.event == "SPRING" or bear.event == "UTAD"), "Confirmed reclaim", "Wyckoff boundary reclaim confirmed on {{ticker}} {{interval}}; inspect test and acceptance status.")
if enableJson and barstate.isconfirmed
for side in array.from(bull, bear)
if side.event != ""
string payload = '{"schema":"wed.v1","symbol":"' + syminfo.tickerid + '","timeframe":"' + timeframe.period + '","barTime":' + str.tostring(time) + ',"side":' + str.tostring(side.dir) + ',"phase":' + str.tostring(side.phase) + ',"event":"' + side.event + '","evidence":' + str.tostring(side.score, "#.0") + ',"volumeAvailable":' + str.tostring(hasVolume) + '}'
alert(payload, alert.freq_all)
// Numeric exports for Data Window and Pine Screener. Scores are uncalibrated rules.
plot(bull.phase, "Accumulation phase 0-5", display = display.data_window)
plot(bear.phase, "Distribution phase 0-5", display = display.data_window)
plot(bull.score, "Accumulation evidence 0-100", display = display.data_window)
plot(bear.score, "Distribution evidence 0-100", display = display.data_window)
plot(eligibleLong ? 1 : eligibleShort ? -1 : 0, "Qualified backup direction", display = display.data_window)
// ── Evidence desk · chart presentation ────────────────────────────────────────
string visualGroup = "05 · Evidence desk"
bool showDashboard = input.bool(true, "Evidence dashboard", group = visualGroup)
bool showTape = input.bool(true, "Last six confirmed events", group = visualGroup)
bool showLabels = input.bool(true, "Confirmed event labels", group = visualGroup)
bool showRange = input.bool(true, "Active hypothesis ranges", group = visualGroup)
bool showVwap = input.bool(true, "Event-anchored VWAP", group = visualGroup)
bool showTrail = input.bool(true, "Phase E structural trail", group = visualGroup)
bool showBars = input.bool(false, "Color bars from phase C onward", group = visualGroup)
int maxEventLabels = input.int(60, "Retained event labels", minval = 10, maxval = 100, group = visualGroup)
string dashboardPosition = input.string("Top right", "Dashboard position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = visualGroup)
color vdInk = #0B151C
color vdPanel = #101F29
color vdGrid = #223642
color vdText = #DCE9ED
color vdMuted = #819AA5
color vdBull = #44D9C0
color vdBear = #FF8177
color vdGold = #EDC778
f_vdPhase(int phase) =>
switch phase
1 => "A · stopping action"
2 => "B · range building"
3 => "C · test / sweep"
4 => "D · directional proof"
5 => "E · follow-through"
=> "SCAN · no range"
f_vdRibbon(int phase) =>
phase == 0 ? "SCAN · WAIT FOR STRUCTURE" : (phase == 1 ? "[A]" : " A ") + " — " + (phase == 2 ? "[B]" : " B ") + " — " + (phase == 3 ? "[C]" : " C ") + " — " + (phase == 4 ? "[D]" : " D ") + " — " + (phase == 5 ? "[E]" : " E ")
f_vdFailed(string eventName) =>
str.contains(str.upper(eventName), "FAIL") or str.contains(str.upper(eventName), "INVALID") or str.contains(str.upper(eventName), "EXPIRE")
f_vdPrice(float value) =>
na(value) ? "—" : str.tostring(value, format.mintick)
f_vdScore(float value) =>
float bounded = math.max(0.0, math.min(100.0, nz(value)))
int blocks = int(math.round(bounded / 20.0))
string meter = ""
for block = 1 to 5
meter += block <= blocks ? "■" : "·"
str.tostring(bounded, "0") + " / 100 " + meter
f_vdBrief(string value, int limit) =>
str.length(value) > limit ? str.substring(value, 0, limit - 1) + "…" : value
f_vdTest(WEngine engine) =>
engine.phase == 0 ? "Awaiting range" : engine.pending == 1 ? "Reclaim pending" : engine.testPassed and engine.testObserved ? "Test confirmed" : engine.testPassed ? "Type 3 · test waived" : engine.phase >= 4 ? "Direct strength path" : "No confirmed test"
f_vdBoundaryTip(WEngine engine, bool isBull) =>
float direction = isBull ? 1.0 : -1.0
string side = isBull ? "below " : "above "
string detail = "No active range."
if engine.phase > 0 and na(engine.hi)
detail := "Phase A candidate: awaiting the automatic reaction to establish both range edges. Observed origin: " + f_vdPrice(direction * engine.lo)
else if engine.phase > 0
detail := "Sustained close invalidation: " + side + f_vdPrice(direction * (engine.lo - atr * tolerance)) + ". More than " + str.tostring(reclaimWindow) + " consecutive closes are required.\nMaximum adverse wick boundary: " + f_vdPrice(direction * (engine.lo - atr * maxSweep)) + ". Crossing it invalidates on the confirmed bar."
if engine.phase == 5
detail += "\nPhase E structural trail: " + f_vdPrice(direction * engine.trail) + ".\nBackup failure close boundary: " + side + f_vdPrice(direction * (engine.hi - atr * tolerance)) + ".\nTrail is structural context, not a broker stop order."
detail
f_vdCell(table panel, int column, int row, string value, color foreground, color background, string tooltipText = "") =>
table.cell(panel, column, row, value, text_color = foreground, bgcolor = background, text_size = size.small, text_halign = column == 0 ? text.align_left : text.align_center, tooltip = tooltipText)
f_vdTape(array<string> tape, array<color> colors, bool isBull, string eventName, string reason) =>
string stamp = str.format_time(na(time_close) ? time : time_close, "dd MMM HH:mm", syminfo.timezone)
array.unshift(tape, stamp + " · " + (isBull ? "ACC" : "DIST") + " " + eventName + " · " + f_vdBrief(reason, 28))
array.unshift(colors, f_vdFailed(eventName) ? vdGold : isBull ? vdBull : vdBear)
if array.size(tape) > 6
array.pop(tape)
array.pop(colors)
f_vdLabel(bool isBull, WEngine engine) =>
color tone = f_vdFailed(engine.event) ? vdGold : isBull ? vdBull : vdBear
float y = isBull ? low - nz(atr, high - low) * 0.24 : high + nz(atr, high - low) * 0.24
string tip = (isBull ? "ACCUMULATION" : "DISTRIBUTION") + " · " + engine.event + "\nConfirmed on this bar close.\n" + f_vdPhase(engine.phase) + "\nEvidence: " + str.tostring(engine.score, "0") + " / 100 (heuristic, not probability)\n" + engine.why
label.new(bar_index, y, engine.event, xloc = xloc.bar_index, yloc = yloc.price, style = isBull ? label.style_label_up : label.style_label_down, color = color.new(tone, 12), textcolor = vdInk, size = size.tiny, tooltip = tip)
bool vdBullActive = bull.phase > 0
bool vdBearActive = bear.phase > 0
bool vdBullRange = vdBullActive and not na(bull.lo) and not na(bull.hi)
bool vdBearRange = vdBearActive and not na(bear.lo) and not na(bear.hi)
bool vdContested = vdBullActive and vdBearActive and math.abs(bull.score - bear.score) <= 10.0
bool vdSelectBull = vdBullActive and (not vdBearActive or bull.score >= bear.score)
bool vdSelectBear = vdBearActive and not vdSelectBull
bool vdShowBull = vdBullActive and (vdSelectBull or vdContested)
bool vdShowBear = vdBearActive and (vdSelectBear or vdContested)
bool vdDrawBull = vdBullRange and vdShowBull
bool vdDrawBear = vdBearRange and vdShowBear
int vdPhase = vdSelectBull ? bull.phase : vdSelectBear ? bear.phase : 0
color vdAccent = vdContested ? vdGold : vdSelectBull ? vdBull : vdSelectBear ? vdBear : vdMuted
bool vdReset = not vdBullActive and not vdBearActive and (f_vdFailed(bull.lastEvent) or f_vdFailed(bear.lastEvent))
string vdMode = vdContested ? "CONTESTED · BOTH HYPOTHESES ACTIVE" : vdSelectBull ? "ACCUMULATION · LEADING EVIDENCE" : vdSelectBear ? "DISTRIBUTION · LEADING EVIDENCE" : vdReset ? "RESET → SCAN · AWAIT NEW STRUCTURE" : "SCANNING · AWAIT STOPPING ACTION"
// Reuse two boxes. Their left edge is the confirmation bar that started the range.
var box vdBullBox = na
var box vdBearBox = na
if showRange and vdDrawBull
int left = math.max(nz(bull.born, bar_index), bar_index - 9999)
if na(vdBullBox)
vdBullBox := box.new(left, bull.hi, bar_index + 1, bull.lo, border_color = color.new(vdBull, 26), border_width = 1, bgcolor = color.new(vdBull, 94), text = "", text_color = color.new(vdBull, 12), text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_top)
box.set_lefttop(vdBullBox, left, bull.hi)
box.set_rightbottom(vdBullBox, bar_index + 1, bull.lo)
box.set_border_style(vdBullBox, vdContested ? line.style_dashed : line.style_solid)
box.set_text(vdBullBox, " ACC · " + f_vdPhase(bull.phase) + " · " + str.tostring(bull.score, "0") + "/100")
else if not na(vdBullBox)
box.delete(vdBullBox)
vdBullBox := na
if showRange and vdDrawBear
int left = math.max(nz(bear.born, bar_index), bar_index - 9999)
if na(vdBearBox)
vdBearBox := box.new(left, -bear.lo, bar_index + 1, -bear.hi, border_color = color.new(vdBear, 26), border_width = 1, bgcolor = color.new(vdBear, 94), text = "", text_color = color.new(vdBear, 12), text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_bottom)
box.set_lefttop(vdBearBox, left, -bear.lo)
box.set_rightbottom(vdBearBox, bar_index + 1, -bear.hi)
box.set_border_style(vdBearBox, vdContested ? line.style_dashed : line.style_solid)
box.set_text(vdBearBox, " DIST · " + f_vdPhase(bear.phase) + " · " + str.tostring(bear.score, "0") + "/100")
else if not na(vdBearBox)
box.delete(vdBearBox)
vdBearBox := na
plot(showVwap and vdShowBull ? bull.vwap : na, "Accumulation event AVWAP", color = vdBull, linewidth = 2, style = plot.style_linebr)
plot(showVwap and vdShowBear ? -bear.vwap : na, "Distribution event AVWAP", color = vdBear, linewidth = 2, style = plot.style_linebr)
float vdSelectedTrail = not vdContested and vdPhase == 5 ? vdSelectBull ? bull.trail : -bear.trail : na
plot(showTrail ? vdSelectedTrail : na, "Leading Phase E structural trail", color = vdGold, linewidth = 1, style = plot.style_linebr)
barcolor(showBars and vdPhase >= 3 ? color.new(vdAccent, 24) : na, title = "Evidence phase tint")
plotshape(eligibleLong and barstate.isconfirmed, title = "Confirmed long setup", style = shape.diamond, location = location.belowbar, color = vdBull, size = size.tiny)
plotshape(eligibleShort and barstate.isconfirmed, title = "Confirmed short setup", style = shape.diamond, location = location.abovebar, color = vdBear, size = size.tiny)
var array<label> vdLabels = array.new<label>()
var array<string> vdTape = array.new<string>()
var array<color> vdTapeColors = array.new<color>()
if barstate.isconfirmed
if bull.event != ""
f_vdTape(vdTape, vdTapeColors, true, bull.event, bull.why)
if showLabels
array.push(vdLabels, f_vdLabel(true, bull))
if bear.event != ""
f_vdTape(vdTape, vdTapeColors, false, bear.event, bear.why)
if showLabels
array.push(vdLabels, f_vdLabel(false, bear))
while array.size(vdLabels) > maxEventLabels
label.delete(array.shift(vdLabels))
string vdPosition = switch dashboardPosition
"Bottom right" => position.bottom_right
"Top left" => position.top_left
"Bottom left" => position.bottom_left
=> position.top_right
var table vdDesk = table.new(vdPosition, 3, 21, bgcolor = vdInk, frame_color = vdGrid, frame_width = 1, border_color = vdInk, border_width = 1)
if barstate.isfirst
table.merge_cells(vdDesk, 0, 0, 2, 0)
table.merge_cells(vdDesk, 0, 1, 2, 1)
table.merge_cells(vdDesk, 0, 2, 2, 2)
table.merge_cells(vdDesk, 0, 13, 2, 13)
table.merge_cells(vdDesk, 0, 14, 2, 14)
for row = 15 to 20
table.merge_cells(vdDesk, 0, row, 2, row)
if barstate.islast or barstate.islastconfirmedhistory
if showDashboard
f_vdCell(vdDesk, 0, 0, "WYCKOFF / EVIDENCE DESK", vdText, vdPanel, "Two independent, sequential hypotheses. Evidence scores are rule-based support, not calibrated probabilities.")
f_vdCell(vdDesk, 0, 1, vdMode, vdAccent, color.new(vdAccent, 91))
f_vdCell(vdDesk, 0, 2, vdContested ? "TWO ACTIVE PATHS · NO SINGLE PHASE RIBBON" : f_vdRibbon(vdPhase), vdAccent, vdInk)
f_vdCell(vdDesk, 0, 3, "HYPOTHESIS", vdMuted, vdPanel)
f_vdCell(vdDesk, 1, 3, "ACCUMULATION", vdBull, vdPanel)
f_vdCell(vdDesk, 2, 3, "DISTRIBUTION", vdBear, vdPanel)
f_vdCell(vdDesk, 0, 4, "Phase", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 4, f_vdPhase(bull.phase), vdBullActive ? vdBull : vdMuted, vdInk)
f_vdCell(vdDesk, 2, 4, f_vdPhase(bear.phase), vdBearActive ? vdBear : vdMuted, vdInk)
f_vdCell(vdDesk, 0, 5, "Evidence", vdMuted, vdPanel, "Heuristic support from the configured rules; 80/100 does not mean an 80% chance of success.")
f_vdCell(vdDesk, 1, 5, f_vdScore(bull.score), vdBull, vdPanel)
f_vdCell(vdDesk, 2, 5, f_vdScore(bear.score), vdBear, vdPanel)
f_vdCell(vdDesk, 0, 6, "Range / limits ⓘ", vdMuted, vdInk, "Hover either hypothesis for its invalidation boundaries and, in Phase E, its structural trail. Live ATR-based boundaries can move until bar close.")
f_vdCell(vdDesk, 1, 6, vdBullRange ? f_vdPrice(bull.lo) + " – " + f_vdPrice(bull.hi) : vdBullActive ? "Awaiting AR" : "—", vdText, vdInk, f_vdBoundaryTip(bull, true))
f_vdCell(vdDesk, 2, 6, vdBearRange ? f_vdPrice(-bear.hi) + " – " + f_vdPrice(-bear.lo) : vdBearActive ? "Awaiting AR" : "—", vdText, vdInk, f_vdBoundaryTip(bear, false))
f_vdCell(vdDesk, 0, 7, "Test", vdMuted, vdPanel)
f_vdCell(vdDesk, 1, 7, f_vdTest(bull), bull.pending == 1 ? vdGold : vdText, vdPanel)
f_vdCell(vdDesk, 2, 7, f_vdTest(bear), bear.pending == 1 ? vdGold : vdText, vdPanel)
f_vdCell(vdDesk, 0, 8, "Latest event", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 8, bull.lastEvent == "" ? "—" : bull.lastEvent, f_vdFailed(bull.lastEvent) ? vdGold : vdBull, vdInk)
f_vdCell(vdDesk, 2, 8, bear.lastEvent == "" ? "—" : bear.lastEvent, f_vdFailed(bear.lastEvent) ? vdGold : vdBear, vdInk)
string contextName = not useContext ? "OFF" : htfBias == 1 ? "BULLISH" : htfBias == -1 ? "BEARISH" : "NEUTRAL"
f_vdCell(vdDesk, 0, 9, "Context · " + contextTf, vdMuted, vdPanel, "Context is read from confirmed higher-timeframe data.")
f_vdCell(vdDesk, 1, 9, contextName, not useContext ? vdMuted : htfBias == 1 ? vdBull : htfBias == -1 ? vdBear : vdGold, vdPanel)
f_vdCell(vdDesk, 2, 9, useContext ? "CLOSED HTF" : "FILTER OFF", vdMuted, vdPanel)
f_vdCell(vdDesk, 0, 10, "Volume / flow", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 10, hasVolume ? "RVOL " + str.tostring(rvol, "0.00") + "×" : "VOLUME UNAVAILABLE", hasVolume ? vdText : vdGold, vdInk)
f_vdCell(vdDesk, 2, 10, footprintBuild ? na(fpDelta) ? "FOOTPRINT N/A" : "Δ VOL " + (fpDelta > 0 ? "+" : "") + str.tostring(fpDelta * 100.0, "0.0") + "%" : "OHLCV", footprintBuild and na(fpDelta) ? vdGold : vdMuted, vdInk, "Footprint delta divided by total classified volume. +25.0% means a +0.25 ratio, not 25 contracts or a probability.")
f_vdCell(vdDesk, 0, 11, "Confirmed setup", vdMuted, vdPanel, "This row is true only on a confirmed eligible setup event. It is not a standing instruction or a profitability estimate.")
f_vdCell(vdDesk, 1, 11, eligibleLong and barstate.isconfirmed ? "LONG · NEW EVENT" : "WAIT", eligibleLong and barstate.isconfirmed ? vdBull : vdMuted, vdPanel)
f_vdCell(vdDesk, 2, 11, eligibleShort and barstate.isconfirmed ? "SHORT · NEW EVENT" : "WAIT", eligibleShort and barstate.isconfirmed ? vdBear : vdMuted, vdPanel)
f_vdCell(vdDesk, 0, 12, "Evidence note", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 12, f_vdBrief(bull.why, 26), vdMuted, vdInk, bull.why)
f_vdCell(vdDesk, 2, 12, f_vdBrief(bear.why, 26), vdMuted, vdInk, bear.why)
f_vdCell(vdDesk, 0, 13, barstate.isconfirmed ? "BAR CLOSED · SCORE ≠ PROBABILITY" : "LIVE BAR · EVENTS WAIT FOR CLOSE", barstate.isconfirmed ? vdMuted : vdGold, vdPanel)
f_vdCell(vdDesk, 0, 14, showTape ? "EVENT TAPE · NEWEST FIRST · EXCHANGE TIME" : "", vdMuted, showTape ? vdPanel : color.new(vdInk, 100))
for index = 0 to 5
bool hasEntry = showTape and index < array.size(vdTape)
string entry = hasEntry ? array.get(vdTape, index) : showTape and index == 0 ? "No confirmed events in loaded history." : ""
color entryColor = hasEntry ? array.get(vdTapeColors, index) : vdMuted
f_vdCell(vdDesk, 0, 15 + index, entry, entryColor, showTape ? index % 2 == 0 ? vdInk : vdPanel : color.new(vdInk, 100))
else
table.clear(vdDesk, 0, 0, 2, 20)
Uses classified buy/sell volume as one extra clue. Requires TradingView Premium or Ultimate and available Footprint data. Base works separately.
Unavailable data means no extra evidence. It is not zero delta or verified exchange aggressor flow. See TradingView’s Footprint documentation.
Copy the whole file. No coding needed.
//@version=6
// Wyckoff Evidence Desk 1.0.0. Original heuristic interpretation, not a probability or profit claim.
indicator("Wyckoff Evidence Desk · Footprint", "WED FP", overlay = true, behind_chart = false, max_labels_count = 120, max_boxes_count = 4, max_lines_count = 20, max_bars_back = 2000)
const bool footprintBuild = true
int fpTicks = input.int(100, "Footprint row ticks", minval = 1, group = "00 · Footprint · Premium / Ultimate")
footprint fp = request.footprint(fpTicks)
float fpTotal = not na(fp) ? fp.total_volume() : na
float fpDelta = not na(fpTotal) and fpTotal > 0 ? fp.delta() / fpTotal : na
// Direction-classified lower-timeframe volume; not verified exchange aggressor flow.
// Causal, direction-normalized engine. The same code runs both hypotheses.
// All episode state changes occur only for committed frames. No future bars are read.
type WConfig
int minB = 8
int reclaimBars = 3
int maxAge = 240
float tolerance = 0.25
float maxSweep = 1.5
float minWidth = 2.5
float trailAtr = 3.0
bool allowBootstrap = true
type WBar
int i = 0
bool closed = true
float o = na
float h = na
float l = na
float c = na
float atr = na
float rv = na
float vol = na
bool volumeOk = false
bool climax = false
bool quiet = false
float rangeLo = na
float rangeHi = na
float ph = na
float pl = na
float pivotVolume = na
float pivotRv = na
int pivotIndex = 0
float spreadRatio = na
bool contraction = false
int context = 0
float delta = na
type WEngine
int dir = 1
int phase = 0
int stage = 0
float lo = na
float hi = na
int born = na
int climaxBar = na
float climaxVolume = na
int arBar = na
int bStarted = na
bool bootstrap = false
int pending = 0
int sweepBar = na
float sweepExtreme = na
float sweepRv = na
int sweepKind = 0
bool testPassed = false
bool testObserved = false
int strengthBar = na
int breakout = na
int outside = 0
int invalidCloses = 0
bool backedUp = false
float trail = na
float lastSwingHigh = na
float pvSum = 0.0
float vSum = 0.0
float vwap = na
float score = 0.0
string event = ""
string lastEvent = "Waiting"
string why = "Waiting for exhaustion or a balanced range"
int resetAt = -10000
int lastTest = -10000
int lastProcessed = -1
method tag(WEngine e, string name, string reason) =>
e.event := name
e.lastEvent := name
e.why := reason
method clearEpisode(WEngine e, int i, string reason) =>
e.phase := 0
e.stage := 0
e.lo := na
e.hi := na
e.born := na
e.climaxBar := na
e.climaxVolume := na
e.arBar := na
e.bStarted := na
e.bootstrap := false
e.pending := 0
e.sweepBar := na
e.sweepExtreme := na
e.sweepRv := na
e.sweepKind := 0
e.testPassed := false
e.testObserved := false
e.strengthBar := na
e.breakout := na
e.outside := 0
e.invalidCloses := 0
e.backedUp := false
e.trail := na
e.lastSwingHigh := na
e.pvSum := 0.0
e.vSum := 0.0
e.vwap := na
e.score := 0.0
e.resetAt := i
e.lastTest := -10000
e.tag("INVALID", reason)
method startEpisode(WEngine e, WBar b, bool bootstrap) =>
e.born := b.i
e.bootstrap := bootstrap
e.climaxBar := b.i
e.climaxVolume := b.volumeOk ? b.vol : na
e.lo := bootstrap ? b.rangeLo : b.l
e.hi := bootstrap ? b.rangeHi : na
e.phase := bootstrap ? 2 : 1
e.stage := bootstrap ? 3 : 1
e.bStarted := bootstrap ? b.i : na
e.tag(bootstrap ? "TR?" : e.dir == 1 ? "SC?" : "BC?", bootstrap ? "Balanced range bootstrap; Phase A was not observed" : "Exhaustion candidate; waiting for a confirmed automatic reaction")
method step(WEngine e, WBar b, WConfig cfg) =>
if b.closed and b.i > e.lastProcessed
e.lastProcessed := b.i
e.event := ""
float tol = b.atr * cfg.tolerance
float width = e.hi - e.lo
int enteredPhase = e.phase
bool inside = not na(e.hi) and b.c >= e.lo and b.c <= e.hi
float closeLocation = b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5
bool pivotLowEffort = not na(b.pivotRv) and b.pivotRv < 1.15
bool demand = b.c > b.o and closeLocation >= 0.65 and b.spreadRatio >= 1.05 and (not b.volumeOk or b.rv >= 1.1)
bool expired = e.phase > 0 and e.phase < 5 and b.i - e.born > cfg.maxAge
if e.phase > 0 and not na(e.hi)
e.invalidCloses := b.c < e.lo - tol ? e.invalidCloses + 1 : 0
bool invalid = e.phase > 0 and e.invalidCloses > cfg.reclaimBars
bool deepAdverse = e.phase > 0 and not na(e.hi) and b.l < e.lo - cfg.maxSweep * b.atr
bool bothEdges = e.phase > 0 and not na(e.hi) and b.l < e.lo - tol and b.h > e.hi + tol
bool trendFailed = e.phase == 5 and (b.c < e.trail or b.c < e.hi - tol)
if expired or invalid or deepAdverse
e.clearEpisode(b.i, expired ? "Range expired before acceptance" : deepAdverse ? "Adverse excursion exceeded maximum structural depth" : "Sustained acceptance beyond the invalidation boundary")
else if trendFailed
e.clearEpisode(b.i, b.c < e.trail ? "Confirmed close crossed the one-way structural trail" : "Backup failed: close crossed the breakout boundary")
else if bothEdges
e.pending := 0
e.testPassed := false
e.testObserved := false
if e.phase >= 3 and e.phase < 5
e.phase := 2
e.bStarted := b.i
e.breakout := na
e.outside := 0
e.tag("AMBIGUOUS", "This bar crossed both range edges; OHLC cannot establish the sequence")
else if e.phase == 0
if b.i - e.resetAt > 3 and not na(b.atr) and b.atr > 0
if b.climax
e.startEpisode(b, false)
else if cfg.allowBootstrap and b.quiet and b.rangeHi - b.rangeLo >= cfg.minWidth * b.atr and b.c >= b.rangeLo and b.c <= b.rangeHi
e.startEpisode(b, true)
else if b.volumeOk and b.rv >= 1.5 and closeLocation >= 0.65 and b.i - e.lastTest > 8
e.lastTest := b.i
e.tag(e.dir == 1 ? "PS?" : "PSY?", "Preliminary stopping activity; no range has been established")
else if e.phase == 1
if e.stage == 1
// SC/BC is provisional until AR establishes the second boundary.
if b.l < e.lo
e.lo := b.l
e.climaxBar := b.i
e.climaxVolume := b.volumeOk ? b.vol : na
if not na(b.ph) and b.pivotIndex > e.climaxBar and b.ph - e.lo >= cfg.minWidth * b.atr
e.hi := b.ph
e.arBar := b.pivotIndex
e.stage := 2
e.tag("AR", "Confirmed swing establishes the other boundary; range is now frozen")
else if e.stage == 2
bool test = not na(b.pl) and b.pivotIndex > e.arBar and b.pl >= e.lo - tol and b.pl <= e.lo + width * 0.3 and inside
bool comparableVolume = not na(b.pivotVolume) and b.pivotVolume > 0 and not na(e.climaxVolume) and e.climaxVolume > 0
bool lessVolume = comparableVolume ? b.pivotVolume < e.climaxVolume : not b.volumeOk
if test and lessVolume
e.phase := 2
e.stage := 3
e.bStarted := b.i
e.lastTest := b.i
e.tag("ST", b.volumeOk ? "Secondary test on lower volume; cause-building hypothesis begins" : "Price-only secondary test; volume unavailable")
else if e.phase == 2 or e.phase == 3
bool mature = b.i - e.bStarted >= cfg.minB
if e.pending == 1
e.sweepExtreme := math.min(e.sweepExtreme, b.l)
e.sweepRv := b.volumeOk ? math.max(nz(e.sweepRv, b.rv), b.rv) : e.sweepRv
bool tooDeep = e.lo - e.sweepExtreme > cfg.maxSweep * b.atr
if b.i - e.sweepBar > cfg.reclaimBars or tooDeep
e.pending := 0
e.phase := 2
e.testPassed := false
e.tag("SWEEP FAILED", "Reclaim window expired or excursion exceeded the allowed depth")
else if b.c >= e.lo and closeLocation >= 0.55
e.pending := 0
e.phase := 3
e.sweepKind := na(e.sweepRv) ? 0 : e.sweepRv >= 2 ? 1 : e.sweepRv >= 1 ? 2 : 3
e.testPassed := e.sweepKind == 3
e.tag(e.dir == 1 ? "SPRING" : "UTAD", b.c > e.hi ? "Recovered beyond the opposite edge; test and strength rules still apply" : e.testPassed ? "Low-volume reclaim confirmed; subsequent strength still required" : "Reclaim confirmed; waiting for a later lower-effort test")
else if mature and b.l < e.lo - tol and b.c <= e.hi and e.lo - b.l <= cfg.maxSweep * b.atr
e.pending := 1
e.sweepBar := b.i
e.sweepExtreme := b.l
e.sweepRv := b.rv
e.testPassed := false
e.testObserved := false
e.tag("SWEEP?", "Boundary breach observed; waiting for an actual close back inside")
if inside and closeLocation >= 0.55
e.pending := 0
e.phase := 3
e.sweepKind := not b.volumeOk ? 0 : b.rv >= 2 ? 1 : b.rv >= 1 ? 2 : 3
e.testPassed := e.sweepKind == 3
e.tag(e.dir == 1 ? "SPRING" : "UTAD", e.testPassed ? "Same-bar low-volume reclaim confirmed; waiting for strength" : "Same-bar reclaim confirmed; later test required")
else if e.phase == 3 and not e.testPassed
bool higherTest = not na(b.pl) and b.pivotIndex > e.sweepBar and b.pl > e.sweepExtreme + tol and b.pl <= e.lo + width * 0.4 and inside
bool dryTest = not na(b.pivotRv) and b.pivotRv < math.min(nz(e.sweepRv, 1), 1.15)
bool priceOnlyTest = e.sweepKind == 0 and not b.volumeOk and na(b.pivotRv)
if higherTest and (dryTest or priceOnlyTest)
e.testPassed := true
e.testObserved := true
e.lastTest := b.i
e.tag("TEST", priceOnlyTest ? "Structural test only; lower-effort volume evidence is unavailable" : "Confirmed higher-low / lower-high test on reduced relative volume")
else if mature and demand and b.c > math.max(nz(e.lastSwingHigh, e.lo + width * 0.65), e.lo + width * 0.5) + tol and (e.phase == 2 or e.testPassed)
e.phase := 4
e.strengthBar := b.i
e.tag(e.dir == 1 ? "SOS" : "SOW", enteredPhase == 2 ? "Directional dominance; no terminal sweep required" : "Directional dominance on expanded effort and result")
if b.c > e.hi + tol
e.breakout := b.i
e.outside := 1
e.tag(e.dir == 1 ? "JAC" : "FTI", "First decisive close beyond the frozen range; acceptance is pending")
else if e.phase == 2 and inside and b.i - e.lastTest > 5
if not na(b.pl) and b.pivotIndex > e.bStarted and b.pl <= e.lo + width * 0.3 and b.pl >= e.lo - tol and pivotLowEffort
e.lastTest := b.i
e.tag("ST-B", "Range-edge test during cause building")
else if b.h > e.hi + tol
e.lastTest := b.i
e.tag(e.dir == 1 ? "UTA" : "mSOW", "Opposite edge rejected back into the range; direction remains unresolved")
else if e.phase == 4
if na(e.breakout)
if demand and b.c > e.hi + tol
e.breakout := b.i
e.outside := 1
e.tag(e.dir == 1 ? "JAC" : "FTI", "Breakout close observed; a second outside close is required")
else if not na(b.pl) and b.pivotIndex > e.strengthBar and b.pl > nz(e.sweepExtreme, e.lo) + tol and inside and pivotLowEffort and b.i - e.lastTest > 5
e.lastTest := b.i
e.tag(e.dir == 1 ? "LPS" : "LPSY", "Confirmed pullback holds above structural origin on reduced effort")
else
e.outside := b.c > e.hi + tol ? e.outside + 1 : 0
if e.outside >= 2
e.phase := 5
e.trail := math.max(e.lo, b.c - cfg.trailAtr * b.atr)
e.tag("ACCEPT", "Two consecutive closes accepted outside the range; awaiting backup")
else if b.c < e.hi - tol or b.i - e.breakout > cfg.reclaimBars
e.breakout := na
e.outside := 0
e.phase := 2
e.bStarted := b.i
e.testPassed := false
e.tag("BREAKOUT FAILED", "Outside acceptance failed; cause-building hypothesis restarted")
else if e.phase == 5
if b.c < e.trail
e.clearEpisode(b.i, "Confirmed close crossed the one-way structural trail")
else if b.c < e.hi - tol
e.clearEpisode(b.i, "Backup failed: price closed back through the breakout boundary")
else
e.trail := math.max(e.trail, b.c - cfg.trailAtr * b.atr)
bool retest = b.i > e.breakout and b.l <= e.hi + tol and b.l >= e.hi - cfg.maxSweep * b.atr and b.c >= e.hi and b.c > b.o
if retest and not e.backedUp and (not b.volumeOk or b.rv <= 1.3)
e.backedUp := true
e.tag("BU", b.volumeOk ? "Accepted breakout retested on controlled effort; one setup per episode" : "Price-only backup; volume confirmation unavailable")
// AVWAP anchors at the observed episode start; missing volume is never invented.
if e.phase > 0
if b.volumeOk
e.pvSum += (b.h + b.l + b.c) / 3 * b.vol
e.vSum += b.vol
e.vwap := e.pvSum / e.vSum
if not na(b.ph) and b.pivotIndex >= e.born and b.ph < nz(e.hi, b.ph + 1)
e.lastSwingHigh := b.ph
float base = e.phase == 1 ? 12 : e.phase == 2 ? 25 : e.phase == 3 ? 40 : e.phase == 4 ? 55 : 65
float support = (e.testPassed ? 8 : 0) + (b.contraction ? 5 : 0) + (not na(e.vwap) and b.c > e.vwap ? 5 : 0) + (b.context == 1 ? 7 : b.context == -1 ? -8 : 0) + (closeLocation >= 0.65 ? 5 : 0) + (not na(b.delta) and b.delta > 0.1 ? 5 : 0)
e.score := math.max(0, math.min(100, base + support - (e.bootstrap ? 5 : 0) - (b.volumeOk ? 0 : 15) - (e.pending == 1 ? 8 : 0)))
e.phase
const string G_ENGINE = "01 · Detection"
int swingBars = input.int(3, "Confirmed swing bars (latency)", minval = 2, maxval = 10, group = G_ENGINE)
int baseline = input.int(50, "Adaptive baseline", minval = 20, maxval = 200, group = G_ENGINE)
int rangeBars = input.int(30, "Balanced-range lookback", minval = 15, maxval = 100, group = G_ENGINE)
bool allowBootstrap = input.bool(true, "Allow balanced-range bootstrap", tooltip = "Recognizes continuation ranges without inventing historical SC/BC. Both directions may remain plausible.", group = G_ENGINE)
int minCause = input.int(8, "Minimum cause-building bars", minval = 3, maxval = 80, group = G_ENGINE)
int reclaimWindow = input.int(3, "Reclaim deadline (closed bars)", minval = 1, maxval = 5, group = G_ENGINE)
int maxAge = input.int(240, "Expire range after bars", minval = 40, maxval = 1500, group = G_ENGINE)
float tolerance = input.float(0.25, "Boundary tolerance / ATR", minval = 0.05, maxval = 1, step = 0.05, group = G_ENGINE)
float maxSweep = input.float(1.5, "Maximum sweep depth / ATR", minval = 0.5, maxval = 4, step = 0.1, group = G_ENGINE)
float minWidth = input.float(2.5, "Minimum range width / ATR", minval = 1, maxval = 8, step = 0.25, group = G_ENGINE)
float trailMultiple = input.float(3, "One-way trail / ATR", minval = 1, maxval = 8, step = 0.25, group = G_ENGINE)
const string G_CONTEXT = "02 · Evidence and setup gates"
bool useContext = input.bool(true, "Use confirmed higher-timeframe context", group = G_CONTEXT)
string contextInput = input.timeframe("", "Context timeframe (blank = automatic 4×)", group = G_CONTEXT)
bool requireVolume = input.bool(true, "Require volume for qualified backup", tooltip = "Price-only hypotheses remain visible. Missing/zero volume cannot qualify a setup when enabled.", group = G_CONTEXT)
float minEvidence = input.float(65, "Minimum backup evidence score", minval = 40, maxval = 95, group = G_CONTEXT)
float leadNeeded = input.float(10, "Minimum lead over opposing hypothesis", minval = 0, maxval = 40, group = G_CONTEXT)
bool enableJson = input.bool(false, "Emit JSON event alerts", group = G_CONTEXT)
float atr = ta.atr(14)
float volMedian = ta.median(volume, baseline)[1]
bool hasVolume = not na(volume) and volume > 0 and not na(volMedian) and volMedian > 0
float rvol = hasVolume ? volume / volMedian : na
float spread = high - low
float spreadMedian = ta.median(spread, baseline)[1]
float spreadRatio = spreadMedian > 0 ? spread / spreadMedian : 0
float volumeExtreme = ta.percentile_nearest_rank(volume, baseline, 90)[1]
float spreadExtreme = ta.percentile_nearest_rank(spread, baseline, 90)[1]
float ph = ta.pivothigh(high, swingBars, swingBars)
float pl = ta.pivotlow(low, swingBars, swingBars)
float recentHi = ta.highest(high, rangeBars)[1]
float recentLo = ta.lowest(low, rangeBars)[1]
float pathLength = math.sum(math.abs(ta.change(close)), rangeBars)[1]
float efficiency = pathLength > 0 ? math.abs(close[1] - close[rangeBars + 1]) / pathLength : 1
float fastVolume = ta.sma(volume, 8)
float slowVolume = ta.sma(volume, 30)
bool contraction = hasVolume and fastVolume < slowVolume * 0.9
float trend = ta.ema(close, 34)
bool priorDown = close[1] < trend[1] and trend[1] < trend[6]
bool priorUp = close[1] > trend[1] and trend[1] > trend[6]
bool quiet = efficiency < 0.3 and recentHi - recentLo <= atr * 10
bool extreme = hasVolume and volume >= volumeExtreme and rvol >= 1.8 and spread >= spreadExtreme and spreadRatio >= 1.3
float closeLocation = spread > 0 ? (close - low) / spread : 0.5
bool warm = bar_index > math.max(baseline, rangeBars) + 35 and not na(atr)
string contextTf = contextInput == "" ? timeframe.from_seconds(int(timeframe.in_seconds() * 4)) : contextInput
if barstate.isfirst and not chart.is_standard
runtime.error("Use standard time-based candles. Synthetic prices are unsuitable for this analysis.")
if barstate.isfirst and useContext and timeframe.in_seconds(contextTf) <= timeframe.in_seconds()
runtime.error("Context timeframe must be higher than the chart timeframe.")
[htfClose, htfMean, htfOldMean] = request.security(syminfo.tickerid, contextTf, [close[1], ta.ema(close, 34)[1], ta.ema(close, 34)[2]], lookahead = barmerge.lookahead_on)
int htfBias = not useContext ? 0 : htfClose > htfMean and htfMean > htfOldMean ? 1 : htfClose < htfMean and htfMean < htfOldMean ? -1 : 0
f_frame(int direction) =>
WBar b = WBar.new()
b.i := bar_index
b.closed := barstate.isconfirmed and warm
b.o := direction * open
b.h := direction == 1 ? high : -low
b.l := direction == 1 ? low : -high
b.c := direction * close
b.atr := atr
b.rv := rvol
b.vol := volume
b.volumeOk := hasVolume
b.climax := extreme and (direction == 1 ? priorDown and low < recentLo and closeLocation >= 0.4 : priorUp and high > recentHi and closeLocation <= 0.6)
b.quiet := quiet
b.rangeLo := direction == 1 ? recentLo : -recentHi
b.rangeHi := direction == 1 ? recentHi : -recentLo
b.ph := direction == 1 ? ph : -pl
b.pl := direction == 1 ? pl : -ph
b.pivotVolume := volume[swingBars]
b.pivotRv := rvol[swingBars]
b.pivotIndex := bar_index - swingBars
b.spreadRatio := spreadRatio
b.contraction := contraction
b.context := direction * htfBias
b.delta := direction * fpDelta
b
var WEngine bull = WEngine.new(1)
var WEngine bear = WEngine.new(-1)
WConfig config = WConfig.new(minCause, reclaimWindow, maxAge, tolerance, maxSweep, minWidth, trailMultiple, allowBootstrap)
bull.step(f_frame(1), config)
bear.step(f_frame(-1), config)
bool eligibleLong = barstate.isconfirmed and warm and bull.event == "BU" and bull.score >= minEvidence and bull.score - bear.score >= leadNeeded and (not requireVolume or hasVolume) and (not useContext or htfBias >= 0)
bool eligibleShort = barstate.isconfirmed and warm and bear.event == "BU" and bear.score >= minEvidence and bear.score - bull.score >= leadNeeded and (not requireVolume or hasVolume) and (not useContext or htfBias <= 0)
alertcondition(eligibleLong, "Qualified bullish backup", "Wyckoff Evidence Desk: qualified bullish backup on {{ticker}} {{interval}}. Evidence is not a probability.")
alertcondition(eligibleShort, "Qualified bearish backup", "Wyckoff Evidence Desk: qualified bearish backup on {{ticker}} {{interval}}. Evidence is not a probability.")
alertcondition(barstate.isconfirmed and (bull.event == "SPRING" or bear.event == "UTAD"), "Confirmed reclaim", "Wyckoff boundary reclaim confirmed on {{ticker}} {{interval}}; inspect test and acceptance status.")
if enableJson and barstate.isconfirmed
for side in array.from(bull, bear)
if side.event != ""
string payload = '{"schema":"wed.v1","symbol":"' + syminfo.tickerid + '","timeframe":"' + timeframe.period + '","barTime":' + str.tostring(time) + ',"side":' + str.tostring(side.dir) + ',"phase":' + str.tostring(side.phase) + ',"event":"' + side.event + '","evidence":' + str.tostring(side.score, "#.0") + ',"volumeAvailable":' + str.tostring(hasVolume) + '}'
alert(payload, alert.freq_all)
// Numeric exports for Data Window and Pine Screener. Scores are uncalibrated rules.
plot(bull.phase, "Accumulation phase 0-5", display = display.data_window)
plot(bear.phase, "Distribution phase 0-5", display = display.data_window)
plot(bull.score, "Accumulation evidence 0-100", display = display.data_window)
plot(bear.score, "Distribution evidence 0-100", display = display.data_window)
plot(eligibleLong ? 1 : eligibleShort ? -1 : 0, "Qualified backup direction", display = display.data_window)
// ── Evidence desk · chart presentation ────────────────────────────────────────
string visualGroup = "05 · Evidence desk"
bool showDashboard = input.bool(true, "Evidence dashboard", group = visualGroup)
bool showTape = input.bool(true, "Last six confirmed events", group = visualGroup)
bool showLabels = input.bool(true, "Confirmed event labels", group = visualGroup)
bool showRange = input.bool(true, "Active hypothesis ranges", group = visualGroup)
bool showVwap = input.bool(true, "Event-anchored VWAP", group = visualGroup)
bool showTrail = input.bool(true, "Phase E structural trail", group = visualGroup)
bool showBars = input.bool(false, "Color bars from phase C onward", group = visualGroup)
int maxEventLabels = input.int(60, "Retained event labels", minval = 10, maxval = 100, group = visualGroup)
string dashboardPosition = input.string("Top right", "Dashboard position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = visualGroup)
color vdInk = #0B151C
color vdPanel = #101F29
color vdGrid = #223642
color vdText = #DCE9ED
color vdMuted = #819AA5
color vdBull = #44D9C0
color vdBear = #FF8177
color vdGold = #EDC778
f_vdPhase(int phase) =>
switch phase
1 => "A · stopping action"
2 => "B · range building"
3 => "C · test / sweep"
4 => "D · directional proof"
5 => "E · follow-through"
=> "SCAN · no range"
f_vdRibbon(int phase) =>
phase == 0 ? "SCAN · WAIT FOR STRUCTURE" : (phase == 1 ? "[A]" : " A ") + " — " + (phase == 2 ? "[B]" : " B ") + " — " + (phase == 3 ? "[C]" : " C ") + " — " + (phase == 4 ? "[D]" : " D ") + " — " + (phase == 5 ? "[E]" : " E ")
f_vdFailed(string eventName) =>
str.contains(str.upper(eventName), "FAIL") or str.contains(str.upper(eventName), "INVALID") or str.contains(str.upper(eventName), "EXPIRE")
f_vdPrice(float value) =>
na(value) ? "—" : str.tostring(value, format.mintick)
f_vdScore(float value) =>
float bounded = math.max(0.0, math.min(100.0, nz(value)))
int blocks = int(math.round(bounded / 20.0))
string meter = ""
for block = 1 to 5
meter += block <= blocks ? "■" : "·"
str.tostring(bounded, "0") + " / 100 " + meter
f_vdBrief(string value, int limit) =>
str.length(value) > limit ? str.substring(value, 0, limit - 1) + "…" : value
f_vdTest(WEngine engine) =>
engine.phase == 0 ? "Awaiting range" : engine.pending == 1 ? "Reclaim pending" : engine.testPassed and engine.testObserved ? "Test confirmed" : engine.testPassed ? "Type 3 · test waived" : engine.phase >= 4 ? "Direct strength path" : "No confirmed test"
f_vdBoundaryTip(WEngine engine, bool isBull) =>
float direction = isBull ? 1.0 : -1.0
string side = isBull ? "below " : "above "
string detail = "No active range."
if engine.phase > 0 and na(engine.hi)
detail := "Phase A candidate: awaiting the automatic reaction to establish both range edges. Observed origin: " + f_vdPrice(direction * engine.lo)
else if engine.phase > 0
detail := "Sustained close invalidation: " + side + f_vdPrice(direction * (engine.lo - atr * tolerance)) + ". More than " + str.tostring(reclaimWindow) + " consecutive closes are required.\nMaximum adverse wick boundary: " + f_vdPrice(direction * (engine.lo - atr * maxSweep)) + ". Crossing it invalidates on the confirmed bar."
if engine.phase == 5
detail += "\nPhase E structural trail: " + f_vdPrice(direction * engine.trail) + ".\nBackup failure close boundary: " + side + f_vdPrice(direction * (engine.hi - atr * tolerance)) + ".\nTrail is structural context, not a broker stop order."
detail
f_vdCell(table panel, int column, int row, string value, color foreground, color background, string tooltipText = "") =>
table.cell(panel, column, row, value, text_color = foreground, bgcolor = background, text_size = size.small, text_halign = column == 0 ? text.align_left : text.align_center, tooltip = tooltipText)
f_vdTape(array<string> tape, array<color> colors, bool isBull, string eventName, string reason) =>
string stamp = str.format_time(na(time_close) ? time : time_close, "dd MMM HH:mm", syminfo.timezone)
array.unshift(tape, stamp + " · " + (isBull ? "ACC" : "DIST") + " " + eventName + " · " + f_vdBrief(reason, 28))
array.unshift(colors, f_vdFailed(eventName) ? vdGold : isBull ? vdBull : vdBear)
if array.size(tape) > 6
array.pop(tape)
array.pop(colors)
f_vdLabel(bool isBull, WEngine engine) =>
color tone = f_vdFailed(engine.event) ? vdGold : isBull ? vdBull : vdBear
float y = isBull ? low - nz(atr, high - low) * 0.24 : high + nz(atr, high - low) * 0.24
string tip = (isBull ? "ACCUMULATION" : "DISTRIBUTION") + " · " + engine.event + "\nConfirmed on this bar close.\n" + f_vdPhase(engine.phase) + "\nEvidence: " + str.tostring(engine.score, "0") + " / 100 (heuristic, not probability)\n" + engine.why
label.new(bar_index, y, engine.event, xloc = xloc.bar_index, yloc = yloc.price, style = isBull ? label.style_label_up : label.style_label_down, color = color.new(tone, 12), textcolor = vdInk, size = size.tiny, tooltip = tip)
bool vdBullActive = bull.phase > 0
bool vdBearActive = bear.phase > 0
bool vdBullRange = vdBullActive and not na(bull.lo) and not na(bull.hi)
bool vdBearRange = vdBearActive and not na(bear.lo) and not na(bear.hi)
bool vdContested = vdBullActive and vdBearActive and math.abs(bull.score - bear.score) <= 10.0
bool vdSelectBull = vdBullActive and (not vdBearActive or bull.score >= bear.score)
bool vdSelectBear = vdBearActive and not vdSelectBull
bool vdShowBull = vdBullActive and (vdSelectBull or vdContested)
bool vdShowBear = vdBearActive and (vdSelectBear or vdContested)
bool vdDrawBull = vdBullRange and vdShowBull
bool vdDrawBear = vdBearRange and vdShowBear
int vdPhase = vdSelectBull ? bull.phase : vdSelectBear ? bear.phase : 0
color vdAccent = vdContested ? vdGold : vdSelectBull ? vdBull : vdSelectBear ? vdBear : vdMuted
bool vdReset = not vdBullActive and not vdBearActive and (f_vdFailed(bull.lastEvent) or f_vdFailed(bear.lastEvent))
string vdMode = vdContested ? "CONTESTED · BOTH HYPOTHESES ACTIVE" : vdSelectBull ? "ACCUMULATION · LEADING EVIDENCE" : vdSelectBear ? "DISTRIBUTION · LEADING EVIDENCE" : vdReset ? "RESET → SCAN · AWAIT NEW STRUCTURE" : "SCANNING · AWAIT STOPPING ACTION"
// Reuse two boxes. Their left edge is the confirmation bar that started the range.
var box vdBullBox = na
var box vdBearBox = na
if showRange and vdDrawBull
int left = math.max(nz(bull.born, bar_index), bar_index - 9999)
if na(vdBullBox)
vdBullBox := box.new(left, bull.hi, bar_index + 1, bull.lo, border_color = color.new(vdBull, 26), border_width = 1, bgcolor = color.new(vdBull, 94), text = "", text_color = color.new(vdBull, 12), text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_top)
box.set_lefttop(vdBullBox, left, bull.hi)
box.set_rightbottom(vdBullBox, bar_index + 1, bull.lo)
box.set_border_style(vdBullBox, vdContested ? line.style_dashed : line.style_solid)
box.set_text(vdBullBox, " ACC · " + f_vdPhase(bull.phase) + " · " + str.tostring(bull.score, "0") + "/100")
else if not na(vdBullBox)
box.delete(vdBullBox)
vdBullBox := na
if showRange and vdDrawBear
int left = math.max(nz(bear.born, bar_index), bar_index - 9999)
if na(vdBearBox)
vdBearBox := box.new(left, -bear.lo, bar_index + 1, -bear.hi, border_color = color.new(vdBear, 26), border_width = 1, bgcolor = color.new(vdBear, 94), text = "", text_color = color.new(vdBear, 12), text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_bottom)
box.set_lefttop(vdBearBox, left, -bear.lo)
box.set_rightbottom(vdBearBox, bar_index + 1, -bear.hi)
box.set_border_style(vdBearBox, vdContested ? line.style_dashed : line.style_solid)
box.set_text(vdBearBox, " DIST · " + f_vdPhase(bear.phase) + " · " + str.tostring(bear.score, "0") + "/100")
else if not na(vdBearBox)
box.delete(vdBearBox)
vdBearBox := na
plot(showVwap and vdShowBull ? bull.vwap : na, "Accumulation event AVWAP", color = vdBull, linewidth = 2, style = plot.style_linebr)
plot(showVwap and vdShowBear ? -bear.vwap : na, "Distribution event AVWAP", color = vdBear, linewidth = 2, style = plot.style_linebr)
float vdSelectedTrail = not vdContested and vdPhase == 5 ? vdSelectBull ? bull.trail : -bear.trail : na
plot(showTrail ? vdSelectedTrail : na, "Leading Phase E structural trail", color = vdGold, linewidth = 1, style = plot.style_linebr)
barcolor(showBars and vdPhase >= 3 ? color.new(vdAccent, 24) : na, title = "Evidence phase tint")
plotshape(eligibleLong and barstate.isconfirmed, title = "Confirmed long setup", style = shape.diamond, location = location.belowbar, color = vdBull, size = size.tiny)
plotshape(eligibleShort and barstate.isconfirmed, title = "Confirmed short setup", style = shape.diamond, location = location.abovebar, color = vdBear, size = size.tiny)
var array<label> vdLabels = array.new<label>()
var array<string> vdTape = array.new<string>()
var array<color> vdTapeColors = array.new<color>()
if barstate.isconfirmed
if bull.event != ""
f_vdTape(vdTape, vdTapeColors, true, bull.event, bull.why)
if showLabels
array.push(vdLabels, f_vdLabel(true, bull))
if bear.event != ""
f_vdTape(vdTape, vdTapeColors, false, bear.event, bear.why)
if showLabels
array.push(vdLabels, f_vdLabel(false, bear))
while array.size(vdLabels) > maxEventLabels
label.delete(array.shift(vdLabels))
string vdPosition = switch dashboardPosition
"Bottom right" => position.bottom_right
"Top left" => position.top_left
"Bottom left" => position.bottom_left
=> position.top_right
var table vdDesk = table.new(vdPosition, 3, 21, bgcolor = vdInk, frame_color = vdGrid, frame_width = 1, border_color = vdInk, border_width = 1)
if barstate.isfirst
table.merge_cells(vdDesk, 0, 0, 2, 0)
table.merge_cells(vdDesk, 0, 1, 2, 1)
table.merge_cells(vdDesk, 0, 2, 2, 2)
table.merge_cells(vdDesk, 0, 13, 2, 13)
table.merge_cells(vdDesk, 0, 14, 2, 14)
for row = 15 to 20
table.merge_cells(vdDesk, 0, row, 2, row)
if barstate.islast or barstate.islastconfirmedhistory
if showDashboard
f_vdCell(vdDesk, 0, 0, "WYCKOFF / EVIDENCE DESK", vdText, vdPanel, "Two independent, sequential hypotheses. Evidence scores are rule-based support, not calibrated probabilities.")
f_vdCell(vdDesk, 0, 1, vdMode, vdAccent, color.new(vdAccent, 91))
f_vdCell(vdDesk, 0, 2, vdContested ? "TWO ACTIVE PATHS · NO SINGLE PHASE RIBBON" : f_vdRibbon(vdPhase), vdAccent, vdInk)
f_vdCell(vdDesk, 0, 3, "HYPOTHESIS", vdMuted, vdPanel)
f_vdCell(vdDesk, 1, 3, "ACCUMULATION", vdBull, vdPanel)
f_vdCell(vdDesk, 2, 3, "DISTRIBUTION", vdBear, vdPanel)
f_vdCell(vdDesk, 0, 4, "Phase", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 4, f_vdPhase(bull.phase), vdBullActive ? vdBull : vdMuted, vdInk)
f_vdCell(vdDesk, 2, 4, f_vdPhase(bear.phase), vdBearActive ? vdBear : vdMuted, vdInk)
f_vdCell(vdDesk, 0, 5, "Evidence", vdMuted, vdPanel, "Heuristic support from the configured rules; 80/100 does not mean an 80% chance of success.")
f_vdCell(vdDesk, 1, 5, f_vdScore(bull.score), vdBull, vdPanel)
f_vdCell(vdDesk, 2, 5, f_vdScore(bear.score), vdBear, vdPanel)
f_vdCell(vdDesk, 0, 6, "Range / limits ⓘ", vdMuted, vdInk, "Hover either hypothesis for its invalidation boundaries and, in Phase E, its structural trail. Live ATR-based boundaries can move until bar close.")
f_vdCell(vdDesk, 1, 6, vdBullRange ? f_vdPrice(bull.lo) + " – " + f_vdPrice(bull.hi) : vdBullActive ? "Awaiting AR" : "—", vdText, vdInk, f_vdBoundaryTip(bull, true))
f_vdCell(vdDesk, 2, 6, vdBearRange ? f_vdPrice(-bear.hi) + " – " + f_vdPrice(-bear.lo) : vdBearActive ? "Awaiting AR" : "—", vdText, vdInk, f_vdBoundaryTip(bear, false))
f_vdCell(vdDesk, 0, 7, "Test", vdMuted, vdPanel)
f_vdCell(vdDesk, 1, 7, f_vdTest(bull), bull.pending == 1 ? vdGold : vdText, vdPanel)
f_vdCell(vdDesk, 2, 7, f_vdTest(bear), bear.pending == 1 ? vdGold : vdText, vdPanel)
f_vdCell(vdDesk, 0, 8, "Latest event", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 8, bull.lastEvent == "" ? "—" : bull.lastEvent, f_vdFailed(bull.lastEvent) ? vdGold : vdBull, vdInk)
f_vdCell(vdDesk, 2, 8, bear.lastEvent == "" ? "—" : bear.lastEvent, f_vdFailed(bear.lastEvent) ? vdGold : vdBear, vdInk)
string contextName = not useContext ? "OFF" : htfBias == 1 ? "BULLISH" : htfBias == -1 ? "BEARISH" : "NEUTRAL"
f_vdCell(vdDesk, 0, 9, "Context · " + contextTf, vdMuted, vdPanel, "Context is read from confirmed higher-timeframe data.")
f_vdCell(vdDesk, 1, 9, contextName, not useContext ? vdMuted : htfBias == 1 ? vdBull : htfBias == -1 ? vdBear : vdGold, vdPanel)
f_vdCell(vdDesk, 2, 9, useContext ? "CLOSED HTF" : "FILTER OFF", vdMuted, vdPanel)
f_vdCell(vdDesk, 0, 10, "Volume / flow", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 10, hasVolume ? "RVOL " + str.tostring(rvol, "0.00") + "×" : "VOLUME UNAVAILABLE", hasVolume ? vdText : vdGold, vdInk)
f_vdCell(vdDesk, 2, 10, footprintBuild ? na(fpDelta) ? "FOOTPRINT N/A" : "Δ VOL " + (fpDelta > 0 ? "+" : "") + str.tostring(fpDelta * 100.0, "0.0") + "%" : "OHLCV", footprintBuild and na(fpDelta) ? vdGold : vdMuted, vdInk, "Footprint delta divided by total classified volume. +25.0% means a +0.25 ratio, not 25 contracts or a probability.")
f_vdCell(vdDesk, 0, 11, "Confirmed setup", vdMuted, vdPanel, "This row is true only on a confirmed eligible setup event. It is not a standing instruction or a profitability estimate.")
f_vdCell(vdDesk, 1, 11, eligibleLong and barstate.isconfirmed ? "LONG · NEW EVENT" : "WAIT", eligibleLong and barstate.isconfirmed ? vdBull : vdMuted, vdPanel)
f_vdCell(vdDesk, 2, 11, eligibleShort and barstate.isconfirmed ? "SHORT · NEW EVENT" : "WAIT", eligibleShort and barstate.isconfirmed ? vdBear : vdMuted, vdPanel)
f_vdCell(vdDesk, 0, 12, "Evidence note", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 12, f_vdBrief(bull.why, 26), vdMuted, vdInk, bull.why)
f_vdCell(vdDesk, 2, 12, f_vdBrief(bear.why, 26), vdMuted, vdInk, bear.why)
f_vdCell(vdDesk, 0, 13, barstate.isconfirmed ? "BAR CLOSED · SCORE ≠ PROBABILITY" : "LIVE BAR · EVENTS WAIT FOR CLOSE", barstate.isconfirmed ? vdMuted : vdGold, vdPanel)
f_vdCell(vdDesk, 0, 14, showTape ? "EVENT TAPE · NEWEST FIRST · EXCHANGE TIME" : "", vdMuted, showTape ? vdPanel : color.new(vdInk, 100))
for index = 0 to 5
bool hasEntry = showTape and index < array.size(vdTape)
string entry = hasEntry ? array.get(vdTape, index) : showTape and index == 0 ? "No confirmed events in loaded history." : ""
color entryColor = hasEntry ? array.get(vdTapeColors, index) : vdMuted
f_vdCell(vdDesk, 0, 15 + index, entry, entryColor, showTape ? index % 2 == 0 ? vdInk : vdPanel : color.new(vdInk, 100))
else
table.clear(vdDesk, 0, 0, 2, 20)
A separate strategy tests qualified backups with simulated orders and costs. It uses fixed quantity, not fixed cash risk. Its initial development sample lost money.
Copy the whole file. No coding needed.
//@version=6
// Wyckoff Evidence Desk 1.0.0. Original heuristic interpretation, not a probability or profit claim.
strategy("Wyckoff Evidence Desk · Validation Lab", "WED Lab", overlay = true, behind_chart = false, max_labels_count = 120, max_boxes_count = 4, max_lines_count = 20, max_bars_back = 2000, initial_capital = 100000, pyramiding = 0, default_qty_type = strategy.fixed, default_qty_value = 1, commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 2, process_orders_on_close = false, calc_on_every_tick = false, calc_on_order_fills = false, calc_on_every_history_tick = false, margin_long = 100, margin_short = 100)
const bool footprintBuild = false
float fpDelta = na
// Causal, direction-normalized engine. The same code runs both hypotheses.
// All episode state changes occur only for committed frames. No future bars are read.
type WConfig
int minB = 8
int reclaimBars = 3
int maxAge = 240
float tolerance = 0.25
float maxSweep = 1.5
float minWidth = 2.5
float trailAtr = 3.0
bool allowBootstrap = true
type WBar
int i = 0
bool closed = true
float o = na
float h = na
float l = na
float c = na
float atr = na
float rv = na
float vol = na
bool volumeOk = false
bool climax = false
bool quiet = false
float rangeLo = na
float rangeHi = na
float ph = na
float pl = na
float pivotVolume = na
float pivotRv = na
int pivotIndex = 0
float spreadRatio = na
bool contraction = false
int context = 0
float delta = na
type WEngine
int dir = 1
int phase = 0
int stage = 0
float lo = na
float hi = na
int born = na
int climaxBar = na
float climaxVolume = na
int arBar = na
int bStarted = na
bool bootstrap = false
int pending = 0
int sweepBar = na
float sweepExtreme = na
float sweepRv = na
int sweepKind = 0
bool testPassed = false
bool testObserved = false
int strengthBar = na
int breakout = na
int outside = 0
int invalidCloses = 0
bool backedUp = false
float trail = na
float lastSwingHigh = na
float pvSum = 0.0
float vSum = 0.0
float vwap = na
float score = 0.0
string event = ""
string lastEvent = "Waiting"
string why = "Waiting for exhaustion or a balanced range"
int resetAt = -10000
int lastTest = -10000
int lastProcessed = -1
method tag(WEngine e, string name, string reason) =>
e.event := name
e.lastEvent := name
e.why := reason
method clearEpisode(WEngine e, int i, string reason) =>
e.phase := 0
e.stage := 0
e.lo := na
e.hi := na
e.born := na
e.climaxBar := na
e.climaxVolume := na
e.arBar := na
e.bStarted := na
e.bootstrap := false
e.pending := 0
e.sweepBar := na
e.sweepExtreme := na
e.sweepRv := na
e.sweepKind := 0
e.testPassed := false
e.testObserved := false
e.strengthBar := na
e.breakout := na
e.outside := 0
e.invalidCloses := 0
e.backedUp := false
e.trail := na
e.lastSwingHigh := na
e.pvSum := 0.0
e.vSum := 0.0
e.vwap := na
e.score := 0.0
e.resetAt := i
e.lastTest := -10000
e.tag("INVALID", reason)
method startEpisode(WEngine e, WBar b, bool bootstrap) =>
e.born := b.i
e.bootstrap := bootstrap
e.climaxBar := b.i
e.climaxVolume := b.volumeOk ? b.vol : na
e.lo := bootstrap ? b.rangeLo : b.l
e.hi := bootstrap ? b.rangeHi : na
e.phase := bootstrap ? 2 : 1
e.stage := bootstrap ? 3 : 1
e.bStarted := bootstrap ? b.i : na
e.tag(bootstrap ? "TR?" : e.dir == 1 ? "SC?" : "BC?", bootstrap ? "Balanced range bootstrap; Phase A was not observed" : "Exhaustion candidate; waiting for a confirmed automatic reaction")
method step(WEngine e, WBar b, WConfig cfg) =>
if b.closed and b.i > e.lastProcessed
e.lastProcessed := b.i
e.event := ""
float tol = b.atr * cfg.tolerance
float width = e.hi - e.lo
int enteredPhase = e.phase
bool inside = not na(e.hi) and b.c >= e.lo and b.c <= e.hi
float closeLocation = b.h > b.l ? (b.c - b.l) / (b.h - b.l) : 0.5
bool pivotLowEffort = not na(b.pivotRv) and b.pivotRv < 1.15
bool demand = b.c > b.o and closeLocation >= 0.65 and b.spreadRatio >= 1.05 and (not b.volumeOk or b.rv >= 1.1)
bool expired = e.phase > 0 and e.phase < 5 and b.i - e.born > cfg.maxAge
if e.phase > 0 and not na(e.hi)
e.invalidCloses := b.c < e.lo - tol ? e.invalidCloses + 1 : 0
bool invalid = e.phase > 0 and e.invalidCloses > cfg.reclaimBars
bool deepAdverse = e.phase > 0 and not na(e.hi) and b.l < e.lo - cfg.maxSweep * b.atr
bool bothEdges = e.phase > 0 and not na(e.hi) and b.l < e.lo - tol and b.h > e.hi + tol
bool trendFailed = e.phase == 5 and (b.c < e.trail or b.c < e.hi - tol)
if expired or invalid or deepAdverse
e.clearEpisode(b.i, expired ? "Range expired before acceptance" : deepAdverse ? "Adverse excursion exceeded maximum structural depth" : "Sustained acceptance beyond the invalidation boundary")
else if trendFailed
e.clearEpisode(b.i, b.c < e.trail ? "Confirmed close crossed the one-way structural trail" : "Backup failed: close crossed the breakout boundary")
else if bothEdges
e.pending := 0
e.testPassed := false
e.testObserved := false
if e.phase >= 3 and e.phase < 5
e.phase := 2
e.bStarted := b.i
e.breakout := na
e.outside := 0
e.tag("AMBIGUOUS", "This bar crossed both range edges; OHLC cannot establish the sequence")
else if e.phase == 0
if b.i - e.resetAt > 3 and not na(b.atr) and b.atr > 0
if b.climax
e.startEpisode(b, false)
else if cfg.allowBootstrap and b.quiet and b.rangeHi - b.rangeLo >= cfg.minWidth * b.atr and b.c >= b.rangeLo and b.c <= b.rangeHi
e.startEpisode(b, true)
else if b.volumeOk and b.rv >= 1.5 and closeLocation >= 0.65 and b.i - e.lastTest > 8
e.lastTest := b.i
e.tag(e.dir == 1 ? "PS?" : "PSY?", "Preliminary stopping activity; no range has been established")
else if e.phase == 1
if e.stage == 1
// SC/BC is provisional until AR establishes the second boundary.
if b.l < e.lo
e.lo := b.l
e.climaxBar := b.i
e.climaxVolume := b.volumeOk ? b.vol : na
if not na(b.ph) and b.pivotIndex > e.climaxBar and b.ph - e.lo >= cfg.minWidth * b.atr
e.hi := b.ph
e.arBar := b.pivotIndex
e.stage := 2
e.tag("AR", "Confirmed swing establishes the other boundary; range is now frozen")
else if e.stage == 2
bool test = not na(b.pl) and b.pivotIndex > e.arBar and b.pl >= e.lo - tol and b.pl <= e.lo + width * 0.3 and inside
bool comparableVolume = not na(b.pivotVolume) and b.pivotVolume > 0 and not na(e.climaxVolume) and e.climaxVolume > 0
bool lessVolume = comparableVolume ? b.pivotVolume < e.climaxVolume : not b.volumeOk
if test and lessVolume
e.phase := 2
e.stage := 3
e.bStarted := b.i
e.lastTest := b.i
e.tag("ST", b.volumeOk ? "Secondary test on lower volume; cause-building hypothesis begins" : "Price-only secondary test; volume unavailable")
else if e.phase == 2 or e.phase == 3
bool mature = b.i - e.bStarted >= cfg.minB
if e.pending == 1
e.sweepExtreme := math.min(e.sweepExtreme, b.l)
e.sweepRv := b.volumeOk ? math.max(nz(e.sweepRv, b.rv), b.rv) : e.sweepRv
bool tooDeep = e.lo - e.sweepExtreme > cfg.maxSweep * b.atr
if b.i - e.sweepBar > cfg.reclaimBars or tooDeep
e.pending := 0
e.phase := 2
e.testPassed := false
e.tag("SWEEP FAILED", "Reclaim window expired or excursion exceeded the allowed depth")
else if b.c >= e.lo and closeLocation >= 0.55
e.pending := 0
e.phase := 3
e.sweepKind := na(e.sweepRv) ? 0 : e.sweepRv >= 2 ? 1 : e.sweepRv >= 1 ? 2 : 3
e.testPassed := e.sweepKind == 3
e.tag(e.dir == 1 ? "SPRING" : "UTAD", b.c > e.hi ? "Recovered beyond the opposite edge; test and strength rules still apply" : e.testPassed ? "Low-volume reclaim confirmed; subsequent strength still required" : "Reclaim confirmed; waiting for a later lower-effort test")
else if mature and b.l < e.lo - tol and b.c <= e.hi and e.lo - b.l <= cfg.maxSweep * b.atr
e.pending := 1
e.sweepBar := b.i
e.sweepExtreme := b.l
e.sweepRv := b.rv
e.testPassed := false
e.testObserved := false
e.tag("SWEEP?", "Boundary breach observed; waiting for an actual close back inside")
if inside and closeLocation >= 0.55
e.pending := 0
e.phase := 3
e.sweepKind := not b.volumeOk ? 0 : b.rv >= 2 ? 1 : b.rv >= 1 ? 2 : 3
e.testPassed := e.sweepKind == 3
e.tag(e.dir == 1 ? "SPRING" : "UTAD", e.testPassed ? "Same-bar low-volume reclaim confirmed; waiting for strength" : "Same-bar reclaim confirmed; later test required")
else if e.phase == 3 and not e.testPassed
bool higherTest = not na(b.pl) and b.pivotIndex > e.sweepBar and b.pl > e.sweepExtreme + tol and b.pl <= e.lo + width * 0.4 and inside
bool dryTest = not na(b.pivotRv) and b.pivotRv < math.min(nz(e.sweepRv, 1), 1.15)
bool priceOnlyTest = e.sweepKind == 0 and not b.volumeOk and na(b.pivotRv)
if higherTest and (dryTest or priceOnlyTest)
e.testPassed := true
e.testObserved := true
e.lastTest := b.i
e.tag("TEST", priceOnlyTest ? "Structural test only; lower-effort volume evidence is unavailable" : "Confirmed higher-low / lower-high test on reduced relative volume")
else if mature and demand and b.c > math.max(nz(e.lastSwingHigh, e.lo + width * 0.65), e.lo + width * 0.5) + tol and (e.phase == 2 or e.testPassed)
e.phase := 4
e.strengthBar := b.i
e.tag(e.dir == 1 ? "SOS" : "SOW", enteredPhase == 2 ? "Directional dominance; no terminal sweep required" : "Directional dominance on expanded effort and result")
if b.c > e.hi + tol
e.breakout := b.i
e.outside := 1
e.tag(e.dir == 1 ? "JAC" : "FTI", "First decisive close beyond the frozen range; acceptance is pending")
else if e.phase == 2 and inside and b.i - e.lastTest > 5
if not na(b.pl) and b.pivotIndex > e.bStarted and b.pl <= e.lo + width * 0.3 and b.pl >= e.lo - tol and pivotLowEffort
e.lastTest := b.i
e.tag("ST-B", "Range-edge test during cause building")
else if b.h > e.hi + tol
e.lastTest := b.i
e.tag(e.dir == 1 ? "UTA" : "mSOW", "Opposite edge rejected back into the range; direction remains unresolved")
else if e.phase == 4
if na(e.breakout)
if demand and b.c > e.hi + tol
e.breakout := b.i
e.outside := 1
e.tag(e.dir == 1 ? "JAC" : "FTI", "Breakout close observed; a second outside close is required")
else if not na(b.pl) and b.pivotIndex > e.strengthBar and b.pl > nz(e.sweepExtreme, e.lo) + tol and inside and pivotLowEffort and b.i - e.lastTest > 5
e.lastTest := b.i
e.tag(e.dir == 1 ? "LPS" : "LPSY", "Confirmed pullback holds above structural origin on reduced effort")
else
e.outside := b.c > e.hi + tol ? e.outside + 1 : 0
if e.outside >= 2
e.phase := 5
e.trail := math.max(e.lo, b.c - cfg.trailAtr * b.atr)
e.tag("ACCEPT", "Two consecutive closes accepted outside the range; awaiting backup")
else if b.c < e.hi - tol or b.i - e.breakout > cfg.reclaimBars
e.breakout := na
e.outside := 0
e.phase := 2
e.bStarted := b.i
e.testPassed := false
e.tag("BREAKOUT FAILED", "Outside acceptance failed; cause-building hypothesis restarted")
else if e.phase == 5
if b.c < e.trail
e.clearEpisode(b.i, "Confirmed close crossed the one-way structural trail")
else if b.c < e.hi - tol
e.clearEpisode(b.i, "Backup failed: price closed back through the breakout boundary")
else
e.trail := math.max(e.trail, b.c - cfg.trailAtr * b.atr)
bool retest = b.i > e.breakout and b.l <= e.hi + tol and b.l >= e.hi - cfg.maxSweep * b.atr and b.c >= e.hi and b.c > b.o
if retest and not e.backedUp and (not b.volumeOk or b.rv <= 1.3)
e.backedUp := true
e.tag("BU", b.volumeOk ? "Accepted breakout retested on controlled effort; one setup per episode" : "Price-only backup; volume confirmation unavailable")
// AVWAP anchors at the observed episode start; missing volume is never invented.
if e.phase > 0
if b.volumeOk
e.pvSum += (b.h + b.l + b.c) / 3 * b.vol
e.vSum += b.vol
e.vwap := e.pvSum / e.vSum
if not na(b.ph) and b.pivotIndex >= e.born and b.ph < nz(e.hi, b.ph + 1)
e.lastSwingHigh := b.ph
float base = e.phase == 1 ? 12 : e.phase == 2 ? 25 : e.phase == 3 ? 40 : e.phase == 4 ? 55 : 65
float support = (e.testPassed ? 8 : 0) + (b.contraction ? 5 : 0) + (not na(e.vwap) and b.c > e.vwap ? 5 : 0) + (b.context == 1 ? 7 : b.context == -1 ? -8 : 0) + (closeLocation >= 0.65 ? 5 : 0) + (not na(b.delta) and b.delta > 0.1 ? 5 : 0)
e.score := math.max(0, math.min(100, base + support - (e.bootstrap ? 5 : 0) - (b.volumeOk ? 0 : 15) - (e.pending == 1 ? 8 : 0)))
e.phase
const string G_ENGINE = "01 · Detection"
int swingBars = input.int(3, "Confirmed swing bars (latency)", minval = 2, maxval = 10, group = G_ENGINE)
int baseline = input.int(50, "Adaptive baseline", minval = 20, maxval = 200, group = G_ENGINE)
int rangeBars = input.int(30, "Balanced-range lookback", minval = 15, maxval = 100, group = G_ENGINE)
bool allowBootstrap = input.bool(true, "Allow balanced-range bootstrap", tooltip = "Recognizes continuation ranges without inventing historical SC/BC. Both directions may remain plausible.", group = G_ENGINE)
int minCause = input.int(8, "Minimum cause-building bars", minval = 3, maxval = 80, group = G_ENGINE)
int reclaimWindow = input.int(3, "Reclaim deadline (closed bars)", minval = 1, maxval = 5, group = G_ENGINE)
int maxAge = input.int(240, "Expire range after bars", minval = 40, maxval = 1500, group = G_ENGINE)
float tolerance = input.float(0.25, "Boundary tolerance / ATR", minval = 0.05, maxval = 1, step = 0.05, group = G_ENGINE)
float maxSweep = input.float(1.5, "Maximum sweep depth / ATR", minval = 0.5, maxval = 4, step = 0.1, group = G_ENGINE)
float minWidth = input.float(2.5, "Minimum range width / ATR", minval = 1, maxval = 8, step = 0.25, group = G_ENGINE)
float trailMultiple = input.float(3, "One-way trail / ATR", minval = 1, maxval = 8, step = 0.25, group = G_ENGINE)
const string G_CONTEXT = "02 · Evidence and setup gates"
bool useContext = input.bool(true, "Use confirmed higher-timeframe context", group = G_CONTEXT)
string contextInput = input.timeframe("", "Context timeframe (blank = automatic 4×)", group = G_CONTEXT)
bool requireVolume = input.bool(true, "Require volume for qualified backup", tooltip = "Price-only hypotheses remain visible. Missing/zero volume cannot qualify a setup when enabled.", group = G_CONTEXT)
float minEvidence = input.float(65, "Minimum backup evidence score", minval = 40, maxval = 95, group = G_CONTEXT)
float leadNeeded = input.float(10, "Minimum lead over opposing hypothesis", minval = 0, maxval = 40, group = G_CONTEXT)
bool enableJson = input.bool(false, "Emit JSON event alerts", group = G_CONTEXT)
float atr = ta.atr(14)
float volMedian = ta.median(volume, baseline)[1]
bool hasVolume = not na(volume) and volume > 0 and not na(volMedian) and volMedian > 0
float rvol = hasVolume ? volume / volMedian : na
float spread = high - low
float spreadMedian = ta.median(spread, baseline)[1]
float spreadRatio = spreadMedian > 0 ? spread / spreadMedian : 0
float volumeExtreme = ta.percentile_nearest_rank(volume, baseline, 90)[1]
float spreadExtreme = ta.percentile_nearest_rank(spread, baseline, 90)[1]
float ph = ta.pivothigh(high, swingBars, swingBars)
float pl = ta.pivotlow(low, swingBars, swingBars)
float recentHi = ta.highest(high, rangeBars)[1]
float recentLo = ta.lowest(low, rangeBars)[1]
float pathLength = math.sum(math.abs(ta.change(close)), rangeBars)[1]
float efficiency = pathLength > 0 ? math.abs(close[1] - close[rangeBars + 1]) / pathLength : 1
float fastVolume = ta.sma(volume, 8)
float slowVolume = ta.sma(volume, 30)
bool contraction = hasVolume and fastVolume < slowVolume * 0.9
float trend = ta.ema(close, 34)
bool priorDown = close[1] < trend[1] and trend[1] < trend[6]
bool priorUp = close[1] > trend[1] and trend[1] > trend[6]
bool quiet = efficiency < 0.3 and recentHi - recentLo <= atr * 10
bool extreme = hasVolume and volume >= volumeExtreme and rvol >= 1.8 and spread >= spreadExtreme and spreadRatio >= 1.3
float closeLocation = spread > 0 ? (close - low) / spread : 0.5
bool warm = bar_index > math.max(baseline, rangeBars) + 35 and not na(atr)
string contextTf = contextInput == "" ? timeframe.from_seconds(int(timeframe.in_seconds() * 4)) : contextInput
if barstate.isfirst and not chart.is_standard
runtime.error("Use standard time-based candles. Synthetic prices are unsuitable for this analysis.")
if barstate.isfirst and useContext and timeframe.in_seconds(contextTf) <= timeframe.in_seconds()
runtime.error("Context timeframe must be higher than the chart timeframe.")
[htfClose, htfMean, htfOldMean] = request.security(syminfo.tickerid, contextTf, [close[1], ta.ema(close, 34)[1], ta.ema(close, 34)[2]], lookahead = barmerge.lookahead_on)
int htfBias = not useContext ? 0 : htfClose > htfMean and htfMean > htfOldMean ? 1 : htfClose < htfMean and htfMean < htfOldMean ? -1 : 0
f_frame(int direction) =>
WBar b = WBar.new()
b.i := bar_index
b.closed := barstate.isconfirmed and warm
b.o := direction * open
b.h := direction == 1 ? high : -low
b.l := direction == 1 ? low : -high
b.c := direction * close
b.atr := atr
b.rv := rvol
b.vol := volume
b.volumeOk := hasVolume
b.climax := extreme and (direction == 1 ? priorDown and low < recentLo and closeLocation >= 0.4 : priorUp and high > recentHi and closeLocation <= 0.6)
b.quiet := quiet
b.rangeLo := direction == 1 ? recentLo : -recentHi
b.rangeHi := direction == 1 ? recentHi : -recentLo
b.ph := direction == 1 ? ph : -pl
b.pl := direction == 1 ? pl : -ph
b.pivotVolume := volume[swingBars]
b.pivotRv := rvol[swingBars]
b.pivotIndex := bar_index - swingBars
b.spreadRatio := spreadRatio
b.contraction := contraction
b.context := direction * htfBias
b.delta := direction * fpDelta
b
var WEngine bull = WEngine.new(1)
var WEngine bear = WEngine.new(-1)
WConfig config = WConfig.new(minCause, reclaimWindow, maxAge, tolerance, maxSweep, minWidth, trailMultiple, allowBootstrap)
bull.step(f_frame(1), config)
bear.step(f_frame(-1), config)
bool eligibleLong = barstate.isconfirmed and warm and bull.event == "BU" and bull.score >= minEvidence and bull.score - bear.score >= leadNeeded and (not requireVolume or hasVolume) and (not useContext or htfBias >= 0)
bool eligibleShort = barstate.isconfirmed and warm and bear.event == "BU" and bear.score >= minEvidence and bear.score - bull.score >= leadNeeded and (not requireVolume or hasVolume) and (not useContext or htfBias <= 0)
alertcondition(eligibleLong, "Qualified bullish backup", "Wyckoff Evidence Desk: qualified bullish backup on {{ticker}} {{interval}}. Evidence is not a probability.")
alertcondition(eligibleShort, "Qualified bearish backup", "Wyckoff Evidence Desk: qualified bearish backup on {{ticker}} {{interval}}. Evidence is not a probability.")
alertcondition(barstate.isconfirmed and (bull.event == "SPRING" or bear.event == "UTAD"), "Confirmed reclaim", "Wyckoff boundary reclaim confirmed on {{ticker}} {{interval}}; inspect test and acceptance status.")
if enableJson and barstate.isconfirmed
for side in array.from(bull, bear)
if side.event != ""
string payload = '{"schema":"wed.v1","symbol":"' + syminfo.tickerid + '","timeframe":"' + timeframe.period + '","barTime":' + str.tostring(time) + ',"side":' + str.tostring(side.dir) + ',"phase":' + str.tostring(side.phase) + ',"event":"' + side.event + '","evidence":' + str.tostring(side.score, "#.0") + ',"volumeAvailable":' + str.tostring(hasVolume) + '}'
alert(payload, alert.freq_all)
// Numeric exports for Data Window and Pine Screener. Scores are uncalibrated rules.
plot(bull.phase, "Accumulation phase 0-5", display = display.data_window)
plot(bear.phase, "Distribution phase 0-5", display = display.data_window)
plot(bull.score, "Accumulation evidence 0-100", display = display.data_window)
plot(bear.score, "Distribution evidence 0-100", display = display.data_window)
plot(eligibleLong ? 1 : eligibleShort ? -1 : 0, "Qualified backup direction", display = display.data_window)
const string G_LAB = "04 · Validation Lab"
int startDate = input.time(timestamp("01 Jan 2024 00:00 +0000"), "Test start", group = G_LAB)
int endDate = input.time(timestamp("01 Jan 2030 00:00 +0000"), "Test end", group = G_LAB)
float orderQty = input.float(1, "Fixed contracts / units", minval = 0.000001, group = G_LAB)
float targetR = input.float(2, "Target / signal-planned risk", minval = 0.5, step = 0.25, tooltip = "Bracket is fixed at signal close and active with the next-open entry. Gaps and slippage change the realized entry risk/reward.", group = G_LAB)
int maxHold = input.int(100, "Maximum holding bars", minval = 5, maxval = 1000, group = G_LAB)
var float labStop = na
var float labTarget = na
var int labEntryBar = na
bool testWindow = time >= startDate and time <= endDate
if barstate.isconfirmed
if strategy.position_size == 0 and strategy.position_size[1] != 0
labStop := na
labTarget := na
labEntryBar := na
if testWindow and strategy.position_size == 0
if eligibleLong and not eligibleShort
labStop := math.min(low - atr * tolerance, close - atr)
labTarget := close + (close - labStop) * targetR
labEntryBar := bar_index
strategy.entry("Long BU", strategy.long, qty = orderQty)
strategy.exit("Long bracket", "Long BU", stop = labStop, limit = labTarget)
else if eligibleShort and not eligibleLong
labStop := math.max(high + atr * tolerance, close + atr)
labTarget := close - (labStop - close) * targetR
labEntryBar := bar_index
strategy.entry("Short BU", strategy.short, qty = orderQty)
strategy.exit("Short bracket", "Short BU", stop = labStop, limit = labTarget)
if strategy.position_size > 0
strategy.exit("Long bracket", "Long BU", stop = labStop, limit = labTarget)
if strategy.position_size < 0
strategy.exit("Short bracket", "Short BU", stop = labStop, limit = labTarget)
if strategy.position_size != 0 and (not testWindow or bar_index - strategy.opentrades.entry_bar_index(0) >= maxHold)
strategy.close_all(comment = "Time exit")
plot(strategy.position_size != 0 ? labStop : na, "Lab frozen stop", color = #ff7d78, style = plot.style_linebr)
plot(strategy.position_size != 0 ? labTarget : na, "Lab frozen target", color = #51d9be, style = plot.style_linebr)
// ── Evidence desk · chart presentation ────────────────────────────────────────
string visualGroup = "05 · Evidence desk"
bool showDashboard = input.bool(true, "Evidence dashboard", group = visualGroup)
bool showTape = input.bool(true, "Last six confirmed events", group = visualGroup)
bool showLabels = input.bool(true, "Confirmed event labels", group = visualGroup)
bool showRange = input.bool(true, "Active hypothesis ranges", group = visualGroup)
bool showVwap = input.bool(true, "Event-anchored VWAP", group = visualGroup)
bool showTrail = input.bool(true, "Phase E structural trail", group = visualGroup)
bool showBars = input.bool(false, "Color bars from phase C onward", group = visualGroup)
int maxEventLabels = input.int(60, "Retained event labels", minval = 10, maxval = 100, group = visualGroup)
string dashboardPosition = input.string("Top right", "Dashboard position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = visualGroup)
color vdInk = #0B151C
color vdPanel = #101F29
color vdGrid = #223642
color vdText = #DCE9ED
color vdMuted = #819AA5
color vdBull = #44D9C0
color vdBear = #FF8177
color vdGold = #EDC778
f_vdPhase(int phase) =>
switch phase
1 => "A · stopping action"
2 => "B · range building"
3 => "C · test / sweep"
4 => "D · directional proof"
5 => "E · follow-through"
=> "SCAN · no range"
f_vdRibbon(int phase) =>
phase == 0 ? "SCAN · WAIT FOR STRUCTURE" : (phase == 1 ? "[A]" : " A ") + " — " + (phase == 2 ? "[B]" : " B ") + " — " + (phase == 3 ? "[C]" : " C ") + " — " + (phase == 4 ? "[D]" : " D ") + " — " + (phase == 5 ? "[E]" : " E ")
f_vdFailed(string eventName) =>
str.contains(str.upper(eventName), "FAIL") or str.contains(str.upper(eventName), "INVALID") or str.contains(str.upper(eventName), "EXPIRE")
f_vdPrice(float value) =>
na(value) ? "—" : str.tostring(value, format.mintick)
f_vdScore(float value) =>
float bounded = math.max(0.0, math.min(100.0, nz(value)))
int blocks = int(math.round(bounded / 20.0))
string meter = ""
for block = 1 to 5
meter += block <= blocks ? "■" : "·"
str.tostring(bounded, "0") + " / 100 " + meter
f_vdBrief(string value, int limit) =>
str.length(value) > limit ? str.substring(value, 0, limit - 1) + "…" : value
f_vdTest(WEngine engine) =>
engine.phase == 0 ? "Awaiting range" : engine.pending == 1 ? "Reclaim pending" : engine.testPassed and engine.testObserved ? "Test confirmed" : engine.testPassed ? "Type 3 · test waived" : engine.phase >= 4 ? "Direct strength path" : "No confirmed test"
f_vdBoundaryTip(WEngine engine, bool isBull) =>
float direction = isBull ? 1.0 : -1.0
string side = isBull ? "below " : "above "
string detail = "No active range."
if engine.phase > 0 and na(engine.hi)
detail := "Phase A candidate: awaiting the automatic reaction to establish both range edges. Observed origin: " + f_vdPrice(direction * engine.lo)
else if engine.phase > 0
detail := "Sustained close invalidation: " + side + f_vdPrice(direction * (engine.lo - atr * tolerance)) + ". More than " + str.tostring(reclaimWindow) + " consecutive closes are required.\nMaximum adverse wick boundary: " + f_vdPrice(direction * (engine.lo - atr * maxSweep)) + ". Crossing it invalidates on the confirmed bar."
if engine.phase == 5
detail += "\nPhase E structural trail: " + f_vdPrice(direction * engine.trail) + ".\nBackup failure close boundary: " + side + f_vdPrice(direction * (engine.hi - atr * tolerance)) + ".\nTrail is structural context, not a broker stop order."
detail
f_vdCell(table panel, int column, int row, string value, color foreground, color background, string tooltipText = "") =>
table.cell(panel, column, row, value, text_color = foreground, bgcolor = background, text_size = size.small, text_halign = column == 0 ? text.align_left : text.align_center, tooltip = tooltipText)
f_vdTape(array<string> tape, array<color> colors, bool isBull, string eventName, string reason) =>
string stamp = str.format_time(na(time_close) ? time : time_close, "dd MMM HH:mm", syminfo.timezone)
array.unshift(tape, stamp + " · " + (isBull ? "ACC" : "DIST") + " " + eventName + " · " + f_vdBrief(reason, 28))
array.unshift(colors, f_vdFailed(eventName) ? vdGold : isBull ? vdBull : vdBear)
if array.size(tape) > 6
array.pop(tape)
array.pop(colors)
f_vdLabel(bool isBull, WEngine engine) =>
color tone = f_vdFailed(engine.event) ? vdGold : isBull ? vdBull : vdBear
float y = isBull ? low - nz(atr, high - low) * 0.24 : high + nz(atr, high - low) * 0.24
string tip = (isBull ? "ACCUMULATION" : "DISTRIBUTION") + " · " + engine.event + "\nConfirmed on this bar close.\n" + f_vdPhase(engine.phase) + "\nEvidence: " + str.tostring(engine.score, "0") + " / 100 (heuristic, not probability)\n" + engine.why
label.new(bar_index, y, engine.event, xloc = xloc.bar_index, yloc = yloc.price, style = isBull ? label.style_label_up : label.style_label_down, color = color.new(tone, 12), textcolor = vdInk, size = size.tiny, tooltip = tip)
bool vdBullActive = bull.phase > 0
bool vdBearActive = bear.phase > 0
bool vdBullRange = vdBullActive and not na(bull.lo) and not na(bull.hi)
bool vdBearRange = vdBearActive and not na(bear.lo) and not na(bear.hi)
bool vdContested = vdBullActive and vdBearActive and math.abs(bull.score - bear.score) <= 10.0
bool vdSelectBull = vdBullActive and (not vdBearActive or bull.score >= bear.score)
bool vdSelectBear = vdBearActive and not vdSelectBull
bool vdShowBull = vdBullActive and (vdSelectBull or vdContested)
bool vdShowBear = vdBearActive and (vdSelectBear or vdContested)
bool vdDrawBull = vdBullRange and vdShowBull
bool vdDrawBear = vdBearRange and vdShowBear
int vdPhase = vdSelectBull ? bull.phase : vdSelectBear ? bear.phase : 0
color vdAccent = vdContested ? vdGold : vdSelectBull ? vdBull : vdSelectBear ? vdBear : vdMuted
bool vdReset = not vdBullActive and not vdBearActive and (f_vdFailed(bull.lastEvent) or f_vdFailed(bear.lastEvent))
string vdMode = vdContested ? "CONTESTED · BOTH HYPOTHESES ACTIVE" : vdSelectBull ? "ACCUMULATION · LEADING EVIDENCE" : vdSelectBear ? "DISTRIBUTION · LEADING EVIDENCE" : vdReset ? "RESET → SCAN · AWAIT NEW STRUCTURE" : "SCANNING · AWAIT STOPPING ACTION"
// Reuse two boxes. Their left edge is the confirmation bar that started the range.
var box vdBullBox = na
var box vdBearBox = na
if showRange and vdDrawBull
int left = math.max(nz(bull.born, bar_index), bar_index - 9999)
if na(vdBullBox)
vdBullBox := box.new(left, bull.hi, bar_index + 1, bull.lo, border_color = color.new(vdBull, 26), border_width = 1, bgcolor = color.new(vdBull, 94), text = "", text_color = color.new(vdBull, 12), text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_top)
box.set_lefttop(vdBullBox, left, bull.hi)
box.set_rightbottom(vdBullBox, bar_index + 1, bull.lo)
box.set_border_style(vdBullBox, vdContested ? line.style_dashed : line.style_solid)
box.set_text(vdBullBox, " ACC · " + f_vdPhase(bull.phase) + " · " + str.tostring(bull.score, "0") + "/100")
else if not na(vdBullBox)
box.delete(vdBullBox)
vdBullBox := na
if showRange and vdDrawBear
int left = math.max(nz(bear.born, bar_index), bar_index - 9999)
if na(vdBearBox)
vdBearBox := box.new(left, -bear.lo, bar_index + 1, -bear.hi, border_color = color.new(vdBear, 26), border_width = 1, bgcolor = color.new(vdBear, 94), text = "", text_color = color.new(vdBear, 12), text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_bottom)
box.set_lefttop(vdBearBox, left, -bear.lo)
box.set_rightbottom(vdBearBox, bar_index + 1, -bear.hi)
box.set_border_style(vdBearBox, vdContested ? line.style_dashed : line.style_solid)
box.set_text(vdBearBox, " DIST · " + f_vdPhase(bear.phase) + " · " + str.tostring(bear.score, "0") + "/100")
else if not na(vdBearBox)
box.delete(vdBearBox)
vdBearBox := na
plot(showVwap and vdShowBull ? bull.vwap : na, "Accumulation event AVWAP", color = vdBull, linewidth = 2, style = plot.style_linebr)
plot(showVwap and vdShowBear ? -bear.vwap : na, "Distribution event AVWAP", color = vdBear, linewidth = 2, style = plot.style_linebr)
float vdSelectedTrail = not vdContested and vdPhase == 5 ? vdSelectBull ? bull.trail : -bear.trail : na
plot(showTrail ? vdSelectedTrail : na, "Leading Phase E structural trail", color = vdGold, linewidth = 1, style = plot.style_linebr)
barcolor(showBars and vdPhase >= 3 ? color.new(vdAccent, 24) : na, title = "Evidence phase tint")
plotshape(eligibleLong and barstate.isconfirmed, title = "Confirmed long setup", style = shape.diamond, location = location.belowbar, color = vdBull, size = size.tiny)
plotshape(eligibleShort and barstate.isconfirmed, title = "Confirmed short setup", style = shape.diamond, location = location.abovebar, color = vdBear, size = size.tiny)
var array<label> vdLabels = array.new<label>()
var array<string> vdTape = array.new<string>()
var array<color> vdTapeColors = array.new<color>()
if barstate.isconfirmed
if bull.event != ""
f_vdTape(vdTape, vdTapeColors, true, bull.event, bull.why)
if showLabels
array.push(vdLabels, f_vdLabel(true, bull))
if bear.event != ""
f_vdTape(vdTape, vdTapeColors, false, bear.event, bear.why)
if showLabels
array.push(vdLabels, f_vdLabel(false, bear))
while array.size(vdLabels) > maxEventLabels
label.delete(array.shift(vdLabels))
string vdPosition = switch dashboardPosition
"Bottom right" => position.bottom_right
"Top left" => position.top_left
"Bottom left" => position.bottom_left
=> position.top_right
var table vdDesk = table.new(vdPosition, 3, 21, bgcolor = vdInk, frame_color = vdGrid, frame_width = 1, border_color = vdInk, border_width = 1)
if barstate.isfirst
table.merge_cells(vdDesk, 0, 0, 2, 0)
table.merge_cells(vdDesk, 0, 1, 2, 1)
table.merge_cells(vdDesk, 0, 2, 2, 2)
table.merge_cells(vdDesk, 0, 13, 2, 13)
table.merge_cells(vdDesk, 0, 14, 2, 14)
for row = 15 to 20
table.merge_cells(vdDesk, 0, row, 2, row)
if barstate.islast or barstate.islastconfirmedhistory
if showDashboard
f_vdCell(vdDesk, 0, 0, "WYCKOFF / EVIDENCE DESK", vdText, vdPanel, "Two independent, sequential hypotheses. Evidence scores are rule-based support, not calibrated probabilities.")
f_vdCell(vdDesk, 0, 1, vdMode, vdAccent, color.new(vdAccent, 91))
f_vdCell(vdDesk, 0, 2, vdContested ? "TWO ACTIVE PATHS · NO SINGLE PHASE RIBBON" : f_vdRibbon(vdPhase), vdAccent, vdInk)
f_vdCell(vdDesk, 0, 3, "HYPOTHESIS", vdMuted, vdPanel)
f_vdCell(vdDesk, 1, 3, "ACCUMULATION", vdBull, vdPanel)
f_vdCell(vdDesk, 2, 3, "DISTRIBUTION", vdBear, vdPanel)
f_vdCell(vdDesk, 0, 4, "Phase", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 4, f_vdPhase(bull.phase), vdBullActive ? vdBull : vdMuted, vdInk)
f_vdCell(vdDesk, 2, 4, f_vdPhase(bear.phase), vdBearActive ? vdBear : vdMuted, vdInk)
f_vdCell(vdDesk, 0, 5, "Evidence", vdMuted, vdPanel, "Heuristic support from the configured rules; 80/100 does not mean an 80% chance of success.")
f_vdCell(vdDesk, 1, 5, f_vdScore(bull.score), vdBull, vdPanel)
f_vdCell(vdDesk, 2, 5, f_vdScore(bear.score), vdBear, vdPanel)
f_vdCell(vdDesk, 0, 6, "Range / limits ⓘ", vdMuted, vdInk, "Hover either hypothesis for its invalidation boundaries and, in Phase E, its structural trail. Live ATR-based boundaries can move until bar close.")
f_vdCell(vdDesk, 1, 6, vdBullRange ? f_vdPrice(bull.lo) + " – " + f_vdPrice(bull.hi) : vdBullActive ? "Awaiting AR" : "—", vdText, vdInk, f_vdBoundaryTip(bull, true))
f_vdCell(vdDesk, 2, 6, vdBearRange ? f_vdPrice(-bear.hi) + " – " + f_vdPrice(-bear.lo) : vdBearActive ? "Awaiting AR" : "—", vdText, vdInk, f_vdBoundaryTip(bear, false))
f_vdCell(vdDesk, 0, 7, "Test", vdMuted, vdPanel)
f_vdCell(vdDesk, 1, 7, f_vdTest(bull), bull.pending == 1 ? vdGold : vdText, vdPanel)
f_vdCell(vdDesk, 2, 7, f_vdTest(bear), bear.pending == 1 ? vdGold : vdText, vdPanel)
f_vdCell(vdDesk, 0, 8, "Latest event", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 8, bull.lastEvent == "" ? "—" : bull.lastEvent, f_vdFailed(bull.lastEvent) ? vdGold : vdBull, vdInk)
f_vdCell(vdDesk, 2, 8, bear.lastEvent == "" ? "—" : bear.lastEvent, f_vdFailed(bear.lastEvent) ? vdGold : vdBear, vdInk)
string contextName = not useContext ? "OFF" : htfBias == 1 ? "BULLISH" : htfBias == -1 ? "BEARISH" : "NEUTRAL"
f_vdCell(vdDesk, 0, 9, "Context · " + contextTf, vdMuted, vdPanel, "Context is read from confirmed higher-timeframe data.")
f_vdCell(vdDesk, 1, 9, contextName, not useContext ? vdMuted : htfBias == 1 ? vdBull : htfBias == -1 ? vdBear : vdGold, vdPanel)
f_vdCell(vdDesk, 2, 9, useContext ? "CLOSED HTF" : "FILTER OFF", vdMuted, vdPanel)
f_vdCell(vdDesk, 0, 10, "Volume / flow", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 10, hasVolume ? "RVOL " + str.tostring(rvol, "0.00") + "×" : "VOLUME UNAVAILABLE", hasVolume ? vdText : vdGold, vdInk)
f_vdCell(vdDesk, 2, 10, footprintBuild ? na(fpDelta) ? "FOOTPRINT N/A" : "Δ VOL " + (fpDelta > 0 ? "+" : "") + str.tostring(fpDelta * 100.0, "0.0") + "%" : "OHLCV", footprintBuild and na(fpDelta) ? vdGold : vdMuted, vdInk, "Footprint delta divided by total classified volume. +25.0% means a +0.25 ratio, not 25 contracts or a probability.")
f_vdCell(vdDesk, 0, 11, "Confirmed setup", vdMuted, vdPanel, "This row is true only on a confirmed eligible setup event. It is not a standing instruction or a profitability estimate.")
f_vdCell(vdDesk, 1, 11, eligibleLong and barstate.isconfirmed ? "LONG · NEW EVENT" : "WAIT", eligibleLong and barstate.isconfirmed ? vdBull : vdMuted, vdPanel)
f_vdCell(vdDesk, 2, 11, eligibleShort and barstate.isconfirmed ? "SHORT · NEW EVENT" : "WAIT", eligibleShort and barstate.isconfirmed ? vdBear : vdMuted, vdPanel)
f_vdCell(vdDesk, 0, 12, "Evidence note", vdMuted, vdInk)
f_vdCell(vdDesk, 1, 12, f_vdBrief(bull.why, 26), vdMuted, vdInk, bull.why)
f_vdCell(vdDesk, 2, 12, f_vdBrief(bear.why, 26), vdMuted, vdInk, bear.why)
f_vdCell(vdDesk, 0, 13, barstate.isconfirmed ? "BAR CLOSED · SCORE ≠ PROBABILITY" : "LIVE BAR · EVENTS WAIT FOR CLOSE", barstate.isconfirmed ? vdMuted : vdGold, vdPanel)
f_vdCell(vdDesk, 0, 14, showTape ? "EVENT TAPE · NEWEST FIRST · EXCHANGE TIME" : "", vdMuted, showTape ? vdPanel : color.new(vdInk, 100))
for index = 0 to 5
bool hasEntry = showTape and index < array.size(vdTape)
string entry = hasEntry ? array.get(vdTape, index) : showTape and index == 0 ? "No confirmed events in loaded history." : ""
color entryColor = hasEntry ? array.get(vdTapeColors, index) : vdMuted
f_vdCell(vdDesk, 0, 15 + index, entry, entryColor, showTape ? index % 2 == 0 ? vdInk : vdPanel : color.new(vdInk, 100))
else
table.clear(vdDesk, 0, 0, 2, 20)
Pine Editor installation walkthrough · Recreate alerts after changing the script or its inputs.
Exact rules, settings and test limits.
The classical route starts with a provisional stopping candidate (SC? or BC?). Its extreme may change until a confirmed automatic reaction (AR) supplies the other boundary. The default pivot needs three bars on each side; its label appears when confirmed, not at the earlier turning point. A subsequent qualifying lower-effort secondary test starts B.
The optional balanced-range start uses the previous 30 bars, movement efficiency below 0.30, and width between 2.5 and 10 current ATR by default. It begins in B as TR? and explicitly says Phase A was not observed. Boundaries freeze once AR or bootstrap establishes them. B must develop for at least eight bars before the relevant sweep or strength transition.
A–E are the engine’s states, not proof of accumulation, distribution or institutional intent. SCAN means no active episode; a preliminary PS?/PSY? observation does not establish a range.
No. A mature B range can move directly to D on the engine’s directional-strength conditions. The optional C route starts with a breach beyond the boundary allowance and within the maximum sweep depth. A pending SWEEP? must recover by the configured deadline; this is a real later close, not future information attached to the past.
A confirmed Spring or UTAD is classified by relative volume: type 1 at least 2, type 2 at least 1, type 3 below 1. Those are this release’s thresholds. Type 1/2 require a subsequent qualifying lower-effort test. Type 3 waives that test but still needs strength, acceptance and backup. “Test confirmed” and “Type 3 · test waived” are different observations.
A candle crossing both boundaries is ambiguous because OHLC cannot reveal its intrabar order. More severe invalidation takes priority. Distribution mirrors the price-direction conditions.
A decisive breakout meeting the directional-strength rule starts the acceptance check. Two consecutive closes beyond the boundary allowance establish ACCEPT and phase E; the original breakout candle counts as the first when the next close follows through. A close back inside the allowance resets that count; later acceptance closes do not recheck the original strength rule. ACCEPT still waits for a later return, called a backup (BU).
For the accumulation side, the later candle reaches the breakout area without exceeding allowed depth, closes at or above the range top with a bullish body, and has RVOL at most 1.3 when volume is available. Existing invalidation rules run first. Distribution reverses the price conditions.
A fresh BU is checked once against the configured gates: default score at least 65, lead over the opposing score at least 10, volume available, and higher-timeframe context not opposing. A blocked BU is consumed; later improvement does not retry it. It is one opportunity per episode, not a queued order.
CONTESTED is a display rule: both hypotheses are active and their score difference is at most 10. The entry lead input is separate. At exactly 10 points, a fresh BU can satisfy the default lead gate while the display still says CONTESTED.
Each hypothesis starts from its current phase: A 12, B 25, C 40, D 55, E 65. Additions are: test requirement satisfied +8 (including type 3 waiver), volume contraction +5, close on the supportive AVWAP side +5, aligned higher timeframe +7, supportive close location +5. Opposing higher-timeframe context subtracts 8.
Bootstrap subtracts 5; unavailable volume subtracts 15; a pending reclaim subtracts 8. Footprint can add 5 when directional delta divided by total volume exceeds 0.10. Scores are bounded to 0–100 independently. They are not calibrated probabilities, and need not sum to 100.
Adaptive RVOL compares volume with the median of earlier bars. The engine also uses prior 90th-percentile volume/spread references. This is a local baseline, not an adjustment for typical activity at each time of day. Volume can mean different things across feeds, including tick volume.
AVWAP starts at the bar when an episode is observed and accumulates typical price times available volume. It is a weighted price reference, not a volume profile or POC. A failed breakout that returns to B keeps the episode’s anchor; a full reset clears it.
The context uses completed higher-timeframe bars, with a one-bar offset and lookahead enabled. Automatic context is approximately four times the chart timeframe, mapped to a supported interval. With the context switch on, the interval must be larger than the chart. Neutral context is permitted; opposing context blocks the default setup. Turning it off removes its score adjustment and gate.
Core events commit on closed chart bars. Pivot confirmation is delayed. Revised feeds, changed inputs or different loaded history can change a recalculated path. Footprint has additional historical-data limitations; absolute non-repainting is not claimed.
Before acceptance, an episode expires when its age exceeds the configured maximum. Phase E does not expire from that age setting; it ends through structural failure. The original Thai manual’s general phase-E expiry wording should be read with this source distinction.
Too many consecutive adverse closes or a wick deeper than the maximum allowed excursion fully reset an episode, even if a deep wick recovers. A failed phase-D breakout can return to B and restart its development clock within the same episode. Phase E can fail on a close through its breakout boundary or its previously established structural trail.
The trail only tightens. Its plot is not a broker stop. ATR-based boundaries can move as ATR changes; decisions use the closed bar. Ended events remain in the short event tape, not in an unlimited permanent history.
The three named conditions are Qualified bullish backup, Qualified bearish backup and Confirmed reclaim. The first two mean a new setup passed its gates; a reclaim alone does not. Create alerts with closed-bar frequency and recreate them after changing source or inputs.
Emit JSON event alerts starts off. With it enabled, create Any alert() function call to receive raw events, including INVALID. The payload includes side, phase, event, evidence and volume availability. Its barTime is the candle’s opening timestamp; the event is confirmed at its close. Only the final event per side on that bar is emitted.
This version has no Setup ID export, frozen trade-plan journal or full-history guarantee. Labels are bounded; the event tape keeps six latest events across both sides, in exchange time. Strategy order-fill alerts are a separate stream.
Base has 26 inputs, Footprint 27 and Lab 31. Shared defaults are identical. The edition label identifies the extra controls.
Requires an eligible TradingView account and feed. Base and Lab make no Footprint request. Classified volume is not verified exchange aggressor flow.
Minimum 1. Directional delta/total above 10% adds five evidence points; unavailable data adds none.
Uses current chart ATR14 and previous-bar volume/spread baselines. With defaults, detection can begin on candle 87 if ATR is available. Change one rule at a time.
Range 2–10. Confirmation arrives three bars after the pivot; labels appear at confirmation.
Range 20–200. Prior median and 90th percentile of volume/spread; no session-seasonality adjustment.
Range 15–100. Bootstrap also needs path efficiency below 0.30 and width at most 10 ATR.
Starts TR? in phase B when observed; never invents an earlier stopping event.
Range 3–80. Counts from phase-B start or restart before a sweep or strength transition.
Range 1–5. Also limits breakout waiting; sustained invalidation needs more than this many consecutive adverse closes.
Range 40–1500. Expires when age exceeds this limit, before phase E only.
Range 0.05–1. Shared edge allowance; also pads the Lab’s signal-candle stop.
Range 0.5–4. A deeper adverse wick ends the episode even if its candle recovers.
Range 1–8. Applies when AR fixes the opposite edge and when bootstrap begins.
Range 1–8. Phase-E trail only tightens. A later close beyond the prior trail ends the episode; it is not a broker stop.
ACC and DIST scores are independent rule-based evidence, not calibrated probabilities. A BU event is checked once; failing a gate does not queue a later entry.
Opposing context blocks qualification; neutral is allowed. Off removes its score adjustment and gate.
TradingView maps 4× to a supported interval. With context On, it must exceed the chart timeframe.
Off allows price-only qualification. Missing volume still lowers the evidence score.
Range 40–95. Applies on the backup event, alongside the other gates.
Range 0–40. Qualification uses this input; the display’s Contested threshold stays fixed at a 10-point gap.
Create Any alert() function call. Raw events include side, phase and score; they are not all qualified setups.
Qualified backups only. Brackets freeze at signal close and submit with next-open entries. Gaps/slippage change realized risk. The producer’s small development sample lost money.
Inclusive signal candle’s opening time; the later fill may fall outside the window.
Inclusive. An open position receives a market-close instruction once outside the window.
Minimum 0.000001. Explicit entry quantity overrides Properties’ default size; it is not fixed cash risk.
Minimum 0.5. Stop distance is at least one current ATR from signal close.
Range 5–1000. Counts from the filled entry bar; market-close instruction processes on the next tick.
Display switches do not change the engine. Earlier plotted history is bounded; changing inputs rebuilds it from loaded data.
Hover table cells for full reasons and boundaries.
Six events across both sides, newest first in exchange time. Needs the dashboard visible.
Labels sit on confirmation candles, including delayed pivots.
Dashed when contested. Boundaries freeze after AR or bootstrap; preliminary climax extremes can move.
Starts when the episode is observed; uses available volume. It is not volume profile or POC.
Visible only for an uncontested leading phase-E hypothesis; separate from the Lab bracket.
Optional chart tint; setup diamonds remain separate.
Range 10–100, shared by both hypotheses. Oldest labels are removed; event tape retains six.
Top or bottom, left or right.
The Lab submits a market entry and fixed bracket after a qualifying BU while flat. The order normally fills at the next available open. Default quantity is one unit, not fixed cash risk. Stop distance is at least one signal-bar ATR; the target is two times the risk measured from the signal close.
Its bracket remains tied to that signal and does not follow a new episode. Gaps and slippage can change actual risk. The 100-bar holding limit counts from the filled entry. The date window filters the signal candle’s opening time, so a later fill can fall outside it. Market-close instructions execute on a later tick.
The default experiment includes 0.05% commission per order and two ticks of slippage. Set costs, quantity and Properties for the instrument. Keep the shipped recalculation switches off when comparing with the supplied results.
The producer’s three native Pine probe suites report 889 assertions over 25 synthetic scenarios and 129 frames. These exercise the embedded engine, not 889 trades or independent market samples. The website import checks the supplied evidence and rebuilds sources/probes; it does not claim a new run of those 889 assertions.
All three served scripts were freshly compiled with TradingView: zero errors and warnings. Compilation does not establish runtime data availability. The producer’s Basic account could not run Footprint, so its runtime remains unverified in that account despite successful server compilation.
The initial Strategy Lab sample lost money. Eight closed trades totalled −$446.03 under its stated development settings and costs. It is a small functional smoke check, not evidence of a profitable strategy or performance across markets.
Website verification and limits · Original verification record · Fresh compiler evidence
What shipped and what changed in Wyckoff Evidence Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.
Wyckoff Evidence Desk v1.0.0 Shipped
Compares two possible market stories and shows what supports or weakens each.
Engine checks and separate market limits Lab note Wyckoff Evidence Desk
Producer engine probes passed; the small development Lab run lost money. Evidence scores remain uncalibrated.
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.