Estrategia de Media Móvil Shift WaveTrend
Esta estrategia combina una media móvil configurable con un oscilador de estilo WaveTrend. Las operaciones largas ocurren cuando el precio está por encima de la media móvil y el oscilador sube, confirmando una tendencia alcista con una EMA a largo plazo y un filtro de volatilidad. Las posiciones cortas se activan en condiciones opuestas. Las posiciones están protegidas por stop loss, take profit y trailing stop porcentuales.
Detalles
- Criterios de entrada:
- Largo: precio por encima de la MA, oscilador > 0 y subiendo, tendencia a largo plazo alcista, ATR por encima de su media, dentro del horario de trading, no ya en ola.
- Corto: precio por debajo de la MA, oscilador < 0 y bajando, tendencia a largo plazo bajista, ATR por encima de su media, dentro del horario de trading, no ya en ola.
- Largo/Corto: Ambos.
- Criterios de salida:
- Reversión del oscilador con cruce de precio y MA, o trailing stop, o protecciones.
- Stops: Sí.
- Valores predeterminados:
MaType= SMAMaLength= 40OscLength= 15TakeProfitPercent= 1.5StopLossPercent= 1TrailPercent= 1LongMaLength= 200AtrLength= 14StartHour= 9EndHour= 17
- Filtros:
- Categoría: Tendencia
- Dirección: Ambos
- Indicadores: MA, Hull MA, ATR
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Medio plazo
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>
/// Moving Average Shift WaveTrend strategy.
/// The oscillator is a Hull MA of the change of the distance between price and a configurable moving average.
/// A long opens when price is above the MA, the oscillator is positive and rising, price is above the long-term EMA,
/// ATR is above its average, the candle is inside the trading hours and no long wave is active yet. Shorts mirror it.
/// A position closes when the oscillator turns against it while price is on the other side of the MA, when a
/// percentage trailing stop is hit, or by the percentage stop loss and take profit.
/// </summary>
public class MovingAverageShiftWaveTrendStrategy : Strategy
{
/// <summary>
/// Moving average types.
/// </summary>
public enum MaTypes
{
/// <summary>Simple moving average.</summary>
SMA,
/// <summary>Exponential moving average.</summary>
EMA,
/// <summary>Smoothed moving average.</summary>
SMMA,
/// <summary>Weighted moving average.</summary>
WMA,
/// <summary>Hull moving average.</summary>
HMA,
}
private readonly StrategyParam<MaTypes> _maType;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _oscLength;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<decimal> _trailPercent;
private readonly StrategyParam<int> _longMaLength;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<DataType> _candleType;
private IIndicator _ma;
private ExponentialMovingAverage _longEma;
private AverageTrueRange _atr;
private HullMovingAverage _osc;
private SimpleMovingAverage _atrAverage;
private readonly Queue<decimal> _diffs = new();
private decimal? _prevOsc;
private bool _inLongWave;
private bool _inShortWave;
private decimal _trailExtreme;
/// <summary>
/// Moving average type.
/// </summary>
public MaTypes MaType
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// Moving average length.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Oscillator length.
/// </summary>
public int OscLength
{
get => _oscLength.Value;
set => _oscLength.Value = value;
}
/// <summary>
/// Take profit percentage.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Trailing stop percentage.
/// </summary>
public decimal TrailPercent
{
get => _trailPercent.Value;
set => _trailPercent.Value = value;
}
/// <summary>
/// Long-term trend EMA length.
/// </summary>
public int LongMaLength
{
get => _longMaLength.Value;
set => _longMaLength.Value = value;
}
/// <summary>
/// ATR length.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// First trading hour (UTC).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Hour when trading stops (UTC, exclusive).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public MovingAverageShiftWaveTrendStrategy()
{
_maType = Param(nameof(MaType), MaTypes.SMA)
.SetDisplay("MA Type", "Moving average type", "Indicators");
_maLength = Param(nameof(MaLength), 40)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Moving average length", "Indicators");
_oscLength = Param(nameof(OscLength), 15)
.SetGreaterThanZero()
.SetDisplay("Oscillator Length", "Change period and Hull MA length of the oscillator", "Indicators");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 1.5m)
.SetNotNegative()
.SetDisplay("Take Profit %", "Take profit percentage", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 1m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_trailPercent = Param(nameof(TrailPercent), 1m)
.SetNotNegative()
.SetDisplay("Trail %", "Trailing stop percentage", "Risk");
_longMaLength = Param(nameof(LongMaLength), 200)
.SetGreaterThanZero()
.SetDisplay("Long MA Length", "Long-term trend EMA length", "Filters");
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR length and its average length", "Filters");
_startHour = Param(nameof(StartHour), 9)
.SetRange(0, 23)
.SetDisplay("Start Hour", "First trading hour (UTC)", "Time");
_endHour = Param(nameof(EndHour), 17)
.SetRange(0, 24)
.SetDisplay("End Hour", "Hour when trading stops (UTC, exclusive)", "Time");
_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();
ResetState();
}
private void ResetState()
{
_diffs.Clear();
_prevOsc = null;
_inLongWave = false;
_inShortWave = false;
_trailExtreme = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
_ma = CreateMa(MaType, MaLength);
_longEma = new ExponentialMovingAverage { Length = LongMaLength };
_atr = new AverageTrueRange { Length = AtrLength };
_osc = new HullMovingAverage { Length = OscLength };
_atrAverage = new SimpleMovingAverage { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ma, _longEma, _atr, ProcessCandle)
.Start();
StartProtection(
new Unit(TakeProfitPercent, UnitTypes.Percent),
new Unit(StopLossPercent, UnitTypes.Percent),
useMarketOrders: true);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma);
DrawIndicator(area, _longEma);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
DrawIndicator(oscillators, _osc);
}
}
private static IIndicator CreateMa(MaTypes type, int length)
{
return type switch
{
MaTypes.EMA => new ExponentialMovingAverage { Length = length },
MaTypes.SMMA => new SmoothedMovingAverage { Length = length },
MaTypes.WMA => new WeightedMovingAverage { Length = length },
MaTypes.HMA => new HullMovingAverage { Length = length },
_ => new SimpleMovingAverage { Length = length },
};
}
private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal longEmaValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_ma.IsFormed || !_atr.IsFormed)
return;
var close = candle.ClosePrice;
var time = candle.OpenTime;
var atrAverageValue = _atrAverage.Process(new DecimalIndicatorValue(_atrAverage, atrValue, time) { IsFinal = true });
// The oscillator smooths how far the price-to-MA distance moved over OscLength candles.
var diff = close - maValue;
_diffs.Enqueue(diff);
if (_diffs.Count <= OscLength)
return;
var change = diff - _diffs.Dequeue();
var oscValue = _osc.Process(new DecimalIndicatorValue(_osc, change, time) { IsFinal = true });
if (!_osc.IsFormed || oscValue.IsEmpty)
return;
var osc = oscValue.ToDecimal();
var prevOsc = _prevOsc;
_prevOsc = osc;
if (prevOsc is not decimal prev)
return;
var rising = osc > prev;
var falling = osc < prev;
// A wave ends when the oscillator crosses back through zero.
if (osc <= 0)
_inLongWave = false;
if (osc >= 0)
_inShortWave = false;
if (!_atrAverage.IsFormed || atrAverageValue.IsEmpty || !_longEma.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
var atrAverage = atrAverageValue.ToDecimal();
if (Position > 0)
{
_trailExtreme = Math.Max(_trailExtreme, close);
var trailHit = TrailPercent > 0 && close <= _trailExtreme * (1 - TrailPercent / 100m);
if ((falling && close < maValue) || trailHit)
{
SellMarket(Position);
return;
}
}
else if (Position < 0)
{
_trailExtreme = _trailExtreme == 0 ? close : Math.Min(_trailExtreme, close);
var trailHit = TrailPercent > 0 && close >= _trailExtreme * (1 + TrailPercent / 100m);
if ((rising && close > maValue) || trailHit)
{
BuyMarket(-Position);
return;
}
}
var hour = candle.OpenTime.Hour;
if (hour < StartHour || hour >= EndHour)
return;
var isVolatile = atrValue > atrAverage;
if (close > maValue && osc > 0 && rising && close > longEmaValue && isVolatile && !_inLongWave && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_inLongWave = true;
_trailExtreme = close;
}
else if (close < maValue && osc < 0 && falling && close < longEmaValue && isVolatile && !_inShortWave && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_inShortWave = true;
_trailExtreme = close;
}
}
}
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
from StockSharp.Algo.Indicators import (SimpleMovingAverage, ExponentialMovingAverage, SmoothedMovingAverage,
WeightedMovingAverage, HullMovingAverage, AverageTrueRange,
DecimalIndicatorValue)
from StockSharp.Algo.Strategies import Strategy
class moving_average_shift_wave_trend_strategy(Strategy):
"""
Moving Average Shift WaveTrend strategy.
The oscillator is a Hull MA of the change of the distance between price and a configurable moving average.
A long opens when price is above the MA, the oscillator is positive and rising, price is above the long-term EMA,
ATR is above its average, the candle is inside the trading hours and no long wave is active yet. Shorts mirror it.
A position closes when the oscillator turns against it while price is on the other side of the MA, when a
percentage trailing stop is hit, or by the percentage stop loss and take profit.
"""
def __init__(self):
super(moving_average_shift_wave_trend_strategy, self).__init__()
self._ma_type = self.Param("MaType", "SMA").SetDisplay("MA Type", "Moving average type: SMA, EMA, SMMA, WMA or HMA", "Indicators")
self._ma_length = self.Param("MaLength", 40).SetGreaterThanZero().SetDisplay("MA Length", "Moving average length", "Indicators")
self._osc_length = self.Param("OscLength", 15).SetGreaterThanZero().SetDisplay("Oscillator Length", "Change period and Hull MA length of the oscillator", "Indicators")
self._take_profit_percent = self.Param("TakeProfitPercent", 1.5).SetNotNegative().SetDisplay("Take Profit %", "Take profit percentage", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 1.0).SetNotNegative().SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._trail_percent = self.Param("TrailPercent", 1.0).SetNotNegative().SetDisplay("Trail %", "Trailing stop percentage", "Risk")
self._long_ma_length = self.Param("LongMaLength", 200).SetGreaterThanZero().SetDisplay("Long MA Length", "Long-term trend EMA length", "Filters")
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR length and its average length", "Filters")
self._start_hour = self.Param("StartHour", 9).SetDisplay("Start Hour", "First trading hour (UTC)", "Time")
self._end_hour = self.Param("EndHour", 17).SetDisplay("End Hour", "Hour when trading stops (UTC, exclusive)", "Time")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._ma = None
self._long_ema = None
self._atr = None
self._osc = None
self._atr_average = None
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._diffs = []
self._prev_osc = None
self._in_long_wave = False
self._in_short_wave = False
self._trail_extreme = 0.0
def OnReseted(self):
super(moving_average_shift_wave_trend_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(moving_average_shift_wave_trend_strategy, self).OnStarted2(time)
self._reset_state()
self._ma = self._create_ma(str(self._ma_type.Value).upper(), self._ma_length.Value)
self._long_ema = ExponentialMovingAverage()
self._long_ema.Length = self._long_ma_length.Value
self._atr = AverageTrueRange()
self._atr.Length = self._atr_length.Value
self._osc = HullMovingAverage()
self._osc.Length = self._osc_length.Value
self._atr_average = SimpleMovingAverage()
self._atr_average.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._ma, self._long_ema, self._atr, self._process_candle).Start()
self.StartProtection(
Unit(Decimal(self._take_profit_percent.Value), UnitTypes.Percent),
Unit(Decimal(self._stop_loss_percent.Value), UnitTypes.Percent),
useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ma)
self.DrawIndicator(area, self._long_ema)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, self._osc)
@staticmethod
def _create_ma(ma_type, length):
if ma_type == "EMA":
ma = ExponentialMovingAverage()
elif ma_type == "SMMA":
ma = SmoothedMovingAverage()
elif ma_type == "WMA":
ma = WeightedMovingAverage()
elif ma_type == "HMA":
ma = HullMovingAverage()
else:
ma = SimpleMovingAverage()
ma.Length = length
return ma
@staticmethod
def _feed(indicator, value, time):
indicator_input = DecimalIndicatorValue(indicator, Decimal(value), time)
indicator_input.IsFinal = True
result = indicator.Process(indicator_input)
if result.IsEmpty:
return None
return float(result.GetValue[Decimal](None))
def _process_candle(self, candle, ma_value, long_ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not self._ma.IsFormed or not self._atr.IsFormed:
return
close = float(candle.ClosePrice)
ma = float(ma_value)
long_ema = float(long_ema_value)
atr = float(atr_value)
time = candle.OpenTime
atr_average = self._feed(self._atr_average, atr, time)
# The oscillator smooths how far the price-to-MA distance moved over OscLength candles.
diff = close - ma
self._diffs.append(diff)
if len(self._diffs) <= self._osc_length.Value:
return
change = diff - self._diffs.pop(0)
osc = self._feed(self._osc, change, time)
if not self._osc.IsFormed or osc is None:
return
prev_osc = self._prev_osc
self._prev_osc = osc
if prev_osc is None:
return
rising = osc > prev_osc
falling = osc < prev_osc
# A wave ends when the oscillator crosses back through zero.
if osc <= 0:
self._in_long_wave = False
if osc >= 0:
self._in_short_wave = False
if not self._atr_average.IsFormed or atr_average is None or not self._long_ema.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
trail = float(self._trail_percent.Value)
if self.Position > 0:
self._trail_extreme = max(self._trail_extreme, close)
trail_hit = trail > 0 and close <= self._trail_extreme * (1.0 - trail / 100.0)
if (falling and close < ma) or trail_hit:
self.SellMarket(self.Position)
return
elif self.Position < 0:
self._trail_extreme = close if self._trail_extreme == 0 else min(self._trail_extreme, close)
trail_hit = trail > 0 and close >= self._trail_extreme * (1.0 + trail / 100.0)
if (rising and close > ma) or trail_hit:
self.BuyMarket(-self.Position)
return
hour = candle.OpenTime.Hour
if hour < self._start_hour.Value or hour >= self._end_hour.Value:
return
is_volatile = atr > atr_average
if close > ma and osc > 0 and rising and close > long_ema and is_volatile and not self._in_long_wave and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._in_long_wave = True
self._trail_extreme = close
elif close < ma and osc < 0 and falling and close < long_ema and is_volatile and not self._in_short_wave and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._in_short_wave = True
self._trail_extreme = close
def CreateClone(self):
return moving_average_shift_wave_trend_strategy()