Estrategia Keltner Volume
Implementación de la estrategia Keltner Channels + Volume. Comprar cuando el precio cierra por debajo del canal Keltner inferior con volumen superior al promedio. Vender cuando el precio cierra por encima del canal Keltner superior con volumen superior al promedio.
Las pruebas indican un retorno anual promedio de aproximadamente el 58%. Funciona mejor en el mercado de acciones.
Los límites del canal Keltner definen posibles reversiones, y el aumento del volumen señala convicción. El sistema opera cuando el precio toca una banda con volumen en expansión.
Los traders que buscan confirmación de volumen alrededor de bandas de volatilidad pueden preferir esta configuración. Los stops se calculan a partir del ATR.
Detalles
- Criterios de entrada:
- Largo:
Close < LowerBand && Volume > AvgVolume - Corto:
Close > UpperBand && Volume > AvgVolume
- Largo:
- Largo/Corto: Ambos
- Criterios de salida:
- El precio cruza la EMA
- Stops: Basados en ATR usando
StopLossAtr - Valores predeterminados:
EmaPeriod= 20AtrPeriod= 14Multiplier= 2.0mVolumeAvgPeriod= 20StopLossAtr= 2CandleType= TimeSpan.FromMinutes(5).TimeFrame() El canal es la EMA más y menos Multiplier ATR, y AvgVolume es el volumen medio de las VolumeAvgPeriod velas anteriores. El stop está a StopLossAtr ATR del cierre de entrada y se comprueba en los cierres de vela. Una señal de entrada contra una posición abierta la invierte.
- Filtros:
- Categoría: Reversión a la media
- Dirección: Ambos
- Indicadores: Keltner Channel, Volume
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Medio plazo
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Keltner Volume strategy.
/// The Keltner Channel is the EmaPeriod EMA plus and minus Multiplier times the AtrPeriod ATR. A close below the lower band on volume above
/// the average of the previous VolumeAvgPeriod candles goes long and a close above the upper band on such volume goes short, reversing
/// an opposite position. The position closes when price crosses the EMA. The stop lies StopLossAtr ATR from the entry close
/// and is checked on candle closes.
/// </summary>
public class KeltnerVolumeStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<int> _volumeAvgPeriod;
private readonly StrategyParam<decimal> _stopLossAtr;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _volumes = [];
private decimal _stopPrice;
/// <summary>
/// Period of the channel EMA.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// Period of the channel and stop ATR.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// ATR multiplier of the channel width.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Previous candles the volume is averaged over.
/// </summary>
public int VolumeAvgPeriod
{
get => _volumeAvgPeriod.Value;
set => _volumeAvgPeriod.Value = value;
}
/// <summary>
/// Stop distance from the entry in ATRs.
/// </summary>
public decimal StopLossAtr
{
get => _stopLossAtr.Value;
set => _stopLossAtr.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public KeltnerVolumeStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Period of the channel EMA", "Keltner");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "Period of the channel and stop ATR", "Keltner");
_multiplier = Param(nameof(Multiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Multiplier", "ATR multiplier of the channel width", "Keltner");
_volumeAvgPeriod = Param(nameof(VolumeAvgPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Volume Average Period", "Previous candles the volume is averaged over", "Volume");
_stopLossAtr = Param(nameof(StopLossAtr), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss ATR", "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();
_volumes.Clear();
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_stopPrice = default;
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(ema, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue emaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
// Volume is compared with the candles before this one.
var average = _volumes.Count == VolumeAvgPeriod ? _volumes.Average() : (decimal?)null;
_volumes.Add(candle.TotalVolume);
if (_volumes.Count > VolumeAvgPeriod)
_volumes.RemoveAt(0);
if (!emaValue.IsFormed || !atrValue.IsFormed || average is not decimal avgVolume)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var middle = emaValue.GetValue<decimal>();
var atr = atrValue.GetValue<decimal>();
var upper = middle + Multiplier * atr;
var lower = middle - Multiplier * atr;
var close = candle.ClosePrice;
var surge = candle.TotalVolume > avgVolume;
if (close < lower && surge && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = close - StopLossAtr * atr;
}
else if (close > upper && surge && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = close + StopLossAtr * atr;
}
else if (Position > 0 && (close > middle || (StopLossAtr > 0 && close <= _stopPrice)))
{
SellMarket(Position);
}
else if (Position < 0 && (close < middle || (StopLossAtr > 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 ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class keltner_volume_strategy(Strategy):
"""
Keltner Volume strategy.
The Keltner Channel is the EmaPeriod EMA plus and minus Multiplier times the AtrPeriod ATR. A close below the lower band on volume above
the average of the previous VolumeAvgPeriod candles goes long and a close above the upper band on such volume goes short, reversing
an opposite position. The position closes when price crosses the EMA. The stop lies StopLossAtr ATR from the entry close
and is checked on candle closes.
"""
def __init__(self):
super(keltner_volume_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetGreaterThanZero().SetDisplay("EMA Period", "Period of the channel EMA", "Keltner")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero().SetDisplay("ATR Period", "Period of the channel and stop ATR", "Keltner")
self._multiplier = self.Param("Multiplier", 2.0).SetGreaterThanZero().SetDisplay("Multiplier", "ATR multiplier of the channel width", "Keltner")
self._volume_avg_period = self.Param("VolumeAvgPeriod", 20).SetGreaterThanZero().SetDisplay("Volume Average Period", "Previous candles the volume is averaged over", "Volume")
self._stop_loss_atr = self.Param("StopLossAtr", 2.0).SetNotNegative().SetDisplay("Stop Loss ATR", "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._volumes = []
self._stop_price = Decimal(0)
def OnReseted(self):
super(keltner_volume_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(keltner_volume_strategy, self).OnStarted2(time)
self._reset_state()
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(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)
def _process_candle(self, candle, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
# Volume is compared with the candles before this one.
period = self._volume_avg_period.Value
average = None
if len(self._volumes) == period:
total = Decimal(0)
for volume in self._volumes:
total += volume
average = total / Decimal(period)
self._volumes.append(candle.TotalVolume)
if len(self._volumes) > period:
self._volumes.pop(0)
if not ema_value.IsFormed or not atr_value.IsFormed or average is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
middle = ema_value.GetValue[Decimal](None)
atr = atr_value.GetValue[Decimal](None)
multiplier = Decimal(self._multiplier.Value)
upper = middle + multiplier * atr
lower = middle - multiplier * atr
close = candle.ClosePrice
surge = candle.TotalVolume > average
stop_atr = Decimal(self._stop_loss_atr.Value)
if close < lower and surge and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = close - stop_atr * atr
elif close > upper and surge 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 keltner_volume_strategy()