Ver en GitHub

Estrategia EA Trix

Descripción general

La estrategia EA Trix replica la lógica del asesor experto de MetaTrader 5 que combina el indicador TRIX ARROWS con herramientas básicas de gestión de riesgo. El sistema espera que la triple media móvil exponencial (TRIX) y su línea de señal se crucen antes de entrar en nuevas posiciones. Puede reaccionar inmediatamente en la vela de señal o retrasar la ejecución hasta la siguiente barra, emulando el comportamiento original de "operar al cierre de barra".

Lógica de Trading

  1. Construir dos medias móviles exponenciales triple-suavizadas:
    • TRIX se calcula aplicando tres EMAs con la longitud TRIX EMA al cierre de la vela y tomando la tasa de cambio de un bar del tercer suavizado.
    • La línea de señal se calcula de la misma manera pero usa la longitud Signal EMA.
  2. Detectar cambios de dirección a través de cruces:
    • Cuando la línea de señal cruza por encima del TRIX, la estrategia prepara una entrada larga.
    • Cuando la línea de señal cruza por debajo del TRIX, prepara una entrada corta.
  3. Dependiendo del ajuste Trade On Close, la estrategia:
    • Ejecuta inmediatamente al precio de cierre de la barra de señal; o
    • Pone en cola la orden y la ejecuta en la apertura de la siguiente barra (coincidiendo con la opción del EA de MT5 para operar en barras cerradas).
  4. Antes de abrir una nueva posición, el algoritmo revierte automáticamente cualquier exposición contraria para que solo exista una posición neta a la vez.

Gestión de Posiciones

  • Stop loss – distancia fija opcional desde el precio de llenado. Se deshabilita cuando se establece en cero.
  • Take profit – objetivo de beneficio opcional. Se deshabilita cuando se establece en cero.
  • Break-even – una vez que el precio avanza a favor de la operación por la distancia seleccionada, el stop se mueve al precio de entrada.
  • Trailing stop – después de que el precio se mueve por la distancia de trailing, el stop sigue al precio con el incremento mínimo de Trailing Step seleccionado.
  • Las salidas protectoras se evalúan en cada vela completada usando los valores high/low de la vela. Cuando se activa una salida protectora, la posición se cierra con una orden de mercado.

Parámetros

Nombre Descripción
CandleType Tipo de datos (marco temporal) de las velas procesadas por la estrategia.
Volume Tamaño de posición usado para nuevas entradas. Las posiciones existentes se revierten automáticamente cuando es necesario.
EmaPeriod Longitud de las medias móviles exponenciales usadas para calcular la curva TRIX.
SignalPeriod Longitud de las medias móviles exponenciales usadas para calcular la curva de señal.
TradeOnCloseBar Si true, las entradas se ponen en cola y se ejecutan en la apertura de la siguiente barra. Si false, la ejecución ocurre inmediatamente al cierre de la barra de señal.
StopLoss Distancia desde el precio de entrada hasta el stop protector. Establecer en 0 para deshabilitar.
TakeProfit Distancia al objetivo de beneficio. Establecer en 0 para deshabilitar.
TrailingStop Distancia para que se active el trailing stop. Establecer en 0 para deshabilitar.
TrailingStep Incremento mínimo aplicado al actualizar el trailing stop.
BreakEven Distancia requerida para mover el stop al precio de entrada. Establecer en 0 para deshabilitar.

Notas de Uso

  • La estrategia se suscribe a un único feed de velas y se apoya exclusivamente en velas completadas según lo requerido por las directrices del API de alto nivel de StockSharp.
  • Las distancias predeterminadas de gestión de riesgo se expresan en unidades de precio. Ajustarlas de acuerdo con el tamaño del tick del instrumento operado.
  • Dado que las órdenes se envían via comandos de mercado, se asume que el precio de llenado es el cierre de la vela (o apertura para señales en cola) en backtests.

Notas de Conversión

  • El experto MQL5 original usa el indicador externo TRIX ARROWS (código 19056). La conversión reconstruye los mismos cálculos usando instancias de ExponentialMovingAverage de StockSharp y lógica de tasa de cambio sin depender de buffers personalizados.
  • La gestión de riesgo de MT5 dependía de órdenes stop y límite del lado del broker. En StockSharp, las salidas protectoras se replican monitoreando los extremos de velas y emitiendo órdenes de mercado.
  • Las alertas, notificaciones de sonido y parámetros específicos del broker se omitieron porque no son parte de la lógica de trading central.
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>
/// TRIX cross strategy based on the "TRIX ARROWS" expert advisor.
/// Opens a long position when the signal line crosses above TRIX and a short position on the opposite crossover.
/// Includes optional stop loss, take profit, break-even and trailing stop logic.
/// </summary>
public class EaTrixStrategy : Strategy
{
	private enum SignalDirections
	{
		Buy,
		Sell,
	}

	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _trailingStep;
	private readonly StrategyParam<decimal> _breakEven;
	private readonly StrategyParam<bool> _tradeOnCloseBar;
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _signalPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _trixEma1 = null!;
	private ExponentialMovingAverage _trixEma2 = null!;
	private ExponentialMovingAverage _trixEma3 = null!;
	private ExponentialMovingAverage _signalEma1 = null!;
	private ExponentialMovingAverage _signalEma2 = null!;
	private ExponentialMovingAverage _signalEma3 = null!;

	private decimal? _prevThirdTrix;
	private decimal? _prevThirdSignal;
	private decimal? _prevTrix;
	private decimal? _prevSignal;

	private SignalDirections? _pendingSignal;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

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

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

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

	/// <summary>
	/// Minimal step for trailing stop updates.
	/// </summary>
	public decimal TrailingStep
	{
		get => _trailingStep.Value;
		set => _trailingStep.Value = value;
	}

	/// <summary>
	/// Break-even trigger distance. The stop is moved to the entry price when the distance is reached.
	/// </summary>
	public decimal BreakEven
	{
		get => _breakEven.Value;
		set => _breakEven.Value = value;
	}

	/// <summary>
	/// Trade using signals confirmed on the previous closed bar.
	/// When disabled the strategy reacts immediately on the bar that generated the crossover.
	/// </summary>
	public bool TradeOnCloseBar
	{
		get => _tradeOnCloseBar.Value;
		set => _tradeOnCloseBar.Value = value;
	}

	/// <summary>
	/// EMA length used to build the TRIX series.
	/// </summary>
	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	/// <summary>
	/// EMA length used to build the signal series.
	/// </summary>
	public int SignalPeriod
	{
		get => _signalPeriod.Value;
		set => _signalPeriod.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="EaTrixStrategy"/>.
	/// </summary>
	public EaTrixStrategy()
	{
		_stopLoss = Param(nameof(StopLoss), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss distance", "Risk")
			;

		_takeProfit = Param(nameof(TakeProfit), 150m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance", "Risk")
			;

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

		_trailingStep = Param(nameof(TrailingStep), 1m)
			.SetNotNegative()
			.SetDisplay("Trailing Step", "Minimal trailing step", "Risk")
			;

		_breakEven = Param(nameof(BreakEven), 2m)
			.SetNotNegative()
			.SetDisplay("Break Even", "Break-even trigger distance", "Risk")
			;

		_tradeOnCloseBar = Param(nameof(TradeOnCloseBar), true)
			.SetDisplay("Trade On Close", "Confirm signals on closed bars", "General");

		_emaPeriod = Param(nameof(EmaPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("TRIX EMA", "TRIX EMA length", "Indicators")
			;

		_signalPeriod = Param(nameof(SignalPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Signal EMA", "Signal EMA length", "Indicators")
			;

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

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

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

		_prevThirdTrix = null;
		_prevThirdSignal = null;
		_prevTrix = null;
		_prevSignal = null;
		_pendingSignal = null;

		ClearPositionState();
	}

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

		// no protection

		_trixEma1 = new ExponentialMovingAverage { Length = EmaPeriod };
		_trixEma2 = new ExponentialMovingAverage { Length = EmaPeriod };
		_trixEma3 = new ExponentialMovingAverage { Length = EmaPeriod };

		_signalEma1 = new ExponentialMovingAverage { Length = SignalPeriod };
		_signalEma2 = new ExponentialMovingAverage { Length = SignalPeriod };
		_signalEma3 = new ExponentialMovingAverage { Length = SignalPeriod };

		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;

		HandlePendingSignal(candle);

		ManageActivePosition(candle);

		if (!TryCalculateIndicators(candle, out var trix, out var signal))
			return;

		if (_prevTrix is null || _prevSignal is null)
		{
			_prevTrix = trix;
			_prevSignal = signal;
			return;
		}

		if (!_trixEma3.IsFormed || !_signalEma3.IsFormed)
		{
			_prevTrix = trix;
			_prevSignal = signal;
			return;
		}

		var crossUp = _prevSignal < _prevTrix && signal > trix;
		var crossDown = _prevSignal > _prevTrix && signal < trix;

		if (crossUp)
		{
			if (TradeOnCloseBar)
				_pendingSignal = SignalDirections.Buy;
			else
				ExecuteSignal(SignalDirections.Buy, candle, candle.ClosePrice);
		}
		else if (crossDown)
		{
			if (TradeOnCloseBar)
				_pendingSignal = SignalDirections.Sell;
			else
				ExecuteSignal(SignalDirections.Sell, candle, candle.ClosePrice);
		}

		_prevTrix = trix;
		_prevSignal = signal;
	}

	private void HandlePendingSignal(ICandleMessage candle)
	{
		if (_pendingSignal is null)
			return;

		if (!_trixEma3.IsFormed || !_signalEma3.IsFormed)
			return;

		ExecuteSignal(_pendingSignal.Value, candle, candle.OpenPrice);
		_pendingSignal = null;
	}

	private void ExecuteSignal(SignalDirections direction, ICandleMessage candle, decimal fillPrice)
	{
		if (Volume <= 0m)
			return;

		var volume = Volume;

		switch (direction)
		{
			case SignalDirections.Buy:
				if (Position < 0m)
					volume += Math.Abs(Position);

				if (volume > 0m)
					BuyMarket();

				_entryPrice = fillPrice;
				_stopPrice = StopLoss > 0m ? fillPrice - StopLoss : null;
				_takePrice = TakeProfit > 0m ? fillPrice + TakeProfit : null;
				break;

			case SignalDirections.Sell:
				if (Position > 0m)
					volume += Position;

				if (volume > 0m)
					SellMarket();

				_entryPrice = fillPrice;
				_stopPrice = StopLoss > 0m ? fillPrice + StopLoss : null;
				_takePrice = TakeProfit > 0m ? fillPrice - TakeProfit : null;
				break;
		}
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0m && _entryPrice is decimal longEntry)
		{
			if (BreakEven > 0m && candle.HighPrice - longEntry >= BreakEven && (_stopPrice is null || _stopPrice < longEntry))
				_stopPrice = longEntry;

			if (TrailingStop > 0m)
			{
				var move = candle.HighPrice - longEntry;
				if (move >= TrailingStop)
				{
					var newStop = candle.HighPrice - TrailingStop;
					if (_stopPrice is null || newStop - _stopPrice >= TrailingStep)
						_stopPrice = newStop;
				}
			}

			if (_takePrice is decimal tp && candle.HighPrice >= tp)
			{
				SellMarket();
				ClearPositionState();
				return;
			}

			if (_stopPrice is decimal sl && candle.LowPrice <= sl)
			{
				SellMarket();
				ClearPositionState();
			}
		}
		else if (Position < 0m && _entryPrice is decimal shortEntry)
		{
			if (BreakEven > 0m && shortEntry - candle.LowPrice >= BreakEven && (_stopPrice is null || _stopPrice > shortEntry))
				_stopPrice = shortEntry;

			if (TrailingStop > 0m)
			{
				var move = shortEntry - candle.LowPrice;
				if (move >= TrailingStop)
				{
					var newStop = candle.LowPrice + TrailingStop;
					if (_stopPrice is null || _stopPrice - newStop >= TrailingStep)
						_stopPrice = newStop;
				}
			}

			if (_takePrice is decimal tp && candle.LowPrice <= tp)
			{
				BuyMarket();
				ClearPositionState();
				return;
			}

			if (_stopPrice is decimal sl && candle.HighPrice >= sl)
			{
				BuyMarket();
				ClearPositionState();
			}
		}
		else if (Position == 0m)
		{
			ClearPositionState();
		}
	}

	private bool TryCalculateIndicators(ICandleMessage candle, out decimal trix, out decimal signal)
	{
		trix = 0m;
		signal = 0m;

		var ema1 = _trixEma1.Process(new DecimalIndicatorValue(_trixEma1, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
		var ema2 = _trixEma2.Process(new DecimalIndicatorValue(_trixEma2, ema1, candle.OpenTime) { IsFinal = true }).ToDecimal();
		var ema3 = _trixEma3.Process(new DecimalIndicatorValue(_trixEma3, ema2, candle.OpenTime) { IsFinal = true }).ToDecimal();

		if (_prevThirdTrix is null)
		{
			_prevThirdTrix = ema3;
			return false;
		}

		trix = _prevThirdTrix != 0m ? (ema3 - _prevThirdTrix.Value) / _prevThirdTrix.Value : 0m;
		_prevThirdTrix = ema3;

		var signal1 = _signalEma1.Process(new DecimalIndicatorValue(_signalEma1, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
		var signal2 = _signalEma2.Process(new DecimalIndicatorValue(_signalEma2, signal1, candle.OpenTime) { IsFinal = true }).ToDecimal();
		var signalBase = _signalEma3.Process(new DecimalIndicatorValue(_signalEma3, signal2, candle.OpenTime) { IsFinal = true }).ToDecimal();

		if (_prevThirdSignal is null)
		{
			_prevThirdSignal = signalBase;
			return false;
		}

		signal = _prevThirdSignal != 0m ? (signalBase - _prevThirdSignal.Value) / _prevThirdSignal.Value : 0m;
		_prevThirdSignal = signalBase;

		return true;
	}

	private void ClearPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}
}