Ver en GitHub

Estrategia Momentum M15

Esta estrategia es una traducción directa del asesor experto Momentum-M15 de MetaTrader 5 (archivo original Momentum-M15.mq5). Opera con velas de 15 minutos y combina un filtro de media móvil desplazada con un oscilador momentum evaluado en las aperturas de barras. La lógica apunta a operar en contra de momentum extremo cuando el precio se encuentra en el lado opuesto de la media desplazada, mientras que un guardián de brechas y un trailing stop opcional limitan la exposición.

Aspectos destacados de la conversión

  • Los indicadores se recrean con componentes StockSharp: una media móvil configurable (por defecto suavizada) y el oscilador Momentum integrado que trabaja con el precio de vela elegido (por defecto Open).
  • El desplazamiento horizontal de la MA de MetaTrader se emula almacenando en búfer los valores del indicador y recuperando el valor MaShift barras terminadas hacia atrás. No se reimplementa matemática de indicador personalizada.
  • Las verificaciones de monotonicidad del Momentum reutilizan los últimos valores del historial y mantienen solo los elementos necesarios para las ventanas de entrada o salida, reflejando los ayudantes originales CheckMO_Up / CheckMO_Down.
  • El bloqueo por brecha grande (GapLevel/GapTimeout) se preserva. La información del paso de precio se usa para convertir los umbrales basados en puntos definidos en la versión MQL en pasos de precio de StockSharp.
  • La gestión del trailing stop se maneja internamente mediante salidas de mercado cuando el precio cruza el nivel rastreado, emulando la rutina MQL que modificaba las órdenes de stop loss una vez por barra completada.

Parámetros

Nombre Descripción Valor predeterminado
TradeVolume Tamaño de orden usado para cada entrada. 0.1
CandleType Marco temporal principal (velas de 15 minutos por defecto). 15m
MaPeriod Longitud de búsqueda de la media móvil. 26
MaShift Número de barras para desplazar horizontalmente la media móvil. 8
MaMethod Tipo de media móvil (Simple, Exponential, Smoothed, Weighted). Smoothed
MaPrice Precio de vela alimentado a la media móvil. Low
MomentumPeriod Longitud de búsqueda del Momentum. 23
MomentumPrice Precio de vela usado para el oscilador Momentum. Open
MomentumThreshold Nivel de Momentum base que separa configuraciones largas/cortas. 100
MomentumShift Valor añadido/restado a MomentumThreshold para construir límites asimétricos. -0.2
MomentumOpenLength Barras requeridas para una secuencia de Momentum no creciente antes de abrir largos / no decreciente para cortos. 6
MomentumCloseLength Barras requeridas para la misma secuencia monótona antes de cerrar posiciones. 10
GapLevel Brecha positiva mínima (en pasos de precio) que pausa nuevas entradas. 30
GapTimeout Número de barras para mantener el trading deshabilitado después de una brecha grande. 100
TrailingStop Distancia opcional del trailing stop medida en pasos de precio. 0 (deshabilitado)

Reglas de trading

Criterios de entrada

  • Entradas largas

    • El último Momentum está por debajo de MomentumThreshold + MomentumShift (para el desplazamiento por defecto de -0.2, esto está ligeramente por debajo del umbral principal).
    • Tanto el cierre de la barra anterior como la apertura de la barra actual están por debajo de la media móvil desplazada.
    • El Momentum ha sido no creciente durante MomentumOpenLength barras (coincidiendo con CheckMO_Down en la fuente MQL).
  • Entradas cortas

    • El último Momentum está por encima de MomentumThreshold - MomentumShift (con el desplazamiento por defecto esto es ligeramente por encima de 100).
    • Tanto el cierre de la barra anterior como la apertura de la barra actual están por encima de la media móvil desplazada.
    • El Momentum ha sido no decreciente durante MomentumOpenLength barras (coincidiendo con CheckMO_Up).

Las entradas solo se evalúan cuando no hay posición abierta y el trading no está suspendido por el filtro de brechas.

Criterios de salida

  • Las posiciones largas se cierran cuando se cumple alguna de las siguientes condiciones:

    • El Momentum ha sido no creciente durante MomentumCloseLength barras.
    • El cierre de la barra anterior cae por debajo de la media móvil desplazada.
    • El trailing stop (si está habilitado) es tocado. El stop sigue el mínimo de la vela menos la distancia configurada expresada en pasos de precio.
  • Las posiciones cortas se cierran cuando se cumple alguna de las siguientes condiciones:

    • El Momentum ha sido no decreciente durante MomentumCloseLength barras.
    • El cierre de la barra anterior sube por encima de la media móvil desplazada.
    • El trailing stop (si está habilitado) es tocado. El stop sigue el máximo de la vela más la distancia configurada.

Lógica de suspensión por brecha

El asesor experto original pausaba el trading después de brechas alcistas fuertes. La versión StockSharp mide la diferencia entre la apertura de la barra actual y el cierre anterior en pasos de precio:

  1. Cuando la brecha excede GapLevel, el temporizador de bloqueo se reinicia a GapTimeout.
  2. El temporizador se decrementa en cada barra cerrada; el trading se reanuda solo después de que llega a cero.

Notas y suposiciones

  • Todos los cálculos usan velas terminadas (CandleStates.Finished) para mantenerse alineados con las prácticas de la API de alto nivel de StockSharp. Como resultado, las órdenes se emiten en la siguiente barra después de observar las condiciones, lo que es consistente con cómo la estrategia original se activaba en el primer tick de una nueva barra.
  • El concepto de "pips" de MetaTrader se aproxima mediante Security.PriceStep. Si el instrumento carece de datos de paso adecuados, el filtro de brechas y el trailing stop se deshabilitarán silenciosamente.
  • Los precios de la media móvil y las entradas del Momentum pueden cambiarse de forma independiente, replicando la flexibilidad de los parámetros de entrada originales.
  • No se registran órdenes de stop automatizadas; en su lugar, las salidas de mercado reproducen los ajustes de stop que el código MQL emitía mediante PositionModify.

Consejos de uso

  1. Asigne el instrumento deseado y asegúrese de que CandleType coincida con el marco temporal histórico usado durante las pruebas retrospectivas (barras de 15 minutos en el script original).
  2. Configure TradeVolume para el tamaño de lote admitido por el lugar de trading.
  3. Ajuste MomentumOpenLength / MomentumCloseLength para controlar qué tan estricto debe ser el filtro de monotonicidad del Momentum.
  4. Si prefiere reflejar exactamente la escala de "pip" por defecto, configure TrailingStop y GapLevel según la relación entre el paso de precio del exchange y un pip para el 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 },
		};
	}
}