Estrategia de Tendencia Exp Hull
Descripción general
La Estrategia de Tendencia Exp Hull se basa en el indicador Hull Moving Average (HMA). El algoritmo compara un cálculo Hull intermedio con una media móvil Hull suavizada. Cuando la línea Hull rápida cruza por encima de la línea suavizada más lenta, la estrategia abre una posición larga. Cuando la línea rápida cruza por debajo de la línea suavizada, la estrategia abre una posición corta.
Lógica de la estrategia
- Calcular una media móvil ponderada (WMA) del precio de cierre con período Length / 2.
- Calcular otra WMA del precio de cierre con período Length.
- Construir el valor Hull intermedio:
fast = 2 * WMA(Length/2) - WMA(Length). - Suavizar el valor intermedio con una WMA de período
sqrt(Length)para obtener el valor Hull finalslow. - Generar señales:
- Entrada Largo – cuando
fastcruza por encima deslow. - Entrada Corto – cuando
fastcruza por debajo deslow.
- Entrada Largo – cuando
- Las posiciones se revierten en señales opuestas. Las órdenes de protección se gestionan a través de
StartProtection.
Parámetros
| Nombre | Descripción |
|---|---|
Hull Length |
Período base para el cálculo Hull. Determina la sensibilidad de ambas WMA. |
Candle Type |
Marco temporal de velas utilizado para los cálculos del indicador. |
Notas
- La estrategia trabaja únicamente con velas completadas.
- Los valores del indicador se vinculan a través de la API de alto nivel para evitar colecciones de datos manuales.
- El volumen se toma de la configuración de la estrategia; cuando cambia la dirección de la señal, la posición se revierte.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Exp Hull Trend strategy based on Hull moving average cross.
/// Opens long when fast hull crosses above smoothed hull and short on opposite.
/// </summary>
public class ExpHullTrendStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _minSpreadPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevFast;
private decimal _prevSlow;
private bool _initialized;
private int _cooldownRemaining;
// Manual WMA for final smoothing
private readonly List<decimal> _finalBuffer = new();
private int _finalLength;
/// <summary>
/// Base period for Hull moving average.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// Minimum normalized spread between the fast and slow lines required for a valid signal.
/// </summary>
public decimal MinSpreadPercent
{
get => _minSpreadPercent.Value;
set => _minSpreadPercent.Value = value;
}
/// <summary>
/// Number of completed candles to wait after a position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Type of candles for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpHullTrendStrategy"/>.
/// </summary>
public ExpHullTrendStrategy()
{
_length = Param(nameof(Length), 20)
.SetDisplay("Hull Length", "Base period for Hull calculation", "Indicator");
_minSpreadPercent = Param(nameof(MinSpreadPercent), 0.0015m)
.SetDisplay("Min Spread %", "Minimum normalized spread between Hull lines", "Signal");
_cooldownBars = Param(nameof(CooldownBars), 12)
.SetDisplay("Cooldown Bars", "Completed candles to wait after a position change", "Signal");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for processing", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security, DataType)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_finalLength = Math.Max(1, (int)Math.Sqrt(Length));
var wmaHalf = new WeightedMovingAverage { Length = Math.Max(1, Length / 2) };
var wmaFull = new WeightedMovingAverage { Length = Length };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(wmaHalf, wmaFull, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevFast = 0m;
_prevSlow = 0m;
_initialized = false;
_cooldownRemaining = 0;
_finalBuffer.Clear();
_finalLength = 0;
}
private decimal CalcWma(decimal newVal)
{
_finalBuffer.Add(newVal);
if (_finalBuffer.Count > _finalLength)
_finalBuffer.RemoveAt(0);
if (_finalBuffer.Count < _finalLength)
return newVal;
decimal sumWeight = 0;
decimal sumVal = 0;
for (int i = 0; i < _finalBuffer.Count; i++)
{
var w = i + 1;
sumVal += _finalBuffer[i] * w;
sumWeight += w;
}
return sumVal / sumWeight;
}
private void ProcessCandle(ICandleMessage candle, decimal halfValue, decimal fullValue)
{
if (candle.State != CandleStates.Finished)
return;
var fast = 2m * halfValue - fullValue; // intermediate Hull value
var slow = CalcWma(fast); // smoothed Hull
if (!_initialized)
{
_prevFast = fast;
_prevSlow = slow;
_initialized = true;
return;
}
var crossUp = _prevFast <= _prevSlow && fast > slow;
var crossDown = _prevFast >= _prevSlow && fast < slow;
var spread = Math.Abs(fast - slow) / Math.Max(Math.Abs(slow), 1m);
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (crossUp && spread >= MinSpreadPercent && _cooldownRemaining == 0 && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (crossDown && spread >= MinSpreadPercent && _cooldownRemaining == 0 && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_cooldownRemaining = CooldownBars;
}
_prevFast = fast;
_prevSlow = slow;
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
class exp_hull_trend_strategy(Strategy):
def __init__(self):
super(exp_hull_trend_strategy, self).__init__()
self._length = self.Param("Length", 20) \
.SetDisplay("Hull Length", "Base period for Hull calculation", "Indicator")
self._min_spread_percent = self.Param("MinSpreadPercent", 0.0015) \
.SetDisplay("Min Spread %", "Minimum normalized spread between Hull lines", "Signal")
self._cooldown_bars = self.Param("CooldownBars", 12) \
.SetDisplay("Cooldown Bars", "Completed candles to wait after a position change", "Signal")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for processing", "General")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._initialized = False
self._cooldown_remaining = 0
self._final_buffer = []
self._final_length = 0
@property
def length(self):
return self._length.Value
@property
def min_spread_percent(self):
return self._min_spread_percent.Value
@property
def cooldown_bars(self):
return self._cooldown_bars.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(exp_hull_trend_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._initialized = False
self._cooldown_remaining = 0
self._final_buffer = []
self._final_length = 0
def OnStarted2(self, time):
super(exp_hull_trend_strategy, self).OnStarted2(time)
self._final_length = max(1, int(Math.Sqrt(self.length)))
wma_half = WeightedMovingAverage()
wma_half.Length = max(1, self.length // 2)
wma_full = WeightedMovingAverage()
wma_full.Length = self.length
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(wma_half, wma_full, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _calc_wma(self, new_val):
self._final_buffer.append(new_val)
if len(self._final_buffer) > self._final_length:
self._final_buffer.pop(0)
if len(self._final_buffer) < self._final_length:
return new_val
sum_weight = 0.0
sum_val = 0.0
for i in range(len(self._final_buffer)):
w = i + 1
sum_val += self._final_buffer[i] * w
sum_weight += w
return sum_val / sum_weight
def process_candle(self, candle, half_value, full_value):
if candle.State != CandleStates.Finished:
return
half_value = float(half_value)
full_value = float(full_value)
fast = 2.0 * half_value - full_value
slow = self._calc_wma(fast)
if not self._initialized:
self._prev_fast = fast
self._prev_slow = slow
self._initialized = True
return
cross_up = self._prev_fast <= self._prev_slow and fast > slow
cross_down = self._prev_fast >= self._prev_slow and fast < slow
spread = abs(fast - slow) / max(abs(slow), 1.0)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
min_spread = float(self.min_spread_percent)
if cross_up and spread >= min_spread and self._cooldown_remaining == 0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self.cooldown_bars
elif cross_down and spread >= min_spread and self._cooldown_remaining == 0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self.cooldown_bars
self._prev_fast = fast
self._prev_slow = slow
def CreateClone(self):
return exp_hull_trend_strategy()