Ver no GitHub

Estratégia TTM Trend de Reabertura

Visão geral

Esta estratégia recria a lógica do consultor especialista do MetaTrader Exp_ttm-trend_ReOpen. Traduz o indicador TTM Trend para o framework StockSharp, usa o suavizado Heikin-Ashi para colorir velas e opera quando a cor muda de baixista para altista ou vice-versa. Cada mudança de cor representa uma mudança de regime na compressão/expansão de volatilidade, portanto o bot fecha imediatamente qualquer exposição oposta e abre uma posição na nova direção.

Lógica do indicador

O indicador original colore cada barra de acordo com o corpo Heikin-Ashi e a vela OHLC clássica:

  • Verde brilhante (4) – O fechamento Heikin-Ashi está acima de sua abertura e a vela padrão fecha mais alto do que abre.
  • Azul-petróleo (3) – Heikin-Ashi está altista mas a vela bruta fecha mais baixo.
  • Rosa intenso (0) – Heikin-Ashi está baixista e a vela bruta fecha mais baixo.
  • Roxo (1) – Heikin-Ashi está baixista enquanto a vela bruta fecha mais alto.
  • Cinza (2) – Fallback neutro se a tendência não puder ser determinada.

Para imitar o suavizado do buffer do MetaTrader, o indicador mantém uma janela deslizante (CompBars) de valores Heikin-Ashi anteriores. Se o último corpo permanece dentro do envelope alto/baixo de qualquer vela armazenada, a cor anterior é reutilizada. Isso previne whipsaws durante pequenos recuos, assim como a implementação fonte.

Regras de trading

  1. Assinar o período configurado por CandleType e avaliar apenas velas finalizadas (SignalBar seleciona quantas barras fechadas observar a partir do último ponto histórico).
  2. Quando uma cor altista (valores 1 ou 4) aparece e o sinal anterior não era altista:
    • Fechar qualquer vendido se EnableShortExits estiver ativo.
    • Abrir uma posição comprada (ou girar de vendido para comprado) se EnableLongEntries for verdadeiro.
  3. Quando uma cor baixista (valores 0 ou 3) aparece e o sinal anterior não era baixista:
    • Fechar qualquer comprado se EnableLongExits estiver ativo.
    • Abrir uma posição vendida (ou girar de comprado para vendido) se EnableShortEntries for verdadeiro.
  4. Cada lado pode piramidear volume adicional quando o preço se move a favor da operação por pelo menos PriceStepPoints (convertidos para preço usando o PriceStep do instrumento). O número acumulado de entradas por direção é limitado por MaxPositions.

Comportamento de pirâmide

  • PriceStepPoints reflete o input "PriceStep" do MetaTrader: assim que o lucro não realizado excede esta distância do preço médio de entrada, o bot adiciona o Volume base novamente.
  • MaxPositions limita a contagem total de entradas empilhadas, incluindo a operação inicial. Definir como 1 para desabilitar as reentradas completamente.

Gestão de risco

StopLossPoints e TakeProfitPoints são medidos em pontos do instrumento, igual ao EA original. Eles são transformados em distâncias de preço absolutas via Security.PriceStep e aplicados através de StartProtection. Definir qualquer parâmetro como zero para desabilitar a proteção respectiva.

Parâmetros

  • CandleType – período usado para o cálculo de TTM Trend (padrão: velas de 4 horas).
  • CompBars – número de velas Heikin-Ashi históricas mantidas para suavização de cor (padrão: 6).
  • SignalBar – número de barras atrás da última vela finalizada a avaliar (padrão: 1 → última barra fechada).
  • PriceStepPoints – movimento favorável mínimo, em pontos, necessário antes de piramidear (padrão: 300).
  • MaxPositions – número máximo de entradas acumuladas por direção (padrão: 10).
  • EnableLongEntries / EnableShortEntries – ativar/desativar aberturas compradas/vendidas em mudanças de cor.
  • EnableLongExits / EnableShortExits – ativar/desativar saídas forçadas quando a cor oposta aparece.
  • StopLossPoints – distância do stop protetor em pontos (padrão: 1000).
  • TakeProfitPoints – distância do objetivo de lucro em pontos (padrão: 2000).

Notas de uso

  • A lógica de cor TTM Trend é sensível ao período escolhido; o sistema original usava o gráfico H4, mas qualquer CandleType pode ser fornecido.
  • Como o indicador trabalha com corpos Heikin-Ashi, gaps repentinos podem não acionar uma mudança de cor imediatamente — aguardar a próxima vela finalizada para confirmar.
  • Definir PriceStepPoints como zero se desejar um sistema de entrada única que nunca piramideia.
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);
	}
}