Keltner Stochastic Strategy
Strategy that combines Keltner Channels and Stochastic Oscillator. Enters positions when price reaches Keltner Channel boundaries and Stochastic confirms oversold/overbought conditions.
Testing indicates an average annual return of about 163%. It performs best in the stocks market.
This setup looks to catch reversals near the Keltner bands while the oscillator confirms momentum shifts. Signals can trigger in both directions whenever price presses against an envelope.
Short-term traders seeking quick reversals may find it useful. Risk is contained by an ATR-based stop distance.
Details
- Entry Criteria:
- Long:
Close < LowerBand && StochK < StochOversold - Short:
Close > UpperBand && StochK > StochOverbought
- Long:
- Long/Short: Both
- Exit Criteria:
- Long:
Close > EMA - Short:
Close < EMA
- Long:
- Stops:
StopLossAtrATR from entry - Default Values:
EmaPeriod= 20AtrPeriod= 14KeltnerMultiplier= 2.0mStochPeriod= 14StochK= 3StochD= 3StochOversold= 20mStochOverbought= 80mStopLossAtr= 2.0mCandleType= TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Mean reversion
- Direction: Both
- Indicators: Keltner Channel, Stochastic Oscillator
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Mid-term
- 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 combines Keltner Channels (EMA + ATR) and manual Stochastic %K.
/// Enters when price reaches Keltner bands and Stochastic confirms oversold/overbought.
/// </summary>
public class KeltnerStochasticStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<decimal> _keltnerMultiplier;
private readonly StrategyParam<decimal> _stochOversold;
private readonly StrategyParam<decimal> _stochOverbought;
private readonly StrategyParam<int> _cooldownBars;
private decimal _atrValue;
private int _cooldown;
private readonly List<decimal> _highs = new();
private readonly List<decimal> _lows = new();
private const int StochPeriod = 14;
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// EMA period for Keltner Channel.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Keltner Channel multiplier.
/// </summary>
public decimal KeltnerMultiplier
{
get => _keltnerMultiplier.Value;
set => _keltnerMultiplier.Value = value;
}
/// <summary>
/// Stochastic oversold level.
/// </summary>
public decimal StochOversold
{
get => _stochOversold.Value;
set => _stochOversold.Value = value;
}
/// <summary>
/// Stochastic overbought level.
/// </summary>
public decimal StochOverbought
{
get => _stochOverbought.Value;
set => _stochOverbought.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Strategy constructor.
/// </summary>
public KeltnerStochasticStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetRange(10, 30)
.SetDisplay("EMA Period", "Period of the EMA for Keltner Channel", "Indicators");
_keltnerMultiplier = Param(nameof(KeltnerMultiplier), 2.0m)
.SetDisplay("Keltner Multiplier", "Multiplier for ATR in Keltner Channel", "Indicators");
_stochOversold = Param(nameof(StochOversold), 20m)
.SetDisplay("Stochastic Oversold", "Level considered oversold", "Indicators");
_stochOverbought = Param(nameof(StochOverbought), 80m)
.SetDisplay("Stochastic Overbought", "Level considered overbought", "Indicators");
_cooldownBars = Param(nameof(CooldownBars), 100)
.SetDisplay("Cooldown Bars", "Bars between trades", "General")
.SetRange(5, 500);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_atrValue = 0;
_cooldown = 0;
_highs.Clear();
_lows.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = 14 };
var subscription = SubscribeCandles(CandleType);
// Bind ATR to capture value
subscription.BindEx(atr, OnAtr);
// Bind EMA for main logic
subscription
.Bind(ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void OnAtr(ICandleMessage candle, IIndicatorValue atrValue)
{
if (atrValue.IsFormed)
_atrValue = atrValue.ToDecimal();
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_atrValue <= 0)
return;
// Track highs/lows for stochastic
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
var maxBuf = StochPeriod * 2;
if (_highs.Count > maxBuf)
{
_highs.RemoveRange(0, _highs.Count - maxBuf);
_lows.RemoveRange(0, _lows.Count - maxBuf);
}
if (_highs.Count < StochPeriod)
return;
// Manual Stochastic %K
var start = _highs.Count - StochPeriod;
var highestHigh = decimal.MinValue;
var lowestLow = decimal.MaxValue;
for (var i = start; i < _highs.Count; i++)
{
if (_highs[i] > highestHigh) highestHigh = _highs[i];
if (_lows[i] < lowestLow) lowestLow = _lows[i];
}
var diff = highestHigh - lowestLow;
if (diff == 0) return;
var stochK = 100m * (candle.ClosePrice - lowestLow) / diff;
// Keltner Channel
var upperBand = emaValue + (KeltnerMultiplier * _atrValue);
var lowerBand = emaValue - (KeltnerMultiplier * _atrValue);
var close = candle.ClosePrice;
if (_cooldown > 0)
{
_cooldown--;
return;
}
// Long: price below lower Keltner + Stochastic oversold
if (close < lowerBand && stochK < StochOversold && Position == 0)
{
BuyMarket();
_cooldown = CooldownBars;
}
// Short: price above upper Keltner + Stochastic overbought
else if (close > upperBand && stochK > StochOverbought && Position == 0)
{
SellMarket();
_cooldown = CooldownBars;
}
// Exit long: price above EMA
if (Position > 0 && close > emaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
// Exit short: price below EMA
else if (Position < 0 && close < 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_stochastic_strategy(Strategy):
"""
Keltner Channel + manual Stochastic %K. Mean reversion at band extremes.
"""
def __init__(self):
super(keltner_stochastic_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetDisplay("EMA Period", "Keltner EMA period", "Indicators")
self._keltner_mult = self.Param("KeltnerMultiplier", 2.0).SetDisplay("Keltner Mult", "ATR multiplier", "Indicators")
self._stoch_oversold = self.Param("StochOversold", 20.0).SetDisplay("Stoch Oversold", "Oversold level", "Indicators")
self._stoch_overbought = self.Param("StochOverbought", 80.0).SetDisplay("Stoch Overbought", "Overbought level", "Indicators")
self._cooldown_bars = self.Param("CooldownBars", 100).SetDisplay("Cooldown", "Bars between trades", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._atr_value = 0.0
self._cooldown = 0
self._highs = []
self._lows = []
self._stoch_period = 14
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(keltner_stochastic_strategy, self).OnReseted()
self._atr_value = 0.0
self._cooldown = 0
self._highs = []
self._lows = []
def OnStarted2(self, time):
super(keltner_stochastic_strategy, self).OnStarted2(time)
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
atr = AverageTrueRange()
atr.Length = 14
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(atr, self._on_atr)
subscription.Bind(ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _on_atr(self, candle, atr_val):
if atr_val.IsFinal:
self._atr_value = float(atr_val.Value)
def _process_candle(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
if self._atr_value <= 0:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._highs.append(high)
self._lows.append(low)
max_buf = self._stoch_period * 2
if len(self._highs) > max_buf:
self._highs = self._highs[-max_buf:]
self._lows = self._lows[-max_buf:]
if len(self._highs) < self._stoch_period:
return
recent_h = self._highs[-self._stoch_period:]
recent_l = self._lows[-self._stoch_period:]
hh = max(recent_h)
ll = min(recent_l)
diff = hh - ll
if diff == 0:
return
stoch_k = 100.0 * (close - ll) / diff
ema = float(ema_val)
upper = ema + self._keltner_mult.Value * self._atr_value
lower = ema - self._keltner_mult.Value * self._atr_value
if self._cooldown > 0:
self._cooldown -= 1
return
if close < lower and stoch_k < self._stoch_oversold.Value and self.Position == 0:
self.BuyMarket()
self._cooldown = self._cooldown_bars.Value
elif close > upper and stoch_k > self._stoch_overbought.Value and self.Position == 0:
self.SellMarket()
self._cooldown = self._cooldown_bars.Value
if self.Position > 0 and close > ema:
self.SellMarket()
self._cooldown = self._cooldown_bars.Value
elif self.Position < 0 and close < ema:
self.BuyMarket()
self._cooldown = self._cooldown_bars.Value
def CreateClone(self):
return keltner_stochastic_strategy()