Ver en GitHub

Estrategia Autotrader Momentum

Descripción general

La Estrategia Autotrader Momentum es una conversión del asesor experto de MetaTrader 5 Autotrader Momentum (edición de barabashkakvn). El algoritmo evalúa el momentum reciente comparando el precio de cierre de la barra de monitoreo con el precio de cierre de una barra de referencia histórica. Cuando se detecta un cambio de momentum alcista, la estrategia compra; cuando aparece un cambio bajista, vende. Todas las órdenes se ejecutan al precio de mercado usando la API de trading de alto nivel de StockSharp.

La implementación mantiene el enfoque original en el control de riesgo basado en puntos. Las distancias de stop-loss, take-profit y trailing-stop se definen en pips y se traducen automáticamente en desplazamientos de precio basados en el PriceStep del instrumento. Se conserva el soporte para cotizaciones de tres y cinco decimales aplicando el mismo ajuste de 10x usado en el código MQL. La lógica de trailing se evalúa en cada vela finalizada antes de considerar nuevas entradas, asegurando que la gestión de riesgo refleje el comportamiento del EA de priorizar salidas protectoras.

Lógica de trading

  1. Suscribirse al CandleType configurado y procesar solo las velas finalizadas, replicando la lógica de "nueva barra" del EA original.
  2. Mantener una ventana deslizante de precios de cierre de tamaño max(CurrentBarIndex, ComparableBarIndex) + 1.
  3. Comparar el cierre de la barra monitoreada (CurrentBarIndex, predeterminado 0) con el cierre de la barra histórica (ComparableBarIndex, predeterminado 15).
  4. Si el cierre monitoreado es mayor que el cierre de referencia, cerrar cualquier exposición corta y comprar el volumen de trading configurado.
  5. Si el cierre monitoreado es menor que el cierre de referencia, cerrar cualquier exposición larga y vender el volumen de trading configurado.
  6. Cada entrada recalcula el precio promedio de entrada y actualiza los niveles de stop-loss, take-profit y trailing-stop.

Dado que las estrategias de StockSharp trabajan con una posición neta, las reversiones combinan el volumen necesario para cerrar la exposición opuesta con el volumen base configurado. Esto coincide con el comportamiento MQL que primero cerraba el lado opuesto y luego abría una nueva orden del tamaño solicitado.

Parámetros

  • CandleType – Marco temporal usado para la comparación de precios. Predeterminado: 1 hora.
  • TradeVolume – Volumen base de la orden de mercado. Se aplica en cada señal además de cualquier volumen necesario para revertir una posición existente.
  • StopLossPips – Distancia de stop protector en pips. Establecer en 0 para deshabilitar el stop-loss fijo.
  • TakeProfitPips – Distancia del objetivo de beneficio en pips. Establecer en 0 para deshabilitar el take-profit fijo.
  • TrailingStopPips – Distancia mantenida por el trailing stop. Establecer en 0 para deshabilitar el trailing.
  • TrailingStepPips – Movimiento favorable mínimo requerido antes de avanzar el trailing stop. Debe ser positivo cuando el trailing está habilitado.
  • CurrentBarIndex – Índice de la vela de monitoreo (0 = barra finalizada más reciente).
  • ComparableBarIndex – Índice de la barra histórica usada para comparación de momentum.

Todos los ajustes basados en pips se convierten en desplazamientos de precio usando el PriceStep del instrumento. Si el step representa tres o cinco dígitos decimales, el desplazamiento se multiplica por 10 para reproducir la definición de pip de MetaTrader.

Gestión de riesgo

  • Stops y objetivos fijos: Cuando StopLossPips o TakeProfitPips son mayores que cero, la estrategia mantiene los niveles de precio correspondientes relativos al precio de entrada promediado.
  • Trailing Stop: Habilitado cuando tanto TrailingStopPips como TrailingStepPips son positivos. La lógica de trailing mueve el stop protector solo después de que el precio se haya movido al menos TrailingStopPips + TrailingStepPips desde el precio de entrada promediado, replicando el requisito del EA que aseguraba que el movimiento fuera suficientemente grande antes de ajustar el stop.
  • Reinicio de estado: Cada vez que la posición vuelve a cero—ya sea mediante salidas impulsadas por la estrategia o intervención externa—el estado de riesgo almacenado en caché se borra para evitar niveles obsoletos de stop o take-profit.

Notas de implementación

  • La estrategia se basa exclusivamente en la API de mercado de alto nivel de StockSharp (BuyMarket, SellMarket) y evita colecciones de indicadores para mantenerse fiel a las pautas de conversión.
  • Los precios de cierre se almacenan en una lista deslizante simple para que CurrentBarIndex y ComparableBarIndex puedan cambiarse en tiempo de ejecución sin necesidad de reinicio.
  • Dado que StockSharp opera sobre una posición neta, los niveles de stop-loss y take-profit se rastrean para la exposición agregada. Cuando se agregan órdenes adicionales en la misma dirección, el código recalcula un precio de entrada promedio ponderado por volumen antes de actualizar los niveles de riesgo.
  • Los ajustes de trailing-stop y las salidas protectoras se procesan antes de las nuevas señales en cada vela, evitando que se evalúen nuevas entradas cuando ya se ha emitido una salida para esa barra.

Referencia de la estrategia original

  • Fuente: MQL/22409/Autotrader Momentum.mq5
  • Autor: barabashkakvn (comunidad MetaTrader)
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 strategy converted from the MetaTrader 5 expert advisor "Autotrader Momentum".
/// Compares the most recent closing price with a historical reference bar and reverses positions when momentum shifts.
/// Includes configurable fixed stops, take profit targets, and an optional trailing stop engine measured in pips.
/// </summary>
public class AutotraderMomentumStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<int> _currentBarIndex;
	private readonly StrategyParam<int> _comparableBarIndex;

	private readonly List<decimal> _closeHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private bool _isLongPosition;

	private decimal _pipValue;
	private decimal _stopLossOffset;
	private decimal _takeProfitOffset;
	private decimal _trailingStopOffset;
	private decimal _trailingStepOffset;
	private int _cooldownLeft;

	/// <summary>
	/// Initializes a new instance of the <see cref="AutotraderMomentumStrategy"/> class.
	/// </summary>
	public AutotraderMomentumStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for price comparisons", "Data");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetDisplay("Trade Volume", "Base order volume used for market entries", "Trading")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk")
			.SetNotNegative();

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetDisplay("Take Profit (pips)", "Profit target distance expressed in pips", "Risk")
			.SetNotNegative();

		_trailingStopPips = Param(nameof(TrailingStopPips), 0)
			.SetDisplay("Trailing Stop (pips)", "Distance maintained by the trailing stop in pips", "Risk")
			.SetNotNegative();

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetDisplay("Trailing Step (pips)", "Minimum progress before the trailing stop advances", "Risk")
			.SetNotNegative();

		_cooldownBars = Param(nameof(CooldownBars), 2)
			.SetDisplay("Cooldown Bars", "Bars to wait after entries and exits", "Risk")
			.SetNotNegative();

		_currentBarIndex = Param(nameof(CurrentBarIndex), 0)
			.SetDisplay("Current Bar Index", "Index of the candle used as the signal source", "Logic")
			.SetNotNegative();

		_comparableBarIndex = Param(nameof(ComparableBarIndex), 8)
			.SetDisplay("Comparable Bar Index", "Historical candle index used for momentum comparison", "Logic")
			.SetNotNegative();
	}

	/// <summary>
	/// Gets or sets the candle type used for generating signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Gets or sets the base order volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Gets or sets the stop-loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the take-profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the trailing step distance in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the index of the candle considered the "current" bar in comparisons.
	/// </summary>
	public int CurrentBarIndex
	{
		get => _currentBarIndex.Value;
		set => _currentBarIndex.Value = value;
	}

	/// <summary>
	/// Gets or sets the index of the historical bar used for comparison.
	/// </summary>
	public int ComparableBarIndex
	{
		get => _comparableBarIndex.Value;
		set => _comparableBarIndex.Value = value;
	}

	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

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

		_closeHistory.Clear();
		ResetPositionState();

		_pipValue = 0m;
		_stopLossOffset = 0m;
		_takeProfitOffset = 0m;
		_trailingStopOffset = 0m;
		_trailingStepOffset = 0m;
		_cooldownLeft = 0;
	}

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

		if (TrailingStopPips > 0 && TrailingStepPips <= 0)
			throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");

		Volume = TradeVolume;

		_pipValue = CalculatePipValue();
		_stopLossOffset = StopLossPips > 0 ? StopLossPips * _pipValue : 0m;
		_takeProfitOffset = TakeProfitPips > 0 ? TakeProfitPips * _pipValue : 0m;
		_trailingStopOffset = TrailingStopPips > 0 ? TrailingStopPips * _pipValue : 0m;
		_trailingStepOffset = TrailingStepPips > 0 ? TrailingStepPips * _pipValue : 0m;
		_cooldownLeft = 0;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore incomplete candles to mirror the original new-bar processing style.
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldownLeft > 0)
			_cooldownLeft--;

		// Update trailing and risk management before evaluating fresh signals.
		UpdateTrailingStop(candle);
		var exitTriggered = ManageProtectiveExits(candle);

		// Maintain the rolling window of closes used for momentum comparisons.
		UpdateCloseHistory(candle.ClosePrice);

		// Skip signal generation if an exit order has just been triggered on this bar.
		if (exitTriggered)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_cooldownLeft > 0)
			return;

		var requiredHistory = Math.Max(CurrentBarIndex, ComparableBarIndex) + 1;
		if (_closeHistory.Count < requiredHistory)
			return;

		var currentClose = GetCloseAtIndex(CurrentBarIndex);
		var comparableClose = GetCloseAtIndex(ComparableBarIndex);
		if (currentClose == null || comparableClose == null)
			return;

		// Enter long when the monitored bar closes above the reference bar.
		if (currentClose > comparableClose && Position <= 0)
		{
			EnterPosition(true, candle);
		}
		// Enter short when the monitored bar closes below the reference bar.
		else if (currentClose < comparableClose && Position >= 0)
		{
			EnterPosition(false, candle);
		}
	}

	private void UpdateCloseHistory(decimal closePrice)
	{
		var maxCount = Math.Max(CurrentBarIndex, ComparableBarIndex) + 1;
		if (maxCount <= 0)
			maxCount = 1;

		_closeHistory.Add(closePrice);
		if (_closeHistory.Count > maxCount)
			_closeHistory.RemoveAt(0);
	}

	private decimal? GetCloseAtIndex(int indexFromCurrent)
	{
		if (indexFromCurrent < 0)
			return null;

		var targetIndex = _closeHistory.Count - 1 - indexFromCurrent;
		if (targetIndex < 0 || targetIndex >= _closeHistory.Count)
			return null;

		return _closeHistory[targetIndex];
	}

	private void EnterPosition(bool isLong, ICandleMessage candle)
	{
		var baseVolume = TradeVolume;
		if (baseVolume <= 0m)
			return;

		var previousPosition = Position;
		decimal volume;

		if (isLong)
		{
			volume = baseVolume;
			if (previousPosition < 0m)
				volume += Math.Abs(previousPosition);

			if (volume <= 0m)
				return;

			// Buy enough volume to close any short exposure and add the configured trade size.
			BuyMarket(volume);

			if (previousPosition <= 0m)
			{
				// Treat reversals and fresh entries as a brand-new long position.
				_entryPrice = candle.ClosePrice;
			}
			else
			{
				// Blend the existing average price with the new fill to keep risk metrics consistent.
				var existingVolume = previousPosition;
				var totalVolume = existingVolume + baseVolume;
				if (totalVolume > 0m)
				{
					var existingEntry = _entryPrice ?? candle.ClosePrice;
					_entryPrice = (existingEntry * existingVolume + candle.ClosePrice * baseVolume) / totalVolume;
				}
			}

			_isLongPosition = true;
		}
		else
		{
			volume = baseVolume;
			if (previousPosition > 0m)
				volume += previousPosition;

			if (volume <= 0m)
				return;

			// Sell enough volume to close any long exposure and add the configured trade size.
			SellMarket(volume);

			if (previousPosition >= 0m)
			{
				// Treat reversals and fresh entries as a brand-new short position.
				_entryPrice = candle.ClosePrice;
			}
			else
			{
				// Blend the existing short average price with the new fill.
				var existingVolume = Math.Abs(previousPosition);
				var totalVolume = existingVolume + baseVolume;
				if (totalVolume > 0m)
				{
					var existingEntry = _entryPrice ?? candle.ClosePrice;
					_entryPrice = (existingEntry * existingVolume + candle.ClosePrice * baseVolume) / totalVolume;
				}
			}

			_isLongPosition = false;
		}

		_stopPrice = CalculateStopPrice(_isLongPosition, _entryPrice);
		_takeProfitPrice = CalculateTakeProfit(_isLongPosition, _entryPrice);
		_cooldownLeft = CooldownBars;
	}

	private decimal? CalculateStopPrice(bool isLong, decimal? entryPrice)
	{
		if (entryPrice == null || _stopLossOffset <= 0m)
			return null;

		return isLong ? entryPrice - _stopLossOffset : entryPrice + _stopLossOffset;
	}

	private decimal? CalculateTakeProfit(bool isLong, decimal? entryPrice)
	{
		if (entryPrice == null || _takeProfitOffset <= 0m)
			return null;

		return isLong ? entryPrice + _takeProfitOffset : entryPrice - _takeProfitOffset;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopOffset <= 0m || _trailingStepOffset <= 0m || _entryPrice == null)
			return;

		if (Position > 0m)
		{
			var progress = candle.HighPrice - _entryPrice.Value;
			if (progress <= _trailingStopOffset + _trailingStepOffset)
				return;

			// Shift the trailing stop only when the move is large enough to respect the configured step.
			var desiredStop = candle.ClosePrice - _trailingStopOffset;
			if (_stopPrice is decimal currentStop)
			{
				if (desiredStop - currentStop >= _trailingStepOffset)
					_stopPrice = desiredStop;
			}
			else
			{
				_stopPrice = desiredStop;
			}
		}
		else if (Position < 0m)
		{
			var progress = _entryPrice.Value - candle.LowPrice;
			if (progress <= _trailingStopOffset + _trailingStepOffset)
				return;

			var desiredStop = candle.ClosePrice + _trailingStopOffset;
			if (_stopPrice is decimal currentStop)
			{
				if (currentStop - desiredStop >= _trailingStepOffset)
					_stopPrice = desiredStop;
			}
			else
			{
				_stopPrice = desiredStop;
			}
		}
	}

	private bool ManageProtectiveExits(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			// Close the long position if the bar traded through the stop level.
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}

			// Lock in profits once the take-profit threshold has been reached.
			if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Position);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}
		}
		else if (Position < 0m)
		{
			var volume = Math.Abs(Position);

			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(volume);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(volume);
				ResetPositionState();
				_cooldownLeft = CooldownBars;
				return true;
			}
		}
		else
		{
			// Ensure cached state is flushed once all positions are closed externally.
			ResetPositionState();
		}

		return false;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_isLongPosition = false;
	}

	private decimal CalculatePipValue()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var scaled = step;
		var digits = 0;
		while (scaled < 1m && digits < 10)
		{
			scaled *= 10m;
			digits++;
		}

		// Adjust for three and five decimal quotes to emulate the MetaTrader point multiplier.
		var adjust = (digits == 3 || digits == 5) ? 10m : 1m;
		return step * adjust;
	}
}