Estratégia de Média Móvel Shift WaveTrend
Esta estratégia combina uma média móvel configurável com um oscilador estilo WaveTrend. Operações compradas ocorrem quando o preço está acima da média móvel e o oscilador sobe, confirmando uma tendência de alta com uma EMA de longo prazo e filtro de volatilidade. Posições vendidas são acionadas nas condições opostas. As posições são protegidas por stop loss, take profit e trailing stop percentuais.
Detalhes
- Critérios de entrada:
- Comprado: preço acima da MA, oscilador > 0 e subindo, tendência de longo prazo para cima, ATR acima de sua média, dentro do horário de negociação, não já em onda.
- Vendido: preço abaixo da MA, oscilador < 0 e caindo, tendência de longo prazo para baixo, ATR acima de sua média, dentro do horário de negociação, não já em onda.
- Comprado/Vendido: Ambos.
- Critérios de saída:
- Reversão do oscilador com cruzamento de preço e MA, ou trailing stop, ou proteções.
- Stops: Sim.
- Valores padrão:
MaType= SMAMaLength= 40OscLength= 15TakeProfitPercent= 1.5StopLossPercent= 1TrailPercent= 1LongMaLength= 200AtrLength= 14StartHour= 9EndHour= 17
- Filtros:
- Categoria: Tendência
- Direção: Ambos
- Indicadores: MA, Hull MA, ATR
- Stops: Sim
- Complexidade: Intermediário
- Período: Médio prazo
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()