What it does
The Keltner Channel wraps an exponential moving average with bands set a fixed number of Average True Ranges away. Because the width comes from ATR rather than standard deviation, the channel reacts smoothly to changes in volatility and is less jumpy than Bollinger Bands.
This version marks the bar that closes outside the channel. A close above the upper line is a bullish breakout, a close below the lower line is a bearish breakout. The channel fill takes the color of the most recent breakout, so you can see at a glance which side last broke.
How it works
- Basis = EMA of the source over EMA length bars (a moving average that weights recent prices more).
- ATR = Average True Range over ATR length bars.
- Upper = basis + multiplier × ATR. Lower = basis − multiplier × ATR.
- A raw bullish breakout occurs when the close is above the upper line and the previous close was at or below the previous upper line. A raw bearish breakout mirrors this below the lower line.
- Optional slope filter: the basis must be higher (for longs) or lower (for shorts) than it was Slope lookback bars ago.
- With "Only first breakout per side" on, a second bullish breakout is ignored until a bearish breakout has happened, and vice versa.
- All signals are evaluated on closed bars only.
How to use it
A close outside the Keltner Channel says price has moved further than its recent average range would suggest. Trend followers often read that as momentum; mean-reversion traders treat the same event as stretched. This tool is written for the first reading, so it works best when the market has a reason to trend, for example after a range on the higher timeframe.
Practical ideas:
- Use the slope filter to avoid breakouts against the prevailing direction of the EMA.
- Combine with a higher-timeframe trend or support and resistance map so you are not buying straight into a known ceiling.
- The basis line can act as a trailing reference. Some traders exit when price closes back on the other side of the EMA.
Place a stop-loss where the breakout idea is clearly wrong, often back inside the channel or beyond the basis, and check the risk-reward ratio before entering.
Settings
| Input | Default | What it changes |
|---|---|---|
| Basis source | close | Price used for the EMA. |
| EMA length | 20 | Smoothness of the center line. |
| ATR length | 10 | Smoothness of the channel width. |
| ATR multiplier | 2.0 | Wider channel = fewer, stronger breakouts. |
| Require EMA slope | off | Filters breakouts against the EMA direction. |
| Slope lookback (bars) | 3 | Bars compared for the slope filter. |
| Only first breakout per side | on | Removes repeated same-side signals. |
| Fill channel | on | Visual only. |
Limitations
- Breakouts fail often in ranging markets. The indicator does not know whether the market is trending.
- The EMA and ATR both lag, so the channel adjusts after volatility changes, not before.
- Spikes around news releases can close outside the channel and reverse on the next bar.
- With "Only first breakout per side" on, a valid continuation breakout after a pullback will not be marked unless the opposite side broke in between.
- Signals appear only when the bar closes, which is later than an intrabar touch.
Pine Script source
In TradingView open Pine Editor, paste the code, click Save and then Add to chart.
FXP_KeltnerChannelBreakout.pine
60 lines
// © 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}}")

