Keltner Volume Strategy
Implementation of strategy - Keltner Channels + Volume. Buy when price breaks above upper Keltner Channel with above average volume. Sell when price breaks below lower Keltner Channel with above average volume.
Testing indicates an average annual return of about 58%. It performs best in the stocks market.
Keltner Channel boundaries define potential reversals, and increased volume signals conviction. The system trades when price touches a band with volume expanding.
Traders wanting volume confirmation around volatility bands may prefer this setup. Stops are computed from ATR.
Details
- Entry Criteria:
- Long:
Close < LowerBand && Volume > AvgVolume - Short:
Close > UpperBand && Volume > AvgVolume
- Long:
- Long/Short: Both
- Exit Criteria:
- Price crosses EMA
- Stops: ATR-based using
StopLoss - Default Values:
EmaPeriod= 20AtrPeriod= 14Multiplier= 2.0mVolumeAvgPeriod= 20StopLoss= new Unit(2, UnitTypes.Absolute)CandleType= TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Mean reversion
- Direction: Both
- Indicators: Keltner Channel, Volume
- 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 combining Keltner Channels with volume confirmation.
/// Buys on upper channel breakout with above-average volume,
/// sells on lower channel breakdown with above-average volume.
/// </summary>
public class KeltnerVolumeStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<int> _volumeAvgPeriod;
private readonly StrategyParam<int> _cooldownBars;
private decimal _averageVolume;
private int _volumeCounter;
private int _cooldown;
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// EMA period for center line.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// ATR period for channel width.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// ATR multiplier for channel width.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Volume average period.
/// </summary>
public int VolumeAvgPeriod
{
get => _volumeAvgPeriod.Value;
set => _volumeAvgPeriod.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initialize strategy.
/// </summary>
public KeltnerVolumeStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetRange(10, 40)
.SetDisplay("EMA Period", "EMA period for center line", "Keltner");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetRange(7, 21)
.SetDisplay("ATR Period", "ATR period for channel width", "Keltner");
_multiplier = Param(nameof(Multiplier), 2.0m)
.SetDisplay("ATR Multiplier", "Multiplier for ATR", "Keltner");
_volumeAvgPeriod = Param(nameof(VolumeAvgPeriod), 20)
.SetRange(10, 50)
.SetDisplay("Volume Avg Period", "Period for volume average", "Volume");
_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();
_averageVolume = 0;
_volumeCounter = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var volume = candle.TotalVolume;
if (_volumeCounter < VolumeAvgPeriod)
{
_volumeCounter++;
_averageVolume = ((_averageVolume * (_volumeCounter - 1)) + volume) / _volumeCounter;
}
else
{
_averageVolume = (_averageVolume * (VolumeAvgPeriod - 1) + volume) / VolumeAvgPeriod;
}
if (_volumeCounter < VolumeAvgPeriod)
{
if (_cooldown > 0)
_cooldown--;
return;
}
var upperBand = emaValue + Multiplier * atrValue;
var lowerBand = emaValue - Multiplier * atrValue;
var highVolume = volume > _averageVolume;
if (_cooldown > 0)
{
_cooldown--;
return;
}
// Buy: price above upper band + high volume
if (close > upperBand && highVolume && Position == 0)
{
BuyMarket();
_cooldown = CooldownBars;
}
// Sell: price below lower band + high volume
else if (close < lowerBand && highVolume && Position == 0)
{
SellMarket();
_cooldown = CooldownBars;
}
// Exit long: price below EMA
if (Position > 0 && close < emaValue)
{
SellMarket();
_cooldown = CooldownBars;
}
// Exit short: price above 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
from datatype_extensions import *
class keltner_volume_strategy(Strategy):
"""
Strategy combining Keltner Channels with volume confirmation.
"""
def __init__(self):
super(keltner_volume_strategy, self).__init__()
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._ema_period = self.Param("EmaPeriod", 20) \
.SetRange(10, 40) \
.SetDisplay("EMA Period", "EMA period for center line", "Keltner")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetRange(7, 21) \
.SetDisplay("ATR Period", "ATR period for channel width", "Keltner")
self._multiplier = self.Param("Multiplier", 2.0) \
.SetDisplay("ATR Multiplier", "Multiplier for ATR", "Keltner")
self._volume_avg_period = self.Param("VolumeAvgPeriod", 20) \
.SetRange(10, 50) \
.SetDisplay("Volume Avg Period", "Period for volume average", "Volume")
self._cooldown_bars = self.Param("CooldownBars", 100) \
.SetDisplay("Cooldown Bars", "Bars between trades", "General") \
.SetRange(5, 500)
self._average_volume = 0.0
self._volume_counter = 0
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(keltner_volume_strategy, self).OnStarted2(time)
self._average_volume = 0.0
self._volume_counter = 0
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.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
volume = float(candle.TotalVolume)
ev = float(ema_value)
av = float(atr_value)
vol_avg_prd = self._volume_avg_period.Value
if self._volume_counter < vol_avg_prd:
self._volume_counter += 1
self._average_volume = ((self._average_volume * (self._volume_counter - 1)) + volume) / self._volume_counter
else:
self._average_volume = (self._average_volume * (vol_avg_prd - 1) + volume) / vol_avg_prd
if self._volume_counter < vol_avg_prd:
if self._cooldown > 0:
self._cooldown -= 1
return
mult = float(self._multiplier.Value)
upper_band = ev + mult * av
lower_band = ev - mult * av
high_volume = volume > self._average_volume
if self._cooldown > 0:
self._cooldown -= 1
return
cd = self._cooldown_bars.Value
# Buy: price above upper band + high volume
if close > upper_band and high_volume and self.Position == 0:
self.BuyMarket()
self._cooldown = cd
elif close < lower_band and high_volume and self.Position == 0:
self.SellMarket()
self._cooldown = cd
# Exit long: price below EMA
if self.Position > 0 and close < ev:
self.SellMarket()
self._cooldown = cd
elif self.Position < 0 and close > ev:
self.BuyMarket()
self._cooldown = cd
def OnReseted(self):
super(keltner_volume_strategy, self).OnReseted()
self._average_volume = 0.0
self._volume_counter = 0
self._cooldown = 0
def CreateClone(self):
return keltner_volume_strategy()