在 GitHub 上查看

T3 MA 方向转折策略

概述

本策略复刻了原始的 T3MA(barabashkakvn's edition) 智能交易系统。原版 EA 使用 "T3MA-ALARM" 指标,对收盘价进行两次指数平滑,并在平滑曲线方向改变时给出信号。StockSharp 版本沿用了相同的思想:计算一条“EMA 的 EMA”,并在其斜率由下行转为上行或由上行转为下行时进行交易。

策略只在完整收盘的 K 线数据上工作。为了模拟原始参数 InpBarNumber,信号可以延迟若干根 K 线(默认延迟 1 根)。下单采用市价方式,使账户在多空之间切换,而不会像对冲账户那样累积多笔同向持仓。

交易规则

  1. 订阅所选的 K 线,并对收盘价计算 EMA;再对前一步的结果再次计算 EMA,得到用于判断的平滑序列。
  2. 将当前平滑值(可选地通过 EMA Shift 向前移动若干根)与上一根的值比较:上升代表多头斜率,下降代表空头斜率。
  3. 当斜率由空头转为多头时,加入 买入 信号;当斜率由多头转为空头时,加入 卖出 信号。没有方向变化的 K 线会往队列里压入零值,以保持延迟计数准确。
  4. 等待设定的 Signal Delay 根 K 线之后,执行队列中的信号。延迟的买入会平掉现有空单并按 Trade Volume 开多;延迟的卖出会平掉多单并开空。
  5. 通过 StartProtection 初始化止损和止盈,两者都以价格最小变动单位(price step)表示,会随标的的最小跳动值自动调整。

参数

名称 说明
EMA Length 两次平滑所使用的 EMA 长度,对应原始指标中的 MAPeriod
EMA Shift 在比较斜率之前,平滑序列向前移动的柱数,对应 MAShift
Signal Delay 执行信号前需要等待的完整 K 线数量,对应 InpBarNumber,值为 1 时表示使用上一根 K 线的信号。
Stop Loss (steps) 止损距离(价格步长),为 0 表示关闭止损。
Take Profit (steps) 止盈距离(价格步长),为 0 表示关闭止盈。
Trade Volume 新开仓时使用的基础手数。在反向开仓时会自动加上当前持仓的绝对值。
Candle Type 用于计算的 K 线类型(默认 5 分钟)。

风险控制

  • StartProtection 在策略启动时自动注册止损与止盈,并始终跟随品种的最小价格步长。
  • 信号执行时使用市价单。如果信号方向与当前持仓一致,则不会再加仓,避免无意的金字塔式累积。
  • 每次下单都会输出日志,记录触发原因以及参考价格,方便复盘。

与 MQL5 版本的区别

  • 原版 EA 需要对冲账户,可能同时持有多笔同方向仓位;StockSharp 版本采用净头寸模式,收到反向信号时直接平仓并反手。
  • 信号处理基于已完成的 K 线,而不是逐笔 tick,更符合 StockSharp 的高级 API 工作方式。
  • 止损/止盈通过 StartProtection 统一管理,无需像原版那样在每笔订单里设置 SL/TP。
  • 代码加入了英文注释、参数分组以及图表辅助,便于在 StockSharp 环境下阅读和调试。

使用建议

  1. 将策略附加到目标证券,并确保 K 线类型与原策略优化所用的周期一致。
  2. 根据品种波动性调整 EMA Length 及风险参数。适当增大 Signal Delay 可以过滤噪声,但也会降低响应速度。
  3. 策略依赖 PriceStep 计算价格步长,请确认证券的该属性已正确设置,以免保护性挂单距离异常。
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);
}