Keltner Channel Breakout
Strategy based on Keltner Channel breakout
Testing indicates an average annual return of about 58%. It performs best in the stocks market.
Keltner Channel Breakout uses volatility bands derived from ATR. Breakouts above the upper band or below the lower band trigger entries. Price moving back through the EMA center or hitting a stop exits the position.
Because the bands expand and contract with volatility, this breakout method aims to capture early stages of a strong move while still allowing price room to breathe within the channel.
Details
- Entry Criteria: Signals based on ATR, Keltner.
- Long/Short: Both directions.
- Exit Criteria: Opposite signal or stop.
- Stops: Yes.
- Default Values:
EmaPeriod= 20AtrPeriod= 14AtrMultiplier= 2mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Breakout
- Direction: Both
- Indicators: ATR, Keltner
- Stops: Yes
- Complexity: Basic
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on Keltner Channel breakout.
/// It enters long position when price breaks through the upper band and short position when price breaks through the lower band.
/// </summary>
public class KeltnerChannelBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
// Current state
private decimal _prevClosePrice;
private decimal _prevUpperBand;
private decimal _prevLowerBand;
private decimal _prevEma;
/// <summary>
/// Period for EMA calculation.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Period for ATR calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Multiplier for ATR to determine channel width.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize the Keltner Channel Breakout strategy.
/// </summary>
public KeltnerChannelBreakoutStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 500)
.SetDisplay("EMA Period", "Period for Exponential Moving Average", "Indicators")
.SetOptimize(10, 50, 5);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for Average True Range", "Indicators")
.SetOptimize(10, 30, 2);
_atrMultiplier = Param(nameof(AtrMultiplier), 10m)
.SetDisplay("ATR Multiplier", "Multiplier for ATR to determine channel width", "Indicators")
.SetOptimize(1, 3, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevClosePrice = default;
_prevUpperBand = default;
_prevLowerBand = default;
_prevEma = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
var keltnerChannel = new KeltnerChannels
{
Length = EmaPeriod,
Multiplier = AtrMultiplier
};
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(keltnerChannel, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, keltnerChannel);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue keltnerValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if strategy is ready to trade
if (!IsFormedAndOnlineAndAllowTrading())
return;
var keltnerTyped = (KeltnerChannelsValue)keltnerValue;
if (keltnerTyped.Upper is not decimal upperValue)
return;
if (keltnerTyped.Lower is not decimal lowerValue)
return;
if (keltnerTyped.Middle is not decimal middleValue)
return;
// Skip the first received value for proper comparison
if (_prevUpperBand == 0)
{
_prevClosePrice = candle.ClosePrice;
_prevUpperBand = upperValue;
_prevLowerBand = lowerValue;
_prevEma = middleValue;
return;
}
// Check for breakouts
var isUpperBreakout = candle.ClosePrice > _prevUpperBand && _prevClosePrice <= _prevUpperBand;
var isLowerBreakout = candle.ClosePrice < _prevLowerBand && _prevClosePrice >= _prevLowerBand;
// Entry logic - breakout reversal only
if (isUpperBreakout && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
}
else if (isLowerBreakout && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
}
// Update previous values
_prevClosePrice = candle.ClosePrice;
_prevUpperBand = upperValue;
_prevLowerBand = lowerValue;
_prevEma = middleValue;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import KeltnerChannels
from StockSharp.Algo.Strategies import Strategy
class keltner_channel_breakout_strategy(Strategy):
"""
Strategy based on Keltner Channel breakout.
Enters long when price breaks above upper band, short when price breaks below lower band.
"""
def __init__(self):
super(keltner_channel_breakout_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 500).SetDisplay("EMA Period", "Period for Exponential Moving Average", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14).SetDisplay("ATR Period", "Period for Average True Range", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 10.0).SetDisplay("ATR Multiplier", "Multiplier for ATR to determine channel width", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_close_price = 0.0
self._prev_upper_band = 0.0
self._prev_lower_band = 0.0
self._prev_ema = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(keltner_channel_breakout_strategy, self).OnReseted()
self._prev_close_price = 0.0
self._prev_upper_band = 0.0
self._prev_lower_band = 0.0
self._prev_ema = 0.0
def OnStarted2(self, time):
super(keltner_channel_breakout_strategy, self).OnStarted2(time)
keltner = KeltnerChannels()
keltner.Length = self._ema_period.Value
keltner.Multiplier = self._atr_multiplier.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(keltner, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, keltner)
self.DrawOwnTrades(area)
def _process_candle(self, candle, keltner_val):
if candle.State != CandleStates.Finished:
return
if keltner_val.Upper is None or keltner_val.Lower is None or keltner_val.Middle is None:
return
upper = float(keltner_val.Upper)
lower = float(keltner_val.Lower)
middle = float(keltner_val.Middle)
if self._prev_upper_band == 0:
self._prev_close_price = float(candle.ClosePrice)
self._prev_upper_band = upper
self._prev_lower_band = lower
self._prev_ema = middle
return
close = float(candle.ClosePrice)
is_upper_breakout = close > self._prev_upper_band and self._prev_close_price <= self._prev_upper_band
is_lower_breakout = close < self._prev_lower_band and self._prev_close_price >= self._prev_lower_band
if is_upper_breakout and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif is_lower_breakout and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._prev_close_price = close
self._prev_upper_band = upper
self._prev_lower_band = lower
self._prev_ema = middle
def CreateClone(self):
return keltner_channel_breakout_strategy()