Ver en GitHub

Estrategia Up3x1 con Dimensionamiento Dinámico

Descripción General

  • Conversión del asesor experto de MetaTrader 5 up3x1.mq5 a la API de alto nivel de StockSharp.
  • Opera un cruce de tres medias móviles exponenciales (EMA) con gestión de stop loss, take profit y trailing stop.
  • Procesa solo las velas terminadas para emular la protección original iTickVolume(0) > 1 que forzaba una decisión por barra.
  • La serie de velas predeterminada es 1 hora, pero el marco temporal es configurable a través del parámetro CandleType.

Lógica de Trading

  1. Indicadores
    • EMA Rápida (FastPeriod, predeterminado 24).
    • EMA Media (MediumPeriod, predeterminado 60).
    • EMA Lenta (SlowPeriod, predeterminado 120).
  2. Entrada larga
    • Barra anterior: EMA rápida por debajo de la EMA media y la media por debajo de la lenta (EMAfast₍t-1₎ < EMAmedium₍t-1₎ < EMAslow₍t-1₎).
    • Barra actual: la EMA media por debajo de la EMA rápida mientras la rápida sigue por debajo de la lenta (EMAmedium₍t₎ < EMAfast₍t₎ < EMAslow₍t₎).
  3. Entrada corta
    • Barra anterior: EMA rápida por encima de la EMA media y la media por encima de la lenta (EMAfast₍t-1₎ > EMAmedium₍t-1₎ > EMAslow₍t-1₎).
    • Barra actual: la EMA media cruza por encima de la EMA rápida mientras ambas permanecen por encima de la EMA lenta (EMAmedium₍t₎ > EMAfast₍t₎ > EMAslow₍t₎).
  4. Lógica de salida para ambas direcciones
    • Take profit cuando el precio avanza TakeProfitOffset desde la entrada (usando el máximo de la vela para largos, el mínimo para cortos).
    • Stop loss cuando el precio retrocede StopLossOffset desde la entrada (usando el mínimo de la vela para largos, el máximo para cortos).
    • El trailing stop se activa una vez que la posición se mueve a favor más que TrailingStopOffset y luego sigue el precio a esa distancia fija, evaluada en los extremos de la vela.
    • Salida de respaldo cuando la EMA rápida cruza de vuelta por debajo de la EMA media mientras ambas permanecen por encima de la EMA lenta (refleja la comprobación ma_one_1 > ma_two_1 > ma_three_1 de la versión MQL).

Dimensionamiento de Posición y Gestión de Riesgos

  • RiskFraction (predeterminado 0.02) multiplica el valor actual del portafolio para aproximar el dimensionamiento de lotes original FreeMargin * 0.02 / 1000.
  • BaseVolume (predeterminado 0.1) actúa como respaldo cuando los datos del portafolio no están disponibles o el tamaño calculado no es positivo.
  • Después de más de una salida perdedora, el volumen se reduce por volume * losses / 3, imitando el contador acumulativo losses del script (el contador no se reinicia después de operaciones rentables, como en el código original).
  • Los volúmenes se redondean hacia abajo a Security.VolumeStep, se limitan por Security.MinVolume / Security.MaxVolume, y se reducen a cero si no se puede cumplir el mínimo del instrumento.

Parámetros

Parámetro Predeterminado Descripción
FastPeriod 24 Longitud de la EMA más rápida.
MediumPeriod 60 Longitud de la EMA media.
SlowPeriod 120 Longitud de la EMA lenta usada como filtro de tendencia a largo plazo.
TakeProfitOffset 0.015 Distancia de precio absoluta para la orden de take profit (adaptar a la cotización del instrumento).
StopLossOffset 0.01 Distancia de precio absoluta para la orden de stop loss.
TrailingStopOffset 0.004 Distancia de trailing que bloquea ganancias una vez que el precio avanza suficientemente; establecer en 0 para deshabilitar.
BaseVolume 0.1 Tamaño de operación de respaldo cuando no se puede calcular el dimensionamiento dinámico.
RiskFraction 0.02 Fracción del valor del portafolio aplicada a la fórmula de dimensionamiento dinámico.
CandleType Marco temporal de 1 hora Serie de velas usada para cálculos de indicadores y toma de decisiones.

Notas de Conversión

  • El trailing stop y las salidas de protección usan máximos/mínimos de velas en lugar de ticks brutos porque la API de alto nivel procesa velas completadas; esto mantiene el comportamiento determinístico a través de backtests y ejecuciones en vivo.
  • El stop loss y el take profit se ejecutan mediante comandos de aplanamiento a mercado en el umbral evaluado en lugar de colocar órdenes de protección separadas, asegurando compatibilidad con el flujo de estrategia de alto nivel.
  • El dimensionamiento dinámico de posición depende de Portfolio.CurrentValue. Cuando no está disponible, la estrategia recurre a BaseVolume, similar al respaldo LotCheck del Lots manual en el original.
  • El contador losses es intencionalmente acumulativo (nunca se reinicia en operaciones ganadoras) para seguir la implementación MQL.
  • Todos los comentarios están en inglés según las pautas del proyecto.

Consejos de Uso

  1. Adjunte la estrategia a un instrumento y portafolio, luego configure CandleType para que coincida con la resolución del gráfico que desea emular desde MT5.
  2. Revise los offsets de precio para que reflejen el tamaño del tick de su instrumento (por ejemplo, para un par Forex de 5 dígitos, 0.015 equivale a 150 puntos como en el expert fuente).
  3. Ajuste RiskFraction / BaseVolume para lograr tamaños de posición realistas en relación a su cuenta.
  4. Opcional: deshabilite el trailing estableciendo TrailingStopOffset en cero.
  5. Monitoree los registros para mensajes como "Enter long" o "Exit short" que reflejan los diagnósticos Print de MetaTrader.

Estructura del Repositorio

API/2512_Up3x1/
├── CS/Up3x1DynamicSizingStrategy.cs      # Estrategia C# convertida
├── README.md                # Documentación en inglés (este archivo)
├── README_zh.md             # Traducción al chino
└── README_ru.md             # Traducción al ruso

Descargo de Responsabilidad

El trading implica un riesgo significativo. Este ejemplo se proporciona con fines educativos y debe validarse con datos históricos y simulados antes de cualquier implementación en vivo.

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>
/// EMA triple crossover strategy converted from the MetaTrader 5 "up3x1" expert.
/// Uses three exponential moving averages with optional stop loss, take profit and trailing logic.
/// Position size is reduced after losing trades similar to the original lot optimization routine.
/// </summary>
public class Up3x1DynamicSizingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _mediumPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _riskFraction;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _mediumEma;
	private ExponentialMovingAverage _slowEma;

	private bool _hasPrevValues;
	private decimal _prevFast;
	private decimal _prevMedium;
	private decimal _prevSlow;

	private decimal _entryPrice;
	private decimal _highestPrice;
	private decimal _lowestPrice;

	private int _losses;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MediumPeriod
	{
		get => _mediumPeriod.Value;
		set => _mediumPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Absolute price distance for take profit.
	/// </summary>
	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	/// <summary>
	/// Absolute price distance for stop loss.
	/// </summary>
	public decimal StopLossOffset
	{
		get => _stopLossOffset.Value;
		set => _stopLossOffset.Value = value;
	}

	/// <summary>
	/// Absolute trailing stop distance.
	/// </summary>
	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

	/// <summary>
	/// Base volume used when dynamic sizing cannot be calculated.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of portfolio value used for dynamic position sizing.
	/// </summary>
	public decimal RiskFraction
	{
		get => _riskFraction.Value;
		set => _riskFraction.Value = value;
	}

	/// <summary>
	/// Candle type to subscribe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public Up3x1DynamicSizingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fastest EMA", "Indicators");

		_mediumPeriod = Param(nameof(MediumPeriod), 60)
			.SetGreaterThanZero()
			.SetDisplay("Medium EMA", "Period of the middle EMA", "Indicators");

		_slowPeriod = Param(nameof(SlowPeriod), 120)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slowest EMA", "Indicators");

		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0.015m)
			.SetDisplay("Take Profit", "Absolute take profit distance in price units", "Risk");

		_stopLossOffset = Param(nameof(StopLossOffset), 0.01m)
			.SetDisplay("Stop Loss", "Absolute stop loss distance in price units", "Risk");

		_trailingStopOffset = Param(nameof(TrailingStopOffset), 0.004m)
			.SetDisplay("Trailing", "Trailing stop distance that follows price", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetDisplay("Base Volume", "Fallback trade volume if dynamic sizing fails", "Money Management");

		_riskFraction = Param(nameof(RiskFraction), 0.02m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio value used for sizing", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for calculations", "General");
	}

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

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

		Volume = BaseVolume;
		ResetState();
		_losses = 0;
		_hasPrevValues = false;
		_prevFast = 0m;
		_prevMedium = 0m;
		_prevSlow = 0m;
	}

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

		Volume = BaseVolume;

		_fastEma = new EMA
		{
			Length = FastPeriod
		};

		_mediumEma = new EMA
		{
			Length = MediumPeriod
		};

		_slowEma = new EMA
		{
			Length = SlowPeriod
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_fastEma, _mediumEma, _slowEma, ProcessCandle)
			.Start();

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_fastEma.IsFormed || !_mediumEma.IsFormed || !_slowEma.IsFormed)
			return;

		if (!_hasPrevValues)
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			_hasPrevValues = true;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			return;
		}

		if (Position > 0)
		{
			if (TryHandleLongExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0)
		{
			if (TryHandleShortExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else
		{
			var bullishSetup = _prevFast < _prevMedium && _prevMedium < _prevSlow && mediumValue < fastValue && fastValue < slowValue;
			var bearishSetup = _prevFast > _prevMedium && _prevMedium > _prevSlow && mediumValue > fastValue && fastValue > slowValue;

			if (bullishSetup)
			{
				TryEnterLong(candle);
			}
			else if (bearishSetup)
			{
				TryEnterShort(candle);
			}
		}

		_prevFast = fastValue;
		_prevMedium = mediumValue;
		_prevSlow = slowValue;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped long entry because calculated volume is below minimum.");
			return;
		}

		BuyMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter long at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped short entry because calculated volume is below minimum.");
			return;
		}

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter short at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private bool TryHandleLongExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice + TakeProfitOffset;
			if (candle.HighPrice >= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice - StopLossOffset;
			if (candle.LowPrice <= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_highestPrice = candle.HighPrice > _highestPrice ? candle.HighPrice : _highestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _highestPrice - _entryPrice > TrailingStopOffset)
		{
			var trail = _highestPrice - TrailingStopOffset;
			if (candle.LowPrice <= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private bool TryHandleShortExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice - TakeProfitOffset;
			if (candle.LowPrice <= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice + StopLossOffset;
			if (candle.HighPrice >= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_lowestPrice = _lowestPrice == 0m || candle.LowPrice < _lowestPrice ? candle.LowPrice : _lowestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _entryPrice - _lowestPrice > TrailingStopOffset)
		{
			var trail = _lowestPrice + TrailingStopOffset;
			if (candle.HighPrice >= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private void ExitPosition(decimal exitPrice, string reason)
	{
		var isLong = Position > 0;
		var volume = Math.Abs(Position);

		if (volume <= 0m)
			return;

		var pnl = isLong
			? (exitPrice - _entryPrice) * volume
			: (_entryPrice - exitPrice) * volume;

		if (isLong)
		{
			SellMarket();
		}
		else
		{
			BuyMarket();
		}

		LogInfo($"Exit {(isLong ? "long" : "short")} at {exitPrice} because {reason}. Approx PnL: {pnl}.");

		if (pnl < 0m)
			_losses++;

		ResetState();
	}

	private decimal CalculateOrderVolume()
	{
		var volume = 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;

		if (portfolioValue > 0m && RiskFraction > 0m)
			volume = portfolioValue * RiskFraction / 1000m;

		if (volume <= 0m)
			volume = BaseVolume;

		if (_losses > 1)
		{
			var reduction = volume * _losses / 3m;
			volume -= reduction;

			if (volume <= 0m)
				volume = BaseVolume;
		}

		volume = AdjustVolumeToInstrument(volume);

		return volume;
	}

	private decimal AdjustVolumeToInstrument(decimal volume)
	{
		var security = Security;

		if (security == null)
			return volume;

		var step = security.VolumeStep ?? 0m;

		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

		var minVolume = security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			return 0m;

		var maxVolume = security.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void ResetState()
	{
		_entryPrice = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
	}
}