Ver en GitHub

Estrategia T3 MA de Cambio de Dirección

Descripción general

Esta estrategia reproduce el comportamiento del asesor experto original T3MA(barabashkakvn's edition). El Asesor Experto depende del indicador "T3MA-ALARM" que aplica suavizado exponencial dos veces y genera una señal cuando la línea suavizada cambia de dirección. El port en StockSharp mantiene el mismo concepto: crea una media móvil exponencial de doble suavizado (EMA de EMA) y opera cada vez que la pendiente de esa curva cambia de bajante a creciente o viceversa.

La estrategia opera solo en velas finalizadas. Las señales pueden retrasarse un número configurable de barras para imitar la opción original InpBarNumber (el retardo predeterminado es una barra). Las órdenes se colocan usando ejecución de mercado para que la cartera cambie entre exposición larga y corta sin acumular múltiples posiciones hedgeadas concurrentes.

Reglas de trading

  1. Suscribirse a la serie de velas configurada y calcular una EMA de los precios de cierre. Ejecutar una segunda EMA sobre el resultado de la primera EMA, produciendo la serie suavizada utilizada por el indicador.
  2. Comparar el valor actual de la serie suavizada (opcionalmente desplazado hacia adelante por EMA Shift) con el valor anterior. La pendiente se considera alcista cuando la serie aumenta y bajista cuando disminuye.
  3. Cuando la pendiente cambia de bajista a alcista, encolar una señal de compra. Cuando cambia de alcista a bajista, encolar una señal de venta. Las velas neutras insertan una señal cero en la cola para que el contador de retardo permanezca preciso.
  4. Después de que pase el número configurado de velas completadas Signal Delay, ejecutar la señal encolada. Una compra retrasada cierra cualquier posición corta abierta y entra largo con el Trade Volume base. Igualmente, una venta retrasada cierra una posición larga y entra corto.
  5. Las órdenes de stop-loss y take-profit protectoras se inicializan via StartProtection. Ambas distancias se expresan en pasos de precio para adaptarse automáticamente al tamaño del tick del instrumento seleccionado.

Parámetros

Nombre Descripción
EMA Length Longitud de la EMA utilizada para ambos pasos de suavizado. Corresponde al parámetro MAPeriod en la implementación de MetaTrader.
EMA Shift Número de barras por las que la EMA suavizada se desplaza antes de comparar pendientes. Equivalente al MAShift del indicador.
Signal Delay Número de velas completadas a esperar antes de ejecutar una señal. Refleja InpBarNumber, por lo que un valor de 1 opera la señal de la barra anterior.
Stop Loss (steps) Distancia del stop-loss medida en pasos de precio. Establecer en cero para desactivar la protección.
Take Profit (steps) Distancia del take-profit medida en pasos de precio. Establecer en cero para desactivar.
Trade Volume Tamaño de orden base usado para nuevas entradas. Al revertir una posición, la estrategia agrega el tamaño absoluto de la posición actual a este valor.
Candle Type Tipo de datos de vela usado para cálculos (por defecto: marco temporal de 5 minutos).

Gestión de riesgo

  • StartProtection registra automáticamente niveles de stop-loss y take-profit cuando comienza la estrategia. Ambos niveles siguen el tamaño del tick del instrumento y permanecen activos durante toda la vida de la estrategia.
  • Los giros de posición se ejecutan usando órdenes de mercado. Cuando la dirección de la señal coincide con la exposición actual, no se emiten operaciones adicionales, evitando pirámides no deseadas.
  • Los registros se emiten en cada operación para rastrear la razón y el precio de referencia tomado de la vela fuente.

Diferencias con la versión MQL5

  • MetaTrader 5 requería una cuenta de hedging y podía acumular múltiples posiciones. La versión StockSharp mantiene una sola posición neta y la revierte cuando se activa la señal opuesta.
  • El procesamiento de señales está basado en velas y ocurre una vez por vela finalizada en lugar de en cada tick, lo cual es más natural dentro de la API de alto nivel de StockSharp.
  • La gestión de stop-loss y take-profit se maneja via StartProtection en lugar de enviar manualmente precios SL/TP con cada orden.
  • Se añadieron comentarios en inglés, parámetros estructurados y asistentes de gráficos para mejor legibilidad en el entorno StockSharp.

Notas de uso

  1. Adjuntar la estrategia al instrumento deseado y asegurarse de que el tipo de vela coincide con el marco temporal que se usó al optimizar el Asesor Experto original.
  2. Ajustar EMA Length y los parámetros de riesgo para adaptarse a la volatilidad del instrumento. Los retrasos mayores (Signal Delay) ralentizan las respuestas y pueden filtrar ruido.
  3. Dado que la estrategia trabaja con pasos de precio, verificar que la propiedad PriceStep del instrumento esté configurada correctamente para que las órdenes protectoras se coloquen a distancias significativas.
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Strategy that trades when a double-smoothed EMA changes its slope direction.
/// </summary>
public class T3MaDirectionChangeStrategy : Strategy
{
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<int> _signalBarOffset;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _signalCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _recentSmoothed = new();
	private readonly Queue<SignalInfo> _pendingSignals = new();
	private ExponentialMovingAverage _emaPrice;
	private ExponentialMovingAverage _emaSmooth;
	private int _previousDirection;
	private int _cooldownRemaining;

	public int MaLength { get => _maLength.Value; set => _maLength.Value = value; }
	public int MaShift { get => _maShift.Value; set => _maShift.Value = value; }
	public int SignalBarOffset { get => _signalBarOffset.Value; set => _signalBarOffset.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
	public decimal TradeVolume { get => _tradeVolume.Value; set => _tradeVolume.Value = value; }
	public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public T3MaDirectionChangeStrategy()
	{
		_maLength = Param(nameof(MaLength), 4)
			.SetGreaterThanZero()
			.SetDisplay("EMA Length", "Length of the EMA used for the double smoothing", "Indicator");

		_maShift = Param(nameof(MaShift), 0)
			.SetNotNegative()
			.SetDisplay("EMA Shift", "Shift applied to the smoothed EMA when evaluating slope changes", "Indicator");

		_signalBarOffset = Param(nameof(SignalBarOffset), 1)
			.SetNotNegative()
			.SetDisplay("Signal Delay", "How many completed candles to wait before acting on a signal", "Trading rules");

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

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

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base volume used for entries", "Trading rules");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait after entries and exits", "Trading rules");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_emaPrice = null;
		_emaSmooth = null;
		_recentSmoothed.Clear();
		_pendingSignals.Clear();
		_previousDirection = 0;
		_cooldownRemaining = 0;
		Volume = TradeVolume;
	}

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

		Volume = TradeVolume;
		_emaPrice = new EMA { Length = MaLength };
		_emaSmooth = new EMA { Length = MaLength };
		_recentSmoothed.Clear();
		_pendingSignals.Clear();
		_previousDirection = 0;
		_cooldownRemaining = 0;

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

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

		var slUnit = StopLossPoints > 0m ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
		var tpUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
		StartProtection(slUnit, tpUnit);
	}

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var emaPriceValue = _emaPrice.Process(new DecimalIndicatorValue(_emaPrice, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var emaSmoothValue = _emaSmooth.Process(emaPriceValue);
		if (!emaSmoothValue.IsFormed)
			return;

		AddSmoothedValue(emaSmoothValue.ToDecimal(), MaShift + 2);
		if (_recentSmoothed.Count < MaShift + 2)
		{
			EnqueueSignal(new SignalInfo(0));
			return;
		}

		var currentIndex = _recentSmoothed.Count - 1 - MaShift;
		var previousIndex = _recentSmoothed.Count - 2 - MaShift;
		var current = _recentSmoothed[currentIndex];
		var previous = _recentSmoothed[previousIndex];
		var direction = _previousDirection;

		if (current > previous)
			direction = 1;
		else if (current < previous)
			direction = -1;

		var signal = 0;
		if (_previousDirection == -1 && direction == 1)
			signal = 1;
		else if (_previousDirection == 1 && direction == -1)
			signal = -1;

		_previousDirection = direction;
		EnqueueSignal(new SignalInfo(signal));
	}

	private void AddSmoothedValue(decimal value, int limit)
	{
		_recentSmoothed.Add(value);
		if (_recentSmoothed.Count > limit)
			_recentSmoothed.RemoveAt(0);
	}

	private void EnqueueSignal(SignalInfo signal)
	{
		_pendingSignals.Enqueue(signal);

		while (_pendingSignals.Count > SignalBarOffset)
		{
			var readySignal = _pendingSignals.Dequeue();
			ExecuteSignal(readySignal);
		}
	}

	private void ExecuteSignal(SignalInfo signal)
	{
		if (signal.Direction == 0 || _cooldownRemaining > 0)
			return;

		if (signal.Direction > 0 && Position <= 0)
		{
			var volume = Volume + Math.Abs(Position);
			BuyMarket(volume);
			_cooldownRemaining = SignalCooldownBars;
		}
		else if (signal.Direction < 0 && Position >= 0)
		{
			var volume = Volume + Math.Abs(Position);
			SellMarket(volume);
			_cooldownRemaining = SignalCooldownBars;
		}
	}

	private readonly record struct SignalInfo(int Direction);
}