Ver en GitHub

Estrategia MACD con Filtro de Línea Cero y Toma de Ganancias

Descripción general

Esta estrategia replica el experto original de MetaTrader 5 "Robot_MACD" que opera cruces de la línea de señal del MACD con filtros adicionales de línea cero. Opera sobre un único instrumento y busca reversiones de momentum confirmadas por la posición de la línea MACD relativa a cero. Se adjunta una toma de ganancias a distancia fija en cada orden, reproduciendo el objetivo de ganancia basado en puntos de la implementación original.

Datos e Indicadores

  • Datos primarios: suscripción a una sola vela (marco temporal de 5 minutos por defecto). El marco temporal puede cambiarse con el parámetro CandleType para adaptarse al mercado negociado.
  • Indicadores:
    • MovingAverageConvergenceDivergenceSignal (MACD + señal + histograma). Los valores predeterminados son EMAs de 12/26 con una línea de señal de 9 períodos, coincidiendo con los parámetros MQL.

Lógica de Operación

  1. Esperar a que el cálculo del MACD proporcione los valores actuales y anteriores de las líneas MACD y de señal.
  2. Identificar cruces alcistas y bajistas:
    • Cruce alcista: MACD anterior ≤ señal anterior y MACD actual > señal actual.
    • Cruce bajista: MACD anterior ≥ señal anterior y MACD actual < señal actual.
  3. Gestión de posiciones:
    • Cerrar una posición larga cuando aparece un cruce bajista.
    • Cerrar una posición corta cuando aparece un cruce alcista.
  4. Criterios de entrada (solo cuando no hay posición abierta y hay capital suficiente):
    • Entrar largo en un cruce alcista mientras tanto el MACD como la señal permanezcan por debajo de cero.
    • Entrar corto en un cruce bajista mientras tanto el MACD como la señal permanezcan por encima de cero.
  5. Adjuntar una toma de ganancias fija en el momento del registro de la orden llamando a StartProtection con una distancia absoluta medida en puntos de precio. La distancia equivale al valor de punto configurado multiplicado por el paso de precio del instrumento.

Gestión de Riesgos

  • Cada orden tiene una toma de ganancias adjunta definida por TakeProfitPoints. No hay stop-loss en la lógica base, manteniendo la paridad con el EA fuente.
  • La estrategia verifica si el valor del portafolio es al menos MinimumCapitalPerVolume * VolumePerTrade antes de colocar una nueva orden. Esto emula la protección de margen libre (FreeMargin() < 1000 * Lots) de la versión MQL.

Parámetros

Parámetro Descripción Valor predeterminado
MacdFast Período EMA rápido para MACD. 12
MacdSlow Período EMA lento para MACD. 26
MacdSignal Período de suavizado de la línea de señal. 9
TakeProfitPoints Distancia de toma de ganancias expresada en puntos de precio. 300
VolumePerTrade Volumen de negociación (lotes) utilizado para cada entrada. 1
MinimumCapitalPerVolume Valor mínimo del portafolio requerido por lote negociado. 1000
CandleType Tipo de vela (marco temporal) utilizado para alimentar el indicador MACD. Velas de 5 minutos

Notas de Implementación

  • Las órdenes se ejecutan con BuyMarket/SellMarket, idéntico al EA que usaba órdenes de mercado vía CTrade.
  • Los filtros de línea cero evitan entrar en operaciones en la mitad opuesta del histograma MACD, tal como en el script MQL.
  • La verificación del valor del portafolio depende de Portfolio.CurrentValue. Si el entorno de trading no suministra este valor, la protección pasa automáticamente, lo que mantiene la estrategia utilizable en cuentas simuladas.
  • La sección de dibujo en el gráfico representa velas, el indicador MACD y marcadores de operaciones cuando hay un área de gráfico disponible en la aplicación host.
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>
/// MACD cross strategy with zero line filter and fixed take profit.
/// </summary>
public class MacdZeroFilterTakeProfitStrategy : Strategy
{
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _volumePerTrade;
	private readonly StrategyParam<decimal> _minimumCapitalPerVolume;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;
	private decimal? _previousMacd;
	private decimal? _previousSignal;

	/// <summary>
	/// Initializes a new instance of the <see cref="MacdZeroFilterTakeProfitStrategy"/> class.
	/// </summary>
	public MacdZeroFilterTakeProfitStrategy()
	{
		_macdFast = Param(nameof(MacdFast), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast Period", "Fast EMA period used by MACD", "Indicators")
			
			.SetOptimize(8, 20, 2);

		_macdSlow = Param(nameof(MacdSlow), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow Period", "Slow EMA period used by MACD", "Indicators")
			
			.SetOptimize(20, 40, 2);

		_macdSignal = Param(nameof(MacdSignal), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal Period", "Signal smoothing period for MACD", "Indicators")
			
			.SetOptimize(6, 15, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
			
			.SetOptimize(100, 600, 50);

		_volumePerTrade = Param(nameof(VolumePerTrade), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Number of lots to trade on each entry", "General")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_minimumCapitalPerVolume = Param(nameof(MinimumCapitalPerVolume), 1000m)
			.SetGreaterThanZero()
			.SetDisplay("Capital per Volume", "Minimum portfolio value required per traded lot", "Risk Management")
			
			.SetOptimize(500m, 5000m, 500m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for MACD calculations", "General");
	}

	/// <summary>
	/// Fast EMA period used by MACD.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period used by MACD.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal smoothing period for MACD.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Number of lots to trade on each entry.
	/// </summary>
	public decimal VolumePerTrade
	{
		get => _volumePerTrade.Value;
		set => _volumePerTrade.Value = value;
	}

	/// <summary>
	/// Minimum portfolio value required per traded lot.
	/// </summary>
	public decimal MinimumCapitalPerVolume
	{
		get => _minimumCapitalPerVolume.Value;
		set => _minimumCapitalPerVolume.Value = value;
	}

	/// <summary>
	/// Timeframe for MACD calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousMacd = null;
		_previousSignal = null;
	}

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

		Volume = VolumePerTrade;

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFast },
				LongMa = { Length = MacdSlow },
			},
			SignalMa = { Length = MacdSignal }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, ProcessCandle)
			.Start();

		var step = Security?.PriceStep ?? 1m;
		StartProtection(
			takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
			stopLoss: new Unit(0m),
			useMarketOrders: true);

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _macd);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Use only completed candles to avoid double counting signals.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure the strategy is ready to trade and has a working connector.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (macdValue is not MovingAverageConvergenceDivergenceSignalValue macdTyped)
			return;

		if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
			return;

		if (_previousMacd is null || _previousSignal is null)
		{
			_previousMacd = macd;
			_previousSignal = signal;
			return;
		}

		var previousMacd = _previousMacd.Value;
		var previousSignal = _previousSignal.Value;

		var crossedUp = previousMacd <= previousSignal && macd > signal;
		var crossedDown = previousMacd >= previousSignal && macd < signal;

		if (Position > 0 && crossedDown)
		{
			// Close long position on bearish crossover.
			SellMarket();
		}
		else if (Position < 0 && crossedUp)
		{
			// Close short position on bullish crossover.
			BuyMarket();
		}

		if (Position == 0)
		{
			var requiredCapital = MinimumCapitalPerVolume * VolumePerTrade;
			if (HasEnoughCapital(requiredCapital))
			{
				if (crossedUp && macd < 0m && signal < 0m)
				{
					// Enter long when MACD crosses above signal under the zero line.
					BuyMarket();
				}
				else if (crossedDown && macd > 0m && signal > 0m)
				{
					// Enter short when MACD crosses below signal above the zero line.
					SellMarket();
				}
			}
		}

		_previousMacd = macd;
		_previousSignal = signal;
	}

	private bool HasEnoughCapital(decimal requiredCapital)
	{
		var currentValue = Portfolio?.CurrentValue;

		if (currentValue is null)
			return true;

		if (currentValue.Value >= requiredCapital)
			return true;

		this.LogInfo($"Insufficient capital: available {currentValue.Value:F2}, required {requiredCapital:F2}.");
		return false;
	}
}