Ver en GitHub

Estrategia TTM Trend de Reapertura

Descripción general

Esta estrategia recrea la lógica del asesor experto de MetaTrader Exp_ttm-trend_ReOpen. Traslada el indicador TTM Trend al framework de StockSharp, usa el suavizado Heikin-Ashi para colorear velas y opera cuando el color cambia de bajista a alcista o viceversa. Cada cambio de color representa un cambio de régimen en la compresión/expansión de volatilidad, por lo que el bot cierra inmediatamente cualquier exposición opuesta y abre una posición en la nueva dirección.

Lógica del indicador

El indicador original colorea cada barra según tanto el cuerpo Heikin-Ashi como la vela OHLC clásica:

  • Verde brillante (4) – El cierre Heikin-Ashi está por encima de su apertura y la vela estándar cierra más alto de lo que abre.
  • Azul verdoso (3) – Heikin-Ashi es alcista pero la vela bruta cierra más bajo.
  • Rosa intenso (0) – Heikin-Ashi es bajista y la vela bruta cierra más bajo.
  • Púrpura (1) – Heikin-Ashi es bajista mientras la vela bruta cierra más alto.
  • Gris (2) – Fallback neutro si la tendencia no puede determinarse.

Para imitar el suavizado del buffer de MetaTrader, el indicador mantiene una ventana rodante (CompBars) de valores Heikin-Ashi anteriores. Si el último cuerpo permanece dentro del rango alto/bajo de cualquier vela almacenada, se reutiliza el color anterior. Esto previene whipsaws durante pequeños retrocesos, igual que la implementación fuente.

Reglas de trading

  1. Suscribirse al marco temporal configurado por CandleType y evaluar solo velas finalizadas (SignalBar selecciona cuántas barras cerradas observar desde el último punto histórico).
  2. Cuando aparece un color alcista (valores 1 o 4) y la señal anterior no era alcista:
    • Cerrar cualquier corto si EnableShortExits está activo.
    • Abrir una posición larga (o girar de corto a largo) si EnableLongEntries es verdadero.
  3. Cuando aparece un color bajista (valores 0 o 3) y la señal anterior no era bajista:
    • Cerrar cualquier largo si EnableLongExits está activo.
    • Abrir una posición corta (o girar de largo a corto) si EnableShortEntries es verdadero.
  4. Cada lado puede piramidear volumen adicional cuando el precio se mueve a favor de la operación por al menos PriceStepPoints (convertidos a precio usando el PriceStep del instrumento). El número acumulado de entradas por dirección está limitado por MaxPositions.

Comportamiento de pirámide

  • PriceStepPoints refleja el input "PriceStep" de MetaTrader: una vez que el beneficio no realizado excede esta distancia desde el precio medio de entrada, el bot añade el Volume base nuevamente.
  • MaxPositions limita el recuento total de entradas apiladas, incluyendo la operación inicial. Establecer en 1 para deshabilitar las reentradas completamente.

Gestión de riesgo

StopLossPoints y TakeProfitPoints se miden en puntos del instrumento, igual que en el EA original. Se transforman en distancias de precio absolutas via Security.PriceStep y se aplican a través de StartProtection. Establecer cualquiera de los parámetros en cero para deshabilitar la protección respectiva.

Parámetros

  • CandleType – marco temporal usado para el cálculo de TTM Trend (por defecto: velas de 4 horas).
  • CompBars – número de velas Heikin-Ashi históricas mantenidas para el suavizado de color (por defecto: 6).
  • SignalBar – número de barras atrás desde la última vela finalizada a evaluar (por defecto: 1 → última barra cerrada).
  • PriceStepPoints – movimiento favorable mínimo, en puntos, requerido antes de piramidear (por defecto: 300).
  • MaxPositions – número máximo de entradas acumuladas por dirección (por defecto: 10).
  • EnableLongEntries / EnableShortEntries – activar/desactivar aperturas largas/cortas en cambios de color.
  • EnableLongExits / EnableShortExits – activar/desactivar salidas forzadas cuando aparece el color opuesto.
  • StopLossPoints – distancia del stop protector en puntos (por defecto: 1000).
  • TakeProfitPoints – distancia del objetivo de ganancia en puntos (por defecto: 2000).

Notas de uso

  • La lógica de color TTM Trend es sensible al marco temporal elegido; el sistema original usaba el gráfico H4, pero puede suministrarse cualquier CandleType.
  • Dado que el indicador trabaja con cuerpos Heikin-Ashi, los gaps repentinos pueden no activar un cambio de color inmediatamente—esperar a la siguiente vela finalizada para confirmar.
  • Establecer PriceStepPoints en cero si se desea un sistema de entrada única que nunca piramidee.
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>
/// TTM Trend strategy with re-entry logic inspired by the MetaTrader expert.
/// Opens positions when the TTM Trend color flips and pyramids after price moves far enough.
/// </summary>
public class TtmTrendReopenStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _compBars;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<decimal> _priceStepPoints;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<bool> _enableLongEntries;
	private readonly StrategyParam<bool> _enableShortEntries;
	private readonly StrategyParam<bool> _enableLongExits;
	private readonly StrategyParam<bool> _enableShortExits;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;

	private TtmTrendIndicator _ttmIndicator;
	private readonly List<int> _colorHistory = new();
	private int _longEntries;
	private int _shortEntries;
	private decimal _entryPrice;

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of Heikin-Ashi comparison bars maintained by the indicator.
	/// </summary>
	public int CompBars
	{
		get => _compBars.Value;
		set => _compBars.Value = Math.Max(1, value);
	}

	/// <summary>
	/// Offset of the bar used for signal detection (0 = latest closed candle).
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = Math.Max(0, value);
	}

	/// <summary>
	/// Minimum favorable move in points before adding to an existing position.
	/// </summary>
	public decimal PriceStepPoints
	{
		get => _priceStepPoints.Value;
		set => _priceStepPoints.Value = Math.Max(0m, value);
	}

	/// <summary>
	/// Maximum number of entries per direction (including the first one).
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = Math.Max(1, value);
	}

	/// <summary>
	/// Allow opening new long positions on bullish colors.
	/// </summary>
	public bool EnableLongEntries
	{
		get => _enableLongEntries.Value;
		set => _enableLongEntries.Value = value;
	}

	/// <summary>
	/// Allow opening new short positions on bearish colors.
	/// </summary>
	public bool EnableShortEntries
	{
		get => _enableShortEntries.Value;
		set => _enableShortEntries.Value = value;
	}

	/// <summary>
	/// Allow closing long positions when a bearish color appears.
	/// </summary>
	public bool EnableLongExits
	{
		get => _enableLongExits.Value;
		set => _enableLongExits.Value = value;
	}

	/// <summary>
	/// Allow closing short positions when a bullish color appears.
	/// </summary>
	public bool EnableShortExits
	{
		get => _enableShortExits.Value;
		set => _enableShortExits.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in instrument points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = Math.Max(0m, value);
	}

	/// <summary>
	/// Take-profit distance expressed in instrument points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = Math.Max(0m, value);
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TtmTrendReopenStrategy"/>.
	/// </summary>
	public TtmTrendReopenStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for the TTM Trend calculation", "General");

		_compBars = Param(nameof(CompBars), 6)
		.SetGreaterThanZero()
		.SetDisplay("Comparison Bars", "Heikin-Ashi bars stored for the color smoothing", "Indicator")
		
		.SetOptimize(3, 12, 1);

		_signalBar = Param(nameof(SignalBar), 1)
		.SetNotNegative()
		.SetDisplay("Signal Bar", "Offset of the bar used for trading decisions", "Indicator")
		
		.SetOptimize(0, 3, 1);

		_priceStepPoints = Param(nameof(PriceStepPoints), 1000m)
		.SetNotNegative()
		.SetDisplay("Re-entry Step", "Minimum favorable move (in points) before pyramiding", "Risk Management")
		
		.SetOptimize(100m, 600m, 100m);

		_maxPositions = Param(nameof(MaxPositions), 1)
		.SetGreaterThanZero()
		.SetDisplay("Max Entries", "Maximum number of stacked entries per direction", "Risk Management")
		
		.SetOptimize(1, 10, 1);

		_enableLongEntries = Param(nameof(EnableLongEntries), true)
		.SetDisplay("Enable Long Entries", "Allow buying when the TTM Trend turns bullish", "Trading Rules");

		_enableShortEntries = Param(nameof(EnableShortEntries), true)
		.SetDisplay("Enable Short Entries", "Allow selling when the TTM Trend turns bearish", "Trading Rules");

		_enableLongExits = Param(nameof(EnableLongExits), true)
		.SetDisplay("Enable Long Exits", "Close longs on bearish colors", "Trading Rules");

		_enableShortExits = Param(nameof(EnableShortExits), true)
		.SetDisplay("Enable Short Exits", "Close shorts on bullish colors", "Trading Rules");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Protective stop distance in price points", "Risk Management")
		
		.SetOptimize(200m, 2000m, 200m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Profit target distance in price points", "Risk Management")
		
		.SetOptimize(500m, 4000m, 500m);
	}

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

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

		_colorHistory.Clear();
		_longEntries = 0;
		_shortEntries = 0;
		_entryPrice = 0m;
		_ttmIndicator = null!;
	}

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

		_colorHistory.Clear();
		_longEntries = 0;
		_shortEntries = 0;

		_ttmIndicator = new TtmTrendIndicator
		{
			CompBars = CompBars
		};

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

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

		var step = Security?.PriceStep ?? 1m;
		Unit stopLossUnit = StopLossPoints > 0m ? new Unit(StopLossPoints * step, UnitTypes.Absolute) : null;
		Unit takeProfitUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints * step, UnitTypes.Absolute) : null;

		if (stopLossUnit != null || takeProfitUnit != null)
			StartProtection(stopLossUnit, takeProfitUnit);
	}

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

		if (!ttmValue.IsFinal)
		return;

		var colorDecimal = ttmValue.GetValue<decimal>();
		var color = (int)Math.Round(colorDecimal);
		_colorHistory.Add(color);

		var offset = Math.Max(0, SignalBar - 1);
		var signalIndex = _colorHistory.Count - 1 - offset;
		if (signalIndex < 0)
		return;

		var currentColor = _colorHistory[signalIndex];
		int? previousColor = signalIndex > 0 ? _colorHistory[signalIndex - 1] : null;

		var isBullishColor = currentColor is 1 or 4;
		var isBearishColor = currentColor is 0 or 3;

		var wasBullish = previousColor.HasValue && (previousColor.Value == 1 || previousColor.Value == 4);
		var wasBearish = previousColor.HasValue && (previousColor.Value == 0 || previousColor.Value == 3);

		// Close existing positions before opening new ones.
		if (EnableLongExits && isBearishColor && Position > 0)
		{
			SellMarket(Position);
			_longEntries = 0;
		}

		if (EnableShortExits && isBullishColor && Position < 0)
		{
			BuyMarket(-Position);
			_shortEntries = 0;
		}

		// Open a fresh long when the color flips to bullish.
		if (EnableLongEntries && isBullishColor && previousColor.HasValue && !wasBullish && Position <= 0)
		{
			BuyMarket(Volume + (Position < 0 ? -Position : 0m));
			_longEntries = 1;
			_shortEntries = 0;
			_entryPrice = candle.ClosePrice;
		}
		// Open a fresh short when the color flips to bearish.
		else if (EnableShortEntries && isBearishColor && previousColor.HasValue && !wasBearish && Position >= 0)
		{
			SellMarket(Volume + (Position > 0 ? Position : 0m));
			_shortEntries = 1;
			_longEntries = 0;
			_entryPrice = candle.ClosePrice;
		}

		var step = Security?.PriceStep ?? 1m;
		var reentryStep = PriceStepPoints * step;

		// Add to an existing long once price moves in favor.
		if (EnableLongEntries && Position > 0 && reentryStep > 0m && _longEntries > 0 && _longEntries < MaxPositions)
		{
			var distance = candle.ClosePrice - _entryPrice;
			if (distance >= reentryStep)
			{
				BuyMarket(Volume);
				_longEntries++;
				_entryPrice = candle.ClosePrice;
			}
		}
		// Add to an existing short once price moves in favor.
		else if (EnableShortEntries && Position < 0 && reentryStep > 0m && _shortEntries > 0 && _shortEntries < MaxPositions)
		{
			var distance = _entryPrice - candle.ClosePrice;
			if (distance >= reentryStep)
			{
				SellMarket(Volume);
				_shortEntries++;
				_entryPrice = candle.ClosePrice;
			}
		}

		var keep = Math.Max(offset + 2, 3);
		if (_colorHistory.Count > keep)
		_colorHistory.RemoveRange(0, _colorHistory.Count - keep);
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			_longEntries = 0;
			_shortEntries = 0;
		}
	}

	/// <summary>
	/// Internal indicator reproducing the MetaTrader TTM Trend color output.
	/// </summary>
	private sealed class TtmTrendIndicator : BaseIndicator
	{
		private readonly List<TtmEntry> _history = [];
		private readonly object _sync = new();

		public int CompBars { get; set; } = 6;

		private decimal? _prevHaOpen;
		private decimal? _prevHaClose;

		/// <inheritdoc />
		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			lock (_sync)
			{
				ICandleMessage candle;
				try { candle = input.GetValue<ICandleMessage>(default); }
				catch { return new DecimalIndicatorValue(this, default, input.Time); }
				if (candle == null || candle.State != CandleStates.Finished)
					return new DecimalIndicatorValue(this, default, input.Time);

				var haClose = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
				decimal haOpen;

				if (_prevHaOpen is null || _prevHaClose is null)
				{
					haOpen = (candle.OpenPrice + candle.ClosePrice) / 2m;
				}
				else
				{
					haOpen = (_prevHaOpen.Value + _prevHaClose.Value) / 2m;
				}

				_prevHaOpen = haOpen;
				_prevHaClose = haClose;

				var color = CalculateBaseColor(candle, haOpen, haClose);

				foreach (var entry in _history)
				{
					var high = Math.Max(entry.HaOpen, entry.HaClose);
					var low = Math.Min(entry.HaOpen, entry.HaClose);

					if (haOpen <= high && haOpen >= low && haClose <= high && haClose >= low)
					{
						color = entry.Color;
						break;
					}
				}

				_history.Insert(0, new TtmEntry(haOpen, haClose, color));

				while (_history.Count > Math.Max(1, CompBars))
					_history.RemoveAt(_history.Count - 1);

				IsFormed = true;
				return new DecimalIndicatorValue(this, color, input.Time);
			}
		}

		/// <inheritdoc />
		public override void Reset()
		{
			lock (_sync)
			{
				base.Reset();
				_history.Clear();
				_prevHaOpen = null;
				_prevHaClose = null;
			}
		}

		private static int CalculateBaseColor(ICandleMessage candle, decimal haOpen, decimal haClose)
		{
			const int neutral = 2;

			if (haClose > haOpen)
			return candle.OpenPrice <= candle.ClosePrice ? 4 : 3;

			if (haClose < haOpen)
			return candle.OpenPrice > candle.ClosePrice ? 0 : 1;

			return neutral;
		}

		private readonly record struct TtmEntry(decimal HaOpen, decimal HaClose, int Color);
	}
}