Estrategia de Reversión por Clímax de Volumen
La Reversión por Clímax de Volumen busca puntos de inflexión marcados por un volumen extremadamente alto tras una tendencia fuerte. Estos picos climáticos sugieren agotamiento a medida que los últimos compradores o vendedores se precipitan antes de que el impulso se desvanezca.
Las pruebas indican un rendimiento anual promedio de aproximadamente el 82%. Funciona mejor en el mercado de acciones.
La estrategia entra en contra del movimiento anterior una vez que se cierra una barra de gran volumen y el precio comienza a retroceder.
Un stop porcentual ajustado protege la posición, y las operaciones salen si el volumen no disminuye o el precio continúa en la dirección original.
Detalles
- Criterios de entrada: señal del indicador
- Largo/Corto: Ambos
- Criterios de salida: stop-loss o señal opuesta
- Stops: Sí, basado en porcentaje
- Valores predeterminados:
CandleType= 15 minuteMaPeriod= 20VolumeMultiplier= 2StopLossPercent= 2 Un clímax es una vela que cierra a favor de la tendencia (alcista por encima de la SMA de MaPeriod, bajista por debajo) con un volumen mayor que VolumeMultiplier veces el promedio de las MaPeriod velas anteriores. La siguiente vela que cierra en sentido contrario abre la operación contra el movimiento. La operación sale cuando un cierre supera el extremo del clímax, cuando el volumen vuelve a dispararse o en el stop porcentual.
- Filtros:
- Categoría: Volumen
- Dirección: Ambos
- Indicadores: Volumen
- 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 Climax Reversal strategy.
/// A climax is a candle closing in the direction of the trend (a bullish candle above the SMA or a bearish one below it)
/// with volume above VolumeMultiplier times the average of the previous MaPeriod candles. When the next candle retraces, the strategy
/// enters against the move while flat. It exits when price closes beyond the climax extreme, when volume spikes again, or at the percent stop.
/// </summary>
public class VolumeClimaxReversalStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _volumes = [];
// The climax of the previous candle: 1 up, -1 down, 0 none, and its extreme.
private int _climax;
private decimal _climaxExtreme;
private decimal _exitLevel;
/// <summary>
/// Period of the trend SMA and of the volume average.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// How many times the average volume a climax must exceed.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public VolumeClimaxReversalStrategy()
{
_maPeriod = Param(nameof(MaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Period of the trend SMA and of the volume average", "Indicators");
_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "How many times the average volume a climax must exceed", "Indicators");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).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();
_climax = 0;
_climaxExtreme = default;
_exitLevel = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_climax = 0;
_climaxExtreme = default;
_exitLevel = default;
var sma = new SimpleMovingAverage { Length = MaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(sma, ProcessCandle)
.Start();
StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
// The stop has to see prices between candles, not only at their close.
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before this callback, also between signal bars.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaValue)
{
if (candle.State != CandleStates.Finished)
return;
// The spike is measured against the candles before this one.
var average = _volumes.Count == MaPeriod ? _volumes.Average() : (decimal?)null;
_volumes.Add(candle.TotalVolume);
if (_volumes.Count > MaPeriod)
_volumes.RemoveAt(0);
var climax = _climax;
var climaxExtreme = _climaxExtreme;
_climax = 0;
if (average is not decimal avgVolume || !smaValue.IsFormed)
return;
var ma = smaValue.GetValue<decimal>();
var close = candle.ClosePrice;
var spike = candle.TotalVolume > avgVolume * VolumeMultiplier;
if (spike && close > candle.OpenPrice && close > ma)
{
_climax = 1;
_climaxExtreme = candle.HighPrice;
}
else if (spike && close < candle.OpenPrice && close < ma)
{
_climax = -1;
_climaxExtreme = candle.LowPrice;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position < 0)
{
if (close > _exitLevel || spike)
BuyMarket(-Position);
}
else if (Position > 0)
{
if (close < _exitLevel || spike)
SellMarket(Position);
}
else if (climax == 1 && close < candle.OpenPrice)
{
SellMarket(Volume);
_exitLevel = climaxExtreme;
}
else if (climax == -1 && close > candle.OpenPrice)
{
BuyMarket(Volume);
_exitLevel = climaxExtreme;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes, Level1Fields
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class volume_climax_reversal_strategy(Strategy):
"""
Volume Climax Reversal strategy.
A climax is a candle closing in the direction of the trend (a bullish candle above the SMA or a bearish one below it)
with volume above VolumeMultiplier times the average of the previous MaPeriod candles. When the next candle retraces, the strategy
enters against the move while flat. It exits when price closes beyond the climax extreme, when volume spikes again, or at the percent stop.
"""
def __init__(self):
super(volume_climax_reversal_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 20).SetGreaterThanZero().SetDisplay("MA Period", "Period of the trend SMA and of the volume average", "Indicators")
self._volume_multiplier = self.Param("VolumeMultiplier", 2.0).SetGreaterThanZero().SetDisplay("Volume Multiplier", "How many times the average volume a climax must exceed", "Indicators")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).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 = []
# The climax of the previous candle: 1 up, -1 down, 0 none, and its extreme.
self._climax = 0
self._climax_extreme = Decimal(0)
self._exit_level = Decimal(0)
def OnReseted(self):
super(volume_climax_reversal_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(volume_climax_reversal_strategy, self).OnStarted2(time)
self._reset_state()
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(sma, self._process_candle).Start()
self.StartProtection(Unit(), Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
# The stop has to see prices between candles, not only at their close.
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _observe_protection_quote(self, quote):
# The high-level handler activates native protection before the callback, including between bars.
pass
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
# The spike is measured against the candles before this one.
period = self._ma_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)
climax = self._climax
climax_extreme = self._climax_extreme
self._climax = 0
if average is None or not sma_value.IsFormed:
return
ma = sma_value.GetValue[Decimal](None)
close = candle.ClosePrice
spike = candle.TotalVolume > average * Decimal(self._volume_multiplier.Value)
if spike and close > candle.OpenPrice and close > ma:
self._climax = 1
self._climax_extreme = candle.HighPrice
elif spike and close < candle.OpenPrice and close < ma:
self._climax = -1
self._climax_extreme = candle.LowPrice
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position < 0:
if close > self._exit_level or spike:
self.BuyMarket(-self.Position)
elif self.Position > 0:
if close < self._exit_level or spike:
self.SellMarket(self.Position)
elif climax == 1 and close < candle.OpenPrice:
self.SellMarket(self.Volume)
self._exit_level = climax_extreme
elif climax == -1 and close > candle.OpenPrice:
self.BuyMarket(self.Volume)
self._exit_level = climax_extreme
def CreateClone(self):
return volume_climax_reversal_strategy()