T3 MA 方向转折策略
概述
本策略复刻了原始的 T3MA(barabashkakvn's edition) 智能交易系统。原版 EA 使用 "T3MA-ALARM" 指标,对收盘价进行两次指数平滑,并在平滑曲线方向改变时给出信号。StockSharp 版本沿用了相同的思想:计算一条“EMA 的 EMA”,并在其斜率由下行转为上行或由上行转为下行时进行交易。
策略只在完整收盘的 K 线数据上工作。为了模拟原始参数 InpBarNumber,信号可以延迟若干根 K 线(默认延迟 1 根)。下单采用市价方式,使账户在多空之间切换,而不会像对冲账户那样累积多笔同向持仓。
交易规则
- 订阅所选的 K 线,并对收盘价计算 EMA;再对前一步的结果再次计算 EMA,得到用于判断的平滑序列。
- 将当前平滑值(可选地通过
EMA Shift向前移动若干根)与上一根的值比较:上升代表多头斜率,下降代表空头斜率。 - 当斜率由空头转为多头时,加入 买入 信号;当斜率由多头转为空头时,加入 卖出 信号。没有方向变化的 K 线会往队列里压入零值,以保持延迟计数准确。
- 等待设定的
Signal Delay根 K 线之后,执行队列中的信号。延迟的买入会平掉现有空单并按Trade Volume开多;延迟的卖出会平掉多单并开空。 - 通过
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 环境下阅读和调试。
使用建议
- 将策略附加到目标证券,并确保 K 线类型与原策略优化所用的周期一致。
- 根据品种波动性调整
EMA Length及风险参数。适当增大Signal Delay可以过滤噪声,但也会降低响应速度。 - 策略依赖
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);
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class t3_ma_direction_change_strategy(Strategy):
"""Double-smoothed EMA slope direction change with signal delay and StartProtection."""
def __init__(self):
super(t3_ma_direction_change_strategy, self).__init__()
self._ma_length = self.Param("MaLength", 4).SetGreaterThanZero().SetDisplay("EMA Length", "Length of EMA for double smoothing", "Indicator")
self._ma_shift = self.Param("MaShift", 0).SetNotNegative().SetDisplay("EMA Shift", "Shift applied to smoothed EMA", "Indicator")
self._signal_bar_offset = self.Param("SignalBarOffset", 1).SetNotNegative().SetDisplay("Signal Delay", "Candles to wait before acting on signal", "Trading rules")
self._sl_points = self.Param("StopLossPoints", 20.0).SetNotNegative().SetDisplay("Stop Loss (steps)", "SL distance in price steps", "Risk management")
self._tp_points = self.Param("TakeProfitPoints", 125.0).SetNotNegative().SetDisplay("Take Profit (steps)", "TP distance in price steps", "Risk management")
self._cooldown = self.Param("SignalCooldownBars", 12).SetGreaterThanZero().SetDisplay("Signal Cooldown", "Bars to wait after entries/exits", "Trading rules")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Type of candles", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(t3_ma_direction_change_strategy, self).OnReseted()
self._recent_smoothed = []
self._pending_signals = []
self._prev_direction = 0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(t3_ma_direction_change_strategy, self).OnStarted2(time)
self._recent_smoothed = []
self._pending_signals = []
self._prev_direction = 0
self._cooldown_remaining = 0
self._ema_price = ExponentialMovingAverage()
self._ema_price.Length = self._ma_length.Value
self._ema_smooth = ExponentialMovingAverage()
self._ema_smooth.Length = self._ma_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
sl_val = float(self._sl_points.Value)
tp_val = float(self._tp_points.Value)
sl_unit = Unit(sl_val, UnitTypes.Absolute) if sl_val > 0 else None
tp_unit = Unit(tp_val, UnitTypes.Absolute) if tp_val > 0 else None
self.StartProtection(sl_unit, tp_unit)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
ema_price_result = process_float(self._ema_price, candle.ClosePrice, candle.OpenTime, True)
ema_smooth_result = self._ema_smooth.Process(ema_price_result)
if not ema_smooth_result.IsFormed:
self._enqueue_signal(0)
return
smoothed_val = float(ema_smooth_result)
shift = self._ma_shift.Value
required = shift + 2
self._recent_smoothed.append(smoothed_val)
if len(self._recent_smoothed) > required:
self._recent_smoothed.pop(0)
if len(self._recent_smoothed) < required:
self._enqueue_signal(0)
return
current_idx = len(self._recent_smoothed) - 1 - shift
prev_idx = len(self._recent_smoothed) - 2 - shift
current = self._recent_smoothed[current_idx]
previous = self._recent_smoothed[prev_idx]
if current > previous:
direction = 1
elif current < previous:
direction = -1
else:
direction = self._prev_direction
signal = 0
if self._prev_direction == -1 and direction == 1:
signal = 1
elif self._prev_direction == 1 and direction == -1:
signal = -1
self._prev_direction = direction
self._enqueue_signal(signal)
def _enqueue_signal(self, signal):
self._pending_signals.append(signal)
offset = self._signal_bar_offset.Value
while len(self._pending_signals) > offset:
ready = self._pending_signals.pop(0)
self._execute_signal(ready)
def _execute_signal(self, direction):
if direction == 0 or self._cooldown_remaining > 0:
return
if direction > 0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self._cooldown.Value
elif direction < 0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self._cooldown.Value
def CreateClone(self):
return t3_ma_direction_change_strategy()