Indicator · Pine Script v6 · On the chart

Vein: Accumulation Phase 4H [WavesUnchained]

by casoon

This indicator detects market accumulation phases and bottom formations over time rather than generating single-point buy signals.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code518
Settings13inputs
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

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

vein_accumulation_phase.pine
//@version=6
// ============================================================================
// Vein Accumulation Phase 4H [WavesUnchained]
// Version: 0.1.5
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Detects bottom formation / accumulation phases on 4H NatGas.
// Not a point signal — a process state over time.
//
// 5 Components (0-2 each, max 10):
//   1. Trend Loss — downtrend weakening
//   2. Range Formation — sideways consolidation
//   3. Volatility Compression — candles getting tighter
//   4. Liquidity Behaviour — sweeps, springs, tests
//   5. Early Bull Structure — first bullish hints
//
// Range Formation and Volatility Compression are direction-neutral; they count
// only for the side whose prior trend is weakening (Trend Loss > 0).
//
// States: none → transition → early → developing → mature → breakout_ready
// ============================================================================
indicator("Vein: Accumulation Phase 4H [WavesUnchained]", "Vein Accum", overlay=true, max_labels_count=50)

// ============================================================================
// INPUTS
// ============================================================================

var gCore = "Core Parameters"
int   atrLen       = input.int(14, "ATR Length", minval=1, group=gCore)
int   lookback     = input.int(20, "Phase Lookback Bars", minval=10, group=gCore)
int   minPhaseBars = input.int(10, "Min Phase Duration", minval=3, group=gCore, tooltip="Minimum bars before phase is recognized.\nFilters out V-reversals.")

var gSwing = "Swing Detection"
int   pivotLeft  = input.int(3, "Pivot Left Bars", minval=1, group=gSwing)
int   pivotRight = input.int(3, "Pivot Right Bars", minval=1, group=gSwing)

var gThresh = "Thresholds"
float erRangeThresh     = input.float(0.3, "Efficiency Ratio Range Threshold", minval=0.05, step=0.05, group=gThresh, tooltip="Below this = ranging. Above = trending.")
float bbwCompressThresh = input.float(0.7, "BB Width Compression Ratio", minval=0.3, step=0.1, group=gThresh, tooltip="Current BBW / avg BBW. Below = compressed.")
float atrCompressThresh = input.float(0.8, "ATR Compression Ratio", minval=0.3, step=0.1, group=gThresh, tooltip="Current ATR / avg ATR. Below = compressed.")

var gDisplay = "Display"
string displayMode = input.string("Light", "Display Mode", options=["Light", "Debug", "Research"], group=gDisplay)
bool   showBgShading = input.bool(true, "Background Shading", group=gDisplay)
bool   showTable     = input.bool(true, "Show Status Table", group=gDisplay)
bool   showPhaseLabel = input.bool(true, "Show Phase Label on Chart", group=gDisplay)

// ============================================================================
// CALCULATIONS — Base indicators
// ============================================================================

float atr14      = ta.atr(atrLen)
float atrAvg     = ta.sma(atr14, lookback)
float atrRatio   = atrAvg > 0 ? atr14 / atrAvg : 1.0

float ema20      = ta.ema(close, 20)
float ema50      = ta.ema(close, 50)
float ema20Slope = (ema20 - ema20[3]) / 3

// Bollinger
float bbBasis = ta.sma(close, 20)
float bbDev   = ta.stdev(close, 20)
float bbWidth = bbBasis > 0 ? (2 * bbDev * 2.0) / bbBasis : 0.0
float bbwAvg  = ta.sma(bbWidth, lookback)
float bbwRatio = bbwAvg > 0 ? bbWidth / bbwAvg : 1.0

// Efficiency Ratio: net move / total path
float netMove   = math.abs(close - close[lookback])
float totalPath = math.sum(math.abs(close - close[1]), lookback)
float effRatio  = totalPath > 0 ? netMove / totalPath : 1.0

// Candle range compression
float candleRange    = high - low
float avgCandleRange = ta.sma(candleRange, lookback)
float candleRatio    = avgCandleRange > 0 ? candleRange / avgCandleRange : 1.0

// Range bounds
float rangeHigh = ta.highest(high, lookback)
float rangeLow  = ta.lowest(low, lookback)
float rangeWidth = atr14 > 0 ? (rangeHigh - rangeLow) / atr14 : 0.0

// RSI for momentum context
float rsi14 = ta.rsi(close, 14)

// ============================================================================
// CALCULATIONS — Swing detection
// ============================================================================

float pivHigh = ta.pivothigh(high, pivotLeft, pivotRight)
float pivLow  = ta.pivotlow(low, pivotLeft, pivotRight)

var float lastSwingHigh = na
var float lastSwingLow  = na
var float prevSwingHigh = na
var float prevSwingLow  = na

if not na(pivHigh)
    prevSwingHigh := lastSwingHigh
    lastSwingHigh := pivHigh

if not na(pivLow)
    prevSwingLow := lastSwingLow
    lastSwingLow := pivLow

// ============================================================================
// CALCULATIONS — Structure events (simplified)
// ============================================================================

// BOS detection
bool bullBOS = not na(lastSwingHigh) and close > lastSwingHigh and close[1] <= lastSwingHigh
bool bearBOS = not na(lastSwingLow) and close < lastSwingLow and close[1] >= lastSwingLow

// Spring: low below swing low, close back above
bool isSpring = not na(lastSwingLow) and low < lastSwingLow and close > lastSwingLow

// Sweep: wick below swing low (broader than spring)
bool isSweepLow = not na(lastSwingLow) and low < lastSwingLow

// Higher Low detection
bool hasHigherLow = not na(lastSwingLow) and not na(prevSwingLow) and lastSwingLow > prevSwingLow

// Lower Low detection (trend still down)
bool hasLowerLow = not na(lastSwingLow) and not na(prevSwingLow) and lastSwingLow < prevSwingLow

// ============================================================================
// COMPONENT 1: Trend Loss (0-2)
// ============================================================================

// EMA slope flattening
bool slopeFlattening = ema20Slope > -0.0001 * close and ema20Slope < 0.0001 * close
bool slopeWasDown    = ema20[lookback] < ema50[lookback]
bool slopeNowFlat    = math.abs(ema20 - ema50) < atr14 * 0.5

float trendLossScore = 0.0
// Was in downtrend, now flattening
if slopeWasDown and slopeNowFlat
    trendLossScore := trendLossScore + 1.0
// No recent bear BOS
if slopeWasDown and not bearBOS and not hasLowerLow
    trendLossScore := trendLossScore + 0.5
// EMA slope actually flattening
if slopeFlattening and slopeWasDown
    trendLossScore := trendLossScore + 0.5
trendLossScore := math.min(trendLossScore, 2.0)

// ============================================================================
// COMPONENT 2: Range Formation (0-2)
// ============================================================================

float rangeScore = 0.0
// Efficiency ratio low = ranging
if effRatio < erRangeThresh
    rangeScore := rangeScore + 1.0
if effRatio < erRangeThresh * 0.5
    rangeScore := rangeScore + 0.5
// Range width reasonable (not too wide = still trending)
if rangeWidth > 2.0 and rangeWidth < 8.0
    rangeScore := rangeScore + 0.5
rangeScore := math.min(rangeScore, 2.0)

// ============================================================================
// COMPONENT 3: Volatility Compression (0-2)
// ============================================================================

float volCompScore = 0.0
// BB width compressed
if bbwRatio < bbwCompressThresh
    volCompScore := volCompScore + 1.0
// ATR compressed
if atrRatio < atrCompressThresh
    volCompScore := volCompScore + 0.5
// Candle ranges shrinking
if candleRatio < 0.7
    volCompScore := volCompScore + 0.5
volCompScore := math.min(volCompScore, 2.0)

// ============================================================================
// COMPONENT 4: Liquidity Behaviour (0-2)
// ============================================================================

// Track sweeps and springs over lookback
var int sweepCount  = 0
var int springCount = 0

// Count events (first bar of each sweep/spring), not every bar the condition persists
if isSweepLow and not isSweepLow[1]
    sweepCount := sweepCount + 1
if isSpring and not isSpring[1]
    springCount := springCount + 1

// Decay
if bar_index % math.round(lookback / 2) == 0
    sweepCount  := math.max(0, sweepCount - 1)
    springCount := math.max(0, springCount - 1)

float liqScore = 0.0
if sweepCount >= 1
    liqScore := liqScore + 0.5
if sweepCount >= 2
    liqScore := liqScore + 0.5
if springCount >= 1
    liqScore := liqScore + 0.5
if springCount >= 2
    liqScore := liqScore + 0.5
liqScore := math.min(liqScore, 2.0)

// ============================================================================
// COMPONENT 5: Early Bull Structure (0-2)
// ============================================================================

float bullStructScore = 0.0
// Higher Low
if hasHigherLow
    bullStructScore := bullStructScore + 1.0
// Bull BOS
if bullBOS
    bullStructScore := bullStructScore + 1.0
// RSI turning from oversold
if rsi14 > 40 and rsi14[lookback] < 35
    bullStructScore := bullStructScore + 0.5
bullStructScore := math.min(bullStructScore, 2.0)

// ============================================================================
// TOTAL SCORE
// ============================================================================

// Shared (direction-neutral) components count only when the prior downtrend is weakening
float accumRangeScore   = trendLossScore > 0 ? rangeScore : 0.0
float accumVolCompScore = trendLossScore > 0 ? volCompScore : 0.0
float accumScore = trendLossScore + accumRangeScore + accumVolCompScore + liqScore + bullStructScore

// ============================================================================
// PHASE DURATION TRACKING
// ============================================================================

var int phaseStartBar = na
var int phaseDuration = 0
var string prevAccumState = "none"
var string prevDistState  = "none"

bool isActive = accumScore >= 3.0
if isActive and na(phaseStartBar)
    phaseStartBar := bar_index
if not isActive
    phaseStartBar := na

phaseDuration := not na(phaseStartBar) ? bar_index - phaseStartBar : 0
bool meetsMinDuration = phaseDuration >= minPhaseBars

// ============================================================================
// STATE MACHINE
// ============================================================================

string accumState = "none"
if not isActive
    accumState := "none"
else if not meetsMinDuration
    accumState := "transition"
else if accumScore >= 8.0 and bullStructScore >= 1.5
    accumState := "breakout_ready"
else if accumScore >= 6.0 and liqScore >= 1.0 and accumRangeScore >= 1.0
    accumState := "mature"
else if accumScore >= 5.0
    accumState := "developing"
else if accumScore >= 3.0
    accumState := "early"
else
    accumState := "transition"

// ============================================================================
// DISTRIBUTION DETECTION (mirror)
// ============================================================================

// Higher High failing
bool hasLowerHigh = not na(lastSwingHigh) and not na(prevSwingHigh) and lastSwingHigh < prevSwingHigh

// Sweep above swing high
bool isSweepHigh = not na(lastSwingHigh) and high > lastSwingHigh

// Upthrust
bool isUpthrust = not na(lastSwingHigh) and high > lastSwingHigh and close < lastSwingHigh

var int sweepHighCount  = 0
var int upthrustCount   = 0

if isSweepHigh and not isSweepHigh[1]
    sweepHighCount := sweepHighCount + 1
if isUpthrust and not isUpthrust[1]
    upthrustCount := upthrustCount + 1

if bar_index % math.round(lookback / 2) == 0
    sweepHighCount  := math.max(0, sweepHighCount - 1)
    upthrustCount   := math.max(0, upthrustCount - 1)

// Distribution trend loss (uptrend weakening)
bool distSlopeWasUp  = ema20[lookback] > ema50[lookback]
bool distSlopeNowFlat = math.abs(ema20 - ema50) < atr14 * 0.5
bool distSlopeFlattening = slopeFlattening and distSlopeWasUp

float distTrendLoss = 0.0
if distSlopeWasUp and distSlopeNowFlat
    distTrendLoss := distTrendLoss + 1.0
if distSlopeWasUp and not bullBOS and not (not na(lastSwingHigh) and not na(prevSwingHigh) and lastSwingHigh > prevSwingHigh)
    distTrendLoss := distTrendLoss + 0.5
if distSlopeFlattening
    distTrendLoss := distTrendLoss + 0.5
distTrendLoss := math.min(distTrendLoss, 2.0)

// Distribution liquidity
float distLiqScore = 0.0
if sweepHighCount >= 1
    distLiqScore := distLiqScore + 0.5
if sweepHighCount >= 2
    distLiqScore := distLiqScore + 0.5
if upthrustCount >= 1
    distLiqScore := distLiqScore + 0.5
if upthrustCount >= 2
    distLiqScore := distLiqScore + 0.5
distLiqScore := math.min(distLiqScore, 2.0)

// Early bear structure
float bearStructScore = 0.0
if hasLowerHigh
    bearStructScore := bearStructScore + 1.0
if bearBOS
    bearStructScore := bearStructScore + 1.0
if rsi14 < 60 and rsi14[lookback] > 65
    bearStructScore := bearStructScore + 0.5
bearStructScore := math.min(bearStructScore, 2.0)

// Distribution total — shared components count only when the prior uptrend is weakening
float distRangeScore   = distTrendLoss > 0 ? rangeScore : 0.0
float distVolCompScore = distTrendLoss > 0 ? volCompScore : 0.0
float distScore = distTrendLoss + distRangeScore + distVolCompScore + distLiqScore + bearStructScore

// Distribution state
var int distStartBar = na
var int distDuration = 0
bool distActive = distScore >= 3.0
if distActive and na(distStartBar)
    distStartBar := bar_index
if not distActive
    distStartBar := na
distDuration := not na(distStartBar) ? bar_index - distStartBar : 0
bool distMeetsMin = distDuration >= minPhaseBars

string distState = "none"
if not distActive
    distState := "none"
else if not distMeetsMin
    distState := "transition"
else if distScore >= 8.0 and bearStructScore >= 1.5
    distState := "breakdown_ready"
else if distScore >= 6.0 and distLiqScore >= 1.0 and distRangeScore >= 1.0
    distState := "mature"
else if distScore >= 5.0
    distState := "developing"
else if distScore >= 3.0
    distState := "early"
else
    distState := "transition"

// ============================================================================
// VISUALIZATION — Background shading
// ============================================================================

color accumBg = accumState == "breakout_ready" ? color.new(#0ea5e9, 85) : accumState == "mature" ? color.new(#00e676, 88) : accumState == "developing" ? color.new(#e65100, 90) : accumState == "early" ? color.new(#e65100, 93) : accumState == "transition" ? color.new(color.gray, 93) : na

color distBg = distState == "breakdown_ready" ? color.new(#0ea5e9, 85) : distState == "mature" ? color.new(#ff1744, 88) : distState == "developing" ? color.new(#e65100, 90) : distState == "early" ? color.new(#e65100, 93) : distState == "transition" ? color.new(color.gray, 93) : na

bgcolor(showBgShading ? (not na(accumBg) ? accumBg : distBg) : na)

// ============================================================================
// VISUALIZATION — Phase label on chart
// ============================================================================

var label phaseLabel = na

if showPhaseLabel and barstate.islast
    label.delete(phaseLabel)

    string activeState = accumState != "none" ? accumState : distState
    float  activeScore = accumState != "none" ? accumScore : distScore
    int    activeDur   = accumState != "none" ? phaseDuration : distDuration
    bool   isAccum     = accumState != "none"
    string phaseType   = isAccum ? "ACCUMULATION" : distState != "none" ? "DISTRIBUTION" : ""

    if phaseType != ""
        color lblCol = isAccum ? (accumState == "breakout_ready" ? #0ea5e9 : accumState == "mature" ? #00e676 : #e65100) : (distState == "breakdown_ready" ? #0ea5e9 : distState == "mature" ? #ff1744 : #e65100)

        string lblText = phaseType + " (" + activeState + ")\nScore: " + str.tostring(activeScore, "#.0") + " | " + str.tostring(activeDur) + " bars"

        phaseLabel := label.new(bar_index + 3, isAccum ? rangeLow - atr14 * 0.5 : rangeHigh + atr14 * 0.5, lblText, style=isAccum ? label.style_label_up : label.style_label_down, color=color.new(lblCol, 70), textcolor=lblCol, size=size.normal, tooltip=phaseType + "\n────────────────\nState: " + activeState + "\nTotal Score: " + str.tostring(activeScore, "#.1") + " / 10\nDuration: " + str.tostring(activeDur) + " bars\n\nComponents:\n• Trend Loss: " + str.tostring(isAccum ? trendLossScore : distTrendLoss, "#.1") + " / 2\n• Range: " + str.tostring(isAccum ? accumRangeScore : distRangeScore, "#.1") + " / 2\n• Vol Compression: " + str.tostring(isAccum ? accumVolCompScore : distVolCompScore, "#.1") + " / 2\n• Liquidity: " + str.tostring(isAccum ? liqScore : distLiqScore, "#.1") + " / 2\n• Structure: " + str.tostring(isAccum ? bullStructScore : bearStructScore, "#.1") + " / 2\n\nRange width: " + str.tostring(rangeWidth, "#.1") + " ATR\nEfficiency: " + str.tostring(effRatio, "#.2") + "\nBBW ratio: " + str.tostring(bbwRatio, "#.2"))

// ============================================================================
// VISUALIZATION — Status table
// ============================================================================

bool isDebug = displayMode == "Debug" or displayMode == "Research"

// Created once (var) — re-creating the table on every realtime tick would stack table objects.
int statusTblRows = isDebug ? 9 : 5
var table statusTbl = table.new(position.top_right, 3, statusTblRows, 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)
if showTable and barstate.islast

    // Determine which phase to show
    bool showAccum = accumState != "none"
    bool showDist  = distState != "none"
    string activePhase = showAccum ? "ACCUM" : showDist ? "DISTRIB" : "NONE"
    string activeStateTbl = showAccum ? accumState : showDist ? distState : "—"
    float  activeScoreTbl = showAccum ? accumScore : showDist ? distScore : 0.0
    int    activeDurTbl   = showAccum ? phaseDuration : showDist ? distDuration : 0

    color phaseCol = activePhase == "ACCUM" ? (accumState == "breakout_ready" ? #0ea5e9 : accumState == "mature" ? #00e676 : accumState == "developing" ? #e65100 : chart.fg_color) : activePhase == "DISTRIB" ? (distState == "breakdown_ready" ? #0ea5e9 : distState == "mature" ? #ff1744 : distState == "developing" ? #e65100 : chart.fg_color) : color.new(chart.fg_color, 60)

    // Row 0: Header
    table.cell(statusTbl, 0, 0, "Vein Accum", text_color=chart.fg_color, text_size=size.normal, tooltip="Vein Accumulation / Distribution Phase Detector\n────────────────\nDetects bottom/top formation processes.\nNot a point signal — a state over time.\n\n5 Components (0-2 each, max 10):\n1. Trend Loss\n2. Range Formation\n3. Volatility Compression\n4. Liquidity Behaviour\n5. Early Structure\n\nStates: none → transition → early →\ndeveloping → mature → breakout/breakdown ready")
    table.cell(statusTbl, 1, 0, activePhase, text_color=phaseCol, text_size=size.normal, text_halign=text.align_right, tooltip="Current phase type.\n\nACCUM = bottom formation (bullish)\nDISTRIB = top formation (bearish)\nNONE = no phase detected")
    table.cell(statusTbl, 2, 0, activeStateTbl, text_color=phaseCol, text_size=size.normal, text_halign=text.align_right, tooltip="Phase state progression:\n\ntransition: first signs, min duration not met\nearly: phase beginning, score 3-4\ndeveloping: clear phase, score 5-6\nmature: strong phase, multiple confirms\nbreakout/breakdown_ready: structure shifting")

    // Row 1: Score + Duration
    table.cell(statusTbl, 0, 1, str.tostring(activeScoreTbl, "#.0") + " / 10", text_color=activeScoreTbl >= 7 ? #00e676 : activeScoreTbl >= 5 ? #e65100 : color.new(chart.fg_color, 40), text_size=size.normal, tooltip="Total phase score (0-10)\n\n0-2: no phase\n3-4: possible\n5-6: probable\n7-8: clear\n9-10: advanced")
    table.cell(statusTbl, 1, 1, str.tostring(activeDurTbl) + " bars", text_color=meetsMinDuration or distMeetsMin ? chart.fg_color : color.new(chart.fg_color, 60), text_size=size.normal, text_halign=text.align_right, tooltip="Phase duration.\n\nMinimum required: " + str.tostring(minPhaseBars) + " bars\nCurrent: " + str.tostring(activeDurTbl) + " bars\n\nLonger duration = more reliable")
    table.cell(statusTbl, 2, 1, rangeWidth > 0 ? str.tostring(rangeWidth, "#.0") + " ATR rng" : "", text_color=color.new(chart.fg_color, 40), text_size=size.normal, text_halign=text.align_right, tooltip="Range width in ATR.\n\n2-6 ATR: typical accumulation range\n> 8: too wide, likely still trending")

    // Row 2: Interpretation
    string interpStr = ""
    if activePhase == "NONE"
        if ema20 < ema50 and rsi14 > 40
            interpStr := "Downtrend active, no accumulation"
        else if ema20 > ema50 and rsi14 < 60
            interpStr := "Uptrend active, no distribution"
        else
            interpStr := "No phase detected"
    else if activeStateTbl == "transition"
        interpStr := "First signs — needs more time"
    else if activeStateTbl == "early"
        interpStr := showAccum ? "Range forming after decline" : "Range forming after rally"
    else if activeStateTbl == "developing"
        interpStr := showAccum ? "Accumulation building — watch sweeps" : "Distribution building — watch upthrusts"
    else if activeStateTbl == "mature"
        interpStr := showAccum ? "Mature — spring/BOS expected" : "Mature — upthrust/BOS expected"
    else if activeStateTbl == "breakout_ready" or activeStateTbl == "breakdown_ready"
        interpStr := showAccum ? "Structure shifting bullish" : "Structure shifting bearish"
    else
        interpStr := "—"

    color interpCol = activePhase == "NONE" ? color.new(chart.fg_color, 50) : phaseCol
    table.cell(statusTbl, 0, 2, interpStr, text_color=interpCol, text_size=size.normal)
    table.cell(statusTbl, 1, 2, "", text_size=size.normal)
    table.cell(statusTbl, 2, 2, "", text_size=size.normal)

    // Row 3: Component bars (visual)
    string compStr = "T:" + str.tostring(showAccum ? trendLossScore : distTrendLoss, "#") + " R:" + str.tostring(showAccum ? accumRangeScore : distRangeScore, "#") + " V:" + str.tostring(showAccum ? accumVolCompScore : distVolCompScore, "#") + " L:" + str.tostring(showAccum ? liqScore : distLiqScore, "#") + " S:" + str.tostring(showAccum ? bullStructScore : bearStructScore, "#")
    table.cell(statusTbl, 0, 3, compStr, text_color=color.new(chart.fg_color, 30), text_size=size.normal, tooltip="Component scores:\n\nT = Trend Loss (" + str.tostring(showAccum ? trendLossScore : distTrendLoss, "#.1") + "/2)\nR = Range (" + str.tostring(showAccum ? accumRangeScore : distRangeScore, "#.1") + "/2)\nV = Volatility Compression (" + str.tostring(showAccum ? accumVolCompScore : distVolCompScore, "#.1") + "/2)\nL = Liquidity (" + str.tostring(showAccum ? liqScore : distLiqScore, "#.1") + "/2)\nS = Structure (" + str.tostring(showAccum ? bullStructScore : bearStructScore, "#.1") + "/2)")
    table.cell(statusTbl, 1, 3, "", text_size=size.normal)
    table.cell(statusTbl, 2, 3, "", text_size=size.normal)

    // Row 4: Context
    string ctxStr = "ER:" + str.tostring(effRatio, "#.2") + " BBW:" + str.tostring(bbwRatio, "#.1") + " ATR:" + str.tostring(atrRatio, "#.1")
    table.cell(statusTbl, 0, 4, ctxStr, text_color=color.new(chart.fg_color, 50), text_size=size.normal, tooltip="Market metrics:\n\nER (Efficiency Ratio): " + str.tostring(effRatio, "#.3") + "\n< " + str.tostring(erRangeThresh) + " = ranging\n\nBBW ratio: " + str.tostring(bbwRatio, "#.2") + "\n< " + str.tostring(bbwCompressThresh) + " = compressed\n\nATR ratio: " + str.tostring(atrRatio, "#.2") + "\n< " + str.tostring(atrCompressThresh) + " = compressed")
    table.cell(statusTbl, 1, 4, "", text_size=size.normal)
    table.cell(statusTbl, 2, 4, "", text_size=size.normal)

    // Debug rows
    if isDebug
        table.cell(statusTbl, 0, 5, "─ ACCUM ─", text_color=color.new(chart.fg_color, 40), text_size=size.normal)
        table.cell(statusTbl, 1, 5, str.tostring(accumScore, "#.0"), text_color=accumState != "none" ? #00e676 : color.new(chart.fg_color, 60), text_size=size.normal, text_halign=text.align_right)
        table.cell(statusTbl, 2, 5, accumState, text_color=accumState != "none" ? #00e676 : color.new(chart.fg_color, 60), text_size=size.normal, text_halign=text.align_right)

        table.cell(statusTbl, 0, 6, "─ DISTRIB ─", text_color=color.new(chart.fg_color, 40), text_size=size.normal)
        table.cell(statusTbl, 1, 6, str.tostring(distScore, "#.0"), text_color=distState != "none" ? #ff1744 : color.new(chart.fg_color, 60), text_size=size.normal, text_halign=text.align_right)
        table.cell(statusTbl, 2, 6, distState, text_color=distState != "none" ? #ff1744 : color.new(chart.fg_color, 60), text_size=size.normal, text_halign=text.align_right)

        table.cell(statusTbl, 0, 7, "Sweeps↓ " + str.tostring(sweepCount) + " Spr " + str.tostring(springCount), text_color=color.new(chart.fg_color, 40), text_size=size.normal)
        table.cell(statusTbl, 1, 7, "Sweeps↑ " + str.tostring(sweepHighCount), text_color=color.new(chart.fg_color, 40), text_size=size.normal, text_halign=text.align_right)
        table.cell(statusTbl, 2, 7, "UT " + str.tostring(upthrustCount), text_color=color.new(chart.fg_color, 40), text_size=size.normal, text_halign=text.align_right)

        table.cell(statusTbl, 0, 8, "HL:" + (hasHigherLow ? "✓" : "·") + " LL:" + (hasLowerLow ? "✓" : "·") + " LH:" + (hasLowerHigh ? "✓" : "·"), text_color=color.new(chart.fg_color, 40), text_size=size.normal)
        table.cell(statusTbl, 1, 8, "", text_size=size.normal)
        table.cell(statusTbl, 2, 8, "", text_size=size.normal)

// ============================================================================
// DATA WINDOW
// ============================================================================

plot(accumScore, "Accum Score [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(distScore, "Distrib Score [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(trendLossScore, "Trend Loss [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(rangeScore, "Range [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(volCompScore, "Vol Compress [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(liqScore, "Liquidity [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(bullStructScore, "Bull Struct [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(effRatio, "Efficiency Ratio [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(bbwRatio, "BBW Ratio [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(atrRatio, "ATR Ratio [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(phaseDuration, "Accum Duration [dw]", display=display.data_window, color=color.new(color.gray, 100))
plot(distDuration, "Distrib Duration [dw]", display=display.data_window, color=color.new(color.gray, 100))

// ============================================================================
// ALERTS
// ============================================================================

bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed

alertcondition(alertOk and (accumState == "developing" and prevAccumState != "developing"), "Accum Developing", "VEIN-ACC · ACCUMULATION DEVELOPING · {{ticker}} {{interval}}")
alertcondition(alertOk and (accumState == "mature" and prevAccumState != "mature"), "Accum Mature", "VEIN-ACC · ACCUMULATION MATURE · {{ticker}} {{interval}}")
alertcondition(alertOk and (accumState == "breakout_ready" and prevAccumState != "breakout_ready"), "Breakout Ready", "VEIN-ACC · BREAKOUT READY · {{ticker}} {{interval}}")
alertcondition(alertOk and (distState == "developing" and prevDistState != "developing"), "Distrib Developing", "VEIN-ACC · DISTRIBUTION DEVELOPING · {{ticker}} {{interval}}")
alertcondition(alertOk and (distState == "mature" and prevDistState != "mature"), "Distrib Mature", "VEIN-ACC · DISTRIBUTION MATURE · {{ticker}} {{interval}}")
alertcondition(alertOk and (distState == "breakdown_ready" and prevDistState != "breakdown_ready"), "Breakdown Ready", "VEIN-ACC · BREAKDOWN READY · {{ticker}} {{interval}}")

// Track previous state for alerts — per side, so both phases can be tracked independently
if barstate.isconfirmed
    prevAccumState := accumState
    prevDistState  := distState

Overview

Vein: Accumulation Phase 4H [WavesUnchained] evaluates five scoring components: trend loss, range formation, volatility compression, liquidity behavior, and early bullish market structure. Each component contributes to a combined score that transitions through states from early development to breakout readiness. It visualizes accumulation progress directly on the price chart using background shading, chart labels, and an optional status table. Settings allow customization of ATR parameters, pivot lengths, and volatility compression thresholds.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Trend, Volatility & bands, SMC / ICT / price action, Support & resistance

Markets

Oil & commodities

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
ATR Lengthint14atrLen
Phase Lookback Barsint20lookback
Min Phase Durationint10minPhaseBars
Pivot Left Barsint3pivotLeft
Pivot Right Barsint3pivotRight
Efficiency Ratio Range Thresholdfloat0.3erRangeThresh
BB Width Compression Ratiofloat0.7bbwCompressThresh
ATR Compression Ratiofloat0.8atrCompressThresh
Display ModestringLightdisplayMode
Background ShadingbooltrueshowBgShading
Show Status TablebooltrueshowTable
Show Phase Label on ChartbooltrueshowPhaseLabel
Show all 13 settings
SettingTypeDefaultVariable
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

575 stars

This indicator detects equal highs and lows by comparing prices within user-defined left and right bars using a tolerance percentage. It plots horizontal lines on the price chart for equal highs in one color and lows in another. A screener allows input of mult

Indicator v5 Support & resistanceTrend
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows with Screener

[RS]Donchian Fans

by ricardosantos79
261 stars

This indicator plots Donchian Channel boundary lines along with dynamic fan projections directly on the price chart. It calculates the highest high, lowest low, and median price over a customizable lookback period. Whenever price establishes a new channel high

Indicator v4 Support & resistanceTrend
Updated 4 years ago · None stated GitHub repository of [RS]Donchian Fans

Dynamic Linear Regression

by TradersPost
218 stars

Dynamic Linear Regression calculates and plots linear regression trendlines and standard deviation channels directly on the price chart over a user-selected time period. Traders can configure static channels locked at the end time, dynamic channels that contin

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Dynamic Linear Regression

Open Source Fractal

by cantolab
183 stars

This indicator implements the Fractal Model concept to plot market structure shifts, liquidity sweeps, and Change in State of Delivery (CISD) levels. It overlays custom higher-timeframe candle previews, open prices, and time markers directly on the price chart

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · MIT GitHub repository of Open Source Fractal

ATR Trailing Stop

by mihakralj
99 stars

Calculates an ATR-based trailing stop overlaid on the price chart to assist with trend identification and stop placement. Using a specified ATR period and multiplier, it projects upper and lower bounds around the closing price. The stop line ratchets up during

Indicator v5 TrendVolatility & bands
Updated 4 weeks ago · Apache-2.0 GitHub repository of ATR Trailing Stop

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