Открыть на GitHub

Стратегия повторного входа по TTM Trend

Обзор

Стратегия переносит логику советника MetaTrader Exp_ttm-trend_ReOpen в инфраструктуру StockSharp. Индикатор TTM Trend рассчитывается через свечи Heikin-Ashi, каждая свеча раскрашивается в соответствии с направлением тела, и при смене цвета система закрывает противоположную позицию и открывает сделку в новом направлении. Таким образом ловится переход от сжатия волатильности к расширению и наоборот.

Работа индикатора

Окраска свечи зависит от комбинации Heikin-Ashi и обычной свечи:

  • Ярко‑зелёный (4) – Heikin-Ashi бычья, обычная свеча тоже закрылась выше открытия.
  • Бирюзовый (3) – Heikin-Ashi бычья, но классическая свеча закрылась ниже открытия.
  • Малиновый (0) – Heikin-Ashi медвежья, и классическая свеча закрылась ниже открытия.
  • Фиолетовый (1) – Heikin-Ashi медвежья, но классическая свеча закрылась выше открытия.
  • Серый (2) – нейтральное состояние, когда определить направление невозможно.

Чтобы воспроизвести сглаживание MetaTrader, индикатор хранит CompBars последних значений Heikin-Ashi. Если новое тело полностью укладывается в диапазон любой из сохранённых свечей, используется предыдущий цвет. Это подавляет мелкие откаты и совпадает с поведением оригинальной реализации.

Правила торговли

  1. Подписаться на свечи типа CandleType и работать только с завершёнными барами. Параметр SignalBar задаёт, сколько закрытых баров брать от конца истории.
  2. При появлении бычьего цвета (значения 1 или 4), которого не было на предыдущем сигнале:
    • Закрыть шорт, если включён EnableShortExits.
    • Открыть или перевернуться в лонг, если разрешён EnableLongEntries.
  3. При появлении медвежьего цвета (значения 0 или 3), которого не было на предыдущем сигнале:
    • Закрыть лонг, если включён EnableLongExits.
    • Открыть или перевернуться в шорт, если разрешён EnableShortEntries.
  4. При движении цены в прибыль на величину PriceStepPoints (переведённую в цену через PriceStep) стратегия добавляет ещё одну позицию базового объёма Volume. Общее число добавлений в одном направлении ограничено параметром MaxPositions.

Пирамидинг

  • PriceStepPoints повторяет одноимённый параметр советника: как только нереализованная прибыль превышает заданный шаг, добавляется следующий лот.
  • MaxPositions ограничивает суммарное количество входов в одном направлении (включая первый). Значение 1 полностью отключает повторные входы.

Управление рисками

StopLossPoints и TakeProfitPoints задаются в пунктах. Стратегия умножает их на Security.PriceStep и передаёт в StartProtection, формируя абсолютный уровень стоп-лосса и тейк-профита. Ноль отключает соответствующую защиту.

Параметры

  • CandleType – таймфрейм для расчёта TTM Trend (по умолчанию 4 часа).
  • CompBars – глубина истории Heikin-Ashi для сглаживания цвета (по умолчанию 6).
  • SignalBar – сколько закрытых баров отступить от последнего при анализе (по умолчанию 1 – последний закрытый бар).
  • PriceStepPoints – минимальное благоприятное движение в пунктах перед добавлением позиции (по умолчанию 300).
  • MaxPositions – максимум входов в одном направлении (по умолчанию 10).
  • EnableLongEntries / EnableShortEntries – разрешение на открытие лонгов/шортов при смене цвета.
  • EnableLongExits / EnableShortExits – разрешение на принудительное закрытие противоположных позиций.
  • StopLossPoints – расстояние стоп-лосса в пунктах (по умолчанию 1000).
  • TakeProfitPoints – расстояние тейк-профита в пунктах (по умолчанию 2000).

Практические советы

  • В оригинале использовался таймфрейм H4, однако вы можете выбрать любой CandleType.
  • Из-за работы с Heikin-Ashi резкие гэпы могут отразиться только на следующей закрытой свече.
  • Чтобы отключить пирамидинг, установите PriceStepPoints равным нулю.
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);
	}
}