Estrategia de Agotamiento de Volumen
Los picos bruscos de volumen suelen señalar el final de un movimiento cuando los traders se apresuran a cerrar o abrir posiciones. Esta estrategia compara el volumen actual con un promedio para detectar agotamiento. Combinada con la dirección de la vela y un filtro de media móvil, puede identificar entradas de reversión.
Las pruebas indican una rentabilidad anual media de aproximadamente el 133%. Funciona mejor en el mercado de criptomonedas.
Cada vela actualiza el volumen promedio. Si el volumen de la nueva barra supera este promedio por un multiplicador definido y la vela cierra en dirección contraria a la tendencia predominante, el sistema abre una operación. Un stop basado en ATR protege la posición.
La operación generalmente se cierra mediante el stop-loss, ya que la estrategia anticipa una reversión rápida tras el pico de volumen.
Detalles
- Criterios de entrada: Pico de volumen por encima del promedio con vela contraria a la tendencia.
- Largo/Corto: Ambos.
- Criterios de salida: Stop-loss.
- Stops: Sí, basado en ATR.
- Valores predeterminados:
VolumePeriod= 20VolumeMultiplier= 2.0MAPeriod= 20AtrMultiplier= 2 ATR La tendencia es la dirección de la media móvil respecto a la vela anterior. El stop empieza a AtrMultiplier ATR (periodo 14) del cierre de entrada y sigue a cada cierre posterior a esa distancia; se comprueba al cierre de las velas.CandleType= 5 minute
- Filtros:
- Categoría: Reversión
- Dirección: Ambos
- Indicadores: Volume, MA, ATR
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Intradía
- 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>
/// Volume Exhaustion strategy.
/// A volume spike is a candle whose volume exceeds VolumeMultiplier times the average of the previous VolumePeriod candles.
/// While flat, a bullish spike against a falling moving average buys and a bearish spike against a rising one sells.
/// The only exit is a stop that trails the close by AtrMultiplier ATRs.
/// </summary>
public class VolumeExhaustionStrategy : Strategy
{
/// <summary>
/// Period of the ATR that sizes the stop.
/// </summary>
public const int AtrPeriod = 14;
private readonly StrategyParam<int> _volumePeriod;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _volumes = [];
private decimal? _prevMa;
private decimal _stopPrice;
/// <summary>
/// Number of previous candles the volume is averaged over.
/// </summary>
public int VolumePeriod
{
get => _volumePeriod.Value;
set => _volumePeriod.Value = value;
}
/// <summary>
/// How many times the average volume a spike must exceed.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <summary>
/// Period of the moving average that defines the trend.
/// </summary>
public int MAPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Distance of the trailing stop in ATR multiples.
/// </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 VolumeExhaustionStrategy()
{
_volumePeriod = Param(nameof(VolumePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Indicators");
_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "How many times the average volume a spike must exceed", "Indicators");
_maPeriod = Param(nameof(MAPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Moving average that defines the trend", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "Distance of the trailing stop in ATR multiples", "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();
_prevMa = null;
_stopPrice = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_prevMa = null;
_stopPrice = default;
var sma = new SimpleMovingAverage { Length = MAPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
// The spike is measured against the candles before this one.
var average = _volumes.Count == VolumePeriod ? _volumes.Average() : (decimal?)null;
_volumes.Add(candle.TotalVolume);
if (_volumes.Count > VolumePeriod)
_volumes.RemoveAt(0);
if (!smaValue.IsFormed || !atrValue.IsFormed)
return;
var ma = smaValue.GetValue<decimal>();
var prevMa = _prevMa;
_prevMa = ma;
if (average is not decimal avgVolume || prevMa is not decimal lastMa || !IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
var distance = AtrMultiplier * atrValue.GetValue<decimal>();
if (Position > 0)
{
if (close <= _stopPrice)
SellMarket(Position);
else
_stopPrice = Math.Max(_stopPrice, close - distance);
return;
}
if (Position < 0)
{
if (close >= _stopPrice)
BuyMarket(-Position);
else
_stopPrice = Math.Min(_stopPrice, close + distance);
return;
}
var spike = candle.TotalVolume > avgVolume * VolumeMultiplier;
if (!spike)
return;
if (close > candle.OpenPrice && ma < lastMa)
{
BuyMarket(Volume);
_stopPrice = close - distance;
}
else if (close < candle.OpenPrice && ma > lastMa)
{
SellMarket(Volume);
_stopPrice = close + distance;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
# Period of the ATR that sizes the stop.
ATR_PERIOD = 14
class volume_exhaustion_strategy(Strategy):
"""
Volume Exhaustion strategy.
A volume spike is a candle whose volume exceeds VolumeMultiplier times the average of the previous VolumePeriod candles.
While flat, a bullish spike against a falling moving average buys and a bearish spike against a rising one sells.
The only exit is a stop that trails the close by AtrMultiplier ATRs.
"""
def __init__(self):
super(volume_exhaustion_strategy, self).__init__()
self._volume_period = self.Param("VolumePeriod", 20).SetGreaterThanZero().SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Indicators")
self._volume_multiplier = self.Param("VolumeMultiplier", 2.0).SetGreaterThanZero().SetDisplay("Volume Multiplier", "How many times the average volume a spike must exceed", "Indicators")
self._ma_period = self.Param("MAPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Moving average that defines the trend", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetGreaterThanZero().SetDisplay("ATR Multiplier", "Distance of the trailing stop in ATR multiples", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._volumes = []
self._prev_ma = None
self._stop_price = Decimal(0)
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(volume_exhaustion_strategy, self).OnReseted()
self._volumes = []
self._prev_ma = None
self._stop_price = Decimal(0)
def OnStarted2(self, time):
super(volume_exhaustion_strategy, self).OnStarted2(time)
self._volumes = []
self._prev_ma = None
self._stop_price = Decimal(0)
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
atr = AverageTrueRange()
atr.Length = ATR_PERIOD
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sma_value, atr_value):
if candle.State != CandleStates.Finished:
return
# The spike is measured against the candles before this one.
period = self._volume_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 sma_value.IsFormed or not atr_value.IsFormed:
return
ma = sma_value.GetValue[Decimal](None)
prev_ma = self._prev_ma
self._prev_ma = ma
if average is None or prev_ma is None or not self.IsFormedAndOnlineAndAllowTrading():
return
close = candle.ClosePrice
distance = Decimal(self._atr_multiplier.Value) * atr_value.GetValue[Decimal](None)
if self.Position > 0:
if close <= self._stop_price:
self.SellMarket(self.Position)
else:
self._stop_price = Math.Max(self._stop_price, close - distance)
return
if self.Position < 0:
if close >= self._stop_price:
self.BuyMarket(-self.Position)
else:
self._stop_price = Math.Min(self._stop_price, close + distance)
return
spike = candle.TotalVolume > average * Decimal(self._volume_multiplier.Value)
if not spike:
return
if close > candle.OpenPrice and ma < prev_ma:
self.BuyMarket(self.Volume)
self._stop_price = close - distance
elif close < candle.OpenPrice and ma > prev_ma:
self.SellMarket(self.Volume)
self._stop_price = close + distance
def CreateClone(self):
return volume_exhaustion_strategy()