Ver en GitHub

Estrategia Universal MA Cross

Descripción general

La Estrategia Universal MA Cross es una conversión directa del asesor experto MQL5 original "UniversalMACrossEA" al framework de estrategias de alto nivel de StockSharp. El algoritmo compara una media móvil rápida y una lenta que pueden configurarse con diferentes métodos de cálculo y fuentes de precio. Filtros opcionales controlan cómo se confirman las señales, si las operaciones se revierten inmediatamente, cómo se realiza la gestión del riesgo y cuándo se permite operar a la estrategia.

Lógica de trading

Procesamiento de indicadores

  • Se calculan dos medias móviles sobre la serie de velas seleccionada. Cada media puede usar su propio período, método de suavizado (SMA, EMA, SMMA o LWMA) y tipo de precio (cierre, apertura, máximo, mínimo, mediana, típico o ponderado).
  • El parámetro MinCrossDistance requiere que las medias rápida y lenta diverjan al menos el número especificado de unidades de precio en la barra de cruce.
  • Cuando ConfirmedOnEntry está habilitado, el cruce se valida en la barra completada anterior (equivalente a usar índices de barra 2 y 1 en el EA original). Si está deshabilitado, la barra finalizada actual se compara con la barra anterior, replicando el comportamiento del "modo tick" de la versión MQL.
  • Configurar ReverseCondition intercambia las señales alcistas y bajistas para que las reglas puedan invertirse sin cambiar ninguna configuración del indicador.

Reglas de entrada

  1. Para una entrada larga, la media rápida debe cruzar por encima de la media lenta al menos MinCrossDistance. Para una entrada corta, la media rápida debe cruzar por debajo de la media lenta esa distancia.
  2. Cuando StopAndReverse está habilitado y llega una señal opuesta, la posición activa se cierra antes de considerar nuevas órdenes.
  3. Si OneEntryPerBar es verdadero, la estrategia recuerda el tiempo de barra de la última entrada y rechaza abrir otra operación durante la misma vela.
  4. El volumen de cada orden se configura mediante el parámetro Volume.

Gestión de posiciones

  • Los niveles de stop-loss y take-profit se miden en unidades de precio. Se ignoran cuando PureSar es verdadero, coincidiendo con el modo "Pure SAR" del experto original.
  • La lógica de trailing stop se activa después de que el precio se mueve TrailingStop + TrailingStep desde el precio de entrada. Cada movimiento adicional de al menos TrailingStep puntos ajusta el stop en la distancia TrailingStop especificada. El trailing no funciona en modo "Pure SAR".
  • Los niveles de protección se monitorean en cada vela finalizada. Si el rango de la vela viola el nivel de stop-loss o take-profit, la posición se cierra por orden de mercado.

Filtro de sesión

  • Cuando UseHourTrade está habilitado, la estrategia opera solo cuando la hora de apertura de la vela está entre StartHour y EndHour (inclusive). La gestión del trailing stop continúa ejecutándose fuera de ese intervalo, pero no se ejecutan nuevas entradas ni acciones de stop-and-reverse.

Parámetros

Parámetro Descripción
FastMaPeriod, SlowMaPeriod Períodos de las medias móviles rápida y lenta.
FastMaType, SlowMaType Métodos de media móvil: Simple, Exponencial, Suavizada (RMA) o Ponderada Lineal.
FastPriceType, SlowPriceType Fuentes de precio alimentadas en las medias.
StopLoss, TakeProfit Distancias de protección en unidades de precio absolutas. Establecer en 0 para deshabilitar.
TrailingStop, TrailingStep Desplazamiento del trailing stop y movimiento extra mínimo requerido antes de desplazar el stop.
MinCrossDistance Distancia mínima entre las medias en la barra de cruce.
ReverseCondition Intercambiar reglas alcistas y bajistas.
ConfirmedOnEntry Usar solo barras completadas para validación.
OneEntryPerBar Permitir como máximo una entrada por vela.
StopAndReverse Cerrar la posición actual y revertir en señales opuestas.
PureSar Deshabilitar la lógica de stop-loss, take-profit y trailing.
UseHourTrade, StartHour, EndHour Filtro de tiempo para sesiones de trading (horas 0–23).
Volume Volumen de orden para cada posición.
CandleType Tipo de datos de velas suscrito para cálculos.

Notas de conversión

  • Las órdenes de protección se manejan internamente verificando los máximos y mínimos de las velas, porque las estrategias de StockSharp operan sobre velas finalizadas en lugar de eventos de tick sin procesar. Esto refleja el comportamiento del experto original mientras se mantiene dentro de la API de alto nivel.
  • Los ajustes del trailing stop siguen la implementación MQL, requiriendo un movimiento de TrailingStop + TrailingStep antes de que el stop se desplace.
  • No se proporciona versión en Python en esta conversión según lo solicitado.
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>
/// Universal moving average crossover strategy converted from the original MQL version.
/// The strategy trades based on a fast and a slow moving average with optional signal confirmation,
/// stop-and-reverse behaviour, trailing stop management and time filtering.
/// </summary>
public class UniversalMaCrossStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources that can feed the moving averages.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<MovingAverageMethods> _fastMaType;
	private readonly StrategyParam<MovingAverageMethods> _slowMaType;
	private readonly StrategyParam<AppliedPrices> _fastPriceType;
	private readonly StrategyParam<AppliedPrices> _slowPriceType;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _trailingStep;
	private readonly StrategyParam<decimal> _minCrossDistance;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<bool> _confirmedOnEntry;
	private readonly StrategyParam<bool> _oneEntryPerBar;
	private readonly StrategyParam<bool> _stopAndReverse;
	private readonly StrategyParam<bool> _pureSar;
	private readonly StrategyParam<bool> _useHourTrade;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _fastMa;
	private IIndicator _slowMa;

	private decimal? _fastPrev;
	private decimal? _fastPrevPrev;
	private decimal? _slowPrev;
	private decimal? _slowPrevPrev;

	private DateTimeOffset? _lastEntryBar;
	private TradeDirections _lastTrade = TradeDirections.None;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Fast moving average method.
	/// </summary>
	public MovingAverageMethods FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	/// <summary>
	/// Slow moving average method.
	/// </summary>
	public MovingAverageMethods SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	/// <summary>
	/// Price type used for the fast moving average.
	/// </summary>
	public AppliedPrices FastPriceType
	{
		get => _fastPriceType.Value;
		set => _fastPriceType.Value = value;
	}

	/// <summary>
	/// Price type used for the slow moving average.
	/// </summary>
	public AppliedPrices SlowPriceType
	{
		get => _slowPriceType.Value;
		set => _slowPriceType.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

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

	/// <summary>
	/// Additional move required before shifting the trailing stop.
	/// </summary>
	public decimal TrailingStep
	{
		get => _trailingStep.Value;
		set => _trailingStep.Value = value;
	}

	/// <summary>
	/// Minimum distance between the averages to validate a crossover.
	/// </summary>
	public decimal MinCrossDistance
	{
		get => _minCrossDistance.Value;
		set => _minCrossDistance.Value = value;
	}

	/// <summary>
	/// Reverse buy and sell conditions.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

	/// <summary>
	/// Confirm signals on closed candles only.
	/// </summary>
	public bool ConfirmedOnEntry
	{
		get => _confirmedOnEntry.Value;
		set => _confirmedOnEntry.Value = value;
	}

	/// <summary>
	/// Limit the strategy to a single entry per bar.
	/// </summary>
	public bool OneEntryPerBar
	{
		get => _oneEntryPerBar.Value;
		set => _oneEntryPerBar.Value = value;
	}

	/// <summary>
	/// Close the current trade and reverse when an opposite signal appears.
	/// </summary>
	public bool StopAndReverse
	{
		get => _stopAndReverse.Value;
		set => _stopAndReverse.Value = value;
	}

	/// <summary>
	/// Disable protective orders and rely purely on signal reversals.
	/// </summary>
	public bool PureSar
	{
		get => _pureSar.Value;
		set => _pureSar.Value = value;
	}

	/// <summary>
	/// Enable trading only within the selected hours.
	/// </summary>
	public bool UseHourTrade
	{
		get => _useHourTrade.Value;
		set => _useHourTrade.Value = value;
	}

	/// <summary>
	/// Hour when trading can start (0-23).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when trading must end (0-23).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}


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

	/// <summary>
	/// Initializes <see cref="UniversalMaCrossStrategy"/>.
	/// </summary>
	public UniversalMaCrossStrategy()
	{
		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators")
			
			.SetOptimize(30, 200, 5);

		_fastMaType = Param(nameof(FastMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Fast MA Type", "Method for fast average", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageMethods.Exponential)
			.SetDisplay("Slow MA Type", "Method for slow average", "Indicators");

		_fastPriceType = Param(nameof(FastPriceType), AppliedPrices.Close)
			.SetDisplay("Fast Price Type", "Price source for fast MA", "Indicators");

		_slowPriceType = Param(nameof(SlowPriceType), AppliedPrices.Close)
			.SetDisplay("Slow Price Type", "Price source for slow MA", "Indicators");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetDisplay("Stop Loss", "Stop-loss distance in price", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetDisplay("Take Profit", "Take-profit distance in price", "Risk");

		_trailingStop = Param(nameof(TrailingStop), 0m)
			.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk");

		_trailingStep = Param(nameof(TrailingStep), 0m)
			.SetDisplay("Trailing Step", "Additional move before trailing", "Risk");

		_minCrossDistance = Param(nameof(MinCrossDistance), 0m)
			.SetDisplay("Min Cross Distance", "Minimum distance between averages", "Filters");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signals", "Swap long and short conditions", "General");

		_confirmedOnEntry = Param(nameof(ConfirmedOnEntry), true)
			.SetDisplay("Confirmed On Entry", "Use closed candles for signals", "General");

		_oneEntryPerBar = Param(nameof(OneEntryPerBar), true)
			.SetDisplay("One Entry Per Bar", "Allow only one entry per candle", "General");

		_stopAndReverse = Param(nameof(StopAndReverse), true)
			.SetDisplay("Stop And Reverse", "Close and reverse on opposite signal", "Risk");

		_pureSar = Param(nameof(PureSar), false)
			.SetDisplay("Pure SAR", "Disable stop-loss, take-profit and trailing", "Risk");

		_useHourTrade = Param(nameof(UseHourTrade), false)
			.SetDisplay("Use Hour Filter", "Limit trading by session hours", "Session");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Session");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Session");


		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle subscription", "General");
	}

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

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

		_fastMa = null;
		_slowMa = null;
		_fastPrev = null;
		_fastPrevPrev = null;
		_slowPrev = null;
		_slowPrevPrev = null;
		_lastEntryBar = null;
		_lastTrade = TradeDirections.None;
		ResetProtection();
	}

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

		_fastMa = CreateMovingAverage(FastMaType, FastMaPeriod);
		_slowMa = CreateMovingAverage(SlowMaType, SlowMaPeriod);

		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;

		ManageExistingPosition(candle);

		if (_fastMa is null || _slowMa is null)
			return;

		var fastPrice = GetPrice(candle, FastPriceType);
		var slowPrice = GetPrice(candle, SlowPriceType);

		var fastResult = _fastMa.Process(new DecimalIndicatorValue(_fastMa, fastPrice, candle.OpenTime) { IsFinal = true });
		var slowResult = _slowMa.Process(new DecimalIndicatorValue(_slowMa, slowPrice, candle.OpenTime) { IsFinal = true });

		if (fastResult.IsEmpty || slowResult.IsEmpty)
			return;

		var fastValue = fastResult.ToDecimal();
		var slowValue = slowResult.ToDecimal();

		var prevFast = _fastPrev;
		var prevSlow = _slowPrev;
		var prevFastPrev = _fastPrevPrev;
		var prevSlowPrev = _slowPrevPrev;

		_fastPrevPrev = prevFast;
		_slowPrevPrev = prevSlow;
		_fastPrev = fastValue;
		_slowPrev = slowValue;


		bool crossUp = false;
		bool crossDown = false;

		if (ConfirmedOnEntry)
		{
			if (prevFast.HasValue && prevSlow.HasValue && prevFastPrev.HasValue && prevSlowPrev.HasValue)
			{
				var fastPrevPrevValue = prevFastPrev.Value;
				var slowPrevPrevValue = prevSlowPrev.Value;
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastPrevValue - slowPrevValue;

				crossUp = fastPrevPrevValue < slowPrevPrevValue && fastPrevValue > slowPrevValue && diff >= MinCrossDistance;
				crossDown = fastPrevPrevValue > slowPrevPrevValue && fastPrevValue < slowPrevValue && -diff >= MinCrossDistance;
			}
		}
		else
		{
			if (prevFast.HasValue && prevSlow.HasValue)
			{
				var fastPrevValue = prevFast.Value;
				var slowPrevValue = prevSlow.Value;
				var diff = fastValue - slowValue;

				crossUp = fastPrevValue < slowPrevValue && fastValue > slowValue && diff >= MinCrossDistance;
				crossDown = fastPrevValue > slowPrevValue && fastValue < slowValue && -diff >= MinCrossDistance;
			}
		}

		bool buySignal;
		bool sellSignal;

		if (!ReverseCondition)
		{
			buySignal = crossUp;
			sellSignal = crossDown;
		}
		else
		{
			buySignal = crossDown;
			sellSignal = crossUp;
		}

		var canTrade = IsWithinTradingHours(candle);

		if (!canTrade)
			return;

		if (StopAndReverse && Position != 0)
		{
			if ((_lastTrade == TradeDirections.Long && sellSignal) || (_lastTrade == TradeDirections.Short && buySignal))
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}

		if (Position != 0)
			return;

		var entryAllowed = !OneEntryPerBar || _lastEntryBar != candle.OpenTime;

		if (!entryAllowed)
			return;

		if (buySignal)
		{
			BuyMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, true);
			_lastTrade = TradeDirections.Long;
			_lastEntryBar = candle.OpenTime;
		}
		else if (sellSignal)
		{
			SellMarket(Volume);
			SetProtectionLevels(candle.ClosePrice, false);
			_lastTrade = TradeDirections.Short;
			_lastEntryBar = candle.OpenTime;
		}
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetProtection();
			return;
		}

		UpdateTrailingStop(candle);

		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetProtection();
			}
		}
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (PureSar || TrailingStop <= 0m || !_entryPrice.HasValue)
			return;

		var activationDistance = TrailingStop + TrailingStep;

		if (Position > 0)
		{
			if (candle.ClosePrice - _entryPrice.Value > activationDistance)
			{
				var activationLevel = candle.ClosePrice - activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value < activationLevel)
				{
					var newStop = candle.ClosePrice - TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
		else if (Position < 0)
		{
			if (_entryPrice.Value - candle.ClosePrice > activationDistance)
			{
				var activationLevel = candle.ClosePrice + activationDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value > activationLevel)
				{
					var newStop = candle.ClosePrice + TrailingStop;
					_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
				}
			}
		}
	}

	private bool IsWithinTradingHours(ICandleMessage candle)
	{
		if (!UseHourTrade)
			return true;

		var hour = candle.OpenTime.Hour;
		var start = StartHour;
		var end = EndHour;

		if (start <= end)
			return hour >= start && hour <= end;

		return hour >= start || hour <= end;
	}

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

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

	private void SetProtectionLevels(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;

		if (PureSar)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		var stop = StopLoss;
		var take = TakeProfit;

		_stopPrice = stop > 0m ? (isLong ? entryPrice - stop : entryPrice + stop) : null;
		_takeProfitPrice = take > 0m ? (isLong ? entryPrice + take : entryPrice - take) : null;
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private enum TradeDirections
	{
		None,
		Long,
		Short
	}
}