Indicator · Pine Script v6 · On the chart

Trend Compass 4H

by chris-c-thomas

Trend Compass 4H plots a multi-EMA ribbon (10/21/50) along with higher-timeframe reference levels on the price chart.

View on GitHub

GitHub stars14on the repository
Pine versionv6current
Lines of code855
Settings40inputs
LicenceMIT
Last update6 months ago8 Apr 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

855 lines of Pine Script v6 from scripts/trend-compass/trend-compass-4h.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

trend-compass-4h.pine
// ============================================================================
// Trend Compass — 4-Hour
// Timeframe: 4-Hour (primary), compatible with 2H/6H
// Version: 1.0.0
// Author: Chris Thomas
// ============================================================================
// Strategic trend assessment overlay for equities, ETFs, and indices.
// Intermediate-term variant bridging intraday monitors and the Daily
// Trend Compass. Answers: What is the multi-day trend? Is it gaining or
// losing momentum? Are divergences forming? Where is price relative to
// institutional structure?
//
// No signal generation. Pure trend context, strength assessment, and
// structural positioning. Ticker-agnostic — works on NVDA, AAPL, GOOGL,
// SPY, QQQ, IWM, or any liquid instrument.
//
// Key difference from Daily variant:
//   - HTF context pulls Daily 50 EMA, Daily 200 EMA, and Weekly 50 EMA
//   - Anchor EMA is the Daily 200 EMA (institutional reference)
//   - Key levels: Prior Day H/L/C + Prior Week H/L/C
//   - Divergence detection tuned for faster pivot detection on 4H bars
//   - Longer percentile lookbacks to cover equivalent calendar time
//
// Components:
//   - Multi-EMA ribbon (10/21/50) + Daily 200 EMA anchor
//   - HTF context: Daily 50 EMA, Daily 200 EMA, Weekly 50 EMA
//   - Trend phase classifier (6 states)
//   - Composite weighted trend score (-11 to +11)
//   - RSI + MACD divergence detection
//   - OBV trend confirmation
//   - ATR regime + BB width percentile
//   - Optional Ichimoku Cloud overlay
//   - Optional Fibonacci retracement levels
//   - 52-week high/low context (via Daily timeframe)
//   - 18-row heads-up dashboard
//   - 12 alert conditions
// ============================================================================

// ============================================================================
// TradingView Pine Script v6
// ============================================================================
//@version=6
indicator(
     title          = "Trend Compass 4H",
     shorttitle     = "TC-4H",
     overlay        = true,
     max_labels_count  = 500,
     max_lines_count   = 500,
     max_boxes_count   = 100,
     max_bars_back     = 5000
 )

// ============================================================================
// CONSTANTS
// ============================================================================
var string GP_EMA      = "EMA Ribbon"
var string GP_HTF      = "Higher Timeframe"
var string GP_TREND    = "Trend Strength (ADX)"
var string GP_RSI      = "RSI"
var string GP_MACD     = "MACD"
var string GP_VOL      = "Volume"
var string GP_ATR      = "Volatility (ATR)"
var string GP_BB       = "Bollinger Band Width"
var string GP_LEVELS   = "Key Levels"
var string GP_FIB      = "Fibonacci Retracement"
var string GP_ICH      = "Ichimoku Cloud"
var string GP_SCORE    = "Scoring Engine"
var string GP_DASH     = "Dashboard"

// Colors — dark-theme optimized, consistent with existing suite
var color C_BULL       = #00E676
var color C_BEAR       = #FF1744
var color C_NEUTRAL    = #B0BEC5
var color C_YELLOW     = #FFD600
var color C_ORANGE     = #FF9100
var color C_CYAN       = #00BCD4
var color C_PURPLE     = #7C4DFF
var color C_WHITE      = #FFFFFF
var color C_GRAY       = #78909C
var color C_DARK_GRAY  = #455A64
var color C_DASH_BG    = color.new(#1A1A2E, 5)
var color C_DASH_TEXT  = #E0E0E0
var color C_DASH_HEADER = #7C4DFF
var color C_CLOUD_BULL = color.new(#00E676, 85)
var color C_CLOUD_BEAR = color.new(#FF1744, 85)
var color C_CLOUD_MIX  = color.new(#FFD600, 90)
var color C_200_EMA    = #FF6D00
var color C_D50_EMA    = #26C6DA
var color C_HTF_EMA    = #E040FB
var color C_DAY_LVL    = #42A5F5
var color C_WEEK_LVL   = #AB47BC
var color C_52W_LVL    = #FF8A65
var color C_FIB_382    = #80CBC4
var color C_FIB_500    = #FFD54F
var color C_FIB_618    = #FF8A65
var color C_FIB_ZONE   = color.new(#FFD54F, 92)
var color C_ICH_BULL   = color.new(#00E676, 88)
var color C_ICH_BEAR   = color.new(#FF1744, 88)
var color C_ICH_TENKAN = #2979FF
var color C_ICH_KIJUN  = #FF6D00

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

// --- EMA Ribbon ---
int    i_emaFastLen  = input.int(10,  "Fast EMA",           minval=2,  group=GP_EMA)
int    i_emaMidLen   = input.int(21,  "Mid EMA",            minval=2,  group=GP_EMA)
int    i_emaSlowLen  = input.int(50,  "Slow EMA",           minval=2,  group=GP_EMA)
bool   i_showEma     = input.bool(true, "Show EMA Ribbon",             group=GP_EMA)
bool   i_showCloud   = input.bool(true, "Show Cloud Fill",             group=GP_EMA)

// --- Higher Timeframe ---
bool   i_useHTF      = input.bool(true,  "Enable HTF Confirmation",   group=GP_HTF)
bool   i_showD200    = input.bool(true,  "Show Daily 200 EMA",        group=GP_HTF, tooltip="The institutional anchor. Orange line.")
bool   i_showD50     = input.bool(true,  "Show Daily 50 EMA",         group=GP_HTF, tooltip="Intermediate structural trend. Cyan line.")
bool   i_showW50     = input.bool(true,  "Show Weekly 50 EMA",        group=GP_HTF, tooltip="Macro trend reference. Purple step line.")

// --- Trend Strength (ADX) ---
int    i_adxLen      = input.int(14,  "ADX Length",          minval=5,  group=GP_TREND)
int    i_adxSlopeLB  = input.int(5,   "ADX Slope Lookback",  minval=2, maxval=20, group=GP_TREND)
int    i_emaSlopeLB  = input.int(5,   "EMA Slope Lookback",  minval=2, maxval=20, group=GP_TREND)
float  i_adxSlopeThr = input.float(0.3, "ADX Slope Threshold", minval=0.05, step=0.05, group=GP_TREND)

// --- RSI ---
int    i_rsiLen      = input.int(14,  "RSI Length",          minval=2,  group=GP_RSI)
bool   i_rsiDiv      = input.bool(true, "Enable RSI Divergence",      group=GP_RSI)
int    i_rsiPivotL   = input.int(4,   "Divergence Pivot Left Bars",  minval=2, maxval=20, group=GP_RSI, tooltip="Shorter than Daily variant (4 vs 5) for faster detection on 4H bars.")
int    i_rsiPivotR   = input.int(2,   "Divergence Pivot Right Bars", minval=1, maxval=10, group=GP_RSI)

// --- MACD ---
int    i_macdFast    = input.int(12,  "MACD Fast",           minval=2,  group=GP_MACD)
int    i_macdSlow    = input.int(26,  "MACD Slow",           minval=2,  group=GP_MACD)
int    i_macdSignal  = input.int(9,   "MACD Signal",         minval=2,  group=GP_MACD)
bool   i_macdDiv     = input.bool(true, "Enable MACD Divergence",     group=GP_MACD)
int    i_macdPivotL  = input.int(4,   "MACD Div Pivot Left",  minval=2, maxval=20, group=GP_MACD)
int    i_macdPivotR  = input.int(2,   "MACD Div Pivot Right", minval=1, maxval=10, group=GP_MACD)

// --- Volume ---
bool   i_useOBV      = input.bool(true, "Enable OBV Analysis",        group=GP_VOL)
int    i_obvEmaLen   = input.int(21,  "OBV EMA Length",      minval=5,  group=GP_VOL)
int    i_relVolLen   = input.int(20,  "Relative Volume Lookback",     minval=5, group=GP_VOL)

// --- ATR / Volatility ---
int    i_atrLen      = input.int(14,  "ATR Length",          minval=2,  group=GP_ATR)
int    i_atrPctlLB   = input.int(200, "ATR Percentile Lookback", minval=20, maxval=500, group=GP_ATR, tooltip="200 bars on 4H ≈ 33 trading days. Wider than Daily variant (100) to cover equivalent calendar time.")

// --- BB Width ---
int    i_bbLen       = input.int(20,  "BB Length",           minval=5,  group=GP_BB)
float  i_bbMult      = input.float(2.0, "BB Multiplier",    minval=0.5, step=0.1, group=GP_BB)
int    i_bbwPctlLB   = input.int(200, "BB Width Percentile Lookback", minval=20, maxval=500, group=GP_BB, tooltip="200 bars on 4H ≈ 33 trading days.")

// --- Key Levels ---
bool   i_show52w     = input.bool(true,  "Show 52-Week High/Low",     group=GP_LEVELS)
bool   i_showPrDay   = input.bool(true,  "Show Prior Day H/L/C",      group=GP_LEVELS)
bool   i_showPrWk    = input.bool(true,  "Show Prior Week H/L/C",     group=GP_LEVELS)

// --- Fibonacci Retracement ---
bool   i_showFib     = input.bool(false, "Show Fibonacci Levels",     group=GP_FIB, tooltip="Auto-calculated over the lookback period. Default OFF.")
int    i_fibLB       = input.int(100,    "Fibonacci Lookback Bars",   minval=10, maxval=500, group=GP_FIB, tooltip="100 bars on 4H ≈ 17 trading days.")
bool   i_showFibFill = input.bool(true,  "Show Fib Zone Fill",        group=GP_FIB)

// --- Ichimoku Cloud ---
bool   i_showIchi    = input.bool(false, "Show Ichimoku Cloud",       group=GP_ICH, tooltip="Traditional Ichimoku Kinko Hyo overlay. Default OFF.")
int    i_ichiTenkan  = input.int(9,      "Tenkan-sen (Conversion)",   minval=2,  group=GP_ICH)
int    i_ichiKijun   = input.int(26,     "Kijun-sen (Base)",          minval=2,  group=GP_ICH)
int    i_ichiSenkouB = input.int(52,     "Senkou Span B (Leading B)", minval=2,  group=GP_ICH)
int    i_ichiDisp    = input.int(26,     "Displacement",              minval=1,  group=GP_ICH)

// --- Scoring Engine ---
bool   i_useWeighted = input.bool(true, "Use Weighted Scoring",       group=GP_SCORE, tooltip="Structural conditions score 2x, confirmation 1x. Disable for equal weighting.")

// --- Dashboard ---
bool   i_showDash    = input.bool(true,  "Show Dashboard",            group=GP_DASH)
string i_dashPos     = input.string("Bottom Right", "Position", options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group=GP_DASH)
string i_dashSize    = input.string("Small", "Size", options=["Tiny", "Small", "Normal", "Large"], group=GP_DASH)

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
f1(float v) => str.tostring(v, "#.#")
f2(float v) => str.tostring(v, "#.##")
f0(float v) => str.tostring(v, "#")

posnegColor(float v) => v > 0 ? C_BULL : v < 0 ? C_BEAR : C_NEUTRAL

biasColor(int score, int maxS) =>
    float pct = maxS != 0 ? math.abs(score) / maxS : 0
    if score > 0
        pct > 0.6 ? C_BULL : pct > 0.3 ? color.new(C_BULL, 30) : C_NEUTRAL
    else if score < 0
        pct > 0.6 ? C_BEAR : pct > 0.3 ? color.new(C_BEAR, 30) : C_NEUTRAL
    else
        C_NEUTRAL

biasLabel(int score, int maxS) =>
    float pct = maxS != 0 ? math.abs(score) / maxS : 0
    if math.abs(score) == 0
        "NEUTRAL"
    else if pct > 0.6
        score > 0 ? "STRONG BULL" : "STRONG BEAR"
    else if pct > 0.3
        score > 0 ? "LEAN BULL" : "LEAN BEAR"
    else
        score > 0 ? "WEAK BULL" : "WEAK BEAR"

// ============================================================================
// EMA CALCULATIONS (local, computed on 4H bars)
// ============================================================================
float emaFast = ta.ema(close, i_emaFastLen)
float emaMid  = ta.ema(close, i_emaMidLen)
float emaSlow = ta.ema(close, i_emaSlowLen)

bool emaBullish = emaFast > emaMid and emaMid > emaSlow
bool emaBearish = emaFast < emaMid and emaMid < emaSlow

// EMA spread — normalized to percentage for ticker-agnostic comparison
float emaSpread     = emaSlow != 0 ? math.abs(emaFast - emaSlow) / emaSlow * 100 : 0.0
bool  emaCompressed = emaSpread < 0.3  // Tighter threshold than Daily (0.5) — 4H EMAs are closer by nature

// ============================================================================
// HIGHER TIMEFRAME DATA
// ============================================================================

// Daily 200 EMA — the institutional anchor (primary structural reference)
float d200Ema = request.security(syminfo.tickerid, "D", ta.ema(close, 200), lookahead=barmerge.lookahead_off)

// Daily 50 EMA — intermediate structural trend
float d50Ema = request.security(syminfo.tickerid, "D", ta.ema(close, 50), lookahead=barmerge.lookahead_off)

// Weekly 50 EMA — macro trend context
float w50Ema = request.security(syminfo.tickerid, "W", ta.ema(close, 50), lookahead=barmerge.lookahead_off)

// Prior Day high / low / close
[pdHigh, pdLow, pdClose] = request.security(syminfo.tickerid, "D", [high[1], low[1], close[1]], lookahead=barmerge.lookahead_on)

// Prior Week high / low / close
[pwHigh, pwLow, pwClose] = request.security(syminfo.tickerid, "W", [high[1], low[1], close[1]], lookahead=barmerge.lookahead_on)

// 52-week high / low (via Daily for accuracy — 252 trading days)
[d52High, d52Low] = request.security(syminfo.tickerid, "D", [ta.highest(high, 252), ta.lowest(low, 252)], lookahead=barmerge.lookahead_off)

// EMA stack including Daily 200 as anchor
bool fullStackBull = emaBullish and not na(d200Ema) and emaSlow > d200Ema
bool fullStackBear = emaBearish and not na(d200Ema) and emaSlow < d200Ema

// EMA stack label
string emaStackStr = fullStackBull ? "FULL BULL" : fullStackBear ? "FULL BEAR" : emaBullish ? "PARTIAL BULL" : emaBearish ? "PARTIAL BEAR" : emaCompressed ? "COMPRESSED" : "MIXED"
color  emaStackClr = fullStackBull ? C_BULL : fullStackBear ? C_BEAR : emaBullish ? color.new(C_BULL, 40) : emaBearish ? color.new(C_BEAR, 40) : emaCompressed ? C_YELLOW : C_NEUTRAL

// HTF trend assessments
bool d200Bullish = not na(d200Ema) and close > d200Ema
bool d200Bearish = not na(d200Ema) and close < d200Ema
bool d50Bullish  = not na(d50Ema)  and close > d50Ema
bool d50Bearish  = not na(d50Ema)  and close < d50Ema
bool w50Bullish  = not na(w50Ema)  and close > w50Ema
bool w50Bearish  = not na(w50Ema)  and close < w50Ema

// ============================================================================
// ADX / DMI
// ============================================================================
[diPlus, diMinus, adxVal] = ta.dmi(i_adxLen, i_adxLen)

// ADX slope (rate of change over lookback)
float adxSlope = adxVal - nz(adxVal[i_adxSlopeLB])

// ADX tier classification
string adxTier = adxVal > 45 ? "EXTREME" : adxVal > 30 ? "STRONG" : adxVal > 20 ? "MODERATE" : "WEAK"
color  adxClr  = adxVal > 45 ? C_ORANGE : adxVal > 30 ? C_BULL : adxVal > 20 ? C_CYAN : C_GRAY

// ADX slope label
string adxSlopeStr = adxSlope > i_adxSlopeThr ? "RISING" : adxSlope < -i_adxSlopeThr ? "FALLING" : "FLAT"
color  adxSlopeClr = adxSlope > i_adxSlopeThr ? C_BULL : adxSlope < -i_adxSlopeThr ? C_BEAR : C_NEUTRAL

// ============================================================================
// 50 EMA SLOPE ANALYSIS
// ============================================================================
float emaSlopeRaw  = emaSlow - nz(emaSlow[i_emaSlopeLB])
float emaSlopeNorm = emaSlow != 0 ? emaSlopeRaw / emaSlow * 100 : 0.0
bool  emaSlopePos  = emaSlopeNorm > 0.005   // Tighter than Daily (0.01) — smaller moves on 4H
bool  emaSlopeNeg  = emaSlopeNorm < -0.005

// Slope acceleration (second derivative)
float prevSlopeNorm  = nz(emaSlow[i_emaSlopeLB]) != 0 ? (nz(emaSlow[i_emaSlopeLB]) - nz(emaSlow[i_emaSlopeLB * 2])) / nz(emaSlow[i_emaSlopeLB]) * 100 : 0.0
float slopeAccel     = emaSlopeNorm - prevSlopeNorm

// ============================================================================
// TREND PHASE CLASSIFICATION
// ============================================================================
// Detect recent EMA crossovers
bool fastCrossMid = ta.cross(emaFast, emaMid)
bool midCrossSlow = ta.cross(emaMid, emaSlow)
int  barsSinceFXM = ta.barssince(fastCrossMid)
int  barsSinceMXS = ta.barssince(midCrossSlow)
// Wider bar window than Daily (4/6 vs 3/5) — 4H bars span less time per bar
bool recentCross  = (not na(barsSinceFXM) and barsSinceFXM <= 4) or (not na(barsSinceMXS) and barsSinceMXS <= 6)

// Phase determination (priority order matters)
string trendPhase = "CONSOLIDATING"
color  phaseClr   = C_NEUTRAL

if recentCross
    trendPhase := "REVERSING"
    phaseClr   := C_ORANGE
else if adxVal > 30 and adxSlope < -i_adxSlopeThr
    trendPhase := "EXHAUSTING"
    phaseClr   := C_YELLOW
else if adxVal > 30 and math.abs(adxSlope) <= i_adxSlopeThr
    trendPhase := "MATURE"
    phaseClr   := C_CYAN
else if adxVal > 20 and adxSlope > i_adxSlopeThr
    trendPhase := "ACCELERATING"
    phaseClr   := C_BULL
else if adxVal <= 25 and adxSlope > i_adxSlopeThr and not emaCompressed
    trendPhase := "EMERGING"
    phaseClr   := color.new(C_BULL, 30)
else if adxVal < 20 and emaCompressed
    trendPhase := "CONSOLIDATING"
    phaseClr   := C_GRAY
else
    trendPhase := "CONSOLIDATING"
    phaseClr   := C_GRAY

// ============================================================================
// RSI
// ============================================================================
float rsiVal = ta.rsi(close, i_rsiLen)

string rsiLbl = rsiVal > 70 ? "OVERBOUGHT" : rsiVal > 55 ? "BULLISH" : rsiVal < 30 ? "OVERSOLD" : rsiVal < 45 ? "BEARISH" : "NEUTRAL"
color  rsiClr = rsiVal > 70 ? C_ORANGE : rsiVal > 55 ? C_BULL : rsiVal < 30 ? C_ORANGE : rsiVal < 45 ? C_BEAR : C_NEUTRAL

// ============================================================================
// RSI DIVERGENCE DETECTION
// ============================================================================
var float  prevRsiPL     = na
var float  prevPricePL   = na
var int    prevPivLowBar = na
var float  prevRsiPH     = na
var float  prevPricePH   = na
var int    prevPivHiBar  = na

float rsiPivotLow  = i_rsiDiv ? ta.pivotlow(rsiVal, i_rsiPivotL, i_rsiPivotR)  : na
float pricePivLow  = i_rsiDiv ? ta.pivotlow(low,    i_rsiPivotL, i_rsiPivotR)   : na
float rsiPivotHigh = i_rsiDiv ? ta.pivothigh(rsiVal, i_rsiPivotL, i_rsiPivotR)  : na
float pricePivHigh = i_rsiDiv ? ta.pivothigh(high,   i_rsiPivotL, i_rsiPivotR)  : na

var string rsiDivState  = "NONE"
var color  rsiDivClr    = C_NEUTRAL
var int    rsiDivBar    = na

// Bullish divergence: price lower low, RSI higher low
if not na(rsiPivotLow) and not na(pricePivLow)
    int curPivBar = bar_index - i_rsiPivotR
    if not na(prevRsiPL) and not na(prevPricePL) and not na(prevPivLowBar)
        if pricePivLow < prevPricePL and rsiPivotLow > prevRsiPL
            rsiDivState := "BULL DIV"
            rsiDivClr   := C_BULL
            rsiDivBar   := bar_index
            line.new(prevPivLowBar, prevPricePL, curPivBar, pricePivLow, color=C_BULL, style=line.style_dashed, width=2)
        else if pricePivLow > prevPricePL and rsiPivotLow < prevRsiPL
            rsiDivState := "HIDDEN BULL"
            rsiDivClr   := color.new(C_BULL, 40)
            rsiDivBar   := bar_index
    prevRsiPL     := rsiPivotLow
    prevPricePL   := pricePivLow
    prevPivLowBar := curPivBar

// Bearish divergence: price higher high, RSI lower high
if not na(rsiPivotHigh) and not na(pricePivHigh)
    int curPivBar = bar_index - i_rsiPivotR
    if not na(prevRsiPH) and not na(prevPricePH) and not na(prevPivHiBar)
        if pricePivHigh > prevPricePH and rsiPivotHigh < prevRsiPH
            rsiDivState := "BEAR DIV"
            rsiDivClr   := C_BEAR
            rsiDivBar   := bar_index
            line.new(prevPivHiBar, prevPricePH, curPivBar, pricePivHigh, color=C_BEAR, style=line.style_dashed, width=2)
        else if pricePivHigh < prevPricePH and rsiPivotHigh > prevRsiPH
            rsiDivState := "HIDDEN BEAR"
            rsiDivClr   := color.new(C_BEAR, 40)
            rsiDivBar   := bar_index
    prevRsiPH     := rsiPivotHigh
    prevPricePH   := pricePivHigh
    prevPivHiBar  := curPivBar

// Decay divergence state after N bars (20 on 4H ≈ ~3.5 days)
if not na(rsiDivBar) and bar_index - rsiDivBar > 20
    rsiDivState := "NONE"
    rsiDivClr   := C_NEUTRAL

// ============================================================================
// MACD
// ============================================================================
[macdLine, macdSignalLine, macdHist] = ta.macd(close, i_macdFast, i_macdSlow, i_macdSignal)

string macdStr = macdHist > 0 ? "BULLISH" : macdHist < 0 ? "BEARISH" : "NEUTRAL"
color  macdClr = macdHist > 0 ? C_BULL : macdHist < 0 ? C_BEAR : C_NEUTRAL

bool macdAboveSignal = macdLine > macdSignalLine
string macdXStr = macdAboveSignal ? "ABOVE SIG" : "BELOW SIG"

// ============================================================================
// MACD DIVERGENCE DETECTION
// ============================================================================
var float  prevMacdPL     = na
var float  prevMacdPrPL   = na
var int    prevMacdPLBar  = na
var float  prevMacdPH     = na
var float  prevMacdPrPH   = na
var int    prevMacdPHBar  = na

float macdPivotLow  = i_macdDiv ? ta.pivotlow(macdHist, i_macdPivotL, i_macdPivotR)  : na
float macdPricePL   = i_macdDiv ? ta.pivotlow(low,      i_macdPivotL, i_macdPivotR)   : na
float macdPivotHigh = i_macdDiv ? ta.pivothigh(macdHist, i_macdPivotL, i_macdPivotR)  : na
float macdPricePH   = i_macdDiv ? ta.pivothigh(high,     i_macdPivotL, i_macdPivotR)  : na

var string macdDivState = "NONE"
var color  macdDivClr   = C_NEUTRAL
var int    macdDivBar   = na

// MACD Bullish divergence
if not na(macdPivotLow) and not na(macdPricePL)
    int curBar = bar_index - i_macdPivotR
    if not na(prevMacdPL) and not na(prevMacdPrPL) and not na(prevMacdPLBar)
        if macdPricePL < prevMacdPrPL and macdPivotLow > prevMacdPL
            macdDivState := "BULL DIV"
            macdDivClr   := C_BULL
            macdDivBar   := bar_index
            line.new(prevMacdPLBar, prevMacdPrPL, curBar, macdPricePL, color=C_CYAN, style=line.style_dotted, width=2)
        else if macdPricePL > prevMacdPrPL and macdPivotLow < prevMacdPL
            macdDivState := "HIDDEN BULL"
            macdDivClr   := color.new(C_CYAN, 40)
            macdDivBar   := bar_index
    prevMacdPL    := macdPivotLow
    prevMacdPrPL  := macdPricePL
    prevMacdPLBar := curBar

// MACD Bearish divergence
if not na(macdPivotHigh) and not na(macdPricePH)
    int curBar = bar_index - i_macdPivotR
    if not na(prevMacdPH) and not na(prevMacdPrPH) and not na(prevMacdPHBar)
        if macdPricePH > prevMacdPrPH and macdPivotHigh < prevMacdPH
            macdDivState := "BEAR DIV"
            macdDivClr   := C_BEAR
            macdDivBar   := bar_index
            line.new(prevMacdPHBar, prevMacdPrPH, curBar, macdPricePH, color=C_PURPLE, style=line.style_dotted, width=2)
        else if macdPricePH < prevMacdPrPH and macdPivotHigh > prevMacdPH
            macdDivState := "HIDDEN BEAR"
            macdDivClr   := color.new(C_PURPLE, 40)
            macdDivBar   := bar_index
    prevMacdPH    := macdPivotHigh
    prevMacdPrPH  := macdPricePH
    prevMacdPHBar := curBar

// Decay (20 bars on 4H ≈ ~3.5 days)
if not na(macdDivBar) and bar_index - macdDivBar > 20
    macdDivState := "NONE"
    macdDivClr   := C_NEUTRAL

// ============================================================================
// OBV (On-Balance Volume)
// ============================================================================
float obvVal    = ta.obv
float obvEma    = ta.ema(obvVal, i_obvEmaLen)
float obvSlope  = obvEma - nz(obvEma[5])

// OBV confirms price trend?
bool priceUp    = emaSlopePos
bool obvUp      = obvSlope > 0
bool priceDown  = emaSlopeNeg
bool obvDown    = obvSlope < 0

string obvStr = "NEUTRAL"
color  obvClr = C_NEUTRAL
if i_useOBV
    if (priceUp and obvUp) or (priceDown and obvDown)
        obvStr := "CONFIRMING"
        obvClr := C_BULL
    else if (priceUp and obvDown) or (priceDown and obvUp)
        obvStr := "DIVERGING"
        obvClr := C_ORANGE
    else
        obvStr := "NEUTRAL"
        obvClr := C_NEUTRAL

// ============================================================================
// RELATIVE VOLUME
// ============================================================================
float avgVol  = ta.sma(volume, i_relVolLen)
float relVol  = avgVol != 0 ? volume / avgVol : 0.0

string relVolStr = relVol > 2.0 ? "SPIKE" : relVol > 1.2 ? "ABOVE" : relVol > 0.8 ? "NORMAL" : "DRY"
color  relVolClr = relVol > 2.0 ? C_ORANGE : relVol > 1.2 ? C_BULL : relVol > 0.8 ? C_NEUTRAL : C_GRAY

// ============================================================================
// ATR + PERCENTILE RANKING
// ============================================================================
float atrVal = ta.atr(i_atrLen)

int atrRank = 0
for i = 1 to i_atrPctlLB
    if atrVal > nz(atrVal[i])
        atrRank += 1
float atrPctl = i_atrPctlLB > 0 ? atrRank / i_atrPctlLB * 100 : 50.0

string atrRegime = atrPctl >= 90 ? "EXTREME" : atrPctl >= 70 ? "HIGH" : atrPctl >= 30 ? "NORMAL" : "LOW"
color  atrClr    = atrPctl >= 90 ? C_ORANGE : atrPctl >= 70 ? C_YELLOW : atrPctl >= 30 ? C_NEUTRAL : C_CYAN

// ============================================================================
// BOLLINGER BAND WIDTH PERCENTILE
// ============================================================================
[bbMid, bbUp, bbLow] = ta.bb(close, i_bbLen, i_bbMult)
float bbWidth = bbMid != 0 ? (bbUp - bbLow) / bbMid * 100 : 0.0

int bbwRank = 0
for i = 1 to i_bbwPctlLB
    if bbWidth > nz(bbWidth[i])
        bbwRank += 1
float bbwPctl = i_bbwPctlLB > 0 ? bbwRank / i_bbwPctlLB * 100 : 50.0

string bbwStr = bbwPctl < 10 ? "COMPRESSED" : bbwPctl < 30 ? "NARROW" : bbwPctl > 90 ? "EXPANDED" : bbwPctl > 70 ? "WIDE" : "NORMAL"
color  bbwClr = bbwPctl < 10 ? C_CYAN : bbwPctl < 30 ? color.new(C_CYAN, 40) : bbwPctl > 90 ? C_ORANGE : bbwPctl > 70 ? C_YELLOW : C_NEUTRAL

// ============================================================================
// 52-WEEK HIGH / LOW (from Daily timeframe for accuracy)
// ============================================================================
float high52w = nz(d52High)
float low52w  = nz(d52Low)
float range52 = high52w - low52w
float pos52   = range52 != 0 ? (close - low52w) / range52 * 100 : 50.0

// ============================================================================
// FIBONACCI RETRACEMENT (optional)
// ============================================================================
float fibHigh = ta.highest(high, i_fibLB)
float fibLow  = ta.lowest(low,   i_fibLB)
float fibRange = fibHigh - fibLow

float fib382 = fibHigh - fibRange * 0.382
float fib500 = fibHigh - fibRange * 0.500
float fib618 = fibHigh - fibRange * 0.618

var line fibLine382 = na
var line fibLine500 = na
var line fibLine618 = na
var line fibLineHi  = na
var line fibLineLo  = na
var label fibLbl382 = na
var label fibLbl500 = na
var label fibLbl618 = na

if i_showFib and barstate.islast
    line.delete(fibLine382)
    line.delete(fibLine500)
    line.delete(fibLine618)
    line.delete(fibLineHi)
    line.delete(fibLineLo)
    label.delete(fibLbl382)
    label.delete(fibLbl500)
    label.delete(fibLbl618)

    int x1 = bar_index - i_fibLB
    int x2 = bar_index + 10

    fibLineHi  := line.new(x1, fibHigh, x2, fibHigh, color=C_GRAY,    style=line.style_dotted, width=1)
    fibLineLo  := line.new(x1, fibLow,  x2, fibLow,  color=C_GRAY,    style=line.style_dotted, width=1)
    fibLine382 := line.new(x1, fib382,  x2, fib382,  color=C_FIB_382, style=line.style_dashed, width=1)
    fibLine500 := line.new(x1, fib500,  x2, fib500,  color=C_FIB_500, style=line.style_dashed, width=1)
    fibLine618 := line.new(x1, fib618,  x2, fib618,  color=C_FIB_618, style=line.style_dashed, width=1)

    fibLbl382 := label.new(x2, fib382, "38.2%", style=label.style_none, textcolor=C_FIB_382, size=size.tiny)
    fibLbl500 := label.new(x2, fib500, "50.0%", style=label.style_none, textcolor=C_FIB_500, size=size.tiny)
    fibLbl618 := label.new(x2, fib618, "61.8%", style=label.style_none, textcolor=C_FIB_618, size=size.tiny)

// ============================================================================
// ICHIMOKU CLOUD (optional)
// ============================================================================
float tenkan  = i_showIchi ? (ta.highest(high, i_ichiTenkan) + ta.lowest(low, i_ichiTenkan)) / 2 : na
float kijun   = i_showIchi ? (ta.highest(high, i_ichiKijun)  + ta.lowest(low, i_ichiKijun))  / 2 : na
float senkouA = i_showIchi ? (tenkan + kijun) / 2 : na
float senkouB = i_showIchi ? (ta.highest(high, i_ichiSenkouB) + ta.lowest(low, i_ichiSenkouB)) / 2 : na
float chikou  = i_showIchi ? close : na

// ============================================================================
// COMPOSITE TREND SCORE
// ============================================================================
int wStructural = i_useWeighted ? 2 : 1
int wConfirm    = 1

int trendScore = 0

// --- Structural conditions (2x weight in weighted mode) ---

// 1. EMA Stack alignment (fast > mid > slow on 4H)
trendScore += emaBullish ? wStructural : emaBearish ? -wStructural : 0

// 2. Price vs Daily 200 EMA (institutional anchor)
if not na(d200Ema)
    trendScore += close > d200Ema ? wStructural : close < d200Ema ? -wStructural : 0

// 3. Price vs Weekly 50 EMA (macro)
if i_useHTF and not na(w50Ema)
    trendScore += close > w50Ema ? wStructural : close < w50Ema ? -wStructural : 0

// --- Confirmation conditions (1x weight) ---

// 4. ADX + DI direction
if adxVal > 20
    trendScore += diPlus > diMinus ? wConfirm : diMinus > diPlus ? -wConfirm : 0

// 5. RSI position
trendScore += rsiVal > 55 ? wConfirm : rsiVal < 45 ? -wConfirm : 0

// 6. MACD above/below signal
trendScore += macdAboveSignal ? wConfirm : -wConfirm

// 7. OBV trend confirmation
if i_useOBV
    trendScore += obvSlope > 0 ? wConfirm : obvSlope < 0 ? -wConfirm : 0

// 8. 50 EMA slope direction (local 4H)
trendScore += emaSlopePos ? wConfirm : emaSlopeNeg ? -wConfirm : 0

// --- Maximum possible score ---
int maxScore = 3 * wStructural + 5 * wConfirm

// --- Trend score delta tracking ---
float scoreAvg5  = ta.sma(trendScore, 5)
string scoreTrend = trendScore > scoreAvg5 + 0.5 ? "IMPROVING" : trendScore < scoreAvg5 - 0.5 ? "FADING" : "STABLE"
color scoreTrendClr = scoreTrend == "IMPROVING" ? C_BULL : scoreTrend == "FADING" ? C_BEAR : C_NEUTRAL

// ============================================================================
// DISTANCE FROM DAILY 200 EMA
// ============================================================================
float dist200 = not na(d200Ema) and d200Ema != 0 ? (close - d200Ema) / d200Ema * 100 : 0.0
color dist200Clr = dist200 > 5 ? C_BULL : dist200 > 0 ? color.new(C_BULL, 40) : dist200 < -5 ? C_BEAR : dist200 < 0 ? color.new(C_BEAR, 40) : C_NEUTRAL

// ============================================================================
// PLOTS — EMA Ribbon (local 4H)
// ============================================================================
plot(i_showEma ? emaFast : na,   "Fast EMA",   color=C_BULL,    linewidth=1)
plot(i_showEma ? emaMid  : na,   "Mid EMA",    color=C_YELLOW,  linewidth=1)
plot(i_showEma ? emaSlow : na,   "Slow EMA",   color=C_BEAR,    linewidth=1)

// EMA cloud fill
p_fast = plot(i_showEma and i_showCloud ? emaFast : na, "Fast (fill)", color=color(na), display=display.none)
p_slow = plot(i_showEma and i_showCloud ? emaSlow : na, "Slow (fill)", color=color(na), display=display.none)
fill(p_fast, p_slow, color=emaBullish ? C_CLOUD_BULL : emaBearish ? C_CLOUD_BEAR : C_CLOUD_MIX, title="EMA Cloud")

// HTF EMAs
plot(i_showD200 ? d200Ema : na, "Daily 200 EMA", color=C_200_EMA, linewidth=2, style=plot.style_line)
plot(i_showD50  ? d50Ema  : na, "Daily 50 EMA",  color=C_D50_EMA, linewidth=2, style=plot.style_stepline)
plot(i_useHTF and i_showW50 ? w50Ema : na, "Weekly 50 EMA", color=C_HTF_EMA, linewidth=2, style=plot.style_stepline)

// ============================================================================
// PLOTS — Ichimoku Cloud (optional)
// ============================================================================
plot(i_showIchi ? tenkan : na,  "Tenkan-sen",  color=C_ICH_TENKAN, linewidth=1)
plot(i_showIchi ? kijun  : na,  "Kijun-sen",   color=C_ICH_KIJUN,  linewidth=1)
plot(i_showIchi ? chikou : na,  "Chikou Span", color=C_PURPLE,      linewidth=1, offset=-i_ichiDisp)

p_senkouA = plot(i_showIchi ? senkouA : na, "Senkou A", color=C_BULL,   linewidth=1, offset=i_ichiDisp, display=display.pane)
p_senkouB = plot(i_showIchi ? senkouB : na, "Senkou B", color=C_BEAR,   linewidth=1, offset=i_ichiDisp, display=display.pane)
fill(p_senkouA, p_senkouB, color=senkouA > senkouB ? C_ICH_BULL : C_ICH_BEAR, title="Kumo Cloud")

// ============================================================================
// KEY LEVELS — drawn as horizontal lines on last bar
// ============================================================================

// --- Prior Day H/L/C ---
var line ldH = na, var line ldL = na, var line ldC = na
if i_showPrDay and barstate.islast and not na(pdHigh)
    line.delete(ldH), line.delete(ldL), line.delete(ldC)
    ldH := line.new(bar_index - 15, pdHigh, bar_index + 5, pdHigh, color=C_DAY_LVL, style=line.style_dashed, width=1)
    ldL := line.new(bar_index - 15, pdLow,  bar_index + 5, pdLow,  color=C_DAY_LVL, style=line.style_dashed, width=1)
    ldC := line.new(bar_index - 15, pdClose, bar_index + 5, pdClose, color=color.new(C_DAY_LVL, 40), style=line.style_dotted, width=1)

// --- Prior Week H/L/C ---
var line lwH = na, var line lwL = na, var line lwC = na
if i_showPrWk and barstate.islast and not na(pwHigh)
    line.delete(lwH), line.delete(lwL), line.delete(lwC)
    lwH := line.new(bar_index - 30, pwHigh, bar_index + 5, pwHigh, color=C_WEEK_LVL, style=line.style_dashed, width=1)
    lwL := line.new(bar_index - 30, pwLow,  bar_index + 5, pwLow,  color=C_WEEK_LVL, style=line.style_dashed, width=1)
    lwC := line.new(bar_index - 30, pwClose, bar_index + 5, pwClose, color=color.new(C_WEEK_LVL, 40), style=line.style_dotted, width=1)

// --- 52-Week H/L ---
var line l52H = na, var line l52L = na
if i_show52w and barstate.islast and not na(high52w) and high52w > 0
    line.delete(l52H), line.delete(l52L)
    l52H := line.new(bar_index - 50, high52w, bar_index + 5, high52w, color=C_52W_LVL, style=line.style_solid, width=1)
    l52L := line.new(bar_index - 50, low52w,  bar_index + 5, low52w,  color=C_52W_LVL, style=line.style_solid, width=1)

// ============================================================================
// DASHBOARD
// ============================================================================
if i_showDash and barstate.islast
    string tblPos = switch i_dashPos
        "Top Right"    => position.top_right
        "Top Left"     => position.top_left
        "Bottom Right" => position.bottom_right
        "Bottom Left"  => position.bottom_left
        => position.top_right

    string cellSize = switch i_dashSize
        "Tiny"   => size.tiny
        "Small"  => size.small
        "Normal" => size.normal
        "Large"  => size.large
        => size.small

    // 3 columns, 18 rows
    var table dash = table.new(tblPos, 3, 18,
         bgcolor      = C_DASH_BG,
         border_width = 0,
         border_color = color.new(#FFFFFF, 85),
         frame_width  = 1,
         frame_color  = color.new(#FFFFFF, 80))

    // === Row 0: Header ===
    table.cell(dash, 0, 0, "  Trend Compass - 4h  ", text_color=#FFFFFF,
         bgcolor=C_DASH_HEADER, text_size=cellSize, text_halign=text.align_center)
    table.cell(dash, 1, 0, "", bgcolor=C_DASH_HEADER, text_size=cellSize)
    table.cell(dash, 2, 0, "", bgcolor=C_DASH_HEADER, text_size=cellSize)
    table.merge_cells(dash, 0, 0, 2, 0)

    // === Row 1: Trend Phase ===
    table.cell(dash, 0, 1, "Phase",      text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 1, trendPhase,   text_color=phaseClr,    text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 1, "",           text_size=cellSize)

    // === Row 2: Trend Score ===
    string tsStr = (trendScore > 0 ? "+" : "") + str.tostring(trendScore) + "/" + str.tostring(maxScore)
    table.cell(dash, 0, 2, "Score",      text_color=C_DASH_TEXT,  text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 2, tsStr,        text_color=biasColor(trendScore, maxScore), text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 2, biasLabel(trendScore, maxScore), text_color=biasColor(trendScore, maxScore), text_size=cellSize, text_halign=text.align_right)

    // === Row 3: Score Trend ===
    table.cell(dash, 0, 3, "Momentum",   text_color=C_DASH_TEXT,    text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 3, scoreTrend,   text_color=scoreTrendClr,  text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 3, i_useWeighted ? "WTD" : "EQL", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)

    // === Row 4: EMA Stack ===
    table.cell(dash, 0, 4, "EMA Stack",  text_color=C_DASH_TEXT,  text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 4, emaStackStr,  text_color=emaStackClr,  text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 4, str.tostring(i_emaFastLen) + "/" + str.tostring(i_emaMidLen) + "/" + str.tostring(i_emaSlowLen),
         text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)

    // === Row 5: ADX ===
    table.cell(dash, 0, 5, "ADX",        text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 5, f1(adxVal),   text_color=adxClr,      text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 5, adxTier,      text_color=adxClr,      text_size=cellSize, text_halign=text.align_right)

    // === Row 6: ADX Slope ===
    table.cell(dash, 0, 6, "ADX Slope",  text_color=C_DASH_TEXT,  text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 6, adxSlopeStr,  text_color=adxSlopeClr,  text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 6, f1(adxSlope), text_color=adxSlopeClr,  text_size=cellSize, text_halign=text.align_right)

    // === Row 7: RSI ===
    table.cell(dash, 0, 7, "RSI",        text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 7, f1(rsiVal),   text_color=rsiClr,      text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 7, rsiLbl,       text_color=rsiClr,      text_size=cellSize, text_halign=text.align_right)

    // === Row 8: MACD ===
    table.cell(dash, 0, 8, "MACD",       text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 8, macdStr,      text_color=macdClr,     text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 8, macdXStr,     text_color=macdClr,     text_size=cellSize, text_halign=text.align_right)

    // === Row 9: RSI Divergence ===
    table.cell(dash, 0, 9, "RSI Div",    text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 9, rsiDivState,  text_color=rsiDivClr,   text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 9, "",           text_size=cellSize)

    // === Row 10: MACD Divergence ===
    table.cell(dash, 0, 10, "MACD Div",   text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 10, macdDivState, text_color=macdDivClr,  text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 10, "",           text_size=cellSize)

    // === Row 11: OBV Trend ===
    table.cell(dash, 0, 11, "OBV",        text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 11, obvStr,       text_color=obvClr,      text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 11, "",           text_size=cellSize)

    // === Row 12: Relative Volume ===
    table.cell(dash, 0, 12, "Rel Vol",    text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 12, f1(relVol) + "x", text_color=relVolClr, text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 12, relVolStr,    text_color=relVolClr,    text_size=cellSize, text_halign=text.align_right)

    // === Row 13: ATR Regime ===
    table.cell(dash, 0, 13, "ATR",        text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 13, f2(atrVal),   text_color=atrClr,      text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 13, atrRegime + " (" + f0(atrPctl) + "%)", text_color=atrClr, text_size=cellSize, text_halign=text.align_right)

    // === Row 14: BB Width Percentile ===
    table.cell(dash, 0, 14, "BB Width",   text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 14, f0(bbwPctl) + "%", text_color=bbwClr, text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 14, bbwStr,       text_color=bbwClr,      text_size=cellSize, text_halign=text.align_right)

    // === Row 15: Daily 200 EMA + distance ===
    string d200Str = d200Bullish ? "ABOVE" : d200Bearish ? "BELOW" : "N/A"
    color  d200DClr = d200Bullish ? C_BULL : d200Bearish ? C_BEAR : C_NEUTRAL
    string d200Dist = (dist200 >= 0 ? "+" : "") + f1(dist200) + "%"
    table.cell(dash, 0, 15, "D 200 EMA",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 15, d200Str,      text_color=d200DClr,    text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 15, d200Dist,     text_color=dist200Clr,  text_size=cellSize, text_halign=text.align_right)

    // === Row 16: Weekly 50 EMA ===
    string w50Str = not na(w50Ema) ? (w50Bullish ? "BULLISH" : w50Bearish ? "BEARISH" : "NEUTRAL") : "N/A"
    color  w50Clr = w50Bullish ? C_BULL : w50Bearish ? C_BEAR : C_NEUTRAL
    table.cell(dash, 0, 16, "W 50 EMA",   text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 16, w50Str,       text_color=w50Clr,      text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 16, "",           text_size=cellSize)

    // === Row 17: 52-Week Position ===
    if i_show52w and not na(high52w) and high52w > 0
        table.cell(dash, 0, 17, "52w Pos",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 17, f0(pos52) + "%", text_color=pos52 > 80 ? C_BULL : pos52 < 20 ? C_BEAR : C_NEUTRAL, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 17, "H:" + f2(high52w) + " L:" + f2(low52w), text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
    else
        table.cell(dash, 0, 17, "52w Pos",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 17, "N/A",     text_color=C_GRAY,      text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 17, "",         text_size=cellSize)

// ============================================================================
// ALERTS
// ============================================================================
string prevPhase = na(trendPhase[1]) ? "CONSOLIDATING" : trendPhase[1]
bool phaseChanged = trendPhase != prevPhase

alertcondition(phaseChanged, "Trend Phase Changed", "{{ticker}} trend phase changed on {{interval}}")

int strongThresh = i_useWeighted ? 7 : 5
alertcondition(trendScore >= strongThresh, "Strong Bullish Trend", "{{ticker}} strong bullish trend score on {{interval}}")
alertcondition(trendScore <= -strongThresh, "Strong Bearish Trend", "{{ticker}} strong bearish trend score on {{interval}}")
alertcondition(ta.cross(trendScore, 0),       "Score Crossed Neutral",  "{{ticker}} trend score crossed zero on {{interval}}")

alertcondition(rsiDivState == "BULL DIV" and rsiDivState[1] != "BULL DIV",     "RSI Bullish Divergence",  "{{ticker}} RSI bullish divergence detected on {{interval}}")
alertcondition(rsiDivState == "BEAR DIV" and rsiDivState[1] != "BEAR DIV",     "RSI Bearish Divergence",  "{{ticker}} RSI bearish divergence detected on {{interval}}")
alertcondition(macdDivState == "BULL DIV" and macdDivState[1] != "BULL DIV",   "MACD Bullish Divergence", "{{ticker}} MACD bullish divergence detected on {{interval}}")
alertcondition(macdDivState == "BEAR DIV" and macdDivState[1] != "BEAR DIV",   "MACD Bearish Divergence", "{{ticker}} MACD bearish divergence detected on {{interval}}")

// Golden / Death cross: use Daily 50 EMA crossing Daily 200 EMA
// Since both are from request.security, we detect crossover on the HTF values
bool goldenCross = not na(d50Ema) and not na(d200Ema) and d50Ema > d200Ema and nz(d50Ema[1]) <= nz(d200Ema[1])
bool deathCross  = not na(d50Ema) and not na(d200Ema) and d50Ema < d200Ema and nz(d50Ema[1]) >= nz(d200Ema[1])
alertcondition(goldenCross, "Golden Cross (D50/D200)", "{{ticker}} Daily 50 EMA crossed above Daily 200 EMA on {{interval}}")
alertcondition(deathCross,  "Death Cross (D50/D200)",  "{{ticker}} Daily 50 EMA crossed below Daily 200 EMA on {{interval}}")

string prevAtrRegime = na(atrRegime[1]) ? "NORMAL" : atrRegime[1]
bool atrRegimeChange = atrRegime != prevAtrRegime
alertcondition(atrRegimeChange, "ATR Regime Change", "{{ticker}} ATR regime changed on {{interval}}")

alertcondition(bbwPctl < 5, "BB Extreme Compression", "{{ticker}} BB width is in extreme compression (<5th percentile) on {{interval}}")

// ============================================================================
// END
// ============================================================================

Overview

Trend Compass 4H classifies trend phase into six states and calculates a composite weighted trend score. The script detects RSI and MACD divergences, analyzes OBV for confirmation, measures volatility with ATR and Bollinger Bands, and optionally overlays an Ichimoku cloud. It includes a heads-up dashboard and configurable inputs for ADX strength, slope thresholds, and daily/weekly anchors. No trade signals or position sizing are generated.

Details

Published by

chris-c-thomas on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Trend, Moving averages, Volatility & bands, Support & resistance

Markets

Any symbol

Timeframes

M15-H1, 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

financeoptionspinescriptpinescript-indicatorspinescript-strategiestradingtradingvewtradingview-chart-analysischarting

Settings

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

SettingTypeDefaultVariable
Fast EMAint10i_emaFastLen
Mid EMAint21i_emaMidLen
Slow EMAint50i_emaSlowLen
Show EMA Ribbonbooltruei_showEma
Show Cloud Fillbooltruei_showCloud
Enable HTF Confirmationbooltruei_useHTF
Show Daily 200 EMAbooltruei_showD200
Show Daily 50 EMAbooltruei_showD50
Show Weekly 50 EMAbooltruei_showW50
ADX Lengthint14i_adxLen
ADX Slope Lookbackint5i_adxSlopeLB
EMA Slope Lookbackint5i_emaSlopeLB
Show all 40 settings
SettingTypeDefaultVariable
ADX Slope Thresholdfloat0.3i_adxSlopeThr
RSI Lengthint14i_rsiLen
Enable RSI Divergencebooltruei_rsiDiv
Divergence Pivot Left Barsint4i_rsiPivotL
Divergence Pivot Right Barsint2i_rsiPivotR
MACD Fastint12i_macdFast
MACD Slowint26i_macdSlow
MACD Signalint9i_macdSignal
Enable MACD Divergencebooltruei_macdDiv
MACD Div Pivot Leftint4i_macdPivotL
MACD Div Pivot Rightint2i_macdPivotR
Enable OBV Analysisbooltruei_useOBV
OBV EMA Lengthint21i_obvEmaLen
Relative Volume Lookbackint20i_relVolLen
ATR Lengthint14i_atrLen
ATR Percentile Lookbackint200i_atrPctlLB
BB Lengthint20i_bbLen
BB Multiplierfloat2.0i_bbMult
BB Width Percentile Lookbackint200i_bbwPctlLB
Show 52-Week High/Lowbooltruei_show52w
Show Prior Day H/L/Cbooltruei_showPrDay
Show Prior Week H/L/Cbooltruei_showPrWk
Show Fibonacci Levelsboolfalsei_showFib
Fibonacci Lookback Barsint100i_fibLB
Show Fib Zone Fillbooltruei_showFibFill
Show Ichimoku Cloudboolfalsei_showIchi
Tenkan-sen (Conversion)int9i_ichiTenkan
Kijun-sen (Base)int26i_ichiKijun
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

MA Envelope (MAE)

by mihakralj
99 stars

The MA Envelope indicator calculates upper and lower channel bands shifted by a user-defined percentage around a baseline moving average. It supports Simple Moving Average, Exponential Moving Average, and Weighted Moving Average calculation types. The script p

Indicator v6 Moving averagesSupport & resistance
Updated 4 weeks ago · Apache-2.0 GitHub repository of MA Envelope (MAE)
10 stars

My Macro Cycle Indicator plots daily and weekly simple and exponential moving averages on the price chart, plus Pi Cycle and Mayer Multiple levels derived from the 200-day average. Optional toggles add support-band fills, a Keltner channel, CME futures gaps, a

Indicator v4 Moving averagesVolatility & bands
Updated 5 years ago · None stated GitHub repository of My Macro Cycle Indicator

VOIDRLM

by voidrlm
6 stars

The script VOIDRLM combines multiple technical analysis tools into a single overlay indicator on the price chart. It calculates multi-timeframe pivot points, volume-weighted average price (VWAP), customizable moving averages, moving average clouds, supply and

Indicator v5 TrendMoving averages Repaints
Updated 3 years ago · None stated GitHub repository of VOIDRLM
318 stars

This indicator plots four customizable moving averages directly on the main price chart using default lengths of 3, 8, 21, and 50 periods. It fills the space between adjacent averages in green or maroon depending on trend alignment. Additionally, it computes B

Indicator v5 Moving averagesTrend
Updated 2 months ago · MIT GitHub repository of Multiple Moving Averages

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