Keltner Reversion
Strategy that trades on mean reversion using Keltner Channels
Testing indicates an average annual return of about 130%. It performs best in the stocks market.
Keltner Reversion fades pushes outside the Keltner Channel. Entries bet on a return toward the middle band, closing trades once price re-enters the channel or the stop is hit.
The channel width expands and contracts with volatility, allowing the system to catch extreme moves while giving trades room to develop. Stops are typically based on ATR multiples.
Details
- Entry Criteria: Signals based on RSI, ATR, Keltner.
- Long/Short: Both directions.
- Exit Criteria: Opposite signal or stop.
- Stops: Yes.
- Default Values:
EmaPeriod= 20AtrPeriod= 14AtrMultiplier= 2.0mStopLossAtrMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: RSI, ATR, Keltner
- Stops: Yes
- Complexity: Basic
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that trades on mean reversion using Keltner Channels.
/// It opens positions when price touches or breaks through the upper or lower Keltner Channel bands
/// and exits when price reverts to the middle band (EMA).
/// </summary>
public class KeltnerReversionStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _stopLossAtrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cooldownBars;
private int _cooldown;
/// <summary>
/// Period for EMA calculation (middle band) (default: 20)
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Period for ATR calculation (default: 14)
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// ATR multiplier for Keltner Channel width (default: 2.0)
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// ATR multiplier for stop-loss calculation (default: 2.0)
/// </summary>
public decimal StopLossAtrMultiplier
{
get => _stopLossAtrMultiplier.Value;
set => _stopLossAtrMultiplier.Value = value;
}
/// <summary>
/// Type of candles used for strategy calculation
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initialize the Keltner Reversion strategy
/// </summary>
public KeltnerReversionStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetDisplay("EMA Period", "Period for EMA calculation (middle band)", "Technical Parameters")
.SetOptimize(10, 50, 5);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for ATR calculation (middle band)", "Technical Parameters")
.SetOptimize(7, 21, 7);
_atrMultiplier = Param(nameof(AtrMultiplier), 2.0m)
.SetDisplay("ATR Multiplier", "ATR multiplier for Keltner Channel width", "Technical Parameters")
.SetOptimize(1.0m, 3.0m, 0.5m);
_stopLossAtrMultiplier = Param(nameof(StopLossAtrMultiplier), 2.0m)
.SetDisplay("ATR Multiplier (Stop Loss)", "ATR multiplier for stop-loss calculation", "Risk Management")
.SetOptimize(1.0m, 3.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "Technical Parameters");
_cooldownBars = Param(nameof(CooldownBars), 500)
.SetRange(1, 1000)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_cooldown = 0;
// Create indicators
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
// Create subscription and bind indicators
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, atr, ProcessCandle)
.Start();
// Configure chart
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
/// <summary>
/// Process candle and check for Keltner Channel signals
/// </summary>
private void ProcessCandle(ICandleMessage candle, decimal emaValue, decimal atrValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if strategy is ready to trade
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
// Calculate Keltner Channel bands
decimal upperBand = emaValue + (atrValue * AtrMultiplier);
decimal lowerBand = emaValue - (atrValue * AtrMultiplier);
if (Position == 0)
{
if (candle.ClosePrice < lowerBand)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (candle.ClosePrice > upperBand)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position > 0)
{
if (candle.ClosePrice > emaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
}
else if (Position < 0)
{
if (candle.ClosePrice < emaValue)
{
BuyMarket();
_cooldown = CooldownBars;
}
}
}
}
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 ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class keltner_reversion_strategy(Strategy):
"""
Keltner Channel mean reversion strategy.
Buys below lower band, sells above upper band, exits at EMA.
"""
def __init__(self):
super(keltner_reversion_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetDisplay("EMA Period", "Period for EMA calculation (middle band)", "Technical Parameters")
self._atr_period = self.Param("AtrPeriod", 14).SetDisplay("ATR Period", "Period for ATR calculation", "Technical Parameters")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetDisplay("ATR Multiplier", "ATR multiplier for Keltner Channel width", "Technical Parameters")
self._stop_loss_atr = self.Param("StopLossAtrMultiplier", 2.0).SetDisplay("ATR Multiplier (Stop Loss)", "ATR multiplier for stop-loss calculation", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "Technical Parameters")
self._cooldown_bars = self.Param("CooldownBars", 500).SetDisplay("Cooldown Bars", "Bars to wait between trades", "General")
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(keltner_reversion_strategy, self).OnReseted()
self._cooldown = 0
def OnStarted2(self, time):
super(keltner_reversion_strategy, self).OnStarted2(time)
self._cooldown = 0
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ema_val, atr_val):
if candle.State != CandleStates.Finished:
return
if self._cooldown > 0:
self._cooldown -= 1
return
ev = float(ema_val)
av = float(atr_val)
mult = float(self._atr_multiplier.Value)
upper = ev + av * mult
lower = ev - av * mult
close = float(candle.ClosePrice)
cd = self._cooldown_bars.Value
if self.Position == 0:
if close < lower:
self.BuyMarket()
self._cooldown = cd
elif close > upper:
self.SellMarket()
self._cooldown = cd
elif self.Position > 0:
if close > ev:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0:
if close < ev:
self.BuyMarket()
self._cooldown = cd
def CreateClone(self):
return keltner_reversion_strategy()