Ver no GitHub

Estratégia Momentum M15

Esta estratégia é uma portagem direta do expert advisor Momentum-M15 do MetaTrader 5 (arquivo original Momentum-M15.mq5). Opera com velas de 15 minutos e combina um filtro de média móvel deslocada com um oscilador de momentum avaliado nas aberturas de barras. A lógica visa operar contra momentum extremo quando o preço está no lado oposto da média deslocada, enquanto um guardião de lacunas e trailing stop opcional limitam a exposição.

Destaques da conversão

  • Os indicadores são recriados com componentes StockSharp: uma média móvel configurável (padrão suavizada) e o oscilador Momentum integrado que trabalha com o preço de vela escolhido (padrão Open).
  • O deslocamento horizontal da MA do MetaTrader é emulado armazenando em buffer os valores do indicador e recuperando o valor MaShift barras terminadas atrás. Nenhuma matemática de indicador personalizada é reimplementada.
  • As verificações de monotonicidade do Momentum reutilizam os últimos valores do histórico e mantêm apenas os elementos necessários para as janelas de entrada ou saída, espelhando os auxiliares originais CheckMO_Up / CheckMO_Down.
  • O bloqueio por lacuna grande (GapLevel/GapTimeout) é preservado. As informações de passo de preço são usadas para converter os limiares baseados em pontos definidos na versão MQL em passos de preço do StockSharp.
  • O gerenciamento do trailing stop é tratado internamente por meio de saídas de mercado quando o preço cruza o nível rastreado, correspondendo à rotina MQL que modificava as ordens de stop loss uma vez por barra concluída.

Parâmetros

Nome Descrição Padrão
TradeVolume Tamanho da ordem usado para cada entrada. 0.1
CandleType Período principal (velas de 15 minutos por padrão). 15m
MaPeriod Comprimento de lookback da média móvel. 26
MaShift Número de barras para deslocar horizontalmente a média móvil. 8
MaMethod Tipo de média móvel (Simple, Exponential, Smoothed, Weighted). Smoothed
MaPrice Preço de vela alimentado à média móvel. Low
MomentumPeriod Comprimento de lookback do Momentum. 23
MomentumPrice Preço de vela usado para o oscilador Momentum. Open
MomentumThreshold Nível de momentum base que separa configurações comprado/vendido. 100
MomentumShift Valor adicionado/subtraído de MomentumThreshold para construir limites assimétricos. -0.2
MomentumOpenLength Barras necessárias para uma sequência de momentum não crescente antes de abrir comprados / não decrescente para vendidos. 6
MomentumCloseLength Barras necessárias para a mesma sequência monótona antes de fechar posições. 10
GapLevel Lacuna positiva mínima (em passos de preço) que pausa novas entradas. 30
GapTimeout Número de barras para manter o trading desabilitado após uma lacuna grande. 100
TrailingStop Distância opcional do trailing stop medida em passos de preço. 0 (desabilitado)

Regras de trading

Critérios de entrada

  • Entradas compradas

    • O último Momentum está abaixo de MomentumThreshold + MomentumShift (para o deslocamento padrão de -0.2, isso está ligeiramente abaixo do limiar principal).
    • Tanto o fechamento da barra anterior quanto a abertura da barra atual estão abaixo da média móvel deslocada.
    • O Momentum foi não crescente por MomentumOpenLength barras (correspondendo a CheckMO_Down no código-fonte MQL).
  • Entradas vendidas

    • O último Momentum está acima de MomentumThreshold - MomentumShift (com o deslocamento padrão isso está ligeiramente acima de 100).
    • Tanto o fechamento da barra anterior quanto a abertura da barra atual estão acima da média móvel deslocada.
    • O Momentum foi não decrescente por MomentumOpenLength barras (correspondendo a CheckMO_Up).

As entradas são avaliadas apenas quando nenhuma posição está aberta e o trading não está suspenso pelo filtro de lacunas.

Critérios de saída

  • As posições compradas fecham quando qualquer uma das seguintes condições é verdadeira:

    • O Momentum foi não crescente por MomentumCloseLength barras.
    • O fechamento da barra anterior cai abaixo da média móvel deslocada.
    • O trailing stop (se habilitado) é tocado. O stop segue o mínimo da vela menos a distância configurada expressa em passos de preço.
  • As posições vendidas fecham quando qualquer uma das seguintes condições é verdadeira:

    • O Momentum foi não decrescente por MomentumCloseLength barras.
    • O fechamento da barra anterior sobe acima da média móvel deslocada.
    • O trailing stop (se habilitado) é tocado. O stop segue a máxima da vela mais a distância configurada.

Lógica de suspensão por lacuna

O expert advisor original pausava o trading após lacunas ascendentes fortes. A versão StockSharp mede a diferença entre a abertura da barra atual e o fechamento anterior em passos de preço:

  1. Quando a lacuna excede GapLevel, o temporizador de bloqueio é reiniciado para GapTimeout.
  2. O temporizador é decrementado a cada barra fechada; o trading é retomado somente após chegar a zero.

Notas e suposições

  • Todos os cálculos usam velas terminadas (CandleStates.Finished) para permanecer alinhados com as práticas da API de alto nível do StockSharp. Como resultado, as ordens são emitidas na próxima barra após as condições serem observadas, o que é consistente com como a estratégia original era acionada no primeiro tick de uma nova barra.
  • O conceito de "pips" do MetaTrader é aproximado via Security.PriceStep. Se o instrumento carecer de dados de passo adequados, o filtro de lacunas e o trailing stop serão silenciosamente desabilitados.
  • Os preços da média móvel e as entradas do Momentum podem ser alterados independentemente, replicando a flexibilidade dos parâmetros de entrada originais.
  • Nenhuma ordem de stop automatizada é registrada; em vez disso, as saídas de mercado reproduzem os ajustes de stop que o código MQL emitia por meio de PositionModify.

Dicas de uso

  1. Atribua o instrumento desejado e garanta que CandleType corresponda ao período histórico usado durante os backtests (barras de 15 minutos no script original).
  2. Configure TradeVolume para o tamanho de lote suportado pelo local de trading.
  3. Ajuste MomentumOpenLength / MomentumCloseLength para controlar quão estrito deve ser o filtro de monotonicidade do Momentum.
  4. Se preferir espelhar exatamente a escala de "pip" padrão, defina TrailingStop e GapLevel de acordo com a proporção entre o passo de preço da exchange e um pip para o instrumento.
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>
/// Momentum based strategy converted from the MetaTrader 5 "Momentum-M15" expert advisor.
/// </summary>
public class MomentumM15Strategy : Strategy
{
	/// <summary>
	/// Moving average method options aligned with the original expert advisor inputs.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

		/// <summary>
		/// Smoothed moving average.
		/// </summary>
		Smoothed,

		/// <summary>
		/// Weighted moving average.
		/// </summary>
		Weighted
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<decimal> _volumeParam;
	private readonly StrategyParam<DataType> _candleTypeParam;
	private readonly StrategyParam<int> _maPeriodParam;
	private readonly StrategyParam<int> _maShiftParam;
	private readonly StrategyParam<MovingAverageMethods> _maMethodParam;
	private readonly StrategyParam<CandlePrices> _maPriceParam;
	private readonly StrategyParam<int> _momentumPeriodParam;
	private readonly StrategyParam<CandlePrices> _momentumPriceParam;
	private readonly StrategyParam<decimal> _momentumThresholdParam;
	private readonly StrategyParam<decimal> _momentumShiftParam;
	private readonly StrategyParam<int> _momentumOpenLengthParam;
	private readonly StrategyParam<int> _momentumCloseLengthParam;
	private readonly StrategyParam<int> _gapLevelParam;
	private readonly StrategyParam<int> _gapTimeoutParam;
	private readonly StrategyParam<decimal> _trailingStopParam;

	private IIndicator _ma = null!;
	private Momentum _momentum = null!;
	private readonly List<decimal> _maHistory = new();
	private readonly List<decimal> _momentumHistory = new();
	private decimal? _previousClose;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private int _gapTimer;

	/// <summary>
	/// Initializes a new instance of <see cref="MomentumM15Strategy"/>.
	/// </summary>
	public MomentumM15Strategy()
	{
		_volumeParam = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Default order volume", "Trading")
			
			.SetOptimize(0.05m, 0.5m, 0.05m);

		_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Common");

		_maPeriodParam = Param(nameof(MaPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average lookback length", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_maShiftParam = Param(nameof(MaShift), 8)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Horizontal shift applied to moving average", "Indicators");

		_maMethodParam = Param(nameof(MaMethod), MovingAverageMethods.Smoothed)
			.SetDisplay("MA Method", "Type of moving average", "Indicators");

		_maPriceParam = Param(nameof(MaPrice), CandlePrices.Low)
			.SetDisplay("MA Price", "Price source for moving average", "Indicators");

		_momentumPeriodParam = Param(nameof(MomentumPeriod), 23)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Momentum indicator lookback", "Indicators")
			
			.SetOptimize(10, 40, 1);

		_momentumPriceParam = Param(nameof(MomentumPrice), CandlePrices.Open)
			.SetDisplay("Momentum Price", "Price source for momentum", "Indicators");

		_momentumThresholdParam = Param(nameof(MomentumThreshold), 100m)
			.SetDisplay("Momentum Threshold", "Baseline momentum threshold", "Trading Rules");

		_momentumShiftParam = Param(nameof(MomentumShift), -0.2m)
			.SetDisplay("Momentum Shift", "Shift applied to momentum threshold", "Trading Rules");

		_momentumOpenLengthParam = Param(nameof(MomentumOpenLength), 6)
			.SetNotNegative()
			.SetDisplay("Momentum Open Length", "Bars required for monotonic momentum on entries", "Trading Rules");

		_momentumCloseLengthParam = Param(nameof(MomentumCloseLength), 10)
			.SetNotNegative()
			.SetDisplay("Momentum Close Length", "Bars required for monotonic momentum on exits", "Trading Rules");

		_gapLevelParam = Param(nameof(GapLevel), 30)
			.SetNotNegative()
			.SetDisplay("Gap Level", "Minimum gap in price steps to pause trading", "Risk Management");

		_gapTimeoutParam = Param(nameof(GapTimeout), 100)
			.SetNotNegative()
			.SetDisplay("Gap Timeout", "Number of bars to skip after a large gap", "Risk Management");

		_trailingStopParam = Param(nameof(TrailingStop), 0m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing stop distance in price steps", "Risk Management");
	}

	/// <summary>
	/// Default trade volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _volumeParam.Value;
		set => _volumeParam.Value = value;
	}

	/// <summary>
	/// Candle type for the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleTypeParam.Value;
		set => _candleTypeParam.Value = value;
	}

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriodParam.Value;
		set => _maPeriodParam.Value = value;
	}

	/// <summary>
	/// Number of bars to shift the moving average.
	/// </summary>
	public int MaShift
	{
		get => _maShiftParam.Value;
		set => _maShiftParam.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MovingAverageMethods MaMethod
	{
		get => _maMethodParam.Value;
		set => _maMethodParam.Value = value;
	}

	/// <summary>
	/// Price source for the moving average.
	/// </summary>
	public CandlePrices MaPrice
	{
		get => _maPriceParam.Value;
		set => _maPriceParam.Value = value;
	}

	/// <summary>
	/// Momentum indicator lookback period.
	/// </summary>
	public int MomentumPeriod
	{
		get => _momentumPeriodParam.Value;
		set => _momentumPeriodParam.Value = value;
	}

	/// <summary>
	/// Price source for the momentum indicator.
	/// </summary>
	public CandlePrices MomentumPrice
	{
		get => _momentumPriceParam.Value;
		set => _momentumPriceParam.Value = value;
	}

	/// <summary>
	/// Baseline momentum threshold.
	/// </summary>
	public decimal MomentumThreshold
	{
		get => _momentumThresholdParam.Value;
		set => _momentumThresholdParam.Value = value;
	}

	/// <summary>
	/// Shift applied to the momentum threshold.
	/// </summary>
	public decimal MomentumShift
	{
		get => _momentumShiftParam.Value;
		set => _momentumShiftParam.Value = value;
	}

	/// <summary>
	/// Sequence length for entry momentum validation.
	/// </summary>
	public int MomentumOpenLength
	{
		get => _momentumOpenLengthParam.Value;
		set => _momentumOpenLengthParam.Value = value;
	}

	/// <summary>
	/// Sequence length for exit momentum validation.
	/// </summary>
	public int MomentumCloseLength
	{
		get => _momentumCloseLengthParam.Value;
		set => _momentumCloseLengthParam.Value = value;
	}

	/// <summary>
	/// Minimum gap (in price steps) that suspends new entries.
	/// </summary>
	public int GapLevel
	{
		get => _gapLevelParam.Value;
		set => _gapLevelParam.Value = value;
	}

	/// <summary>
	/// Number of bars to wait after a gap before trading resumes.
	/// </summary>
	public int GapTimeout
	{
		get => _gapTimeoutParam.Value;
		set => _gapTimeoutParam.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStopParam.Value;
		set => _trailingStopParam.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ma = null!;
		_momentum = null!;
		_maHistory.Clear();
		_momentumHistory.Clear();
		_previousClose = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_gapTimer = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_momentum = new Momentum { Length = MomentumPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (_ma is null || _momentum is null)
		return;

		var maValue = ProcessMovingAverage(candle);
		var momentumValue = ProcessMomentum(candle);

		if (maValue is null || momentumValue is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var previousClose = _previousClose;
		_previousClose = candle.ClosePrice;

		if (previousClose is null)
		return;

		HandleGapFilter(previousClose.Value, candle.OpenPrice);

		if (_gapTimer > 0)
		{
			_gapTimer--;
			if (_gapTimer > 0)
			return;
		}

		if (Position == 0)
		{
			TryOpenPositions(previousClose.Value, candle.OpenPrice, maValue.Value, momentumValue.Value);
		}
		else
		{
			ManageExistingPosition(previousClose.Value, candle, maValue.Value, momentumValue.Value);
		}
	}

	private decimal? ProcessMovingAverage(ICandleMessage candle)
	{
		var price = GetPrice(candle, MaPrice);
		var value = _ma.Process(new DecimalIndicatorValue(_ma, price, candle.OpenTime) { IsFinal = true });

		if (value.IsEmpty || !_ma.IsFormed)
		return null;

		var ma = value.ToDecimal();
		_maHistory.Add(ma);

		var maxCount = MaShift + 1;
		while (_maHistory.Count > maxCount)
		_maHistory.RemoveAt(0);

		var index = _maHistory.Count - 1 - MaShift;
		if (index < 0 || index >= _maHistory.Count)
		return null;

		return _maHistory[index];
	}

	private decimal? ProcessMomentum(ICandleMessage candle)
	{
		var price = GetPrice(candle, MomentumPrice);
		var value = _momentum.Process(new DecimalIndicatorValue(_momentum, price, candle.OpenTime) { IsFinal = true });

		if (value.IsEmpty || !_momentum.IsFormed)
		return null;

		var momentum = value.ToDecimal();
		_momentumHistory.Add(momentum);

		var maxLen = Math.Max(Math.Max(MomentumOpenLength, MomentumCloseLength), 1);
		while (_momentumHistory.Count > maxLen)
		_momentumHistory.RemoveAt(0);

		return momentum;
	}

	private void HandleGapFilter(decimal previousClose, decimal currentOpen)
	{
		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var gap = (currentOpen - previousClose) / priceStep;
		if (gap > GapLevel)
		_gapTimer = GapTimeout;
	}

	private void TryOpenPositions(decimal previousClose, decimal currentOpen, decimal maValue, decimal momentumValue)
	{
		var longMomentumOk = MomentumOpenLength > 0 && IsMomentumDownSequence(MomentumOpenLength);
		var shortMomentumOk = MomentumOpenLength > 0 && IsMomentumUpSequence(MomentumOpenLength);

		var longCondition = momentumValue < MomentumThreshold + MomentumShift
		&& previousClose < maValue
		&& currentOpen < maValue
		&& longMomentumOk;

		var shortCondition = momentumValue > MomentumThreshold - MomentumShift
		&& previousClose > maValue
		&& currentOpen > maValue
		&& shortMomentumOk;

		if (longCondition)
		{
			BuyMarket(TradeVolume);
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}
		else if (shortCondition)
		{
			SellMarket(TradeVolume);
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}
	}

	private void ManageExistingPosition(decimal previousClose, ICandleMessage candle, decimal maValue, decimal momentumValue)
	{
		if (Position > 0)
		{
			var exitMomentum = MomentumCloseLength > 0 && IsMomentumDownSequence(MomentumCloseLength);
			var shouldClose = exitMomentum || previousClose < maValue;

			if (shouldClose)
			{
				SellMarket(Position);
				_longTrailingStop = null;
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			var exitMomentum = MomentumCloseLength > 0 && IsMomentumUpSequence(MomentumCloseLength);
			var shouldClose = exitMomentum || previousClose > maValue;

			if (shouldClose)
			{
				BuyMarket(Math.Abs(Position));
				_shortTrailingStop = null;
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (TrailingStop <= 0m)
		return;

		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var distance = TrailingStop * priceStep;
		var candidate = candle.LowPrice - distance;

		if (_longTrailingStop is null || candidate > _longTrailingStop)
		_longTrailingStop = candidate;

		if (_longTrailingStop is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket(Position);
			_longTrailingStop = null;
		}
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (TrailingStop <= 0m)
		return;

		var priceStep = Security.PriceStep ?? 0m;
		if (priceStep <= 0m)
		return;

		var distance = TrailingStop * priceStep;
		var candidate = candle.HighPrice + distance;

		if (_shortTrailingStop is null || candidate < _shortTrailingStop)
		_shortTrailingStop = candidate;

		if (_shortTrailingStop is decimal stop && candle.HighPrice >= stop)
		{
			BuyMarket(Math.Abs(Position));
			_shortTrailingStop = null;
		}
	}

	private bool IsMomentumDownSequence(int length)
	{
		if (length <= 0 || _momentumHistory.Count < length)
		return false;

		var start = _momentumHistory.Count - length;
		var previous = _momentumHistory[start];

		for (var i = start + 1; i < _momentumHistory.Count; i++)
		{
			var current = _momentumHistory[i];
			if (current > previous)
			return false;

			previous = current;
		}

		return true;
	}

	private bool IsMomentumUpSequence(int length)
	{
		if (length <= 0 || _momentumHistory.Count < length)
		return false;

		var start = _momentumHistory.Count - length;
		var previous = _momentumHistory[start];

		for (var i = start + 1; i < _momentumHistory.Count; i++)
		{
			var current = _momentumHistory[i];
			if (current < previous)
			return false;

			previous = current;
		}

		return true;
	}

	private static decimal GetPrice(ICandleMessage candle, CandlePrices price)
	{
		return price switch
		{
			CandlePrices.Open => candle.OpenPrice,
			CandlePrices.High => candle.HighPrice,
			CandlePrices.Low => candle.LowPrice,
			CandlePrices.Close => candle.ClosePrice,
			CandlePrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			CandlePrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			CandlePrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int period)
	{
		return method switch
		{
			MovingAverageMethods.Simple => new SimpleMovingAverage { Length = period },
			MovingAverageMethods.Exponential => new ExponentialMovingAverage { Length = period },
			MovingAverageMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MovingAverageMethods.Weighted => new WeightedMovingAverage { Length = period },
			_ => new SimpleMovingAverage { Length = period },
		};
	}
}