// © FXPIPS ONLINE — https://fxpips.online/tradingview-indicators/keltner-channel-breakout/
// License: MIT. Educational tool — not financial advice. Trading involves substantial risk.
//@version=6
indicator("FXP Keltner Channel Breakout", shorttitle = "FXP Keltner", overlay = true)

// ── Inputs ─────────────────────────────────────────────────────────────
grpKC     = "Keltner Channel"
src       = input.source(close, "Basis source", group = grpKC)
emaLen    = input.int(20, "EMA length", minval = 1, group = grpKC)
atrLen    = input.int(10, "ATR length", minval = 1, group = grpKC)
atrMult   = input.float(2.0, "ATR multiplier", minval = 0.1, step = 0.1, group = grpKC)

grpSig    = "Signals"
useSlope  = input.bool(false, "Require EMA slope in breakout direction", group = grpSig)
slopeBars = input.int(3, "Slope lookback (bars)", minval = 1, group = grpSig)
oncePerSide = input.bool(true, "Only first breakout per side", group = grpSig, tooltip = "Ignore repeat breakouts in the same direction until the opposite side breaks.")

grpVis    = "Display"
showFill  = input.bool(true, "Fill channel", group = grpVis)
bullCol   = input.color(color.teal, "Bullish color", group = grpVis)
bearCol   = input.color(color.red, "Bearish color", group = grpVis)

// ── Calculation ────────────────────────────────────────────────────────
basis = ta.ema(src, emaLen)
atr   = ta.atr(atrLen)
upper = basis + atrMult * atr
lower = basis - atrMult * atr

bool slopeUp = basis > basis[slopeBars]
bool slopeDn = basis < basis[slopeBars]

bool rawBull = close > upper and close[1] <= upper[1] and (not useSlope or slopeUp)
bool rawBear = close < lower and close[1] >= lower[1] and (not useSlope or slopeDn)

// Last confirmed breakout direction: 1 = up, -1 = down, 0 = none yet
var int lastDir = 0
bool bullBreak = false
bool bearBreak = false
if barstate.isconfirmed
    if rawBull and (not oncePerSide or lastDir != 1)
        bullBreak := true
        lastDir := 1
    else if rawBear and (not oncePerSide or lastDir != -1)
        bearBreak := true
        lastDir := -1

// ── Plots ──────────────────────────────────────────────────────────────
color stateCol = lastDir == 1 ? bullCol : lastDir == -1 ? bearCol : color.gray
plot(basis, "Basis (EMA)", color = color.new(stateCol, 20))
pU = plot(upper, "Upper channel", color = color.new(bullCol, 30))
pL = plot(lower, "Lower channel", color = color.new(bearCol, 30))
fill(pU, pL, color = showFill ? color.new(stateCol, 92) : na, title = "Channel fill")

plotshape(bullBreak, "Bullish breakout", shape.triangleup, location.belowbar, bullCol, size = size.small)
plotshape(bearBreak, "Bearish breakout", shape.triangledown, location.abovebar, bearCol, size = size.small)

// ── Alerts ─────────────────────────────────────────────────────────────
alertcondition(bullBreak, "Bullish Keltner breakout", "FXP Keltner: close above upper channel on {{ticker}} {{interval}} at {{close}}")
alertcondition(bearBreak, "Bearish Keltner breakout", "FXP Keltner: close below lower channel on {{ticker}} {{interval}} at {{close}}")
