Indicator · Pine Script v6 · Own pane

Vein Feature Exporter Core 4H [WavesUnchained]

by casoon

This indicator computes and plots multiple technical analysis features on a separate pane.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code825
Settings30inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

825 lines of Pine Script v6 from indicators/trend_direction/vein/vein_feature_exporter.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

vein_feature_exporter.pine
//@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}}")

Overview

Vein Feature Exporter Core 4H [WavesUnchained] calculates EMA-based trend indicators including fast and slow EMAs along with their slope, momentum oscillators such as RSI, Stochastic RSI, and MACD histogram, WaveTrend oscillator with overbought and oversold levels, volume and money flow metrics including MFI and relative volume, volatility measures using ATR and Bollinger Bands, Z-score based reversion bands, and swing detection based on left and right bars. Inputs allow customization of lengths and parameters for each indicator group, display modes for light, debug, or research views, and a plot group for organization. The script provides these features for manual analysis rather than automated signals.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

Trend, Oscillators & momentum, Moving averages, Volatility & bands

Markets

Any symbol

Timeframes

H4 and above

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
EMA Fast Lengthint20emaFastLen
EMA Slow Lengthint50emaSlowLen
EMA Slope Lookbackint3emaSlopeLen
RSI Lengthint14rsiLen
Stoch RSI Lengthint14stochRsiLen
Stoch RSI K Smoothint3stochK
Stoch RSI D Smoothint3stochD
MACD Fastint12macdFast
MACD Slowint26macdSlow
MACD Signalint9macdSig
Channel Lengthint10wtChLen
Average Lengthint21wtAvgLen
Show all 30 settings
SettingTypeDefaultVariable
Signal Lengthint4wtSigLen
Overbought Levelint60wtOBLevel
Oversold Levelint-60wtOSLevel
MFI Lengthint14mfiLen
Relative Volume Lookbackint20volLen
ATR Lengthint14atrLen
Bollinger Lengthint20bbLen
Bollinger Multiplierfloat2.0bbMult
Z-Score Lookbackint100zsLen
1σ Band Multiplierfloat1.0zs1Mult
2σ Band Multiplierfloat2.0zs2Mult
Swing Left Barsint3swingLeftBars
Swing Right Barsint3swingRightBars
Display ModestringLightdisplayMode
Plot GroupstringMomentumplotGroup
Show Interpretation TablebooltrueshowTable
Show Line LabelsbooltrueshowLineLabels
Alerts only on bar close (confirmed)booltruealertsOnBarClose
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

MultiIndicator

by capissimo
114 stars

MultiIndicator combines multiple technical indicators into a single separate pane to save chart space. It calculates and displays market regime detection based on ordinary least squares regression, a Holt exponential smoothing crossover, normalized RSI, ADX, a

Indicator v4 Oscillators & momentumTrend
Updated 1 year ago · None stated GitHub repository of MultiIndicator
29 stars

The Supertrended RSI [AlgoAlpha] indicator plots the Relative Strength Index combined with a Supertrend overlay in a separate indicator pane. It first computes standard RSI, with an optional Hull Moving Average smoothing step, and computes a secondary moving a

Indicator v5 Oscillators & momentumTrend
Updated 11 months ago · Custom licence GitHub repository of Supertrended RSI [AlgoAlpha]
56 stars

Double Screened Decision is a multi-timeframe trend and momentum indicator overlaid directly on the price chart. It dynamically selects a higher timeframe based on the active chart resolution to evaluate higher-timeframe MACD, Bollinger Bands, RSI, and Stochas

Indicator v5 TrendMoving averages Repaints
Updated 4 months ago · None stated GitHub repository of Double Screened Decision
24 stars

This indicator plots Bollinger Bands on the price chart alongside a baseline moving average to identify trend-aligned swing setups. It calculates trend bias by comparing the Bollinger Bands basis to a customizable moving average, such as a 200 EMA. Trade signa

Indicator v5 Volatility & bandsOscillators & momentum
Updated 4 years ago · MPL-2.0 GitHub repository of Bollinger Band Swing

Index Intraday Analyst

by chris-c-thomas
14 stars

Index Intraday Analyst overlays CBOE cash indices such as SPX, NDX, RUT, and DJX. It plots a multi-EMA ribbon, a session-anchored TWAP with standard-deviation bands, and session levels such as prior-day high, low, and close, the opening range, and the high and

Indicator v6 TrendOscillators & momentum
Updated 6 months ago · MIT GitHub repository of Index Intraday Analyst

RePaNoCHa V4

by wocclyl
9 stars

RePaNoCHa V4 is an overlay alert indicator that plots a Tillson T3 moving average on the price chart and colors it by whether the line is rising or falling. It combines that slope with a range filter, ADX, parabolic SAR, RSI, and MACD to mark long and short si

Indicator v4 TrendOscillators & momentum
Updated 7 years ago · MIT GitHub repository of RePaNoCHa V4

More indicators

Trade Your Charts With a Regulated Broker

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

Compare brokers