Ver en GitHub

Ejemplo de Estrategia MACD Automatizada

Descripción general

La estrategia replica el asesor experto "Example of MACD Automated" de MetaTrader 4 usando la API de alto nivel de StockSharp. Monitorea la línea principal del MACD en dos marcos temporales y abre una única posición cuando ambos filtros de tendencia coinciden. Las distancias de stop-loss y take-profit se aplican en pasos de precio, y el tamaño de la posición sigue la lógica original de AdvancedMM que acumula el volumen de las operaciones perdedoras recientes.

Lógica de trading

  1. Filtro de marco temporal superior – un MACD (12, 26, 9) calculado en el marco temporal superior (por defecto: velas diarias) debe tener una línea principal positiva para señales largas o negativa para señales cortas.
  2. Confirmación del marco temporal de entrada – los mismos ajustes de MACD en el marco temporal de entrada (por defecto: velas de 15 minutos) deben apuntar en la misma dirección que el filtro de marco temporal superior.
  3. Posición única – la estrategia opera una posición a la vez. Las nuevas entradas se omiten hasta que la posición existente sea cerrada por los niveles protectores.
  4. Órdenes protectoras – los niveles de stop-loss y take-profit se miden en múltiplos del paso de precio del instrumento, replicando las entradas StopLoss y TakeProfit del MT4 original. Un valor de 0 deshabilita la protección correspondiente.
  5. Gestión monetaria avanzada – el volumen de la operación aumenta después de operaciones perdedoras consecutivas sumando el tamaño de las pérdidas, y vuelve al volumen base después de operaciones ganadoras, emulando la función AdvancedMM() del EA fuente.

Parámetros

Nombre Descripción Predeterminado
BaseVolume Volumen base de la orden usado por la lógica de AdvancedMM. 0.01
StopLossPoints Distancia del stop-loss expresada en pasos de precio. 0 deshabilita el stop. 50
TakeProfitPoints Distancia del take-profit expresada en pasos de precio. 0 deshabilita el objetivo. 30
MacdFastLength Período de la EMA rápida del MACD en ambos marcos temporales. 12
MacdSlowLength Período de la EMA lenta del MACD. 26
MacdSignalLength Período de la EMA de la línea de señal. 9
EntryCandleType Marco temporal para la ejecución de operaciones. Velas de 15m
FilterCandleType Marco temporal superior usado como filtro de tendencia. Velas de 1d

Gestión de la posición

  • Los precios de stop-loss y take-profit se recalculan en cada nueva posición basándose en el paso de precio del instrumento.
  • Cuando cualquier nivel protector es tocado dentro de una barra, la estrategia asume que la orden se ejecuta a ese nivel y registra la ganancia o pérdida realizada.
  • Después de cada operación cerrada, la lógica de AdvancedMM actualiza el tamaño de la siguiente orden:
    • Menos de dos operaciones históricas → usar el volumen base.
    • La operación más reciente fue una pérdida → repetir su volumen.
    • Pérdidas consecutivas antes de la última ganancia → sumar sus volúmenes para recuperar.
    • De lo contrario → volver al volumen base.

Notas

  • La conversión mantiene el comportamiento original de mantener una posición hasta que se alcanza un nivel protector; no hay salida en los cruces del MACD.
  • Asegúrese de que el instrumento tenga información válida de PriceStep para que las distancias de stop y objetivo basadas en puntos se calculen correctamente.
  • La estrategia se basa en velas completadas y debe usarse con datos históricos o feeds en vivo que proporcionen actualizaciones de velas terminadas.
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>
/// Conversion of the "Example of MACD Automated" MQL4 expert advisor.
/// The strategy waits for MACD agreement on two timeframes and uses AdvancedMM sizing.
/// </summary>
public class ExampleOfMacdAutomatedStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _macdSignalLength;
	private readonly StrategyParam<DataType> _entryCandleType;
	private readonly StrategyParam<DataType> _filterCandleType;

	private MovingAverageConvergenceDivergenceSignal _entryMacd = null!;
	private MovingAverageConvergenceDivergenceSignal _filterMacd = null!;

	private decimal? _lastEntryMacd;
	private decimal? _lastFilterMacd;

	private readonly List<TradeInfo> _tradeHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryVolume;
	private int _entryDirection;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExampleOfMacdAutomatedStrategy"/> class.
	/// </summary>
	public ExampleOfMacdAutomatedStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Starting order volume for AdvancedMM", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop-loss distance in price steps", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take-profit distance in price steps", "Risk")
			;

		_macdFastLength = Param(nameof(MacdFastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
			;

		_macdSlowLength = Param(nameof(MacdSlowLength), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow", "Slow EMA length", "Indicators")
			;

		_macdSignalLength = Param(nameof(MacdSignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			;

		_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Entry Timeframe", "Working timeframe for entries", "General");

		_filterCandleType = Param(nameof(FilterCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Filter Timeframe", "Higher timeframe used as trend filter", "General");
	}

	/// <summary>
	/// Base volume parameter.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// MACD fast EMA length.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// MACD slow EMA length.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// MACD signal EMA length.
	/// </summary>
	public int MacdSignalLength
	{
		get => _macdSignalLength.Value;
		set => _macdSignalLength.Value = value;
	}

	/// <summary>
	/// Timeframe used for entries.
	/// </summary>
	public DataType EntryCandleType
	{
		get => _entryCandleType.Value;
		set => _entryCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used as a trend filter.
	/// </summary>
	public DataType FilterCandleType
	{
		get => _filterCandleType.Value;
		set => _filterCandleType.Value = value;
	}

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

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

		_lastEntryMacd = null;
		_lastFilterMacd = null;
		_tradeHistory.Clear();
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;

		_entryMacd?.Reset();
		_filterMacd?.Reset();
	}

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

		// Create MACD indicators for entry and filter timeframes.
		_entryMacd = CreateMacd();
		_filterMacd = CreateMacd();

		var entrySubscription = SubscribeCandles(EntryCandleType);
		entrySubscription
			.BindEx(_entryMacd, ProcessEntryCandle)
			.Start();

		var filterSubscription = SubscribeCandles(FilterCandleType);
		filterSubscription
			.BindEx(_filterMacd, ProcessFilterCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, entrySubscription);
			DrawIndicator(area, _entryMacd);
			DrawIndicator(area, _filterMacd);
			DrawOwnTrades(area);
		}
	}

	private MovingAverageConvergenceDivergenceSignal CreateMacd()
	{
		// Instantiate MACD with shared parameters for both timeframes.
		return new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFastLength },
				LongMa = { Length = MacdSlowLength },
			},
			SignalMa = { Length = MacdSignalLength }
		};
	}

	private void ProcessFilterCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Process only completed candles on the filter timeframe.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		_lastFilterMacd = macd.Macd;
	}

	private void ProcessEntryCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Ensure that we operate on final candle values only.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		var currentEntryMacd = macd.Macd;

		// Manage protective exits before searching for new entries.
		if (HandleProtection(candle))
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Skip further processing if there is still an open position.
		if (Position != 0)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!_entryMacd.IsFormed || !_filterMacd.IsFormed)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (_lastEntryMacd is not decimal previousEntryMacd || _lastFilterMacd is not decimal filterMacdValue)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Enter only on a zero-line crossover aligned with the higher timeframe filter.
		if (previousEntryMacd <= 0m && currentEntryMacd > 0m && filterMacdValue > 0m)
		{
			EnterPosition(candle.ClosePrice, true);
		}
		else if (previousEntryMacd >= 0m && currentEntryMacd < 0m && filterMacdValue < 0m)
		{
			EnterPosition(candle.ClosePrice, false);
		}

		_lastEntryMacd = currentEntryMacd;
	}

	private void EnterPosition(decimal price, bool isLong)
	{
		var volume = CalculateTradeVolume();
		if (volume <= 0m)
		return;

		if (isLong)
		{
			BuyMarket(volume);
			RegisterEntry(price, volume, 1);
		}
		else
		{
			SellMarket(volume);
			RegisterEntry(price, volume, -1);
		}
	}

	private void RegisterEntry(decimal price, decimal volume, int direction)
	{
		// Store entry information for later profit calculation.
		_entryPrice = price;
		_entryVolume = volume;
		_entryDirection = direction;

		UpdateProtectionLevels(price, direction > 0);
	}

	private void UpdateProtectionLevels(decimal price, bool isLong)
	{
		var point = GetPointValue();

		if (point <= 0m)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		if (isLong)
		{
			_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price + TakeProfitPoints * point : null;
		}
		else
		{
			_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price - TakeProfitPoints * point : null;
		}
	}

	private bool HandleProtection(ICandleMessage candle)
	{
		if (Position == 0 || _entryDirection == 0)
		return false;

		if (_entryDirection > 0)
		{
			if (TryGetLongExitPrice(candle, out var exitPrice))
			{
				SellMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}
		else
		{
			if (TryGetShortExitPrice(candle, out var exitPrice))
			{
				BuyMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}

		return false;
	}

	private bool TryGetLongExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private bool TryGetShortExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private void RegisterClosedTrade(decimal exitPrice)
	{
		if (!_entryPrice.HasValue || _entryVolume <= 0m || _entryDirection == 0)
		return;

		var entryPrice = _entryPrice.Value;
		var volume = _entryVolume;
		var direction = _entryDirection;

		var profit = (exitPrice - entryPrice) * direction * volume;

		_tradeHistory.Add(new TradeInfo(volume, profit));
		if (_tradeHistory.Count > 200)
		_tradeHistory.RemoveAt(0);

		_entryPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private decimal CalculateTradeVolume()
	{
		var baseVolume = BaseVolume;
		if (baseVolume <= 0m)
		return 0m;

		if (_tradeHistory.Count < 2)
		return baseVolume;

		var advancedLots = 0m;
		var profit1 = false;
		var profit2 = false;
		var firstIteration = true;

		for (var i = _tradeHistory.Count - 1; i >= 0; i--)
		{
			var trade = _tradeHistory[i];
			var isProfit = trade.Profit >= 0m;

			if (isProfit && profit1)
			return baseVolume;

			if (firstIteration)
			{
				if (isProfit)
				{
					profit1 = true;
				}
				else
				{
					return trade.Volume;
				}

				firstIteration = false;
			}

			if (isProfit && profit2)
			return advancedLots > 0m ? advancedLots : baseVolume;

			if (isProfit)
			{
				profit2 = true;
			}
			else
			{
				profit1 = false;
				profit2 = false;
				advancedLots += trade.Volume;
			}
		}

		return advancedLots > 0m ? advancedLots : baseVolume;
	}

	private decimal GetPointValue()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}

	private readonly struct TradeInfo
	{
		public TradeInfo(decimal volume, decimal profit)
		{
			Volume = volume;
			Profit = profit;
		}

		public decimal Volume { get; }
		public decimal Profit { get; }
	}
}