在 GitHub 上查看
2526 TDI-2 再开仓策略
概述
该策略是 MetaTrader 5 专家顾问 Exp_TDI-2_ReOpen 的 C# 版本。它使用趋势方向指数(TDI-2)指标,并保留原始 EA 的加仓与再入场逻辑。移植后的策略依靠 StockSharp 高阶 API,在 TDI 动量线与指数线之间发生交叉时做出反应,在价格沿趋势运行并满足设定距离后追加仓位,并可通过保护性止损/止盈来管理仓位。
指标
- TDI-2 指标 – 在仓库中实现的自定义动量指标,生成两条曲线:
- 动量线:
周期 × 平滑动量,其中动量等于所选价格减去 周期 根之前的价格。
- 指数线:
|动量线| − (2 × 周期 × 平滑(|动量|, 2×周期) − |动量|)。
- 支持的平滑方法:简单、指数、平滑(RMA)以及线性加权移动平均。
- 支持的价格类型完全复刻 MQL 版本,包括 TrendFollow 与 Demark 公式。
交易逻辑
- 每根完成的 K 线都会读取 信号柱(默认上一根已收盘的 K 线)及其前一根的 TDI-2 数值。
- 当动量线先位于指数线上方、随后下穿指数线时:
- 如果启用了 允许做多建仓,且当前没有多头仓位,则准备新的多头交易。
- 若存在空头仓位且启用了 允许空头平仓,则关闭空头。
- 当动量线先位于指数线下方、随后上穿指数线时:
- 如果启用了 允许做空建仓,且当前没有空头仓位,则准备新的空头交易。
- 若存在多头仓位且启用了 允许多头平仓,则关闭多头。
- 再入场(加仓)逻辑:
- 持有多头时会跟踪最近一次多头成交价。当价格向有利方向移动至少 加仓步长(点),且多头成交次数未超过 最大入场次数 时,以基础手数追加多头。
- 空头仓位使用同样的规则,比较最近一笔空头成交价。
- 当需要反手时,策略会发送一笔合并市价单,用以先平掉反向仓位,再以基础手数建立新方向仓位。
- 可选的止损和止盈通过
StartProtection 激活,距离会自动乘以标的的 PriceStep。
参数
| 名称 |
说明 |
默认值 |
| Money Management |
每笔下单的基础手数。 |
0.1 |
| Max Entries |
单方向允许的最大入场次数(含初始建仓)。 |
10 |
| Stop Loss (points) |
止损距离(以合约最小变动点为单位)。 |
1000 |
| Take Profit (points) |
止盈距离(以合约最小变动点为单位)。 |
2000 |
| Slippage (points) |
为兼容保留的滑点参数,高阶 API 未使用。 |
10 |
| Re-entry Step (points) |
触发加仓所需的最小有利点数。 |
300 |
| Allow Long/Short Entries |
是否允许开多/开空。 |
true |
| Allow Long/Short Exits |
是否允许平多/平空。 |
true |
| Candle Type |
用于计算的 K 线数据。 |
H4 K 线 |
| TDI Smoothing |
TDI-2 使用的平滑方法。 |
简单均线 |
| TDI Period |
动量回溯周期。 |
20 |
| TDI Phase |
为兼容保留,对当前平滑方法无影响。 |
15 |
| Applied Price |
TDI-2 使用的价格类型。 |
收盘价 |
| Signal Bar |
判断交叉时所回看的收盘 K 线数量。 |
1 |
额外说明
- 目前仅实现 SMA、EMA、SMMA 与 LWMA 四种平滑方式,其他 MQL 特有方法(如 JJMA、T3)暂不支持。
- TDI Phase 参数仅为兼容旧版界面而保留,在可用的平滑方式下不会产生效果。
- Slippage (points) 参数不会传递给下单函数,但保留以保持与原 EA 的参数结构一致。
- 当净头寸归零时,加仓计数器会自动重置。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trend Direction Index re-entry strategy.
/// Trades based on crossings between the TDI momentum line and the TDI index line.
/// </summary>
public class Tdi2ReOpenStrategy : Strategy
{
private readonly StrategyParam<int> _tdiPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal? _lastClose;
private decimal? _directional;
private decimal? _index;
private decimal? _prevDirectional;
private decimal? _prevIndex;
public int TdiPeriod { get => _tdiPeriod.Value; set => _tdiPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public Tdi2ReOpenStrategy()
{
_tdiPeriod = Param(nameof(TdiPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("TDI Period", "Momentum lookback period", "Indicator")
.SetOptimize(5, 30, 5);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Data series", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_lastClose = null;
_directional = null;
_index = null;
_prevDirectional = null;
_prevIndex = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_lastClose = null;
_directional = null;
_index = null;
_prevDirectional = null;
_prevIndex = null;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (_lastClose is not decimal lastClose)
{
_lastClose = close;
return;
}
var momentum = close - lastClose;
_lastClose = close;
var alpha = 2m / (TdiPeriod + 1m);
if (_directional is not decimal prevDirectionalLine || _index is not decimal prevIndexLine)
{
_directional = momentum;
_index = momentum;
return;
}
var directional = prevDirectionalLine + alpha * (momentum - prevDirectionalLine);
var index = prevIndexLine + alpha * (directional - prevIndexLine);
if (_prevDirectional is not decimal prevDir || _prevIndex is not decimal prevIdx)
{
_directional = directional;
_index = index;
_prevDirectional = prevDirectionalLine;
_prevIndex = prevIndexLine;
return;
}
var crossUp = prevDir <= prevIdx && directional > index;
var crossDown = prevDir >= prevIdx && directional < index;
if (crossUp && Position <= 0)
BuyMarket();
else if (crossDown && Position >= 0)
SellMarket();
_directional = directional;
_index = index;
_prevDirectional = directional;
_prevIndex = index;
}
}
}
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
from StockSharp.Algo.Strategies import Strategy
class tdi_2_re_open_strategy(Strategy):
"""TDI momentum/index crossover: custom EMA-smoothed momentum lines."""
def __init__(self):
super(tdi_2_re_open_strategy, self).__init__()
self._tdi_period = self.Param("TdiPeriod", 10).SetGreaterThanZero().SetDisplay("TDI Period", "Momentum lookback period", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(tdi_2_re_open_strategy, self).OnReseted()
self._last_close = 0
self._directional = None
self._index = None
self._prev_dir = None
self._prev_idx = None
def OnStarted2(self, time):
super(tdi_2_re_open_strategy, self).OnStarted2(time)
self._last_close = 0
self._directional = None
self._index = None
self._prev_dir = None
self._prev_idx = None
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)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if self._last_close == 0:
self._last_close = close
return
momentum = close - self._last_close
self._last_close = close
alpha = 2.0 / (self._tdi_period.Value + 1.0)
if self._directional is None or self._index is None:
self._directional = momentum
self._index = momentum
return
directional = self._directional + alpha * (momentum - self._directional)
index = self._index + alpha * (directional - self._index)
if self._prev_dir is None or self._prev_idx is None:
self._directional = directional
self._index = index
self._prev_dir = self._directional
self._prev_idx = self._index
return
cross_up = self._prev_dir <= self._prev_idx and directional > index
cross_down = self._prev_dir >= self._prev_idx and directional < index
if cross_up and self.Position <= 0:
self.BuyMarket()
elif cross_down and self.Position >= 0:
self.SellMarket()
self._directional = directional
self._index = index
self._prev_dir = directional
self._prev_idx = index
def CreateClone(self):
return tdi_2_re_open_strategy()