Ver en GitHub

Estrategia de Cruce de Media Móvil Suavizada

Descripción general

La Estrategia de Cruce de Media Móvil Suavizada replica la lógica del Asesor Experto MQL5 original Smoothing Average (barabashkakvn's edition). Combina una media móvil configurable con un filtro de distancia de precio medido en pips. Cuando el mercado se aleja lo suficiente de la media suavizada, la estrategia abre una posición en la dirección del movimiento (o en el lado opuesto si el modo de reversión está habilitado). Las posiciones se cierran una vez que el precio revierte a través de un canal expandido alrededor de la media móvil.

Lógica de Trading

Modo predeterminado (ReverseSignals = false)

  • Entrada largo: el precio de cierre sube por encima de la media móvil menos Entry Delta (pips).
  • Entrada corto: el precio de cierre cae por debajo de la media móvil más Entry Delta (pips).
  • Salida corto: el precio de cierre sube por encima de la media móvil más Entry Delta (pips) × Close Delta Coefficient.
  • Salida largo: el precio de cierre cae por debajo de la media móvil menos Entry Delta (pips) × Close Delta Coefficient.

Modo de reversión (ReverseSignals = true)

  • Entrada largo: el precio de cierre cae por debajo de la media móvil más Entry Delta (pips).
  • Entrada corto: el precio de cierre sube por encima de la media móvil menos Entry Delta (pips).
  • Salida largo: el precio de cierre cae por debajo de la media móvil menos Entry Delta (pips) × Close Delta Coefficient.
  • Salida corto: el precio de cierre sube por encima de la media móvil más Entry Delta (pips) × Close Delta Coefficient.

La media móvil puede desplazarse hacia adelante varios candles. La estrategia emula este comportamiento manteniendo un pequeño búfer de los valores más recientes del indicador y usando el valor de MaShift barras atrás. Esto coincide con la línea desplazada producida por la implementación original de MetaTrader.

Parámetros

  • Candle Type – serie de datos utilizada para los cálculos.
  • MA Length – período de la media de suavizado.
  • MA Shift – número de barras que la media móvil se desplaza hacia adelante.
  • MA Type – método de media móvil (simple, exponencial, suavizada o ponderada linealmente).
  • Price Source – precio de vela introducido en la media móvil (predeterminado: precio típico).
  • Entry Delta (pips) – distancia desde la media móvil requerida para activar entradas. Convertida a precio usando el tamaño de pip del instrumento.
  • Close Delta Coefficient – multiplicador aplicado al delta de entrada al verificar condiciones de salida.
  • Reverse Signals – invierte la lógica de entrada largo/corto.
  • Trade Volume – tamaño de orden utilizado para entradas largas y cortas.

Gestión de Riesgo

  • Las órdenes se envían con el parámetro fijo Trade Volume. La estrategia no escala mientras una posición está abierta.
  • Todas las salidas están basadas en reglas. No se envían órdenes de stop-loss ni take-profit, pero se invoca StartProtection() para habilitar la red de seguridad a nivel de plataforma.
  • El modo de reversión está disponible para comportamiento a contratendencia sin alterar otros ajustes.

Notas de Implementación

  • El tamaño de pip se deriva de Security.PriceStep. Los símbolos FX de tres o cinco dígitos reciben el mismo ajuste de 10× que en el código MQL5.
  • La media móvil utiliza la selección de Price Source para que los precios típicos, medianos u otros precios de velas puedan coincidir con los ajustes del EA original.
  • Las comparaciones de entrada y salida usan el cierre de vela como proxy estable para las comprobaciones bid/ask en el Asesor Experto de origen.
  • Todos los comentarios dentro del código C# se proporcionan en inglés, según lo requerido por las pautas de conversión.
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>
/// Smoothing Average strategy converted from MQL5.
/// Opens trades when price moves away from the moving average by a configurable delta.
/// Supports reversing the signals and shifting the moving average output.
/// </summary>
public class SmoothingAverageCrossoverStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MovingAverageKinds> _maType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<decimal> _entryDeltaPips;
	private readonly StrategyParam<decimal> _closeDeltaCoefficient;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<decimal> _tradeVolume;

	private readonly Queue<decimal> _maShiftBuffer = new();

	private decimal _entryDelta;
	private decimal _closeDelta;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
        public SmoothingAverageCrossoverStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");

		_maLength = Param(nameof(MaLength), 60)
			.SetGreaterThanZero()
			.SetDisplay("MA Length", "Period of the smoothing average", "Moving Average");

		_maShift = Param(nameof(MaShift), 3)
			.SetNotNegative()
			.SetDisplay("MA Shift", "Horizontal shift applied to the average", "Moving Average");

		_maType = Param(nameof(MaType), MovingAverageKinds.Simple)
			.SetDisplay("MA Type", "Type of smoothing applied", "Moving Average");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Typical)
			.SetDisplay("Price Source", "Price used for the moving average", "Moving Average");

		_entryDeltaPips = Param(nameof(EntryDeltaPips), 60m)
			.SetNotNegative()
			.SetDisplay("Entry Delta (pips)", "Distance from MA to trigger entries", "Trading Rules");

		_closeDeltaCoefficient = Param(nameof(CloseDeltaCoefficient), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Close Delta Coefficient", "Multiplier applied to entry delta for exits", "Trading Rules");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short logic", "Trading Rules");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume for each entry", "Risk");
	}

	/// <summary>
	/// Primary candle series used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <summary>
	/// Number of candles used to shift the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average type.
	/// </summary>
	public MovingAverageKinds MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Candle price source for the moving average.
	/// </summary>
	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	/// <summary>
	/// Delta in pip units used to open new positions.
	/// </summary>
	public decimal EntryDeltaPips
	{
		get => _entryDeltaPips.Value;
		set => _entryDeltaPips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the entry delta when evaluating exits.
	/// </summary>
	public decimal CloseDeltaCoefficient
	{
		get => _closeDeltaCoefficient.Value;
		set => _closeDeltaCoefficient.Value = value;
	}

	/// <summary>
	/// If true, swaps long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Volume sent with market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

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

		_maShiftBuffer.Clear();
		_entryDelta = 0m;
		_closeDelta = 0m;
	}

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

		// Sync the base strategy volume with the parameter value.
		Volume = TradeVolume;

		// Calculate pip-based offsets once at the start to avoid repeated computations.
		_entryDelta = CalculateEntryDelta();
		_closeDelta = _entryDelta * CloseDeltaCoefficient;

		var movingAverage = CreateMovingAverage(MaType, MaLength);
		//movingAverage.CandlePrice = PriceSource;

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

		// Enable built-in protection helpers (no additional parameters required).
		StartProtection(null, null);
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var shiftedMa = ApplyShift(maValue);

		// Use candle close as a proxy for bid/ask checks from the original Expert Advisor.
		var askPrice = candle.ClosePrice;
		var bidPrice = candle.ClosePrice;

		var entryUpper = shiftedMa + _entryDelta;
		var entryLower = shiftedMa - _entryDelta;
		var closeUpper = shiftedMa + _closeDelta;
		var closeLower = shiftedMa - _closeDelta;

		if (Position == 0m)
		{
			if (!ReverseSignals)
			{
				if (askPrice > entryUpper)
				{
					OpenLong();
					return;
				}

				if (bidPrice < entryLower)
				{
					OpenShort();
					return;
				}
			}
			else
			{
				if (askPrice > entryUpper)
				{
					OpenShort();
					return;
				}

				if (bidPrice < entryLower)
				{
					OpenLong();
					return;
				}
			}
		}
		else
		{
			if (!ReverseSignals)
			{
				if (Position < 0m && bidPrice > closeUpper)
					CloseShort();

				if (Position > 0m && askPrice < closeLower)
					CloseLong();
			}
			else
			{
				if (Position > 0m && askPrice < closeLower)
					CloseLong();

				if (Position < 0m && bidPrice > closeUpper)
					CloseShort();
			}
		}
	}

	private decimal ApplyShift(decimal currentValue)
	{
		if (MaShift <= 0)
			return currentValue;

		var shifted = _maShiftBuffer.Count < MaShift ? currentValue : _maShiftBuffer.Peek();

		_maShiftBuffer.Enqueue(currentValue);

		if (_maShiftBuffer.Count > MaShift)
			_maShiftBuffer.Dequeue();

		return shifted;
	}

	private decimal CalculateEntryDelta()
	{
		var pip = CalculatePipSize();
		return pip * EntryDeltaPips;
	}

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

		var digits = (int)Math.Round(Math.Log10((double)(1m / step)));
		return digits == 3 || digits == 5 ? step * 10m : step;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageKinds type, int length)
	{
		return type switch
		{
			MovingAverageKinds.Simple => new SMA { Length = length },
			MovingAverageKinds.Exponential => new EMA { Length = length },
			MovingAverageKinds.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageKinds.LinearWeighted => new WeightedMovingAverage { Length = length },
			_ => new SMA { Length = length }
		};
	}

	private void OpenLong()
	{
		var volume = TradeVolume + Math.Max(0m, -Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
	}

	private void OpenShort()
	{
		var volume = TradeVolume + Math.Max(0m, Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
	}

	private void CloseLong()
	{
		if (Position <= 0m)
			return;

		SellMarket(Position);
	}

	private void CloseShort()
	{
		if (Position >= 0m)
			return;

		BuyMarket(Math.Abs(Position));
	}

	/// <summary>
	/// Supported moving average types replicating the MQL5 enumeration.
	/// </summary>
	public enum MovingAverageKinds
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

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