Stochastic Keltner Strategy
This strategy uses Stochastic Keltner indicators to generate signals. Long entry occurs when Stoch %K < StochOversold && Price < Keltner lower band (oversold at lower band). Short entry occurs when Stoch %K > StochOverbought && Price > Keltner upper band (overbought at upper band). It is suitable for traders seeking opportunities in mixed markets.
Testing indicates an average annual return of about 61%. It performs best in the crypto market.
Details
- Entry Criteria:
- Long: Stoch %K < StochOversold && Price < Keltner lower band (oversold at lower band)
- Short: Stoch %K > StochOverbought && Price > Keltner upper band (overbought at upper band)
- Long/Short: Both sides.
- Exit Criteria:
- Long: Exit long position when price returns to middle band
- Short: Exit short position when price returns to middle band
- Stops: Yes.
- Default Values:
StochPeriod= 14StochK= 3StochOversold= 20StochOverbought= 80EmaPeriod= 20KeltnerMultiplier= 2AtrPeriod= 14AtrMultiplier= 2 20 and 80 are the defaults of the %K levels the rules quote. The bands are the EmaPeriod EMA plus and minus KeltnerMultiplier times the AtrPeriod ATR, and the middle band is the EMA itself. The stop lies AtrMultiplier times the same ATR from the entry close and is checked on candle closes; 0 disables it. StochK in the rules is %K: the stochastic over StochPeriod candles smoothed over StochK candles; %D plays no part, so it has no setting. An entry signal against an open position reverses it.CandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mixed
- Direction: Both
- Indicators: Stochastic Keltner
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- 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>
/// Stochastic Keltner strategy.
/// The Keltner Channel is the EmaPeriod EMA plus and minus KeltnerMultiplier times the AtrPeriod ATR; %K is the stochastic over StochPeriod
/// candles smoothed over StochK candles. %K below StochOversold with a close below the lower band goes long and %K above StochOverbought
/// with a close above the upper band goes short, reversing an opposite position. A position closes once price returns to the middle band.
/// The stop lies AtrMultiplier ATR from the entry close and is checked on candle closes.
/// </summary>
public class StochasticKeltnerStrategy : Strategy
{
private readonly StrategyParam<int> _stochPeriod;
private readonly StrategyParam<int> _stochK;
private readonly StrategyParam<decimal> _stochOversold;
private readonly StrategyParam<decimal> _stochOverbought;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<decimal> _keltnerMultiplier;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private decimal _stopPrice;
/// <summary>
/// Lookback period of the raw stochastic.
/// </summary>
public int StochPeriod
{
get => _stochPeriod.Value;
set => _stochPeriod.Value = value;
}
/// <summary>
/// Smoothing period of %K.
/// </summary>
public int StochK
{
get => _stochK.Value;
set => _stochK.Value = value;
}
/// <summary>
/// %K level for longs.
/// </summary>
public decimal StochOversold
{
get => _stochOversold.Value;
set => _stochOversold.Value = value;
}
/// <summary>
/// %K level for shorts.
/// </summary>
public decimal StochOverbought
{
get => _stochOverbought.Value;
set => _stochOverbought.Value = value;
}
/// <summary>
/// Period of the channel EMA.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// ATR multiplier of the channel width.
/// </summary>
public decimal KeltnerMultiplier
{
get => _keltnerMultiplier.Value;
set => _keltnerMultiplier.Value = value;
}
/// <summary>
/// Period of the channel and stop ATR.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// Stop distance from the entry in ATRs.
/// </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>
/// Constructor.
/// </summary>
public StochasticKeltnerStrategy()
{
_stochPeriod = Param(nameof(StochPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Stochastic Period", "Lookback period of the raw stochastic", "Stochastic");
_stochK = Param(nameof(StochK), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic %K", "Smoothing period of %K", "Stochastic");
_stochOversold = Param(nameof(StochOversold), 20m)
.SetDisplay("Stochastic Oversold", "%K level for longs", "Stochastic");
_stochOverbought = Param(nameof(StochOverbought), 80m)
.SetDisplay("Stochastic Overbought", "%K level for shorts", "Stochastic");
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Period of the channel EMA", "Keltner");
_keltnerMultiplier = Param(nameof(KeltnerMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Keltner Multiplier", "ATR multiplier of the channel width", "Keltner");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period of the channel and stop ATR", "Keltner");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetNotNegative()
.SetDisplay("ATR Multiplier", "Stop distance from the entry in ATRs", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).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();
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_stopPrice = default;
var stochastic = new StochasticOscillator
{
K = { Length = StochPeriod },
D = { Length = StochK },
};
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(stochastic, ema, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, stochastic);
}
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochasticValue, IIndicatorValue emaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!stochasticValue.IsFormed || !emaValue.IsFormed || !atrValue.IsFormed)
return;
// The smoothed %K is the moving average the core oscillator exposes as D.
if (stochasticValue is not IStochasticOscillatorValue { D: decimal k })
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var middle = emaValue.GetValue<decimal>();
var atr = atrValue.GetValue<decimal>();
var upper = middle + KeltnerMultiplier * atr;
var lower = middle - KeltnerMultiplier * atr;
var close = candle.ClosePrice;
if (k < StochOversold && close < lower && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - AtrMultiplier * atr;
}
else if (k > StochOverbought && close > upper && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + AtrMultiplier * atr;
}
else if (Position > 0 && (close >= middle || (AtrMultiplier > 0 && close <= _stopPrice)))
{
SellMarket(Position);
}
else if (Position < 0 && (close <= middle || (AtrMultiplier > 0 && close >= _stopPrice)))
{
BuyMarket(-Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import StochasticOscillator, ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class stochastic_keltner_strategy(Strategy):
"""
Stochastic Keltner strategy.
The Keltner Channel is the EmaPeriod EMA plus and minus KeltnerMultiplier times the AtrPeriod ATR; %K is the stochastic over StochPeriod
candles smoothed over StochK candles. %K below StochOversold with a close below the lower band goes long and %K above StochOverbought
with a close above the upper band goes short, reversing an opposite position. A position closes once price returns to the middle band.
The stop lies AtrMultiplier ATR from the entry close and is checked on candle closes.
"""
def __init__(self):
super(stochastic_keltner_strategy, self).__init__()
self._stoch_period = self.Param("StochPeriod", 14).SetGreaterThanZero().SetDisplay("Stochastic Period", "Lookback period of the raw stochastic", "Stochastic")
self._stoch_k = self.Param("StochK", 3).SetGreaterThanZero().SetDisplay("Stochastic %K", "Smoothing period of %K", "Stochastic")
self._stoch_oversold = self.Param("StochOversold", 20.0).SetDisplay("Stochastic Oversold", "%K level for longs", "Stochastic")
self._stoch_overbought = self.Param("StochOverbought", 80.0).SetDisplay("Stochastic Overbought", "%K level for shorts", "Stochastic")
self._ema_period = self.Param("EmaPeriod", 20).SetGreaterThanZero().SetDisplay("EMA Period", "Period of the channel EMA", "Keltner")
self._keltner_multiplier = self.Param("KeltnerMultiplier", 2.0).SetGreaterThanZero().SetDisplay("Keltner Multiplier", "ATR multiplier of the channel width", "Keltner")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period of the channel and stop ATR", "Keltner")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative().SetDisplay("ATR Multiplier", "Stop distance from the entry in ATRs", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._stop_price = Decimal(0)
def OnReseted(self):
super(stochastic_keltner_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(stochastic_keltner_strategy, self).OnStarted2(time)
self._reset_state()
stochastic = StochasticOscillator()
stochastic.K.Length = self._stoch_period.Value
stochastic.D.Length = self._stoch_k.Value
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(stochastic, ema, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, stochastic)
def _process_candle(self, candle, stochastic_value, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not stochastic_value.IsFormed or not ema_value.IsFormed or not atr_value.IsFormed:
return
# The smoothed %K is the moving average the core oscillator exposes as D.
if stochastic_value.D is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
k = stochastic_value.D
middle = ema_value.GetValue[Decimal](None)
atr = atr_value.GetValue[Decimal](None)
multiplier = Decimal(self._keltner_multiplier.Value)
upper = middle + multiplier * atr
lower = middle - multiplier * atr
close = candle.ClosePrice
stop_atr = Decimal(self._atr_multiplier.Value)
if k < Decimal(self._stoch_oversold.Value) and close < lower and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - stop_atr * atr
elif k > Decimal(self._stoch_overbought.Value) and close > upper and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = close + stop_atr * atr
elif self.Position > 0 and (close >= middle or (stop_atr > 0 and close <= self._stop_price)):
self.SellMarket(self.Position)
elif self.Position < 0 and (close <= middle or (stop_atr > 0 and close >= self._stop_price)):
self.BuyMarket(-self.Position)
def CreateClone(self):
return stochastic_keltner_strategy()