Inicio Bolsa

Fordumen

//@version=5
indicator("Fordumen", overlay=true)

//----------------------------------------------------
// PARAMETROS
//----------------------------------------------------
volPeriod = input.int(15, "Media de Volumen")
rsiPeriod = input.int(14, "Periodo RSI")
rsiMaPeriod = input.int(21, "Media RSI")
macdFast = input.int(12)
macdSlow = input.int(26)
macdSignal = input.int(9)

diLength = input.int(14, "DI Length")
adxSmoothing = input.int(14, "ADX Smoothing")

//----------------------------------------------------
// VOLUMEN
//----------------------------------------------------
volMA = ta.sma(volume, volPeriod)

volScore =
     volume < volMA * 0.8 ? 1 :
     volume <= volMA * 1.2 ? 2 : 3

//----------------------------------------------------
// RSI
//----------------------------------------------------
rsi = ta.rsi(close, rsiPeriod)
rsiMA = ta.ema(rsi, rsiMaPeriod)

rsiScore =
     rsi < 50 and ta.crossover(rsi, rsiMA) ? 3 :
     rsi > rsiMA ? 2 : 1

//----------------------------------------------------
// MACD
//----------------------------------------------------
[macdLine, macdSignalLine, hist] = ta.macd(close, macdFast, macdSlow, macdSignal)

macdScore =
     macdLine > macdSignalLine and hist > 0 ? 3 :
     macdLine > macdSignalLine ? 2 : 1

//----------------------------------------------------
// TENDENCIA
//----------------------------------------------------
ema9 = ta.ema(close, 9)
emaSlope = ema9 - ema9[5]

slopeScore =
     emaSlope > 0 ? 3 :
     emaSlope > -0.1 ? 2 : 1

//----------------------------------------------------
// KONCORDE (simplificado)
//----------------------------------------------------
nvi = ta.nvi
nvim = ta.ema(nvi, 15)

bigMoneyBuying = nvi > nvim

//----------------------------------------------------
// ADX / DI
//----------------------------------------------------
[plusDI, minusDI, adx] = ta.dmi(diLength, adxSmoothing)

adxRising = adx > adx[1]
bullishDI = plusDI > minusDI
bearishDI = minusDI > plusDI

//----------------------------------------------------
// SCORE BASE
//----------------------------------------------------
rawScore = volScore + rsiScore + macdScore + slopeScore
baseScore = math.round(((rawScore - 4) / 8) * 9 + 1)

//----------------------------------------------------
// BONUS ADX
//----------------------------------------------------
adxBonus = (bullishDI and adxRising) ? 1 : 0
scoreWithADX = math.min(baseScore + adxBonus, 10)

//----------------------------------------------------
// FILTROS ESTRUCTURALES
//----------------------------------------------------
scoreAfterKoncorde = bigMoneyBuying ? scoreWithADX : math.min(scoreWithADX, 6)

finalScore = bearishDI ? math.min(scoreAfterKoncorde, 6) : scoreAfterKoncorde

//----------------------------------------------------
// SCORE ANTERIOR
//----------------------------------------------------
prevScore = finalScore[1]

//----------------------------------------------------
// COLOR SCORE
//----------------------------------------------------
scoreColor =
     finalScore >= 9 ? color.lime :
     finalScore >= 7 ? color.green :
     finalScore >= 5 ? color.yellow :
     finalScore >= 3 ? color.orange :
     color.red

//----------------------------------------------------
// LABEL
//----------------------------------------------------
var label scoreLabel = na

if barstate.islast
    label.delete(scoreLabel)

    labelText =
         str.tostring(prevScore) + " | " +
         str.tostring(finalScore) + " | " +
         str.tostring(math.round(rsi))

    scoreLabel := label.new(
         bar_index + 3,
         close,
         labelText,
         style = label.style_label_left,
         color = scoreColor,
         textcolor = color.black,
         size = size.large)

//----------------------------------------------------
// MEDIAS
//----------------------------------------------------
plot(ta.ema(close,10), color=color.aqua, linewidth=2)
plot(ta.ema(close,21), color=color.black, linewidth=2)
plot(ta.sma(close,150), color=color.yellow, linewidth=2)
plot(ta.sma(close,200), color=color.orange, linewidth=2)


Canales (Nombre original: "SRChannels")

//@version=6
// ══════════════════════════════════════════════════════════
// AUTO SUPPORT & RESISTANCE CHANNELS [WillyAlgoTrader]
// ══════════════════════════════════════════════════════════
// Author:  Willy | WillyAlgoTrader
// Version: 1.4.3


indicator(
    title            = "Auto S/R Channels [WillyAlgoTrader]",
    shorttitle       = "Canales",
    overlay          = true,
    max_lines_count  = 500,
    max_labels_count = 500,
    max_boxes_count  = 500,
    max_bars_back    = 5000)

// ══════════════════════════════════════
// CONSTANTS
// ══════════════════════════════════════
GRP_MAIN       = "⚙️ Main Settings"
GRP_VISUAL     = "🎨 Visual Settings"
GRP_CHAN       = "📏 Channels"
GRP_STYLE_BASE = "📐 Base Line Style"
GRP_STYLE_PAR  = "📐 Parallel Line Style"
GRP_STYLE_MID  = "📐 Midline Style"
GRP_DASH       = "📊 Dashboard"
GRP_ALERT      = "🔔 Alerts"
GRP_COLORS     = "🎨 Colors"

INDICATOR_VERSION = "v1.4.3"
int MAX_PIVOTS = 40

// ══════════════════════════════════════
// INPUTS
// ══════════════════════════════════════

pivotLenInput = input.int(21, "Pivot Length", minval = 2, maxval = 50, group = GRP_MAIN, tooltip = "Lookback/forward bars for pivot detection.\n• Higher = fewer, stronger pivots\n• Lower = more pivots, noisier\n• Recommended: 3–10")
atrLenInput = input.int(14, "ATR Length", minval = 5, maxval = 100, group = GRP_MAIN, tooltip = "ATR period for calculations.\nRecommended: 14")
minChannelBarsInput = input.int(10, "Min Channel Bars", minval = 3, maxval = 200, group = GRP_MAIN, tooltip = "Minimum distance between two base pivots.\nRecommended: 10–30")
maxChannelBarsInput = input.int(400, "Max Channel Bars", minval = 30, maxval = 2000, group = GRP_MAIN, tooltip = "Maximum lookback for channel base pivots.\nRecommended: 200–500")
channelQualityInput = input.float(0.55, "Min Channel Quality", minval = 0.2, maxval = 1.0, step = 0.05, group = GRP_MAIN, tooltip = "Min fraction of bars contained in channel.\n• Higher = stricter, fewer channels\n• Recommended: 0.5–0.8")

themeInput = input.string("Auto", "Theme", options = ["Auto", "Dark", "Light"], group = GRP_VISUAL, tooltip = "Chart theme.\n• Auto: detect from background\n• Dark/Light: manual override")
showMidlineInput = input.bool(true, "Show Midlines", group = GRP_VISUAL, tooltip = "Dotted midline inside channels")
showFillInput = input.bool(true, "Show Channel Fill", group = GRP_VISUAL, tooltip = "Very subtle channel fill")
showWatermarkInput = input.bool(true, "Show Watermark", group = GRP_VISUAL, tooltip = "Display 'WillyAlgoTrader - by Willy' watermark")
extendInput = input.string("Right", "Extend Channels", options = ["None", "Right", "Both"], group = GRP_VISUAL, tooltip = "Extend channel lines beyond pivot range")
deletePrevInput = input.bool(true, "Delete Previous", group = GRP_VISUAL, tooltip = "Remove old channels when new ones form")
showBreakoutLblInput = input.bool(true, "Show Breakout Label", group = GRP_VISUAL, tooltip = "Label on the last channel breakout bar")

showUpInput = input.bool(true, "Show Up Channels", group = GRP_CHAN, tooltip = "Ascending channels from pivot lows")
showDownInput = input.bool(true, "Show Down Channels", group = GRP_CHAN, tooltip = "Descending channels from pivot highs")

baseWidthInput = input.int(2, "Width", minval = 1, maxval = 5, group = GRP_STYLE_BASE)
baseStyleInput = input.string("Solid", "Style", options = ["Solid", "Dashed", "Dotted"], group = GRP_STYLE_BASE)

parWidthInput = input.int(2, "Width", minval = 1, maxval = 5, group = GRP_STYLE_PAR)
parStyleInput = input.string("Solid", "Style", options = ["Solid", "Dashed", "Dotted"], group = GRP_STYLE_PAR)

midWidthInput = input.int(1, "Width", minval = 1, maxval = 3, group = GRP_STYLE_MID)
midStyleInput = input.string("Dashed", "Style", options = ["Solid", "Dashed", "Dotted"], group = GRP_STYLE_MID)

bullColorInput = input.color(#00E676, "Bull / Up", inline = "col1", group = GRP_COLORS)
bearColorInput = input.color(#FF5252, "Bear / Down", inline = "col1", group = GRP_COLORS)
fillTranspInput = input.int(95, "Fill Transparency", minval = 80, maxval = 99, group = GRP_COLORS)

showDashInput = input.bool(false, "Show Dashboard", group = GRP_DASH)
dashPosStr = input.string("Top Right", "Position", options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group = GRP_DASH)

alertBreakInput = input.bool(true, "Alert on Signal", group = GRP_ALERT)
alertBreakoutInput = input.bool(true, "Alert on Breakout", group = GRP_ALERT)
alertReactInput = input.bool(true, "Alert on React", group = GRP_ALERT)
webhookInput = input.bool(false, "Webhook JSON", group = GRP_ALERT)

// ══════════════════════════════════════
// THEME
// ══════════════════════════════════════
isDark = switch themeInput
    "Dark"  => true
    "Light" => false
    =>         color.r(chart.bg_color) < 128

TEXT_COLOR    = isDark ? #E0E0E0 : #1A1A1A
TEXT_MUTED    = isDark ? color.new(#9E9E9E, 0) : color.new(#757575, 0)
TABLE_BG      = isDark ? color.new(#131722, 5) : color.new(#FFFFFF, 5)
TABLE_BORDER  = isDark ? color.new(#2A2E39, 0) : color.new(#D0D0D0, 0)
TABLE_ROW_ALT = isDark ? color.new(#1C2030, 0) : color.new(#F0F4F8, 0)
HEADER_BG     = color.new(#2962FF, 0)
HEADER_TEXT   = #FFFFFF
WM_COLOR      = isDark ? color.new(#FFFFFF, 80) : color.new(#000000, 80)

// ══════════════════════════════════════
// FUNCTIONS
// ══════════════════════════════════════

safeDiv(float num, float den, float fallback = 0.0) =>
    den != 0 and not na(num) and not na(den) ? num / den : fallback

getLineStyle(string s) =>
    switch s
        "Dashed" => line.style_dashed
        "Dotted" => line.style_dotted
        =>          line.style_solid

getExtend(string s) =>
    switch s
        "Right" => extend.right
        "Both"  => extend.both
        =>         extend.none

linePrice(int x1, float y1, int x2, float y2, int bx) =>
    x2 == x1 ? y1 : y1 + (y2 - y1) / (x2 - x1) * (bx - x1)

clearLabels(array<label> arr) =>
    if array.size(arr) > 0
        for i = array.size(arr) - 1 to 0
            label.delete(array.get(arr, i))
        array.clear(arr)

// Direction-agnostic "is price inside channel?"
isInside(float p, float baseLine, float off) =>
    float upper = math.max(baseLine, baseLine + off)
    float lower = math.min(baseLine, baseLine + off)
    p >= lower and p <= upper

// ══════════════════════════════════════
// PIVOT DETECTION
// ══════════════════════════════════════

int WARMUP_BARS = math.max(pivotLenInput * 4, 50)
bool isWarmedUp = bar_index >= WARMUP_BARS

float atrVal = nz(ta.atr(atrLenInput), high - low)

float pivotHi = ta.pivothigh(high, pivotLenInput, pivotLenInput)
float pivotLo = ta.pivotlow(low, pivotLenInput, pivotLenInput)

var array<float> hiPrices  = array.new_float()
var array<int>   hiIndices = array.new_int()
var array<float> loPrices  = array.new_float()
var array<int>   loIndices = array.new_int()

if not na(pivotHi)
    array.push(hiPrices, pivotHi)
    array.push(hiIndices, bar_index - pivotLenInput)
    if array.size(hiPrices) > MAX_PIVOTS
        array.shift(hiPrices)
        array.shift(hiIndices)

if not na(pivotLo)
    array.push(loPrices, pivotLo)
    array.push(loIndices, bar_index - pivotLenInput)
    if array.size(loPrices) > MAX_PIVOTS
        array.shift(loPrices)
        array.shift(loIndices)

// ══════════════════════════════════════════════════════════
// CHANNEL SEARCH
// ══════════════════════════════════════════════════════════

findUpChannel(array<float> basePrices, array<int> baseIndices, array<float> oppPrices, array<int> oppIndices) =>
    int bestX1 = na
    float bestY1 = na
    int bestX2 = na
    float bestY2 = na
    float bestOffset = na
    float bestQual = 0.0

    int sz = array.size(basePrices)
    if sz >= 2
        int pairsToCheck = math.min(sz, 8)
        for i = sz - 1 to math.max(0, sz - pairsToCheck)
            for j = i - 1 to math.max(0, i - 5)
                int ix1 = array.get(baseIndices, j)
                float iy1 = array.get(basePrices, j)
                int ix2 = array.get(baseIndices, i)
                float iy2 = array.get(basePrices, i)
                int span = ix2 - ix1
                if span < minChannelBarsInput or span > maxChannelBarsInput
                    continue
                if iy2 <= iy1
                    continue
                float maxOff = 0.0
                int oppSz = array.size(oppPrices)
                if oppSz > 0
                    for k = 0 to oppSz - 1
                        int oppBar = array.get(oppIndices, k)
                        if oppBar >= ix1 and oppBar <= bar_index
                            float oppVal = array.get(oppPrices, k)
                            float baseAtOpp = linePrice(ix1, iy1, ix2, iy2, oppBar)
                            float diff = oppVal - baseAtOpp
                            if diff > maxOff
                                maxOff := diff
                if maxOff <= 0
                    continue
                int totalBars = bar_index - ix1
                if totalBars <= 0
                    continue
                int checkLen = math.min(totalBars, 300)
                int contained = 0
                float tol = atrVal * 0.05
                for k = 0 to checkLen - 1
                    int bx = bar_index - k
                    if bx < ix1
                        break
                    float bLine = linePrice(ix1, iy1, ix2, iy2, bx)
                    float pLine = bLine + maxOff
                    if low[k] >= bLine - tol and high[k] <= pLine + tol
                        contained += 1
                float qual = safeDiv(float(contained), float(checkLen), 0.0)
                if qual >= channelQualityInput and qual > bestQual
                    bestQual := qual
                    bestX1 := ix1
                    bestY1 := iy1
                    bestX2 := ix2
                    bestY2 := iy2
                    bestOffset := maxOff
    [bestX1, bestY1, bestX2, bestY2, bestOffset, bestQual]

findDownChannel(array<float> basePrices, array<int> baseIndices, array<float> oppPrices, array<int> oppIndices) =>
    int bestX1 = na
    float bestY1 = na
    int bestX2 = na
    float bestY2 = na
    float bestOffset = na
    float bestQual = 0.0

    int sz = array.size(basePrices)
    if sz >= 2
        int pairsToCheck = math.min(sz, 8)
        for i = sz - 1 to math.max(0, sz - pairsToCheck)
            for j = i - 1 to math.max(0, i - 5)
                int ix1 = array.get(baseIndices, j)
                float iy1 = array.get(basePrices, j)
                int ix2 = array.get(baseIndices, i)
                float iy2 = array.get(basePrices, i)
                int span = ix2 - ix1
                if span < minChannelBarsInput or span > maxChannelBarsInput
                    continue
                if iy2 >= iy1
                    continue
                float minOff = 0.0
                int oppSz = array.size(oppPrices)
                if oppSz > 0
                    for k = 0 to oppSz - 1
                        int oppBar = array.get(oppIndices, k)
                        if oppBar >= ix1 and oppBar <= bar_index
                            float oppVal = array.get(oppPrices, k)
                            float baseAtOpp = linePrice(ix1, iy1, ix2, iy2, oppBar)
                            float diff = oppVal - baseAtOpp
                            if diff < minOff
                                minOff := diff
                if minOff >= 0
                    continue
                int totalBars = bar_index - ix1
                if totalBars <= 0
                    continue
                int checkLen = math.min(totalBars, 300)
                int contained = 0
                float tol = atrVal * 0.05
                for k = 0 to checkLen - 1
                    int bx = bar_index - k
                    if bx < ix1
                        break
                    float bLine = linePrice(ix1, iy1, ix2, iy2, bx)
                    float pLine = bLine + minOff
                    if high[k] <= bLine + tol and low[k] >= pLine - tol
                        contained += 1
                float qual = safeDiv(float(contained), float(checkLen), 0.0)
                if qual >= channelQualityInput and qual > bestQual
                    bestQual := qual
                    bestX1 := ix1
                    bestY1 := iy1
                    bestX2 := ix2
                    bestY2 := iy2
                    bestOffset := minOff
    [bestX1, bestY1, bestX2, bestY2, bestOffset, bestQual]

// ══════════════════════════════════════
// CHANNEL STATE
// ══════════════════════════════════════

var int   chUpX1 = na,      var float chUpY1 = na
var int   chUpX2 = na,      var float chUpY2 = na
var float chUpOff = na,     var float chUpQual = na
var bool  chUpOn = false

var int   chDnX1 = na,      var float chDnY1 = na
var int   chDnX2 = na,      var float chDnY2 = na
var float chDnOff = na,     var float chDnQual = na
var bool  chDnOn = false

var line chUpLnBase = na, var line chUpLnPar = na, var line chUpLnMid = na
var line chDnLnBase = na, var line chDnLnPar = na, var line chDnLnMid = na
var linefill chUpFill1 = na, var linefill chUpFill2 = na
var linefill chDnFill1 = na, var linefill chDnFill2 = na

// ══════════════════════════════════════
// BREAKOUT STATE & LABEL ARRAYS
// ══════════════════════════════════════

var bool upBullFired = false
var bool upBearFired = false
var bool dnBullFired = false
var bool dnBearFired = false

var array<label> upLabels = array.new_label()
var array<label> dnLabels = array.new_label()

// ══════════════════════════════════════
// CHANNEL SEARCH TRIGGER
// ══════════════════════════════════════
// FIX: Both channels are searched on ANY new pivot (newHi or newLo).
//      Previously down-channel was only searched on newHi, causing it
//      to go stale and never produce breakouts.

bool newHi = not na(pivotHi)
bool newLo = not na(pivotLo)

if (newHi or newLo) and isWarmedUp

    // ── Up channel (needs ≥2 lows) ──
    if array.size(loPrices) >= 2
        [bx1, by1, bx2, by2, boff, bq] = findUpChannel(loPrices, loIndices, hiPrices, hiIndices)
        if not na(bx1) and bq > nz(chUpQual, 0.0) * 0.7
            // Check if channel actually changed (different pivot bars)
            bool upChanged = bx1 != chUpX1 or bx2 != chUpX2
            chUpX1 := bx1
            chUpY1 := by1
            chUpX2 := bx2
            chUpY2 := by2
            chUpOff := boff
            chUpQual := bq
            chUpOn := true
            if upChanged
                clearLabels(upLabels)
                float newB = linePrice(bx1, by1, bx2, by2, bar_index)
                if isInside(close, newB, boff)
                    upBullFired := false
                    upBearFired := false
                else
                    upBullFired := close > math.max(newB, newB + boff)
                    upBearFired := close < math.min(newB, newB + boff)

    // ── Down channel (needs ≥2 highs) ──
    if array.size(hiPrices) >= 2
        [bx1, by1, bx2, by2, boff, bq] = findDownChannel(hiPrices, hiIndices, loPrices, loIndices)
        if not na(bx1) and bq > nz(chDnQual, 0.0) * 0.7
            // Check if channel actually changed (different pivot bars)
            bool dnChanged = bx1 != chDnX1 or bx2 != chDnX2
            chDnX1 := bx1
            chDnY1 := by1
            chDnX2 := bx2
            chDnY2 := by2
            chDnOff := boff
            chDnQual := bq
            chDnOn := true
            if dnChanged
                clearLabels(dnLabels)
                float newB = linePrice(bx1, by1, bx2, by2, bar_index)
                if isInside(close, newB, boff)
                    dnBullFired := false
                    dnBearFired := false
                else
                    dnBullFired := close > math.max(newB, newB + boff)
                    dnBearFired := close < math.min(newB, newB + boff)

// ══════════════════════════════════════
// DRAWING
// ══════════════════════════════════════

lineExt  = getExtend(extendInput)
baseStyl = getLineStyle(baseStyleInput)
parStyl  = getLineStyle(parStyleInput)
midStyl  = getLineStyle(midStyleInput)

drawCh(int x1, float y1, int x2, float y2, float off, bool active, bool show, line prevBase, line prevPar, line prevMid, linefill prevF1, linefill prevF2, color col, bool del) =>
    line nBase = prevBase
    line nPar = prevPar
    line nMid = prevMid
    linefill nF1 = prevF1
    linefill nF2 = prevF2
    if active and show and not na(x1) and not na(x2) and not na(off)
        if del
            line.delete(prevBase)
            line.delete(prevPar)
            line.delete(prevMid)
            linefill.delete(prevF1)
            linefill.delete(prevF2)
        nBase := line.new(x1, y1, x2, y2, color = col, width = baseWidthInput, style = baseStyl, extend = lineExt)
        nPar := line.new(x1, y1 + off, x2, y2 + off, color = col, width = parWidthInput, style = parStyl, extend = lineExt)
        if showMidlineInput
            nMid := line.new(x1, y1 + off / 2, x2, y2 + off / 2, color = col, width = midWidthInput, style = midStyl, extend = lineExt)
        if showFillInput
            color fc = color.new(col, fillTranspInput)
            if not na(nMid)
                nF1 := linefill.new(nBase, nMid, fc)
                nF2 := linefill.new(nMid, nPar, fc)
            else
                nF1 := linefill.new(nBase, nPar, fc)
    [nBase, nPar, nMid, nF1, nF2]

if (newHi or newLo) and isWarmedUp
    [a1, a2, a3, a4, a5] = drawCh(chUpX1, chUpY1, chUpX2, chUpY2, chUpOff, chUpOn, showUpInput, chUpLnBase, chUpLnPar, chUpLnMid, chUpFill1, chUpFill2, bullColorInput, deletePrevInput)
    chUpLnBase := a1
    chUpLnPar := a2
    chUpLnMid := a3
    chUpFill1 := a4
    chUpFill2 := a5

    [b1, b2, b3, b4, b5] = drawCh(chDnX1, chDnY1, chDnX2, chDnY2, chDnOff, chDnOn, showDownInput, chDnLnBase, chDnLnPar, chDnLnMid, chDnFill1, chDnFill2, bearColorInput, deletePrevInput)
    chDnLnBase := b1
    chDnLnPar := b2
    chDnLnMid := b3
    chDnFill1 := b4
    chDnFill2 := b5

// ══════════════════════════════════════════════════════════
// BREAKOUT & SIGNAL DETECTION
// ══════════════════════════════════════════════════════════
// Direction-agnostic upper/lower boundaries:
//   Up channel:   upper = base + off (off > 0),  lower = base
//   Down channel: upper = base,                   lower = base + off (off < 0)
//
// GUARD 1: max extrapolation = maxChannelBarsInput beyond X2
// GUARD 2: crossover — close[1] within 1.5 ATR of boundary
// GUARD 3: smart flag reset on channel rebuild (price-aware)
// GUARD 4: all old labels deleted on channel rebuild

bool upBreakSup   = false
bool upBreakRes   = false
bool upReactSup   = false
bool upReactRes   = false
bool dnBreakSup   = false
bool dnBreakRes   = false
bool dnReactSup   = false
bool dnReactRes   = false

int  upBoDir = 0
int  dnBoDir = 0

if barstate.isconfirmed and isWarmedUp

    // ══ Up channel (off > 0: base = support, par = resistance) ══
    if chUpOn and not na(chUpX1) and not na(chUpX2) and not na(chUpOff)
        if (bar_index - chUpX2) <= maxChannelBarsInput                                // GUARD 1
            float bNow  = linePrice(chUpX1, chUpY1, chUpX2, chUpY2, bar_index)
            float pNow  = bNow + chUpOff                                              // resistance
            float bPrev = linePrice(chUpX1, chUpY1, chUpX2, chUpY2, bar_index - 1)
            float pPrev = bPrev + chUpOff
            float tol      = atrVal * 0.12
            float crossTol = atrVal * 1.5                                             // GUARD 2

            if close >= bNow and close <= pNow
                upBullFired := false
                upBearFired := false

            // Bull breakout: close > resistance
            if close > pNow and not upBullFired and close[1] <= pPrev + crossTol
                upBoDir     := 1
                upBullFired := true
                upBreakRes  := true
                if showBreakoutLblInput
                    array.push(upLabels, label.new(bar_index, low, "Breakout ▲", color = bullColorInput, textcolor = #FFFFFF, style = label.style_label_up, size = size.small))

            // Bear breakout: close < support
            if close < bNow and not upBearFired and close[1] >= bPrev - crossTol
                upBoDir     := -1
                upBearFired := true
                upBreakSup  := true
                if showBreakoutLblInput
                    array.push(upLabels, label.new(bar_index, high, "Breakout ▼", color = bearColorInput, textcolor = #FFFFFF, style = label.style_label_down, size = size.small))

            if not upBreakSup and low <= bNow + tol and close > bNow and open > bNow
                upReactSup := true
            if not upBreakRes and high >= pNow - tol and close < pNow and open < pNow
                upReactRes := true

    // ══ Down channel (off < 0: base = resistance [upper], par = support [lower]) ══
    if chDnOn and not na(chDnX1) and not na(chDnX2) and not na(chDnOff)
        if (bar_index - chDnX2) <= maxChannelBarsInput                                // GUARD 1
            float bNow  = linePrice(chDnX1, chDnY1, chDnX2, chDnY2, bar_index)       // upper boundary
            float pNow  = bNow + chDnOff                                              // lower boundary (off < 0)
            float bPrev = linePrice(chDnX1, chDnY1, chDnX2, chDnY2, bar_index - 1)
            float pPrev = bPrev + chDnOff
            float tol      = atrVal * 0.12
            float crossTol = atrVal * 1.5                                             // GUARD 2

            if close <= bNow and close >= pNow
                dnBullFired := false
                dnBearFired := false

            // Bull breakout: close > base (upper boundary / resistance)
            if close > bNow and not dnBullFired and close[1] <= bPrev + crossTol
                dnBoDir     := 1
                dnBullFired := true
                dnBreakRes  := true
                if showBreakoutLblInput
                    array.push(dnLabels, label.new(bar_index, low, "Breakout ▲", color = bullColorInput, textcolor = #FFFFFF, style = label.style_label_up, size = size.small))

            // Bear breakout: close < parallel (lower boundary / support)
            if close < pNow and not dnBearFired and close[1] >= pPrev - crossTol
                dnBoDir     := -1
                dnBearFired := true
                dnBreakSup  := true
                if showBreakoutLblInput
                    array.push(dnLabels, label.new(bar_index, high, "Breakout ▼", color = bearColorInput, textcolor = #FFFFFF, style = label.style_label_down, size = size.small))

            // React at resistance (upper = base)
            if not dnBreakRes and high >= bNow - tol and close < bNow and open < bNow
                dnReactRes := true
            // React at support (lower = parallel)
            if not dnBreakSup and low <= pNow + tol and close > pNow and open > pNow
                dnReactSup := true

// ═══ Aggregated signals ═══
bool bullSig = upReactSup or dnBreakRes
bool bearSig = upBreakSup or dnReactRes

bool hasBullBreakout = upBoDir == 1 or dnBoDir == 1
bool hasBearBreakout = upBoDir == -1 or dnBoDir == -1
bool hasBreakout = hasBullBreakout or hasBearBreakout

bool anyReact = upReactSup or upReactRes or dnReactSup or dnReactRes

// ══════════════════════════════════════
// TREND & DASHBOARD DATA
// ══════════════════════════════════════

float upBase = chUpOn and not na(chUpX1) ? linePrice(chUpX1, chUpY1, chUpX2, chUpY2, bar_index) : na
float dnBase = chDnOn and not na(chDnX1) ? linePrice(chDnX1, chDnY1, chDnX2, chDnY2, bar_index) : na

int trendDir = 0
if not na(upBase) and close > upBase
    trendDir := 1
if not na(dnBase) and close < dnBase
    trendDir := -1

string trendStr = trendDir > 0 ? "Bullish" : trendDir < 0 ? "Bearish" : "Neutral"
color trendCol = trendDir > 0 ? bullColorInput : trendDir < 0 ? bearColorInput : TEXT_MUTED

string sigStr = bullSig ? "Buy" : bearSig ? "Sell" : "Wait"
color sigCol = bullSig ? bullColorInput : bearSig ? bearColorInput : TEXT_MUTED

float bestQ = math.max(nz(chUpQual, 0.0), nz(chDnQual, 0.0))
float sigPower = (bullSig or bearSig) ? bestQ * 100 : 0.0
string strStr = sigPower >= 70 ? "Strong" : sigPower >= 40 ? "Medium" : sigPower > 0 ? "Weak" : "—"
color strCol = sigPower >= 70 ? bullColorInput : sigPower >= 40 ? #FFEB3B : sigPower > 0 ? bearColorInput : TEXT_MUTED

// ══════════════════════════════════════
// DASHBOARD
// ══════════════════════════════════════

dashPos = switch dashPosStr
    "Top Left"     => position.top_left
    "Top Right"    => position.top_right
    "Bottom Left"  => position.bottom_left
    "Bottom Right" => position.bottom_right
    => position.top_right

if showDashInput and barstate.islast
    var dashTable = table.new(dashPos, 2, 7, TABLE_BG, TABLE_BORDER, 1, TABLE_BORDER, 1)

    table.cell(dashTable, 0, 0, "WillyAlgoTrader", text_color = HEADER_TEXT, bgcolor = HEADER_BG, text_size = size.small, text_halign = text.align_center)
    table.merge_cells(dashTable, 0, 0, 1, 0)

    table.cell(dashTable, 0, 1, "Trend", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_BG)
    table.cell(dashTable, 1, 1, trendStr, text_color = trendCol, text_size = size.small, bgcolor = TABLE_BG)

    table.cell(dashTable, 0, 2, "Signal", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
    table.cell(dashTable, 1, 2, sigStr, text_color = sigCol, text_size = size.small, bgcolor = TABLE_ROW_ALT)

    table.cell(dashTable, 0, 3, "Strength", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_BG)
    table.cell(dashTable, 1, 3, strStr, text_color = strCol, text_size = size.small, bgcolor = TABLE_BG)

    int actCh = (chUpOn ? 1 : 0) + (chDnOn ? 1 : 0)
    table.cell(dashTable, 0, 4, "Channels", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
    table.cell(dashTable, 1, 4, str.tostring(actCh), text_color = TEXT_COLOR, text_size = size.small, bgcolor = TABLE_ROW_ALT)

    string qStr = bestQ > 0 ? str.tostring(bestQ * 100, "#.0") + "%" : "—"
    table.cell(dashTable, 0, 5, "Quality", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_BG)
    table.cell(dashTable, 1, 5, qStr, text_color = TEXT_COLOR, text_size = size.small, bgcolor = TABLE_BG)

    table.cell(dashTable, 0, 6, "TF", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
    table.cell(dashTable, 1, 6, timeframe.period, text_color = TEXT_COLOR, text_size = size.small, bgcolor = TABLE_ROW_ALT)

// ══════════════════════════════════════
// WATERMARK
// ══════════════════════════════════════

if barstate.islast and showWatermarkInput
    var wmTable = table.new(position = position.bottom_center, columns = 1, rows = 1, bgcolor = color.new(color.black, 100), border_color = color.new(color.black, 100), border_width = 0, frame_color = color.new(color.black, 100), frame_width = 0)
    table.cell(wmTable, 0, 0, "WillyAlgoTrader", text_color = WM_COLOR, text_size = size.normal, text_halign = text.align_center, bgcolor = color.new(color.black, 100))

// ══════════════════════════════════════
// ALERTS
// ══════════════════════════════════════

string ticker = syminfo.tickerid
string tf = timeframe.period
string priceS = str.tostring(close, format.mintick)

if hasBreakout and alertBreakoutInput
    string dir = hasBullBreakout ? "bull" : "bear"
    string ico = hasBullBreakout ? "🟢" : "🔴"
    string jm = '{"action":"breakout","direction":"' + dir + '","ticker":"' + ticker + '","price":' + priceS + ',"tf":"' + tf + '"}'
    string tm = ico + " BREAKOUT " + str.upper(dir) + " | " + ticker + " | TF: " + tf + " | Price: " + priceS
    alert(webhookInput ? jm : tm, alert.freq_once_per_bar_close)

else if bullSig and alertBreakInput
    string jm = '{"action":"buy","ticker":"' + ticker + '","price":' + priceS + ',"tf":"' + tf + '"}'
    string tm = "🟢 BULL | " + ticker + " | TF: " + tf + " | Price: " + priceS + " | Channel Signal"
    alert(webhookInput ? jm : tm, alert.freq_once_per_bar_close)

else if bearSig and alertBreakInput
    string jm = '{"action":"sell","ticker":"' + ticker + '","price":' + priceS + ',"tf":"' + tf + '"}'
    string tm = "🔴 BEAR | " + ticker + " | TF: " + tf + " | Price: " + priceS + " | Channel Signal"
    alert(webhookInput ? jm : tm, alert.freq_once_per_bar_close)

else if anyReact and alertReactInput
    string jm = '{"action":"react","ticker":"' + ticker + '","price":' + priceS + ',"tf":"' + tf + '"}'
    string tm = "🟡 REACT | " + ticker + " | TF: " + tf + " | Price: " + priceS + " | Channel Reaction"
    alert(webhookInput ? jm : tm, alert.freq_once_per_bar_close)