GitHub で見る

T3 MA Direction Change Strategy

Overview

This strategy reproduces the behavior of the original T3MA(barabashkakvn's edition) expert advisor. The Expert Advisor relies on the "T3MA-ALARM" indicator that applies exponential smoothing twice and raises a signal when the smoothed line changes direction. The StockSharp port keeps the same concept: it creates a double-smoothed exponential moving average (EMA of EMA) and trades whenever the slope of that curve flips from falling to rising or vice versa.

The strategy operates on finished candles only. Signals can be delayed by a configurable number of bars to mimic the original InpBarNumber option (default delay is one bar). Orders are placed using market execution so that the portfolio switches between long and short exposure without accumulating multiple concurrent hedged positions.

Trading Rules

  1. Subscribe to the configured candle series and calculate an EMA of the close prices. Run a second EMA on top of the first EMA output, producing the smoothed series used by the indicator.
  2. Compare the current value of the smoothed series (optionally shifted forward by EMA Shift) with the previous value. The slope is considered bullish when the series increases and bearish when it decreases.
  3. When the slope flips from bearish to bullish, enqueue a buy signal. When the slope flips from bullish to bearish, enqueue a sell signal. Neutral candles push a zero signal into the queue so that the delay counter remains accurate.
  4. After the configured Signal Delay number of completed candles passes, execute the queued signal. A delayed buy closes any open short position and enters long with the base Trade Volume. Likewise, a delayed sell closes a long position and enters short.
  5. Protective stop-loss and take-profit orders are initialized via StartProtection. Both distances are expressed in price steps so they automatically adapt to the selected instrument tick size.

Parameters

Name Description
EMA Length Length of the EMA used for both smoothing passes. This matches the MAPeriod input in the MetaTrader implementation.
EMA Shift Number of bars by which the smoothed EMA is shifted before comparing slopes. Equivalent to the indicator's MAShift.
Signal Delay Number of completed candles to wait before executing a signal. This mirrors InpBarNumber so a value of 1 trades the previous bar's signal.
Stop Loss (steps) Stop-loss distance measured in price steps. Set to zero to disable the stop-loss protection.
Take Profit (steps) Take-profit distance measured in price steps. Set to zero to disable the take-profit protection.
Trade Volume Base order size used for new entries. When reversing a position the strategy adds the current absolute position size to this value.
Candle Type Candle data type used for calculations (default: 5-minute time frame).

Risk Management

  • StartProtection automatically registers stop-loss and take-profit levels when the strategy starts. Both levels follow the instrument's tick size and remain active for the entire life of the strategy.
  • Position flips are executed using market orders. When the signal direction matches the current exposure, no additional trades are issued, preventing unwanted pyramiding.
  • Logging statements are emitted on every trade to keep track of the reason and the reference price taken from the source candle.

Differences from the MQL5 Version

  • The MetaTrader 5 expert required a hedging account and could accumulate multiple positions. The StockSharp version keeps a single net position and reverses it when the opposite signal fires.
  • Signal processing is candle-based and happens once per finished candle instead of on every tick, which is more natural within StockSharp's high-level API.
  • Stop-loss and take-profit management is handled via StartProtection instead of manually submitting SL/TP prices with each order.
  • English comments, structured parameters, and chart helpers are added for better readability in the StockSharp environment.

Usage Notes

  1. Attach the strategy to the desired security and ensure that the candle type matches the timeframe that was used when optimizing the original Expert Advisor.
  2. Adjust EMA Length and the risk parameters to fit the instrument volatility. Higher delays (Signal Delay) slow down responses and may filter noise.
  3. Because the strategy works with price steps, verify that the security's PriceStep property is configured correctly so the protective orders are placed at meaningful distances.
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);
}