vein_feature_exporter.pine
Copy
//@version=6
// ============================================================================
// Vein Feature Exporter Core 4H [WavesUnchained]
// Version: 0.1.6
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Core feature calculation for 4H NatGas reversal research.
// Computes trend, momentum, volume, volatility, candle structure, and
// swing distance features per bar.
//
// Features:
// ✓ EMA20/50 trend & slope
// ✓ RSI14, Stoch RSI, MACD histogram
// ✓ MFI14, Relative Volume
// ✓ ATR14, Bollinger Bands, BB Width
// ✓ Wick ratios, body/range
// ✓ Swing distance (ATR-normalized)
// ✓ Light / Debug / Research display modes
//
// Data Contract:
// Price: REQUIRED chart symbol
// Volume: OPTIONAL real trade volume only — MFI/relVolume export as na, never 0
// OI: NO
// Reference: NO
// Verdict: CFD-degraded
//
// ============================================================================
indicator("Vein: Feature Exporter Core 4H [WavesUnchained]", "Vein Features", format=format.price, precision=2, overlay=false, max_labels_count=50)
// ============================================================================
// MODULE: inputs
// ============================================================================
var gTrend = "Trend / EMA"
int emaFastLen = input.int(20, "EMA Fast Length", minval=1, group=gTrend)
int emaSlowLen = input.int(50, "EMA Slow Length", minval=1, group=gTrend)
int emaSlopeLen = input.int(3, "EMA Slope Lookback", minval=1, group=gTrend)
var gMomentum = "Momentum"
int rsiLen = input.int(14, "RSI Length", minval=1, group=gMomentum)
int stochRsiLen = input.int(14, "Stoch RSI Length", minval=1, group=gMomentum)
int stochK = input.int(3, "Stoch RSI K Smooth", minval=1, group=gMomentum)
int stochD = input.int(3, "Stoch RSI D Smooth", minval=1, group=gMomentum)
int macdFast = input.int(12, "MACD Fast", minval=1, group=gMomentum)
int macdSlow = input.int(26, "MACD Slow", minval=1, group=gMomentum)
int macdSig = input.int(9, "MACD Signal", minval=1, group=gMomentum)
var gWaveTrend = "WaveTrend"
int wtChLen = input.int(10, "Channel Length", minval=1, group=gWaveTrend)
int wtAvgLen = input.int(21, "Average Length", minval=1, group=gWaveTrend)
int wtSigLen = input.int(4, "Signal Length", minval=1, group=gWaveTrend)
int wtOBLevel = input.int(60, "Overbought Level", group=gWaveTrend)
int wtOSLevel = input.int(-60, "Oversold Level", group=gWaveTrend)
var gVolume = "Volume / Money Flow"
int mfiLen = input.int(14, "MFI Length", minval=1, group=gVolume)
int volLen = input.int(20, "Relative Volume Lookback", minval=1, group=gVolume)
var gVol = "Volatility / Bollinger"
int atrLen = input.int(14, "ATR Length", minval=1, group=gVol)
int bbLen = input.int(20, "Bollinger Length", minval=1, group=gVol)
float bbMult = input.float(2.0, "Bollinger Multiplier", minval=0.5, step=0.1, group=gVol)
var gZScore = "Z-Score Reversion"
int zsLen = input.int(100, "Z-Score Lookback", minval=20, group=gZScore, tooltip="Lookback for standard deviation calculation.")
float zs1Mult = input.float(1.0, "1σ Band Multiplier", minval=0.5, step=0.1, group=gZScore)
float zs2Mult = input.float(2.0, "2σ Band Multiplier", minval=1.0, step=0.1, group=gZScore)
var gSwing = "Swing Detection"
int swingLeftBars = input.int(3, "Swing Left Bars", minval=1, group=gSwing)
int swingRightBars = input.int(3, "Swing Right Bars", minval=1, group=gSwing)
var gDisplay = "Display"
string displayMode = input.string("Light", "Display Mode", options=["Light", "Debug", "Research"], group=gDisplay)
string plotGroup = input.string("Momentum", "Plot Group", options=["Momentum", "Volume", "Volatility", "Trend", "Candle", "WaveTrend", "Z-Score"], group=gDisplay)
bool showTable = input.bool(true, "Show Interpretation Table", group=gDisplay)
bool showLineLabels = input.bool(true, "Show Line Labels", group=gDisplay)
// ============================================================================
// MODULE: calculations — Trend / Lage
// ============================================================================
float ema20 = ta.ema(close, emaFastLen)
float ema50 = ta.ema(close, emaSlowLen)
float ema20Slope = (ema20 - ema20[emaSlopeLen]) / emaSlopeLen
float atr14 = ta.atr(atrLen)
float priceToEma20ATR = atr14 > 0 ? (close - ema20) / atr14 : 0.0
float ema20ToEma50ATR = atr14 > 0 ? (ema20 - ema50) / atr14 : 0.0
// ============================================================================
// MODULE: calculations — Momentum
// ============================================================================
float rsi14 = ta.rsi(close, rsiLen)
// Stoch RSI
float stochRsiRaw = ta.stoch(rsi14, rsi14, rsi14, stochRsiLen)
float stochRsiK = ta.sma(stochRsiRaw, stochK)
float stochRsiD = ta.sma(stochRsiK, stochD)
// MACD Histogram
[macdLine, signalLine, macdHist] = ta.macd(close, macdFast, macdSlow, macdSig)
// ── RSI Divergence Detection (pivot-based) ──
int rsiDivLb = 5
float rsiPivLow = ta.pivotlow(rsi14, rsiDivLb, rsiDivLb)
float rsiPivHigh = ta.pivothigh(rsi14, rsiDivLb, rsiDivLb)
var float rsiPrevLowPrice = na
var float rsiPrevLowRsi = na
var float rsiPrevHighPrice = na
var float rsiPrevHighRsi = na
bool rsiRegBullDiv = false
bool rsiHidBullDiv = false
bool rsiRegBearDiv = false
bool rsiHidBearDiv = false
if not na(rsiPivLow)
float currPrice = low[rsiDivLb]
float currRsi = rsi14[rsiDivLb]
if not na(rsiPrevLowPrice)
// Regular bullish: price LL, RSI HL
if currPrice < rsiPrevLowPrice and currRsi > rsiPrevLowRsi
rsiRegBullDiv := true
// Hidden bullish: price HL, RSI LL
if currPrice > rsiPrevLowPrice and currRsi < rsiPrevLowRsi
rsiHidBullDiv := true
rsiPrevLowPrice := currPrice
rsiPrevLowRsi := currRsi
if not na(rsiPivHigh)
float currPrice = high[rsiDivLb]
float currRsi = rsi14[rsiDivLb]
if not na(rsiPrevHighPrice)
// Regular bearish: price HH, RSI LH
if currPrice > rsiPrevHighPrice and currRsi < rsiPrevHighRsi
rsiRegBearDiv := true
// Hidden bearish: price LH, RSI HH
if currPrice < rsiPrevHighPrice and currRsi > rsiPrevHighRsi
rsiHidBearDiv := true
rsiPrevHighPrice := currPrice
rsiPrevHighRsi := currRsi
bool rsiAnyBullDiv = rsiRegBullDiv or rsiHidBullDiv
bool rsiAnyBearDiv = rsiRegBearDiv or rsiHidBearDiv
// ============================================================================
// MODULE: calculations — WaveTrend
// ============================================================================
float wtEsa = ta.ema(hlc3, wtChLen)
float wtD = ta.ema(math.abs(hlc3 - wtEsa), wtChLen)
float wtCi = wtD != 0 ? (hlc3 - wtEsa) / (0.015 * wtD) : 0.0
float wtOsc = ta.ema(wtCi, wtAvgLen)
float wtSig = ta.sma(wtOsc, wtSigLen)
float wtHist = wtOsc - wtSig
bool wtCrossUp = ta.crossover(wtOsc, wtSig)
bool wtCrossDown = ta.crossunder(wtOsc, wtSig)
// ── WaveTrend Divergence Detection ──
int wtDivLb = 5 // pivot lookback
float wtPivLow = ta.pivotlow(wtOsc, wtDivLb, wtDivLb)
float wtPivHigh = ta.pivothigh(wtOsc, wtDivLb, wtDivLb)
var float wtPrevLowPrice = na
var float wtPrevLowOsc = na
var float wtPrevHighPrice = na
var float wtPrevHighOsc = na
bool wtRegBullDiv = false
bool wtHidBullDiv = false
bool wtRegBearDiv = false
bool wtHidBearDiv = false
if not na(wtPivLow)
float currPrice = low[wtDivLb]
float currOsc = wtOsc[wtDivLb]
if not na(wtPrevLowPrice)
// Regular bullish: price lower low, osc higher low
if currPrice < wtPrevLowPrice and currOsc > wtPrevLowOsc
wtRegBullDiv := true
// Hidden bullish: price higher low, osc lower low
if currPrice > wtPrevLowPrice and currOsc < wtPrevLowOsc
wtHidBullDiv := true
wtPrevLowPrice := currPrice
wtPrevLowOsc := currOsc
if not na(wtPivHigh)
float currPrice = high[wtDivLb]
float currOsc = wtOsc[wtDivLb]
if not na(wtPrevHighPrice)
// Regular bearish: price higher high, osc lower high
if currPrice > wtPrevHighPrice and currOsc < wtPrevHighOsc
wtRegBearDiv := true
// Hidden bearish: price lower high, osc higher high
if currPrice < wtPrevHighPrice and currOsc > wtPrevHighOsc
wtHidBearDiv := true
wtPrevHighPrice := currPrice
wtPrevHighOsc := currOsc
bool wtAnyBullDiv = wtRegBullDiv or wtHidBullDiv
bool wtAnyBearDiv = wtRegBearDiv or wtHidBearDiv
// ── WaveTrend Composite Bias Score ──
float wtSlope = wtOsc - nz(wtOsc[1])
// Oscillator position: +1/-1
float wtOscBias = wtOsc > 0 ? 1.0 : wtOsc < 0 ? -1.0 : 0.0
// Momentum: slope + position
float wtMomBias = wtSlope > 0 and wtOsc > wtSig ? 1.0 : wtSlope < 0 and wtOsc < wtSig ? -1.0 : 0.0
// Cross state with decay
var float wtCrossState = 0.0
if wtCrossUp
wtCrossState := 1.0
else if wtCrossDown
wtCrossState := -1.0
else
wtCrossState := nz(wtCrossState[1]) * 0.8
float wtCrossBias = wtCrossState > 0.2 ? 1.0 : wtCrossState < -0.2 ? -1.0 : 0.0
// Divergence state with decay
var float wtDivState = 0.0
if wtAnyBullDiv
wtDivState := 2.0
else if wtAnyBearDiv
wtDivState := -2.0
else
wtDivState := nz(wtDivState[1]) * 0.85
// Regime: both lines same side + spread expanding
float wtSpread = math.abs(wtOsc - wtSig)
float wtSpreadAvg = ta.ema(wtSpread, 20)
bool wtStrongTrend = wtSpread > wtSpreadAvg
float wtRegimeBias = wtOsc > 0 and wtSig > 0 and wtStrongTrend ? 2.0 : wtOsc < 0 and wtSig < 0 and wtStrongTrend ? -2.0 : 0.0
// Composite: max ±7
float wtBiasRaw = wtOscBias + wtMomBias + wtCrossBias + wtDivState + wtRegimeBias
float wtBiasMax = 7.0
int wtConfidence = math.round(math.abs(wtBiasRaw) / wtBiasMax * 100)
string wtBiasLabel = wtBiasRaw >= 3 ? "S↑" : wtBiasRaw >= 1 ? "↑" : wtBiasRaw > 0 ? "w↑" : wtBiasRaw == 0 ? "—" : wtBiasRaw > -1 ? "w↓" : wtBiasRaw > -3 ? "↓" : "S↓"
// ============================================================================
// MODULE: calculations — Volume / Money Flow
// ============================================================================
// Echtes Handelsvolumen? Nur "tick" schließt aus; "n/a" zählt, wenn der Feed Volumen liefert (DATA_VALIDITY §2).
bool volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)
// Exported for downstream analysis — degraded features export `na`, never 0,
// so a research script consuming this data can tell "no signal" from
// "no data" instead of silently treating a CFD run as a zero-volume bar.
float mfi14 = volumeIsReal ? ta.mfi(hlc3, mfiLen) : na
float avgVol = volumeIsReal ? ta.sma(volume, volLen) : na
float relVolume20 = volumeIsReal and avgVol > 0 ? volume / avgVol : na
// ============================================================================
// MODULE: calculations — Volatility / Bollinger
// ============================================================================
float candleRange = high - low
float candleRangeATR = atr14 > 0 ? candleRange / atr14 : 0.0
float bbBasis = ta.sma(close, bbLen)
float bbDev = ta.stdev(close, bbLen) * bbMult
float bbUpper = bbBasis + bbDev
float bbLower = bbBasis - bbDev
float bbWidth = bbBasis > 0 ? (bbUpper - bbLower) / bbBasis : 0.0
float bbDistanceNorm = bbDev > 0 ? (close - bbBasis) / bbDev : 0.0
bool touchUpperBB = high >= bbUpper
bool touchLowerBB = low <= bbLower
// ============================================================================
// MODULE: calculations — Z-Score Reversion
// ============================================================================
float zsMean = ta.sma(close, zsLen)
float zsStdev = ta.stdev(close, zsLen)
float zScore = zsStdev > 0 ? (close - zsMean) / zsStdev : 0.0
float zsBand1Upper = zsMean + zsStdev * zs1Mult
float zsBand1Lower = zsMean - zsStdev * zs1Mult
float zsBand2Upper = zsMean + zsStdev * zs2Mult
float zsBand2Lower = zsMean - zsStdev * zs2Mult
// Z-Score heatmap color: blue (normal) → yellow → orange → fuchsia (extreme)
color zsHeatmap = math.abs(zScore) > 3.0 ? color.new(#e040fb, 0) : math.abs(zScore) > 2.0 ? color.new(#ff6d00, 0) : math.abs(zScore) > 1.0 ? color.new(#e65100, 40) : color.new(#0ea5e9, 60)
// ============================================================================
// MODULE: calculations — Candle Structure
// ============================================================================
float bodySize = math.abs(close - open)
float bodyToRange = candleRange > 0 ? bodySize / candleRange : 0.0
float upperWick = high - math.max(open, close)
float lowerWick = math.min(open, close) - low
float upperWickRatio = candleRange > 0 ? upperWick / candleRange : 0.0
float lowerWickRatio = candleRange > 0 ? lowerWick / candleRange : 0.0
// ============================================================================
// MODULE: calculations — Swing Distance
// ============================================================================
float pivotHigh = ta.pivothigh(high, swingLeftBars, swingRightBars)
float pivotLow = ta.pivotlow(low, swingLeftBars, swingRightBars)
var float lastSwingHigh = na
var float lastSwingLow = na
if not na(pivotHigh)
lastSwingHigh := pivotHigh
if not na(pivotLow)
lastSwingLow := pivotLow
float distToSwingHighATR = not na(lastSwingHigh) and atr14 > 0 ? (close - lastSwingHigh) / atr14 : na
float distToSwingLowATR = not na(lastSwingLow) and atr14 > 0 ? (close - lastSwingLow) / atr14 : na
// ============================================================================
// MODULE: visuals — Selectable plot group
// ============================================================================
bool showMom = plotGroup == "Momentum"
bool showVol = plotGroup == "Volume"
bool showVolat = plotGroup == "Volatility"
bool showTrend = plotGroup == "Trend"
bool showCandle = plotGroup == "Candle"
bool showWT = plotGroup == "WaveTrend"
bool showZS = plotGroup == "Z-Score"
// Momentum plots
plot(showMom ? rsi14 : na, "RSI 14", color=color.yellow, linewidth=2)
plot(showMom ? stochRsiK : na, "Stoch RSI K", color=color.aqua)
plot(showMom ? stochRsiD : na, "Stoch RSI D", color=color.orange)
hline(showMom ? 80 : na, "Overbought", color=color.new(color.red, 70))
hline(showMom ? 20 : na, "Oversold", color=color.new(color.green, 70))
hline(showMom ? 50 : na, "Midline", color=color.new(color.gray, 80))
// ── Line labels: var + delete to prevent duplicates on TF change ──
var label[] lineLabels = array.new_label()
// Helper: create label and track it
f_lineLabel(_x, _y, _text, _col, _tip) =>
label l = label.new(_x, _y, _text, style=label.style_none, textcolor=_col, size=size.small, tooltip=_tip, textalign=text.align_left)
array.push(lineLabels, l)
// Helper: space labels vertically (min 3 units apart)
f_spaced(_vals) =>
float minGap = 3.0
result = array.copy(_vals)
for i = 1 to array.size(result) - 1
float prev = array.get(result, i - 1)
float curr = array.get(result, i)
if math.abs(curr - prev) < minGap
array.set(result, i, prev + (curr >= prev ? minGap : -minGap))
result
// Delete old labels on every last bar (prevents TF change duplicates)
if barstate.islast and array.size(lineLabels) > 0
for i = 0 to array.size(lineLabels) - 1
label.delete(array.get(lineLabels, i))
array.clear(lineLabels)
if showMom and showLineLabels and barstate.islast
var vals = array.new_float()
array.clear(vals)
array.push(vals, rsi14)
array.push(vals, stochRsiK)
array.push(vals, stochRsiD)
spaced = f_spaced(vals)
f_lineLabel(bar_index + 6, array.get(spaced, 0), "RSI " + str.tostring(rsi14, "#.0"), color.yellow, "RSI 14\n────────────────\nCurrent: " + str.tostring(rsi14, "#.0") + "\n\n< 30: oversold\n30-40: sellers dominating\n60-70: buyers dominating\n> 70: overbought")
f_lineLabel(bar_index + 6, array.get(spaced, 1), "K " + str.tostring(stochRsiK, "#"), color.aqua, "Stochastic RSI K\n────────────────\nK: " + str.tostring(stochRsiK, "#") + " D: " + str.tostring(stochRsiD, "#") + "\n\n< 20: oversold zone\n> 80: overbought zone\nK crosses D: momentum shift")
f_lineLabel(bar_index + 6, array.get(spaced, 2), "D " + str.tostring(stochRsiD, "#"), color.orange, "Stochastic RSI D (signal)\n────────────────\nD: " + str.tostring(stochRsiD, "#") + "\n\nSmoothed K line.\nK > D: bullish\nK < D: bearish")
// Volume plots
plot(showVol ? mfi14 : na, "MFI 14", color=color.teal, linewidth=2)
plot(showVol ? relVolume20 * 25 : na, "Rel Volume (scaled)", color=color.new(#ab47bc, 0))
if showVol and showLineLabels and barstate.islast and volumeIsReal
var vals = array.new_float()
array.clear(vals)
array.push(vals, mfi14)
array.push(vals, relVolume20 * 25)
spaced = f_spaced(vals)
f_lineLabel(bar_index + 6, array.get(spaced, 0), "MFI " + str.tostring(mfi14, "#.0"), color.teal, "Money Flow Index 14\n────────────────\nCurrent: " + str.tostring(mfi14, "#.0") + "\n\n< 20: extreme outflow\n20-35: outflow dominant\n65-80: inflow dominant\n> 80: extreme inflow")
f_lineLabel(bar_index + 6, array.get(spaced, 1), "Vol " + str.tostring(relVolume20, "#.1") + "x", color.new(#ab47bc, 0), "Relative Volume\n────────────────\nCurrent: " + str.tostring(relVolume20, "#.1") + "x avg\n\n> 2x: extreme activity\n> 1.3x: elevated\n< 0.5x: low conviction")
else if showVol and showLineLabels and barstate.islast and not volumeIsReal
f_lineLabel(bar_index + 6, 50.0, "Volume: " + syminfo.volumetype + " — n/a", color.new(color.gray, 20), "MFI and Relative Volume are not available — " + syminfo.volumetype + " volume is not real trade volume.")
// Volatility plots
plot(showVolat ? bbWidth * 100 : na, "BB Width (scaled)", color=color.orange, linewidth=2)
plot(showVolat ? candleRangeATR * 25 : na, "Candle/ATR (scaled)", color=color.new(#69f0ae, 0))
plot(showVolat ? bbDistanceNorm * 25 + 50 : na, "BB Dist Norm (scaled)", color=color.aqua)
if showVolat and showLineLabels and barstate.islast
var vals = array.new_float()
array.clear(vals)
array.push(vals, bbWidth * 100)
array.push(vals, candleRangeATR * 25)
array.push(vals, bbDistanceNorm * 25 + 50)
spaced = f_spaced(vals)
f_lineLabel(bar_index + 6, array.get(spaced, 0), "BBW " + str.tostring(bbWidth * 100, "#.0"), color.orange, "Bollinger Band Width\n────────────────\nCurrent: " + str.tostring(bbWidth * 100, "#.1") + "%\n\nHigh = volatile\nLow = squeeze, breakout likely\n\nUpper: " + str.tostring(bbUpper, format.mintick) + "\nLower: " + str.tostring(bbLower, format.mintick))
f_lineLabel(bar_index + 6, array.get(spaced, 1), "C/ATR " + str.tostring(candleRangeATR, "#.1"), color.new(#69f0ae, 0), "Candle Range / ATR\n────────────────\nCurrent: " + str.tostring(candleRangeATR, "#.2") + "x\n\n> 1.5: expansion\n< 0.5: indecision\n\nRange: " + str.tostring(candleRange, format.mintick) + "\nATR: " + str.tostring(atr14, format.mintick))
f_lineLabel(bar_index + 6, array.get(spaced, 2), "BBD " + str.tostring(bbDistanceNorm, "#.1"), color.aqua, "BB Distance Normalized\n────────────────\nCurrent: " + str.tostring(bbDistanceNorm, "#.2") + "\n\n> 2: upper extreme\n< -2: lower extreme\n0 = midline\n\nClose: " + str.tostring(close, format.mintick))
// Trend plots
plot(showTrend ? priceToEma20ATR * 25 + 50 : na, "Price-EMA20 ATR (scaled)", color=color.yellow, linewidth=2)
plot(showTrend ? ema20ToEma50ATR * 25 + 50 : na, "EMA20-EMA50 ATR (scaled)", color=color.orange)
plot(showTrend ? ema20Slope > 0 ? 70 : 30 : na, "EMA20 Slope Direction", color=color.new(#b0bec5, 20), style=plot.style_circles)
if showTrend and showLineLabels and barstate.islast
var vals = array.new_float()
array.clear(vals)
array.push(vals, priceToEma20ATR * 25 + 50)
array.push(vals, ema20ToEma50ATR * 25 + 50)
array.push(vals, ema20Slope > 0 ? 70.0 : 30.0)
spaced = f_spaced(vals)
f_lineLabel(bar_index + 6, array.get(spaced, 0), "P/EMA " + str.tostring(priceToEma20ATR, "#.1"), color.yellow, "Price to EMA20 in ATR\n────────────────\nCurrent: " + str.tostring(priceToEma20ATR, "#.2") + "x\n\n< -1: overextended down\n> +1: overextended up\n\nEMA20: " + str.tostring(ema20, format.mintick) + "\nClose: " + str.tostring(close, format.mintick))
f_lineLabel(bar_index + 6, array.get(spaced, 1), "E20/50 " + str.tostring(ema20ToEma50ATR, "#.1"), color.orange, "EMA20 vs EMA50 in ATR\n────────────────\nCurrent: " + str.tostring(ema20ToEma50ATR, "#.2") + "x\n\n> 0: EMA20 above EMA50 (bullish)\n< 0: EMA20 below EMA50 (bearish)\n\nEMA20: " + str.tostring(ema20, format.mintick) + "\nEMA50: " + str.tostring(ema50, format.mintick))
f_lineLabel(bar_index + 6, array.get(spaced, 2), "Slope " + (ema20Slope > 0 ? "↑" : "↓"), color.new(#b0bec5, 20), "EMA20 Slope Direction\n────────────────\nSlope: " + str.tostring(ema20Slope, format.mintick) + "\n\nUp = price acceleration\nDown = price deceleration")
// Candle plots
plot(showCandle ? bodyToRange * 100 : na, "Body/Range %", color=color.yellow, linewidth=2)
plot(showCandle ? upperWickRatio * 100 : na, "Upper Wick %", color=color.red)
plot(showCandle ? lowerWickRatio * 100 : na, "Lower Wick %", color=color.green)
if showCandle and showLineLabels and barstate.islast
var vals = array.new_float()
array.clear(vals)
array.push(vals, bodyToRange * 100)
array.push(vals, upperWickRatio * 100)
array.push(vals, lowerWickRatio * 100)
spaced = f_spaced(vals)
f_lineLabel(bar_index + 6, array.get(spaced, 0), "Body " + str.tostring(bodyToRange * 100, "#") + "%", color.yellow, "Body / Range Ratio\n────────────────\nCurrent: " + str.tostring(bodyToRange * 100, "#") + "%\n\nHigh = strong directional candle\nLow = indecision / doji\n\nBody: " + str.tostring(math.abs(close - open), format.mintick) + "\nRange: " + str.tostring(candleRange, format.mintick))
f_lineLabel(bar_index + 6, array.get(spaced, 1), "UWick " + str.tostring(upperWickRatio * 100, "#") + "%", color.red, "Upper Wick Ratio\n────────────────\nCurrent: " + str.tostring(upperWickRatio * 100, "#") + "%\n\nHigh = rejection from above\n> 60%: strong selling pressure at highs")
f_lineLabel(bar_index + 6, array.get(spaced, 2), "LWick " + str.tostring(lowerWickRatio * 100, "#") + "%", color.green, "Lower Wick Ratio\n────────────────\nCurrent: " + str.tostring(lowerWickRatio * 100, "#") + "%\n\nHigh = rejection from below\n> 60%: strong buying pressure at lows")
// WaveTrend plots
plot(showWT ? wtOsc : na, "WT Oscillator", color=#0ea5e9, linewidth=2)
plot(showWT ? wtSig : na, "WT Signal", color=#f97316)
plot(showWT ? wtHist : na, "WT Histogram", color=wtHist >= 0 ? color.new(#00e676, 60) : color.new(#ff1744, 60), style=plot.style_columns)
hline(showWT ? wtOBLevel : na, "WT Overbought", color=color.new(color.red, 70))
hline(showWT ? wtOSLevel : na, "WT Oversold", color=color.new(color.green, 70))
hline(showWT ? 0 : na, "WT Zero", color=color.new(color.gray, 80))
if showWT and showLineLabels and barstate.islast
var vals = array.new_float()
array.clear(vals)
array.push(vals, wtOsc)
array.push(vals, wtSig)
spaced = f_spaced(vals)
f_lineLabel(bar_index + 6, array.get(spaced, 0), "Osc " + str.tostring(wtOsc, "#"), #0ea5e9, "WaveTrend Oscillator\n────────────────\nOsc: " + str.tostring(wtOsc, "#.0") + "\n\n> " + str.tostring(wtOBLevel) + ": overbought\n< " + str.tostring(wtOSLevel) + ": oversold\nCross in extreme = reversal signal")
f_lineLabel(bar_index + 6, array.get(spaced, 1), "Sig " + str.tostring(wtSig, "#"), #f97316, "WaveTrend Signal\n────────────────\nSignal: " + str.tostring(wtSig, "#.0") + "\n\nSmoothed oscillator.\nOsc crosses Signal = momentum shift")
// Z-Score plots
plot(showZS ? zScore * 25 + 50 : na, "Z-Score (scaled)", color=zsHeatmap, linewidth=2, style=plot.style_columns)
plot(showZS ? 50 : na, "Zero Line", color=color.new(color.gray, 70))
hline(showZS ? 75 : na, "+1σ", color=color.new(color.gray, 80))
hline(showZS ? 25 : na, "-1σ", color=color.new(color.gray, 80))
hline(showZS ? 100 : na, "+2σ", color=color.new(color.red, 70))
hline(showZS ? 0 : na, "-2σ", color=color.new(color.green, 70))
if showZS and showLineLabels and barstate.islast
string zsZone = math.abs(zScore) > 3.0 ? "EXTREME" : math.abs(zScore) > 2.0 ? "Extended" : math.abs(zScore) > 1.0 ? "Elevated" : "Normal"
f_lineLabel(bar_index + 6, zScore * 25 + 50, "Z " + str.tostring(zScore, "#.1"), math.abs(zScore) > 2.0 ? #e65100 : chart.fg_color, "Z-Score\n────────────────\nCurrent: " + str.tostring(zScore, "#.2") + "σ\nZone: " + zsZone + "\n\nMean: " + str.tostring(zsMean, format.mintick) + "\nStdev: " + str.tostring(zsStdev, format.mintick) + "\n\n±1σ: 68% of price action\n±2σ: 95% — reversal likely\n±3σ: 99.7% — extreme")
// ============================================================================
// MODULE: data window — all features always available
// ============================================================================
plot(ema20, "EMA20", display=display.data_window, color=color.new(color.blue, 100))
plot(ema50, "EMA50", display=display.data_window, color=color.new(color.blue, 100))
plot(ema20Slope, "EMA20 Slope", display=display.data_window, color=color.new(color.blue, 100))
plot(priceToEma20ATR, "Price→EMA20 ATR", display=display.data_window, color=color.new(color.blue, 100))
plot(ema20ToEma50ATR, "EMA20→EMA50 ATR", display=display.data_window, color=color.new(color.blue, 100))
plot(rsi14, "RSI14 [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(stochRsiK, "StochRSI K [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(stochRsiD, "StochRSI D [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(macdHist, "MACD Hist [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(mfi14, "MFI14 [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(relVolume20, "Rel Volume [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(atr14, "ATR14 [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(candleRangeATR, "Candle/ATR [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(bbWidth, "BB Width [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(bbDistanceNorm, "BB Dist Norm [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(touchUpperBB ? 1 : 0, "Touch Upper BB [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(touchLowerBB ? 1 : 0, "Touch Lower BB [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(bodyToRange, "Body/Range [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(upperWickRatio, "Upper Wick Ratio [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(lowerWickRatio, "Lower Wick Ratio [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(nz(distToSwingHighATR), "Dist→SwingHigh ATR [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(nz(distToSwingLowATR), "Dist→SwingLow ATR [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(wtOsc, "WT Osc [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(wtSig, "WT Signal [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(wtHist, "WT Hist [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(wtBiasRaw, "WT Bias [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(wtRegBullDiv ? 1 : wtHidBullDiv ? 0.5 : wtRegBearDiv ? -1 : wtHidBearDiv ? -0.5 : 0, "WT Div [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(rsiRegBullDiv ? 1 : rsiHidBullDiv ? 0.5 : rsiRegBearDiv ? -1 : rsiHidBearDiv ? -0.5 : 0, "RSI Div [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(zScore, "Z-Score [dw]", display=display.data_window, color=color.new(color.blue, 100))
plot(zsMean, "Z-Mean [dw]", display=display.data_window, color=color.new(color.blue, 100))
// ============================================================================
// MODULE: debug table
// ============================================================================
bool isResearch = displayMode == "Research"
var table featureTable = na
if isResearch and barstate.islast
if na(featureTable)
featureTable := table.new(position.bottom_left, 2, 12, bgcolor=color.new(color.black, 80), border_color=color.gray, border_width=1)
table.cell(featureTable, 0, 0, "Feature", text_color=color.white, text_size=size.small)
table.cell(featureTable, 1, 0, "Value", text_color=color.white, text_size=size.small)
table.cell(featureTable, 0, 1, "RSI14", text_color=color.yellow, text_size=size.small)
table.cell(featureTable, 1, 1, str.tostring(rsi14, "#.0"), text_color=color.yellow, text_size=size.small)
table.cell(featureTable, 0, 2, "StochK", text_color=color.aqua, text_size=size.small)
table.cell(featureTable, 1, 2, str.tostring(stochRsiK, "#.0"), text_color=color.aqua, text_size=size.small)
table.cell(featureTable, 0, 3, "MFI14", text_color=color.teal, text_size=size.small)
table.cell(featureTable, 1, 3, volumeIsReal ? str.tostring(mfi14, "#.0") : "n/a", text_color=color.teal, text_size=size.small)
table.cell(featureTable, 0, 4, "MACD Hist", text_color=color.orange, text_size=size.small)
table.cell(featureTable, 1, 4, str.tostring(macdHist, "#.000"), text_color=color.orange, text_size=size.small)
table.cell(featureTable, 0, 5, "Price→EMA20", text_color=color.lime, text_size=size.small)
table.cell(featureTable, 1, 5, str.tostring(priceToEma20ATR, "#.00"), text_color=color.lime, text_size=size.small)
table.cell(featureTable, 0, 6, "BB Dist", text_color=color.aqua, text_size=size.small)
table.cell(featureTable, 1, 6, str.tostring(bbDistanceNorm, "#.00"), text_color=color.aqua, text_size=size.small)
table.cell(featureTable, 0, 7, "Rel Vol", text_color=color.purple, text_size=size.small)
table.cell(featureTable, 1, 7, volumeIsReal ? str.tostring(relVolume20, "#.00") : "n/a", text_color=color.purple, text_size=size.small)
table.cell(featureTable, 0, 8, "BB Width", text_color=color.orange, text_size=size.small)
table.cell(featureTable, 1, 8, str.tostring(bbWidth, "#.0000"), text_color=color.orange, text_size=size.small)
table.cell(featureTable, 0, 9, "Upper Wick", text_color=color.red, text_size=size.small)
table.cell(featureTable, 1, 9, str.tostring(upperWickRatio, "#.00"), text_color=color.red, text_size=size.small)
table.cell(featureTable, 0, 10, "Lower Wick", text_color=color.green, text_size=size.small)
table.cell(featureTable, 1, 10, str.tostring(lowerWickRatio, "#.00"), text_color=color.green, text_size=size.small)
table.cell(featureTable, 0, 11, "Candle/ATR", text_color=color.lime, text_size=size.small)
table.cell(featureTable, 1, 11, str.tostring(candleRangeATR, "#.00"), text_color=color.lime, text_size=size.small)
// ============================================================================
// MODULE: interpretation panel (always visible)
// ============================================================================
// Dynamic plain-language explanation of what the current readings mean.
// ── Build interpretation strings ──
// Momentum interpretation
string momText = ""
if rsi14 < 25
momText := "RSI deeply oversold (" + str.tostring(rsi14, "#") + ") — strong selling pressure"
else if rsi14 < 35
momText := "RSI in lower range (" + str.tostring(rsi14, "#") + ") — sellers dominating"
else if rsi14 > 75
momText := "RSI deeply overbought (" + str.tostring(rsi14, "#") + ") — strong buying pressure"
else if rsi14 > 65
momText := "RSI in upper range (" + str.tostring(rsi14, "#") + ") — buyers dominating"
else
momText := "RSI neutral (" + str.tostring(rsi14, "#") + ") — no extreme"
// Append RSI divergence
string rsiDivText = ""
if rsiRegBullDiv
rsiDivText := "Reg Bull Div"
else if rsiHidBullDiv
rsiDivText := "Hid Bull Div"
else if rsiRegBearDiv
rsiDivText := "Reg Bear Div"
else if rsiHidBearDiv
rsiDivText := "Hid Bear Div"
if rsiDivText != ""
momText := momText + " | " + rsiDivText
// Stoch RSI interpretation
string stochText = ""
if stochRsiK < 20 and stochRsiK < stochRsiD
stochText := "StochRSI low + K below D — strong downward momentum"
else if stochRsiK < 20 and stochRsiK > stochRsiD
stochText := "StochRSI low, K turns above D — possible momentum reversal up"
else if stochRsiK > 80 and stochRsiK > stochRsiD
stochText := "StochRSI high + K above D — strong upward momentum"
else if stochRsiK > 80 and stochRsiK < stochRsiD
stochText := "StochRSI high, K turns below D — possible momentum reversal down"
else if stochRsiK > stochRsiD
stochText := "StochRSI: K above D — short-term bullish"
else
stochText := "StochRSI: K below D — short-term bearish"
// MFI interpretation
string mfiText = ""
if not volumeIsReal
mfiText := "MFI n/a — " + syminfo.volumetype + " volume is not real trade volume"
else if mfi14 < 20
mfiText := "MFI extremely low (" + str.tostring(mfi14, "#") + ") — money outflow, possible exhaustion"
else if mfi14 < 35
mfiText := "MFI low (" + str.tostring(mfi14, "#") + ") — more money flowing out than in"
else if mfi14 > 80
mfiText := "MFI extremely high (" + str.tostring(mfi14, "#") + ") — strong money inflow, possible overheating"
else if mfi14 > 65
mfiText := "MFI high (" + str.tostring(mfi14, "#") + ") — money inflow dominant"
else
mfiText := "MFI neutral (" + str.tostring(mfi14, "#") + ") — balanced money flow"
// Trend interpretation
string trendText = ""
if priceToEma20ATR < -2.0
trendText := "Price far below EMA20 (" + str.tostring(priceToEma20ATR, "#.1") + "x ATR) — extreme downside overextension"
else if priceToEma20ATR < -1.0
trendText := "Price below EMA20 — overextended, mean reversion possible"
else if priceToEma20ATR > 2.0
trendText := "Price far above EMA20 (" + str.tostring(priceToEma20ATR, "#.1") + "x ATR) — extreme upside overextension"
else if priceToEma20ATR > 1.0
trendText := "Price above EMA20 — overextended, pullback possible"
else if ema20 > ema50
trendText := "EMA20 above EMA50 — higher timeframe uptrend"
else if ema20 < ema50
trendText := "EMA20 below EMA50 — higher timeframe downtrend"
else
trendText := "EMAs close together — no clear trend"
// Volatility interpretation
string volText = ""
if bbDistanceNorm > 2.0
volText := "Price at upper Bollinger extreme — overextended upside"
else if bbDistanceNorm < -2.0
volText := "Price at lower Bollinger extreme — overextended downside"
else if bbWidth > 0.08
volText := "Bollinger wide — high volatility"
else if bbWidth < 0.03
volText := "Bollinger narrow — low volatility, breakout possible"
else
volText := "Volatility normal"
// Volume interpretation
string volBarText = ""
if not volumeIsReal
volBarText := "Volume n/a — " + syminfo.volumetype + " volume is not real trade volume"
else if relVolume20 > 2.0
volBarText := "Volume extremely high (" + str.tostring(relVolume20, "#.1") + "x) — strong activity"
else if relVolume20 > 1.3
volBarText := "Volume above average — elevated interest"
else if relVolume20 < 0.5
volBarText := "Volume very low — little conviction"
else
volBarText := "Volume normal"
// WaveTrend interpretation
string wtText = ""
if wtOsc > wtOBLevel and wtCrossDown
wtText := "WT OB + bear cross — strong reversal ↓"
else if wtOsc < wtOSLevel and wtCrossUp
wtText := "WT OS + bull cross — strong reversal ↑"
else if wtOsc > wtOBLevel
wtText := "WT overbought (" + str.tostring(wtOsc, "#") + ") — watch for cross"
else if wtOsc < wtOSLevel
wtText := "WT oversold (" + str.tostring(wtOsc, "#") + ") — watch for cross"
else if wtCrossUp and wtOsc < 0
wtText := "WT bull cross below zero — early shift ↑"
else if wtCrossDown and wtOsc > 0
wtText := "WT bear cross above zero — early shift ↓"
else if wtOsc > 0 and wtOsc > wtSig
wtText := "WT positive + rising — bullish momentum"
else if wtOsc < 0 and wtOsc < wtSig
wtText := "WT negative + falling — bearish momentum"
else
wtText := "WT neutral (" + str.tostring(wtOsc, "#") + ") — no clear signal"
// Divergence text
string wtDivText = ""
if wtRegBullDiv
wtDivText := "Regular Bull Div"
else if wtHidBullDiv
wtDivText := "Hidden Bull Div"
else if wtRegBearDiv
wtDivText := "Regular Bear Div"
else if wtHidBearDiv
wtDivText := "Hidden Bear Div"
// Append divergence to WT text if active
if wtDivText != ""
wtText := wtText + " | " + wtDivText
// Z-Score interpretation
string zsText = ""
if zScore > 3.0
zsText := "Z " + str.tostring(zScore, "#.1") + "σ EXTREME — statistical outlier"
else if zScore > 2.0
zsText := "Z " + str.tostring(zScore, "#.1") + "σ extended up — reversal likely"
else if zScore < -3.0
zsText := "Z " + str.tostring(zScore, "#.1") + "σ EXTREME — statistical outlier"
else if zScore < -2.0
zsText := "Z " + str.tostring(zScore, "#.1") + "σ extended down — reversal likely"
else if math.abs(zScore) > 1.0
zsText := "Z " + str.tostring(zScore, "#.1") + "σ — elevated"
else
zsText := "Z " + str.tostring(zScore, "#.1") + "σ — normal range"
// Overall summary — short for table, long for tooltip
string sumShort = ""
string sumLong = ""
if math.abs(zScore) > 2.5 and rsi14 < 30 and priceToEma20ATR < -1.5
sumShort := "Z+RSI extreme — bull rev"
sumLong := "Z-Score " + str.tostring(zScore, "#.1") + "σ + RSI oversold + overextended — strong bullish reversal zone"
else if math.abs(zScore) > 2.5 and rsi14 > 70 and priceToEma20ATR > 1.5
sumShort := "Z+RSI extreme — bear rev"
sumLong := "Z-Score " + str.tostring(zScore, "#.1") + "σ + RSI overbought + overextended — strong bearish reversal zone"
else if rsi14 < 30 and volumeIsReal and mfi14 < 30 and priceToEma20ATR < -1.5
sumShort := "Oversold — bull rev possible"
sumLong := "Multiple indicators oversold + exhaustion — bullish reversal possible, confirmation needed"
else if rsi14 > 70 and volumeIsReal and mfi14 > 70 and priceToEma20ATR > 1.5
sumShort := "Overbought — bear rev possible"
sumLong := "Multiple indicators overbought + overheating — bearish reversal possible, confirmation needed"
else if wtOsc < wtOSLevel and wtCrossUp and rsi14 < 35
sumShort := "WT+RSI bull signal"
sumLong := "WaveTrend oversold cross + RSI low — strong bullish reversal signal"
else if wtOsc > wtOBLevel and wtCrossDown and rsi14 > 65
sumShort := "WT+RSI bear signal"
sumLong := "WaveTrend overbought cross + RSI high — strong bearish reversal signal"
else if rsi14 < 35 and stochRsiK > stochRsiD and stochRsiK < 30
sumShort := "Early bull signal"
sumLong := "RSI low + StochRSI turning up — early bullish momentum signal"
else if rsi14 > 65 and stochRsiK < stochRsiD and stochRsiK > 70
sumShort := "Early bear signal"
sumLong := "RSI high + StochRSI turning down — early bearish momentum signal"
else
sumShort := "No extreme — wait"
sumLong := "No clear extreme across indicators — wait or watch for structure events"
// ── Render interpretation table ──
var table interpTable = na
if barstate.islast and showTable
// 4 columns, 2 features per row: Feature | Value | Feature | Value
if na(interpTable)
interpTable := table.new(position.top_right, 4, 6, bgcolor=color.new(color.white, 100), border_color=color.new(color.gray, 80), border_width=0, frame_color=color.new(color.gray, 80), frame_width=0)
// ── Color calculations ──
bool rsiRising = rsi14 > nz(rsi14[1])
color rsiCol = rsi14 < 30 ? (rsiRising ? #e65100 : #ff1744) : rsi14 < 40 ? (rsiRising ? color.new(#e65100, 40) : color.new(#ff1744, 40)) : rsi14 > 70 ? (rsiRising ? #00e676 : #e65100) : rsi14 > 60 ? (rsiRising ? color.new(#00e676, 40) : color.new(#e65100, 40)) : chart.fg_color
color stochCol = stochRsiK < 20 ? (stochRsiK > stochRsiD ? #e65100 : #ff1744) : stochRsiK > 80 ? (stochRsiK < stochRsiD ? #e65100 : #00e676) : stochRsiK > stochRsiD ? color.new(#00e676, 40) : color.new(#ff1744, 40)
bool mfiRising = volumeIsReal and mfi14 > nz(mfi14[1])
color mfiCol = not volumeIsReal ? color.new(chart.fg_color, 60) : mfi14 < 20 ? (mfiRising ? #e65100 : #ff1744) : mfi14 < 35 ? (mfiRising ? color.new(#e65100, 40) : color.new(#ff1744, 40)) : mfi14 > 80 ? (mfiRising ? #00e676 : #e65100) : mfi14 > 65 ? (mfiRising ? color.new(#00e676, 40) : color.new(#e65100, 40)) : chart.fg_color
color trendCol = priceToEma20ATR < -1.5 ? #ff1744 : priceToEma20ATR < -0.5 ? color.new(#ff1744, 40) : priceToEma20ATR > 1.5 ? #00e676 : priceToEma20ATR > 0.5 ? color.new(#00e676, 40) : chart.fg_color
color volCol = bbDistanceNorm > 2.0 ? #ff1744 : bbDistanceNorm < -2.0 ? #00e676 : bbWidth < 0.03 ? #e65100 : chart.fg_color
color vBarCol = not volumeIsReal ? color.new(chart.fg_color, 60) : relVolume20 > 2.0 ? #e65100 : relVolume20 > 1.3 ? color.new(#e65100, 40) : relVolume20 < 0.5 ? color.new(#ff1744, 40) : chart.fg_color
color wtCol = wtOsc < wtOSLevel ? (wtCrossUp ? #e65100 : #ff1744) : wtOsc > wtOBLevel ? (wtCrossDown ? #e65100 : #00e676) : math.abs(wtOsc) < 10 ? chart.fg_color : wtOsc > wtSig ? color.new(#00e676, 40) : color.new(#ff1744, 40)
color wtBiasCol = wtBiasRaw >= 3 ? #00e676 : wtBiasRaw >= 1 ? color.new(#00e676, 40) : wtBiasRaw <= -3 ? #ff1744 : wtBiasRaw <= -1 ? color.new(#ff1744, 40) : chart.fg_color
// ── Tooltip texts ──
string rsiTip = "RSI 14\n────────────────\n" + momText + "\n\n< 30: oversold\n30-40: sellers dominating\n60-70: buyers dominating\n> 70: overbought\n\nCurrent: " + str.tostring(rsi14, "#.0") + (rsiRising ? " ↑" : " ↓") + "\n\nRSI Divergence: " + (rsiDivText != "" ? rsiDivText : "none") + "\n\nReg Bull: price LL + RSI HL → reversal up\nHid Bull: price HL + RSI LL → trend continuation\nReg Bear: price HH + RSI LH → reversal down\nHid Bear: price LH + RSI HH → trend continuation"
string trendTip = "Price vs EMA20 in ATR\n────────────────\n" + trendText + "\n\n< -1.0: overextended down\n> +1.0: overextended up\nSlope: " + (ema20Slope > 0 ? "up" : "down") + "\nEMA20/50: " + str.tostring(ema20ToEma50ATR, "#.1") + "x"
string stochTip = "Stochastic RSI\n────────────────\n" + stochText + "\n\nK:" + str.tostring(stochRsiK, "#") + " D:" + str.tostring(stochRsiD, "#") + "\n< 20: oversold > 80: overbought\nK×D: momentum shift"
string volTip = "Bollinger Bands\n────────────────\n" + volText + "\n\nDist: " + str.tostring(bbDistanceNorm, "#.1") + " Width: " + str.tostring(bbWidth * 100, "#.1") + "%\n> 2: upper extreme < -2: lower extreme"
string mfiTip = "Money Flow Index\n────────────────\n" + mfiText + "\n\n< 20: extreme outflow\n> 80: extreme inflow\nCurrent: " + (volumeIsReal ? str.tostring(mfi14, "#.0") + (mfiRising ? " ↑" : " ↓") : "n/a")
string volBarTip = "Relative Volume\n────────────────\n" + volBarText + "\n\n> 2x: extreme > 1.3x: elevated\n< 0.5x: low conviction"
string wtDivShort = wtDivText != "" ? wtDivText : "No Div"
string wtTip = "WaveTrend\n────────────────\n" + wtText + "\n\nOsc:" + str.tostring(wtOsc, "#") + " Sig:" + str.tostring(wtSig, "#") + " Hist:" + str.tostring(wtHist, "#") + "\n>" + str.tostring(wtOBLevel) + ": OB <" + str.tostring(wtOSLevel) + ": OS\nDiv: " + wtDivShort
string wtBiasTip = "WT Composite Bias\n────────────────\nScore: " + str.tostring(wtBiasRaw, "#.0") + "/7 Conf: " + str.tostring(wtConfidence) + "%\n\nOsc: " + str.tostring(wtOscBias, "#") + " Mom: " + str.tostring(wtMomBias, "#") + " Cross: " + str.tostring(wtCrossBias, "#") + "\nDiv: " + str.tostring(wtDivState, "#.1") + " Regime: " + str.tostring(wtRegimeBias, "#") + "\n\n" + wtDivShort
// ── Row 0: RSI | Trend ──
table.cell(interpTable, 0, 0, " RSI ", text_color=chart.fg_color, text_size=size.small, tooltip=rsiTip)
table.cell(interpTable, 1, 0, str.tostring(rsi14, "#.0") + " ", text_color=rsiCol, text_size=size.small, text_halign=text.align_right, tooltip=rsiTip)
table.cell(interpTable, 2, 0, " Trend ", text_color=chart.fg_color, text_size=size.small, tooltip=trendTip)
table.cell(interpTable, 3, 0, str.tostring(priceToEma20ATR, "#.1") + "x ", text_color=trendCol, text_size=size.small, text_halign=text.align_right, tooltip=trendTip)
// ── Row 1: StochRSI | Volatility ──
table.cell(interpTable, 0, 1, " StchRSI ", text_color=chart.fg_color, text_size=size.small, tooltip=stochTip)
table.cell(interpTable, 1, 1, str.tostring(stochRsiK, "#") + "/" + str.tostring(stochRsiD, "#") + " ", text_color=stochCol, text_size=size.small, text_halign=text.align_right, tooltip=stochTip)
table.cell(interpTable, 2, 1, " Volatil ", text_color=chart.fg_color, text_size=size.small, tooltip=volTip)
table.cell(interpTable, 3, 1, str.tostring(bbDistanceNorm, "#.1") + " ", text_color=volCol, text_size=size.small, text_halign=text.align_right, tooltip=volTip)
// ── Row 2: MFI | Volume ──
table.cell(interpTable, 0, 2, " MFI ", text_color=chart.fg_color, text_size=size.small, tooltip=mfiTip)
table.cell(interpTable, 1, 2, (volumeIsReal ? str.tostring(mfi14, "#.0") : "n/a") + " ", text_color=mfiCol, text_size=size.small, text_halign=text.align_right, tooltip=mfiTip)
table.cell(interpTable, 2, 2, " Volume ", text_color=chart.fg_color, text_size=size.small, tooltip=volBarTip)
table.cell(interpTable, 3, 2, (volumeIsReal ? str.tostring(relVolume20, "#.1") + "x" : "n/a") + " ", text_color=vBarCol, text_size=size.small, text_halign=text.align_right, tooltip=volBarTip)
// ── Row 3: WaveTrend | WT Bias ──
table.cell(interpTable, 0, 3, " WT ", text_color=chart.fg_color, text_size=size.small, tooltip=wtTip)
table.cell(interpTable, 1, 3, str.tostring(wtOsc, "#") + "/" + str.tostring(wtSig, "#") + " ", text_color=wtCol, text_size=size.small, text_halign=text.align_right, tooltip=wtTip)
table.cell(interpTable, 2, 3, " WT Bias ", text_color=chart.fg_color, text_size=size.small, tooltip=wtBiasTip)
table.cell(interpTable, 3, 3, wtBiasLabel + str.tostring(wtConfidence) + "% ", text_color=wtBiasCol, text_size=size.small, text_halign=text.align_right, tooltip=wtBiasTip)
// ── Row 4: Z-Score | Divergence ──
color zsCol = math.abs(zScore) > 2.5 ? #e65100 : math.abs(zScore) > 1.5 ? color.new(#e65100, 40) : chart.fg_color
string zsTip = "Z-Score Reversion\n────────────────\n" + zsText + "\n\nMean: " + str.tostring(zsMean, format.mintick) + "\nStdev: " + str.tostring(zsStdev, format.mintick) + "\n\n±1σ: 68% normal range\n±2σ: 95% — reversal zone\n±3σ: 99.7% — extreme"
// Combine WT + RSI divergences
string allDivs = ""
if wtDivText != ""
allDivs := "WT:" + wtDivText
if rsiDivText != ""
allDivs := allDivs + (allDivs != "" ? " " : "") + "RSI:" + rsiDivText
string divDisplay = allDivs != "" ? allDivs : "No Div"
bool anyBullDiv = wtAnyBullDiv or rsiAnyBullDiv
bool anyBearDiv = wtAnyBearDiv or rsiAnyBearDiv
color divCol = anyBullDiv ? #00e676 : anyBearDiv ? #ff1744 : color.new(chart.fg_color, 60)
string divTip = "Divergences\n────────────────\nWT: " + (wtDivText != "" ? wtDivText : "none") + "\nRSI: " + (rsiDivText != "" ? rsiDivText : "none") + "\n\nRegular Bull: price LL, indicator HL → reversal up\nHidden Bull: price HL, indicator LL → continuation up\nRegular Bear: price HH, indicator LH → reversal down\nHidden Bear: price LH, indicator HH → continuation down"
table.cell(interpTable, 0, 4, " Z-Score ", text_color=chart.fg_color, text_size=size.small, tooltip=zsTip)
table.cell(interpTable, 1, 4, str.tostring(zScore, "#.1") + "σ ", text_color=zsCol, text_size=size.small, text_halign=text.align_right, tooltip=zsTip)
table.cell(interpTable, 2, 4, " Div ", text_color=chart.fg_color, text_size=size.small, tooltip=divTip)
table.cell(interpTable, 3, 4, divDisplay + " ", text_color=divCol, text_size=size.small, text_halign=text.align_right, tooltip=divTip)
// ── Row 5: Summary (spans full width) ──
color sumColor = str.contains(sumShort, "bull") ? #00e676 : str.contains(sumShort, "bear") ? #ff1744 : str.contains(sumShort, "Oversold") ? #00e676 : str.contains(sumShort, "Overbought") ? #ff1744 : chart.fg_color
string sumTip = "Overall Assessment\n────────────────\n" + sumLong + "\n\nRSI: " + str.tostring(rsi14, "#") + " StochRSI: " + str.tostring(stochRsiK, "#") + "/" + str.tostring(stochRsiD, "#") + "\nMFI: " + (volumeIsReal ? str.tostring(mfi14, "#") : "n/a") + " WT: " + str.tostring(wtOsc, "#") + "/" + str.tostring(wtSig, "#") + "\nTrend: " + str.tostring(priceToEma20ATR, "#.1") + "x BB: " + str.tostring(bbDistanceNorm, "#.1") + "\nVol: " + (volumeIsReal ? str.tostring(relVolume20, "#.1") + "x" : "n/a") + " Z: " + str.tostring(zScore, "#.1") + "σ Bias: " + wtBiasLabel + str.tostring(wtConfidence) + "%"
table.cell(interpTable, 0, 5, sumShort, text_color=sumColor, text_size=size.small, tooltip=sumTip)
table.merge_cells(interpTable, 0, 5, 3, 5)
// ============================================================================
// MODULE: alert export (Research mode)
// ============================================================================
// alertcondition must be at global scope
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (isResearch), "Feature Export", "VEIN-FX · FEATURE EXPORT · {{ticker}} {{interval}} · Features={{plot_0}}")
Show all 825 lines