//@version=6
indicator("HA Pulse — Weekly + Monthly Reversal Desk", overlay = true, max_bars_back = 500)

// ============================================================================
// 1. INPUTS
// ============================================================================
s0 = syminfo.tickerid

g_help = "Strategy legend"
_helpText = "MONTHLY = long-term trend. WEEKLY sits in front as the faster pulse.\n\nSTRONG TREND CHANGE UP\nFirst strong green monthly HA candle after ≥ N strong red months. Opposite color after an extended run.\n\nSTRONG TREND CHANGE DOWN\nFirst strong red monthly HA candle after ≥ N strong green months.\n\nWeekly confirms when the weekly trigger already points the same way.\n\nSTRONG BULL — HA green, no lower wick (shaved bottom)\nWEAK BULL — HA green with a lower wick\nSTRONG BEAR — HA red, no upper wick (shaved top)\nWEAK BEAR — HA red with an upper wick\n\nCountdowns are time left until the current weekly / monthly candle closes."
showLegend = input.bool(true, title = "Hover the (i) for rules  →", tooltip = _helpText, group = g_help)

g_strat = "Strategy thresholds"
buyRevThresh        = input.int(4, "1. Monthly buy reversal (prev red months)",  minval = 1, group = g_strat, tooltip = "First strong green after this many strong red months = STRONG TREND CHANGE UP.")
sellRevThresh       = input.int(4, "2. Monthly sell reversal (prev green months)", minval = 1, group = g_strat, tooltip = "First strong red after this many strong green months = STRONG TREND CHANGE DOWN.")
weekBuyRevThresh    = input.int(4, "3. Weekly buy reversal (prev red weeks)",   minval = 1, group = g_strat)
weekSellRevThresh   = input.int(4, "4. Weekly sell reversal (prev green weeks)", minval = 1, group = g_strat)
extThresh1          = input.int(4, "5. Overextended tier 1", minval = 1, group = g_strat)
extThresh2          = input.int(6, "6. Overextended tier 2", minval = 1, group = g_strat)
extThresh3          = input.int(8, "7. Overextended tier 3", minval = 1, group = g_strat)
requireStrongerBody = input.bool(false, "8. Require larger body on first reversal", group = g_strat)

g1 = "Watchlist (19 + this chart)  ·  TV allows 40 data requests; weekly+monthly uses 2 each"
s1  = input.symbol("BINANCE:BTCUSDT.P",  inline = "t1",  group = g1, title = "1")
s2  = input.symbol("BINANCE:ETHUSDT.P",  inline = "t1",  group = g1, title = "2")
s3  = input.symbol("BINANCE:SOLUSDT.P",  inline = "t2",  group = g1, title = "3")
s4  = input.symbol("BYBIT:HYPEUSDT.P",   inline = "t2",  group = g1, title = "4")
s5  = input.symbol("BYBIT:UNIUSDT.P",    inline = "t3",  group = g1, title = "5")
s6  = input.symbol("BYBIT:AAVEUSDT.P",   inline = "t3",  group = g1, title = "6")
s7  = input.symbol("BYBIT:ONDOUSDT.P",   inline = "t4",  group = g1, title = "7")
s8  = input.symbol("BYBIT:LINKUSDT.P",   inline = "t4",  group = g1, title = "8")
s9  = input.symbol("BYBIT:JUPUSDT.P",    inline = "t5",  group = g1, title = "9")
s10 = input.symbol("BYBIT:JTOUSDT.P",    inline = "t5",  group = g1, title = "10")
s11 = input.symbol("BYBIT:ENAUSDT.P",    inline = "t6",  group = g1, title = "11")
s12 = input.symbol("BYBIT:PENDLEUSDT.P", inline = "t6",  group = g1, title = "12")
s13 = input.symbol("BYBIT:PYTHUSDT.P",   inline = "t7",  group = g1, title = "13")
s14 = input.symbol("BYBIT:ASTERUSDT.P",  inline = "t7",  group = g1, title = "14")
s15 = input.symbol("BYBIT:MORPHOUSDT.P", inline = "t8",  group = g1, title = "15")
s16 = input.symbol("BYBIT:ARBUSDT.P",    inline = "t8",  group = g1, title = "16")
s17 = input.symbol("BYBIT:SUIUSDT.P",    inline = "t9",  group = g1, title = "17")
s18 = input.symbol("BYBIT:AVAXUSDT.P",   inline = "t9",  group = g1, title = "18")
s19 = input.symbol("BYBIT:TAOUSDT.P",    inline = "t10", group = g1, title = "19")

g2 = "Layout"
fillScreen   = input.bool(true, "Fill the chart (full screen)", group = g2, tooltip = "Uses 100% of the pane. Turn off for a compact corner table. Drag this pane tall if you also want it on a separate pane.")
dashPos      = input.string("Top Left", "Corner when not filling", options = ["Top Right", "Bottom Right", "Top Left", "Bottom Left"], group = g2)
textSize     = input.string(size.large, "Text size", options = [size.small, size.normal, size.large, size.huge], group = g2)
dimChart     = input.int(72, "Dim chart behind table", minval = 0, maxval = 90, group = g2)

g3 = "Colors"
colStrongBull = input.color(#12d18a, "Strong bull", group = g3)
colWeakBull   = input.color(#0e9f6e, "Weak bull", group = g3)
colStrongBear = input.color(#f6465d, "Strong bear", group = g3)
colWeakBear   = input.color(#c73e52, "Weak bear", group = g3)
colNeutral    = input.color(#6b7280, "Neutral", group = g3)
colHeaderBg   = input.color(#0b0e13, "Header", group = g3)
colRowBg      = input.color(#10141c, "Row", group = g3)
colAltRow     = input.color(#161b24, "Alt row", group = g3)
colDividerBg  = input.color(#1c2430, "Group header", group = g3)
colFrame      = input.color(#2a3340, "Frame", group = g3)
colWeekTxt    = input.color(#5eead4, "Weekly countdown", group = g3)
colMonthTxt   = input.color(#e8edf5, "Monthly countdown", group = g3)

tfW = "1W"
tfM = "1M"

pos = fillScreen ? position.top_left : dashPos == "Top Right" ? position.top_right : dashPos == "Bottom Right" ? position.bottom_right : dashPos == "Top Left" ? position.top_left : position.bottom_left

// ============================================================================
// 2. CORE LOGIC
// ============================================================================
getHA_Status() =>
    haBullish = close > open
    haBearish = close < open
    noLowerWick = open == low
    noUpperWick = open == high
    int currCandleType = haBullish ? (noLowerWick ? 2 : 1) : haBearish ? (noUpperWick ? -2 : -1) : 0
    bullCond = haBullish and noLowerWick
    bearCond = haBearish and noUpperWick

    var int barsBull = 0
    var int barsBear = 0
    var bool isBullActive = false
    var bool isBearActive = false
    var int prevTriggerCount = 0
    var int colorBarsBull = 0
    var int colorBarsBear = 0

    if haBullish
        colorBarsBull += 1
        colorBarsBear := 0
    else if haBearish
        colorBarsBear += 1
        colorBarsBull := 0

    int colorStreak = haBullish ? colorBarsBull : haBearish ? -colorBarsBear : 0

    if haBearish
        if isBullActive
            prevTriggerCount := barsBull
        isBullActive := false
        barsBull := 0

    if haBullish
        if isBearActive
            prevTriggerCount := -barsBear
        isBearActive := false
        barsBear := 0

    if bullCond and not isBullActive
        isBullActive := true
        barsBull := 1
    else if isBullActive
        barsBull += 1

    if bearCond and not isBearActive
        isBearActive := true
        barsBear := 1
    else if isBearActive
        barsBear += 1

    int currentTrigger = isBullActive ? barsBull : isBearActive ? -barsBear : 0
    float body = math.abs(close - open)
    float prevBody = math.abs(close[1] - open[1])
    int stronger = (not na(prevBody) and body > prevBody) ? 1 : 0
    [prevTriggerCount, currCandleType, currentTrigger, colorStreak, stronger]

pad2(int n) =>
    n < 10 ? "0" + str.tostring(n) : str.tostring(n)

formatLeft(int closeMs) =>
    int left = math.max(0, closeMs - timenow)
    int sec = int(left / 1000)
    int d = int(sec / 86400)
    int h = int((sec % 86400) / 3600)
    int m = int((sec % 3600) / 60)
    int s = int(sec % 60)
    (d > 0 ? str.tostring(d) + "d " : "") + pad2(h) + ":" + pad2(m) + ":" + pad2(s)

progBar(int startMs, int closeMs) =>
    float span = closeMs - startMs
    float pct = span > 0 ? math.min(1.0, math.max(0.0, (timenow - startMs) / span)) : 0.0
    int filled = int(math.round(pct * 8))
    string b = ""
    for i = 1 to 8
        b += i <= filled ? "█" : "░"
    b + " " + str.tostring(int(pct * 100)) + "%"

candleName(int cType) =>
    switch cType
        2  => "STRONG BULL"
        1  => "WEAK BULL"
        -1 => "WEAK BEAR"
        -2 => "STRONG BEAR"
        => "INDECISION"

unitWord(string tf, int n) =>
    tf == "1M" ? (n == 1 ? "month" : "months") : tf == "1W" ? (n == 1 ? "week" : "weeks") : (n == 1 ? "bar" : "bars")

candleCell(int cType, int streak, string tf) =>
    string name = candleName(cType)
    if streak == 0
        name
    else
        int n = math.abs(streak)
        string dir = streak > 0 ? "up" : "down"
        name + "\n" + str.tostring(n) + " " + unitWord(tf, n) + " " + dir

triggerCell(int status, string tf) =>
    if status == 0
        "—"
    else
        int n = math.abs(status)
        string dir = status > 0 ? "up" : "down"
        str.tostring(n) + " " + unitWord(tf, n) + " " + dir

typeBg(int cType) =>
    switch cType
        2  => color.new(colStrongBull, 8)
        1  => color.new(colWeakBull, 42)
        -1 => color.new(colWeakBear, 42)
        -2 => color.new(colStrongBear, 8)
        => color.new(colNeutral, 78)

trigBg(int val) =>
    val > 0 ? color.new(colStrongBull, 38) : val < 0 ? color.new(colStrongBear, 38) : color.new(colNeutral, 78)

getAnalysis(int pTrig, int cTrig, int wTrig, int bThresh, int sThresh, int e1, int e2, int e3, int stronger, bool reqStrong) =>
    bool isStrongOk = not reqStrong or stronger == 1
    bool wUp = wTrig > 0
    bool wDn = wTrig < 0
    if cTrig == 0
        "Awaiting setup"
    else if cTrig == 1
        if pTrig <= -bThresh and isStrongOk
            wUp ? "▲ STRONG TREND CHANGE UP  ·  weekly confirms\n" + str.tostring(math.abs(pTrig)) + " red months → 1 green" : "▲ STRONG TREND CHANGE UP\n" + str.tostring(math.abs(pTrig)) + " red months → 1 green"
        else
            "Early buy (minor)"
    else if cTrig == -1
        if pTrig >= sThresh and isStrongOk
            wDn ? "▼ STRONG TREND CHANGE DOWN  ·  weekly confirms\n" + str.tostring(pTrig) + " green months → 1 red" : "▼ STRONG TREND CHANGE DOWN\n" + str.tostring(pTrig) + " green months → 1 red"
        else
            "Early sell (minor)"
    else if cTrig >= e3
        "Overextended 3  ·  extreme short risk"
    else if cTrig <= -e3
        "Overextended 3  ·  extreme long risk"
    else if cTrig >= e2
        "Overextended 2  ·  high short risk"
    else if cTrig <= -e2
        "Overextended 2  ·  high long risk"
    else if cTrig >= e1
        "Overextended 1  ·  watch for short"
    else if cTrig <= -e1
        "Overextended 1  ·  watch for long"
    else if cTrig > 1
        "Trend active  ·  hold long"
    else if cTrig < -1
        "Trend active  ·  hold short"
    else
        "—"

getScore(int pTrig, int cTrig, int wTrig, int bThresh, int sThresh, int e1, int e2, int e3, int stronger, bool reqStrong) =>
    float score = 0.0
    bool isStrongOk = not reqStrong or stronger == 1
    if cTrig == 1
        if pTrig <= -bThresh and isStrongOk
            score := wTrig > 0 ? 110.0 : 100.0
        else
            score := 80.0
    else if cTrig <= -e3
        score := 95.0
    else if cTrig <= -e2
        score := 90.0
    else if cTrig <= -e1
        score := 85.0
    else if cTrig > 1
        score := 60.0
    else if cTrig == 0
        score := 0.0
    else if cTrig == -1
        if pTrig >= sThresh and isStrongOk
            score := wTrig < 0 ? -5.0 : -10.0
        else
            score := -30.0
    else if cTrig >= e3
        score := -15.0
    else if cTrig >= e2
        score := -20.0
    else if cTrig >= e1
        score := -25.0
    else if cTrig < -1
        score := -60.0
    score

getGroup(float s) =>
    if s >= 999000
        "THIS CHART"
    else if s >= 109
        "CONFIRMED TREND CHANGE UP"
    else if s >= 99
        "STRONG TREND CHANGE UP"
    else if s >= 84
        "BUY WATCH  ·  EXTENDED"
    else if s >= 79
        "EARLY BUYS"
    else if s >= 59
        "ACTIVE LONGS"
    else if s >= -1
        "NEUTRAL / AWAITING"
    else if s >= -6
        "CONFIRMED TREND CHANGE DOWN"
    else if s >= -11
        "STRONG TREND CHANGE DOWN"
    else if s >= -26
        "SELL WATCH  ·  EXTENDED"
    else if s >= -31
        "EARLY SELLS"
    else
        "ACTIVE SHORTS"

ideaBg(float score) =>
    if score >= 99
        color.new(colStrongBull, 55)
    else if score >= 79
        color.new(colStrongBull, 78)
    else if score <= -5 and score >= -11
        color.new(colStrongBear, 55)
    else if score <= -26 and score >= -31
        color.new(colStrongBear, 78)
    else
        color.new(colRowBg, 30)

haTicker(string sym) =>
    ticker.heikinashi(sym == "" ? s0 : sym)

cleanName(string raw) =>
    if raw == "[This chart]"
        raw
    else
        parts = str.split(raw, ":")
        array.size(parts) > 1 ? array.get(parts, 1) : raw

// ============================================================================
// 3. DATA  (20 symbols × 2 timeframes = 40 requests — TV cap)
// ============================================================================
[p0,  t0,  tr0,  c0,  sb0]  = request.security(ticker.heikinashi(s0), tfM, getHA_Status())
[pw0, tw0, trw0, cw0, sbw0] = request.security(ticker.heikinashi(s0), tfW, getHA_Status())

[p1,  t1,  tr1,  c1,  sb1]  = request.security(haTicker(s1),  tfM, getHA_Status())
[pw1, tw1, trw1, cw1, sbw1] = request.security(haTicker(s1),  tfW, getHA_Status())
[p2,  t2,  tr2,  c2,  sb2]  = request.security(haTicker(s2),  tfM, getHA_Status())
[pw2, tw2, trw2, cw2, sbw2] = request.security(haTicker(s2),  tfW, getHA_Status())
[p3,  t3,  tr3,  c3,  sb3]  = request.security(haTicker(s3),  tfM, getHA_Status())
[pw3, tw3, trw3, cw3, sbw3] = request.security(haTicker(s3),  tfW, getHA_Status())
[p4,  t4,  tr4,  c4,  sb4]  = request.security(haTicker(s4),  tfM, getHA_Status())
[pw4, tw4, trw4, cw4, sbw4] = request.security(haTicker(s4),  tfW, getHA_Status())
[p5,  t5,  tr5,  c5,  sb5]  = request.security(haTicker(s5),  tfM, getHA_Status())
[pw5, tw5, trw5, cw5, sbw5] = request.security(haTicker(s5),  tfW, getHA_Status())
[p6,  t6,  tr6,  c6,  sb6]  = request.security(haTicker(s6),  tfM, getHA_Status())
[pw6, tw6, trw6, cw6, sbw6] = request.security(haTicker(s6),  tfW, getHA_Status())
[p7,  t7,  tr7,  c7,  sb7]  = request.security(haTicker(s7),  tfM, getHA_Status())
[pw7, tw7, trw7, cw7, sbw7] = request.security(haTicker(s7),  tfW, getHA_Status())
[p8,  t8,  tr8,  c8,  sb8]  = request.security(haTicker(s8),  tfM, getHA_Status())
[pw8, tw8, trw8, cw8, sbw8] = request.security(haTicker(s8),  tfW, getHA_Status())
[p9,  t9,  tr9,  c9,  sb9]  = request.security(haTicker(s9),  tfM, getHA_Status())
[pw9, tw9, trw9, cw9, sbw9] = request.security(haTicker(s9),  tfW, getHA_Status())
[p10, t10, tr10, c10, sb10] = request.security(haTicker(s10), tfM, getHA_Status())
[pw10,tw10,trw10,cw10,sbw10]= request.security(haTicker(s10), tfW, getHA_Status())
[p11, t11, tr11, c11, sb11] = request.security(haTicker(s11), tfM, getHA_Status())
[pw11,tw11,trw11,cw11,sbw11]= request.security(haTicker(s11), tfW, getHA_Status())
[p12, t12, tr12, c12, sb12] = request.security(haTicker(s12), tfM, getHA_Status())
[pw12,tw12,trw12,cw12,sbw12]= request.security(haTicker(s12), tfW, getHA_Status())
[p13, t13, tr13, c13, sb13] = request.security(haTicker(s13), tfM, getHA_Status())
[pw13,tw13,trw13,cw13,sbw13]= request.security(haTicker(s13), tfW, getHA_Status())
[p14, t14, tr14, c14, sb14] = request.security(haTicker(s14), tfM, getHA_Status())
[pw14,tw14,trw14,cw14,sbw14]= request.security(haTicker(s14), tfW, getHA_Status())
[p15, t15, tr15, c15, sb15] = request.security(haTicker(s15), tfM, getHA_Status())
[pw15,tw15,trw15,cw15,sbw15]= request.security(haTicker(s15), tfW, getHA_Status())
[p16, t16, tr16, c16, sb16] = request.security(haTicker(s16), tfM, getHA_Status())
[pw16,tw16,trw16,cw16,sbw16]= request.security(haTicker(s16), tfW, getHA_Status())
[p17, t17, tr17, c17, sb17] = request.security(haTicker(s17), tfM, getHA_Status())
[pw17,tw17,trw17,cw17,sbw17]= request.security(haTicker(s17), tfW, getHA_Status())
[p18, t18, tr18, c18, sb18] = request.security(haTicker(s18), tfM, getHA_Status())
[pw18,tw18,trw18,cw18,sbw18]= request.security(haTicker(s18), tfW, getHA_Status())
[p19, t19, tr19, c19, sb19] = request.security(haTicker(s19), tfM, getHA_Status())
[pw19,tw19,trw19,cw19,sbw19]= request.security(haTicker(s19), tfW, getHA_Status())

syms   = array.new_string()
pTrigs = array.new_int()
cTypes = array.new_int()
cTrigs = array.new_int()
cCols  = array.new_int()
strongers = array.new_int()
wTypes = array.new_int()
wTrigs = array.new_int()
wCols  = array.new_int()
wPrevs = array.new_int()

pushRow(string sym, int p, int t, int tr, int c, int sb, int wp, int wt, int wtr, int wc) =>
    if sym != ""
        array.push(syms, sym)
        array.push(pTrigs, p)
        array.push(cTypes, t)
        array.push(cTrigs, tr)
        array.push(cCols, c)
        array.push(strongers, sb)
        array.push(wPrevs, wp)
        array.push(wTypes, wt)
        array.push(wTrigs, wtr)
        array.push(wCols, wc)

pushRow("[This chart]", p0, t0, tr0, c0, sb0, pw0, tw0, trw0, cw0)
pushRow(s1,  p1,  t1,  tr1,  c1,  sb1,  pw1,  tw1,  trw1,  cw1)
pushRow(s2,  p2,  t2,  tr2,  c2,  sb2,  pw2,  tw2,  trw2,  cw2)
pushRow(s3,  p3,  t3,  tr3,  c3,  sb3,  pw3,  tw3,  trw3,  cw3)
pushRow(s4,  p4,  t4,  tr4,  c4,  sb4,  pw4,  tw4,  trw4,  cw4)
pushRow(s5,  p5,  t5,  tr5,  c5,  sb5,  pw5,  tw5,  trw5,  cw5)
pushRow(s6,  p6,  t6,  tr6,  c6,  sb6,  pw6,  tw6,  trw6,  cw6)
pushRow(s7,  p7,  t7,  tr7,  c7,  sb7,  pw7,  tw7,  trw7,  cw7)
pushRow(s8,  p8,  t8,  tr8,  c8,  sb8,  pw8,  tw8,  trw8,  cw8)
pushRow(s9,  p9,  t9,  tr9,  c9,  sb9,  pw9,  tw9,  trw9,  cw9)
pushRow(s10, p10, t10, tr10, c10, sb10, pw10, tw10, trw10, cw10)
pushRow(s11, p11, t11, tr11, c11, sb11, pw11, tw11, trw11, cw11)
pushRow(s12, p12, t12, tr12, c12, sb12, pw12, tw12, trw12, cw12)
pushRow(s13, p13, t13, tr13, c13, sb13, pw13, tw13, trw13, cw13)
pushRow(s14, p14, t14, tr14, c14, sb14, pw14, tw14, trw14, cw14)
pushRow(s15, p15, t15, tr15, c15, sb15, pw15, tw15, trw15, cw15)
pushRow(s16, p16, t16, tr16, c16, sb16, pw16, tw16, trw16, cw16)
pushRow(s17, p17, t17, tr17, c17, sb17, pw17, tw17, trw17, cw17)
pushRow(s18, p18, t18, tr18, c18, sb18, pw18, tw18, trw18, cw18)
pushRow(s19, p19, t19, tr19, c19, sb19, pw19, tw19, trw19, cw19)

// ============================================================================
// 4. COUNTDOWN (no extra security calls)
// ============================================================================
wClose = time_close(tfW)
mClose = time_close(tfM)
wOpen  = time(tfW)
mOpen  = time(tfM)
wLeft  = formatLeft(wClose)
mLeft  = formatLeft(mClose)
wBar   = progBar(wOpen, wClose)
mBar   = progBar(mOpen, mClose)

// ============================================================================
// 5. TABLES
// ============================================================================
var table veil = table.new(position.middle_center, 1, 1)
var table dash = table.new(position.top_left, 5, 56, frame_color = colFrame, frame_width = 1, border_color = color.new(colFrame, 40), border_width = 1)

if barstate.islast
    table.set_position(dash, pos)
    if fillScreen and dimChart > 0
        table.cell(veil, 0, 0, "", width = 100, height = 100, bgcolor = color.new(#07080d, 100 - dimChart))
    else
        table.clear(veil, 0, 0, 0, 0)

    table.clear(dash, 0, 0, 4, 55)

    int w0 = fillScreen ? 14 : 0
    int w1 = fillScreen ? 20 : 0
    int w2 = fillScreen ? 20 : 0
    int w3 = fillScreen ? 14 : 0
    int w4 = fillScreen ? 32 : 0
    int hdrH = fillScreen ? 9 : 0
    int rowH = fillScreen ? 4 : 0

    table.cell(dash, 0, 0, "HA PULSE", text_color = color.white, bgcolor = colHeaderBg, text_size = textSize, text_halign = text.align_left, text_font_family = font.family_monospace, width = w0, height = hdrH, text_formatting = text.format_bold)
    table.cell(dash, 1, 0, "WEEKLY  " + wLeft + "\n" + wBar, text_color = colWeekTxt, bgcolor = colHeaderBg, text_size = textSize, text_font_family = font.family_monospace, width = w1, height = hdrH, text_formatting = text.format_bold, tooltip = "Time left in the current weekly candle")
    table.cell(dash, 2, 0, "MONTHLY  " + mLeft + "\n" + mBar, text_color = colMonthTxt, bgcolor = colHeaderBg, text_size = textSize, text_font_family = font.family_monospace, width = w2, height = hdrH, text_formatting = text.format_bold, tooltip = "Time left in the current monthly candle — long-term trend")
    table.cell(dash, 3, 0, "PREV MONTHS", text_color = color.new(color.white, 25), bgcolor = colHeaderBg, text_size = textSize, text_font_family = font.family_monospace, width = w3, height = hdrH)
    table.cell(dash, 4, 0, "TRADE IDEA", text_color = color.new(color.white, 25), bgcolor = colHeaderBg, text_size = textSize, text_font_family = font.family_monospace, width = w4, height = hdrH)

    int activeSize = array.size(syms)
    if activeSize > 0
        scores = array.new_float()
        for i = 0 to activeSize - 1
            if i == 0
                array.push(scores, 999999.0)
            else
                p  = array.get(pTrigs, i)
                c  = array.get(cTrigs, i)
                sb = array.get(strongers, i)
                wt = array.get(wTrigs, i)
                array.push(scores, getScore(p, c, wt, buyRevThresh, sellRevThresh, extThresh1, extThresh2, extThresh3, sb, requireStrongerBody) - i * 0.001)

        sortedIndices = array.sort_indices(scores, order.descending)
        int rowIndex = 1
        string prevGroup = ""

        for i = 0 to activeSize - 1
            idx = array.get(sortedIndices, i)
            rawScore = array.get(scores, idx)
            groupLabel = getGroup(rawScore)

            if groupLabel != prevGroup
                table.cell(dash, 0, rowIndex, "  " + groupLabel, text_color = color.new(color.white, 10), bgcolor = colDividerBg, text_size = textSize, text_halign = text.align_left, text_font_family = font.family_monospace, height = rowH, text_formatting = text.format_bold)
                table.cell(dash, 1, rowIndex, "", bgcolor = colDividerBg, height = rowH)
                table.cell(dash, 2, rowIndex, "", bgcolor = colDividerBg, height = rowH)
                table.cell(dash, 3, rowIndex, "", bgcolor = colDividerBg, height = rowH)
                table.cell(dash, 4, rowIndex, "", bgcolor = colDividerBg, height = rowH)
                rowIndex += 1
                prevGroup := groupLabel

            rawSym = array.get(syms, idx)
            displaySym = cleanName(rawSym)
            prevTrigger = array.get(pTrigs, idx)
            currType    = array.get(cTypes, idx)
            currTrigger = array.get(cTrigs, idx)
            stronger    = array.get(strongers, idx)
            wType       = array.get(wTypes, idx)
            wTrig       = array.get(wTrigs, idx)
            wStreak     = array.get(wCols, idx)
            mStreak     = array.get(cCols, idx)

            string analysisText = getAnalysis(prevTrigger, currTrigger, wTrig, buyRevThresh, sellRevThresh, extThresh1, extThresh2, extThresh3, stronger, requireStrongerBody)
            color rowBg = idx % 2 == 0 ? colRowBg : colAltRow
            color ideaC = ideaBg(rawScore)

            table.cell(dash, 0, rowIndex, displaySym, text_color = color.white, bgcolor = rowBg, text_size = textSize, text_halign = text.align_left, text_font_family = font.family_monospace, height = rowH, text_formatting = text.format_bold)
            table.cell(dash, 1, rowIndex, candleCell(wType, wStreak, tfW), text_color = color.white, bgcolor = typeBg(wType), text_size = textSize, text_font_family = font.family_monospace, height = rowH)
            table.cell(dash, 2, rowIndex, candleCell(currType, mStreak, tfM), text_color = color.white, bgcolor = typeBg(currType), text_size = textSize, text_font_family = font.family_monospace, height = rowH)
            table.cell(dash, 3, rowIndex, triggerCell(prevTrigger, tfM), text_color = color.white, bgcolor = trigBg(prevTrigger), text_size = textSize, text_font_family = font.family_monospace, height = rowH)
            table.cell(dash, 4, rowIndex, analysisText, text_color = color.white, bgcolor = ideaC, text_size = textSize, text_halign = text.align_left, text_font_family = font.family_monospace, height = rowH)
            rowIndex += 1

// ============================================================================
// 6. ALERTS
// ============================================================================
isMajorBuy  = tr0 == 1 and p0 <= -buyRevThresh and (not requireStrongerBody or sb0 == 1)
isMajorSell = tr0 == -1 and p0 >= sellRevThresh and (not requireStrongerBody or sb0 == 1)
isConfirmedBuy  = isMajorBuy and trw0 > 0
isConfirmedSell = isMajorSell and trw0 < 0

alertcondition(isConfirmedBuy,  title = "Confirmed trend change UP",   message = "CONFIRMED TREND CHANGE UP: {{ticker}} — first strong monthly green after extended red, weekly agrees")
alertcondition(isMajorBuy,      title = "Strong trend change UP",      message = "STRONG TREND CHANGE UP: {{ticker}} — first strong monthly green after extended red")
alertcondition(isConfirmedSell, title = "Confirmed trend change DOWN", message = "CONFIRMED TREND CHANGE DOWN: {{ticker}} — first strong monthly red after extended green, weekly agrees")
alertcondition(isMajorSell,     title = "Strong trend change DOWN",    message = "STRONG TREND CHANGE DOWN: {{ticker}} — first strong monthly red after extended green")
