Idea in brief
A classic breakout has one level: the N-day high. Here there are two levels. Donchian marks the N-day high and low, Keltner draws a band around an average at a distance of several ATRs. They react differently to volatility, and on different days one or the other ends up closer to price. The rule is simple: the signal comes from whichever channel price touches first.
Gary Antonacci and Carlo Zarattini applied this to 48 US industry sectors over a hundred years. A long entry on the first touch of the upper boundary of a 20-day channel. An exit on the first touch of the lower boundary of a 40-day channel: the exit length was doubled to give the trade room and avoid excess turnover. There are no shorts, and position size is normalized to volatility.
Tom Basso uses the same technique with three indicators (Donchian, Bollinger, Keltner) and a short length of 9 days. This version is always in the market and reverses in both directions.
Why it might work
Both authors describe channels as a noise band. Inside it price moves without direction, and there is no point trading there: a range produces only whipsaws. Antonacci treats a move beyond the band as a sign that the move will continue, and the hypothesis is that "a new four-week high promises continuation". Basso compares this to a thermostat that needs a margin of a couple of degrees so it does not click every minute.
The width of Keltner and Bollinger bands depends on volatility. In a quiet market the lines converge, in a nervous one they spread apart automatically. Basso believes this is why such indicators survive changing conditions better than fixed moving average crossovers. The first touch picks whichever noise measure is currently closer to price. Basso says plainly that it does not matter which indicator places the order: all three measure the same noise.
In Antonacci's decomposition of the result, the main contribution to Sharpe came from timing, not from sector selection. Host Rich Brennan explains this by the fact that breakouts skip most small moves and work on the tails of the distribution. We have not verified these explanations.
Rules
Channels
// daily bars
DonchianUpper(N) = highest High over the last N closed bars
DonchianLower(N) = lowest Low over the last N closed bars
KeltnerUpper(N) = EMA(Close, N) + K * ATR(N)
KeltnerLower(N) = EMA(Close, N) - K * ATR(N)
// Basso describes Keltner as EMA 21 plus ATR over roughly 21 days, times a multiplier.
// Finetiq: for Antonacci the Keltner length equals the channel length (20 or 40), K = 2 as a starting value
BollingerUpper(N) = SMA(Close, N) + B * StdDev(Close, N)
BollingerLower(N) = SMA(Close, N) - B * StdDev(Close, N)
// Finetiq: B = 2, the width is not named in the video
First touch
// the level closest to price triggers first
FirstUp(N) = min(DonchianUpper(N), KeltnerUpper(N))
FirstDown(N) = max(DonchianLower(N), KeltnerLower(N))
// Finetiq: a Keltner or Bollinger band can already be beyond price at the close.
// Then the touch has already happened: enter at market at the next open, without a stop order
Core system: Antonacci, long only
IF position = FLAT
IF Close > KeltnerUpper(20)
BUY AT NEXT BAR OPEN // Finetiq: level already passed at the close
ELSE
BUY STOP at FirstUp(20) // author: first touch of the 20-day channel
IF position = LONG
IF Close < KeltnerLower(40)
EXIT AT NEXT BAR OPEN // Finetiq
ELSE
SELL STOP at FirstDown(40) // author: first downside touch of the 40-day channel
// no shorts: stocks have an upward drift (author)
// no separate stop-loss, the exit on the 40-day channel plays that role
Position size (author, numbers Finetiq)
Vol = StdDev(daily returns, 20) * sqrt(252) // Finetiq: window not named in the video
Weight = TargetVol / Vol / M // author: volatility normalization
TargetVol = 10% // author, from memory
M = number of instruments in the basket // Finetiq: M = 1 for a single ETF
Weight = min(Weight, 1 / M) // Finetiq: no leverage
// daily rebalancing (author)
// basket: hold all sectors with a signal, no ranking (author)
Variant B. Basso: 9 days, three channels, always in the market
// index futures, always long or short, reversals in both directions (author)
// Basso uses the same 9-day combination on bitcoin and ether micro futures, the direction there is not specified
N = 9 // author, for all three indicators
UpperLevel = min(DonchianUpper(9), BollingerUpper(9), KeltnerUpper(9))
LowerLevel = max(DonchianLower(9), BollingerLower(9), KeltnerLower(9))
// author: the line closest to price places the order, and it does not matter which indicator that is
IF position = SHORT OR position = FLAT
BUY STOP at UpperLevel
IF position = LONG OR position = FLAT
SELL SHORT STOP at LowerLevel
// Finetiq: the same rule for a level already passed at the close
// size: Basso's general principle
Units = Equity * 0.5% / (|EntryPrice - LowerLevel| * PointValue) // for a long
// author: in a winning position the risk can grow to 1%, beyond that the position is trimmed
Variant C. Basso: three-day Donchian
// about 26–27 futures, always in the market (author)
BUY STOP at DonchianUpper(3)
SELL SHORT STOP at DonchianLower(3)
// the lower line is the long's trailing stop, no separate trailing stop is needed (author)
// inside days do not break the channel and are ignored automatically (author)
Parameters
| Parameter | Value | Source |
|---|---|---|
| Entry channel (A) | 20 days, Donchian and Keltner | author |
| Exit channel (A) | 40 days, Donchian and Keltner | author |
| Trigger rule | first touch of either channel | author |
| Direction (A) | long only | author |
| Keltner multiplier K | 2 | Finetiq |
| Keltner center and width | EMA and ATR of the same length | Finetiq (Basso uses EMA 21 and ATR around 21) |
| Size (A) | volatility normalization, target about 10% | author |
| Rebalancing (A) | daily | author |
| Volatility window | 20 days | Finetiq |
| Level passed at the close | market entry at the next open | Finetiq |
| Length (B) | 9 days, three indicators | author |
| Bollinger width B | 2 standard deviations | Finetiq |
| Direction (B, C) | always in the market, reversals | author |
| Length (C) | 3 days, Donchian only | author |
| Risk per position (B, C) | 0.5% of capital, up to 1% in a winning position | author (Basso's general principle) |
What to test
- First touch versus a single channel. Three runs of variant A: Donchian 20/40 only, Keltner 20/40 only, first touch. If the combination is not better than the better of the two, the second channel only adds trades.
- Keltner multiplier. K = 1.5, 2, 2.5, 3. With a small K the Keltner band is almost always closer to price and effectively replaces Donchian. Record which line gave the signal in each trade and look at the shares.
- The 20/40 asymmetry. Compare 20/40 with 20/20 and 20/10. The author doubled the exit length to reduce turnover. Check what this costs in profit given back and in drawdown.
- Daily rebalancing. Normalization every day versus rebalancing only when a weight drifts by more than 20%. The author admits that the daily version noticeably increases the number of trades.
- Neighborhood of Basso's 9 days. 7, 9, 12, 15 days on an index future with full costs. The system is always in the market and reverses often, so costs are decisive here.
- Sector versus index. Variant A on one sector ETF versus the same rule on the S&P 500. If the result on the broad index is similar, timing does most of the work, as the author says.
- Market entry after a close beyond the band. Count the share of such entries and the average slippage from the level. On gaps the entry price is noticeably worse than in an idealized test at the level.
Platform notes
TradingView (Pine Script)
ta.kccomputes the center and width with its own implementation, and Keltner implementations differ. To match the rules, build the band yourself:ta.ema(close, 20) + k * ta.atr(20).ta.atruses Wilder smoothing.- A buy stop order below the current price fills at the open of the next bar. If the Keltner band is already below price,
strategy.entry(..., stop = firstUp)turns into a market entry. Handle this case explicitly, otherwise the entry will look like a breakout of the level. ta.stdevis the population standard deviation by default, likeStdDevin EasyLanguage andiBandsin MQL5. Bollinger bands on the three platforms will match at the same length.- A strategy trades one symbol. A basket of 48 sectors with common normalization cannot be tested in Pine. Variant A is tested on a single ETF, and the basket is assembled outside TradingView.
MultiCharts and TradeStation (EasyLanguage)
- Build Keltner yourself:
XAverage(Close, 20) + K * ATRw, whereATRwis Wilder's ATR calculated manually.AvgTrueRangeis a simple average of TrueRange, and bands built with it will differ. Pick one ATR version and keep it on all platforms. Buy next bar at FirstUp stoplives for one bar. Send it on every bar. If the level is below the open of the next bar, the order fills at the open.- For futures (variants B and C), the daily bar close in TradeStation is the settlement, while a 1440-minute bar closes on the last trade (Kevin Davey, The Algorithmic Advantage #036). A three-day channel on the two bar types gives different levels almost every day.
- A sector basket with volatility normalization is tested in the portfolio module: Portfolio Trader in MultiCharts, Portfolio Maestro in TradeStation.
MetaTrader 5 (MQL5)
- There is no built-in Keltner. Take the average via
iMA(..., MODE_EMA, ...)and calculate ATR yourself:iATRis a simple average of TR. - Not every broker offers sector ETFs. CFDs on ETFs accrue swaps, and a variant A position is held for weeks and months.
- Daily normalization runs into the lot step
SYMBOL_VOLUME_STEP. On a small account the weight changes in steps, and some rebalances do not happen. - For variants B and C, on a netting account a reversal is a single deal of double volume, on a hedging account the old position has to be closed explicitly.
Where the idea can break
- The Keltner multiplier and the Bollinger width are not named in the video. They determine which channel triggers first, that is, the essence of the rule. Antonacci admits to not remembering the paper's details, so the exact settings have to be taken from the paper itself.
- The century of data consists of industry indices, not tradable securities. Sector ETF history is short, and the author tested ETFs separately.
- The author openly shows the unevenness: the century-long table has excellent and weak periods, and regimes last 15–20 years. A test on 5–10 years can fall entirely into a weak regime.
- Hedging with a short improved Sharpe and drawdown but reduced the total return, so the model was kept long only. Nothing protects the long version against drawdowns in a bear market.
- Basso gives no result figures. Versions B and C are described as Basso's current strategies, without statistics and without band widths.
- Daily volatility normalization increases turnover. On a small account and on CFDs, costs can eat the difference from simple buy and hold.