GitHub で見る

TTM Trend Re-Entry Strategy

Overview

This strategy recreates the logic of the MetaTrader expert advisor Exp_ttm-trend_ReOpen. It translates the TTM Trend indicator into the StockSharp framework, uses Heikin-Ashi smoothing to color candles, and trades when the color flips from bearish to bullish or vice versa. Each color change represents a regime shift in volatility compression/expansion, so the bot immediately closes any opposing exposure and opens a position in the new direction.

Indicator Logic

The original indicator colors each bar according to both the Heikin-Ashi body and the classic OHLC candle:

  • Bright green (4) – Heikin-Ashi close above its open and the standard candle closes higher than it opens.
  • Teal (3) – Heikin-Ashi is bullish but the raw candle closes lower.
  • Deep pink (0) – Heikin-Ashi is bearish and the raw candle closes lower.
  • Purple (1) – Heikin-Ashi is bearish while the raw candle closes higher.
  • Gray (2) – Neutral fallback if the trend cannot be determined.

To mimic the MetaTrader buffer smoothing, the indicator keeps a rolling window (CompBars) of previous Heikin-Ashi values. If the latest body remains inside the high/low envelope of any stored candle, the previous color is reused. This prevents whipsaws during small pullbacks, just like the source implementation.

Trading Rules

  1. Subscribe to the timeframe configured by CandleType and evaluate only finished candles (SignalBar selects how many closed bars to look back from the latest history point).
  2. When a bullish color (values 1 or 4) appears and the previous signal was not bullish:
    • Close any short if EnableShortExits is active.
    • Open a long position (or flip from short to long) if EnableLongEntries is true.
  3. When a bearish color (values 0 or 3) appears and the previous signal was not bearish:
    • Close any long if EnableLongExits is active.
    • Open a short position (or flip from long to short) if EnableShortEntries is true.
  4. Each side can pyramid additional volume whenever price moves in the trade’s favor by at least PriceStepPoints (converted to price using the instrument’s PriceStep). The cumulative number of entries per direction is capped by MaxPositions.

Pyramiding Behaviour

  • PriceStepPoints mirrors the MetaTrader “PriceStep” input: once unrealized profit exceeds this distance from the average entry price, the bot adds the base Volume again.
  • MaxPositions limits the total count of stacked entries, including the initial trade. Set it to 1 to disable re-entries entirely.

Risk Management

StopLossPoints and TakeProfitPoints are measured in instrument points, just like in the original EA. They are transformed into absolute price distances via Security.PriceStep and applied through StartProtection. Set either parameter to zero to disable the respective protection leg.

Parameters

  • CandleType – timeframe used for the TTM Trend calculation (default: 4-hour candles).
  • CompBars – number of historical Heikin-Ashi candles kept for color smoothing (default: 6).
  • SignalBar – number of bars back from the latest finished candle to evaluate (default: 1 → last closed bar).
  • PriceStepPoints – minimum favorable move, in points, required before pyramiding (default: 300).
  • MaxPositions – maximum number of cumulative entries per direction (default: 10).
  • EnableLongEntries / EnableShortEntries – toggle long/short openings on color flips.
  • EnableLongExits / EnableShortExits – toggle forced exits when the opposite color appears.
  • StopLossPoints – protective stop distance in points (default: 1000).
  • TakeProfitPoints – profit target distance in points (default: 2000).

Usage Notes

  • The TTM Trend color logic is sensitive to the chosen timeframe; the original system used the H4 chart, but any CandleType can be supplied.
  • Because the indicator works with Heikin-Ashi bodies, sudden gaps may not trigger a color flip immediately—wait for the next finished candle to confirm.
  • Set PriceStepPoints to zero if you want a single-entry system that never pyramids.
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);
	}
}