在 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 公式。

交易逻辑

  1. 每根完成的 K 线都会读取 信号柱(默认上一根已收盘的 K 线)及其前一根的 TDI-2 数值。
  2. 当动量线先位于指数线上方、随后下穿指数线时:
    • 如果启用了 允许做多建仓,且当前没有多头仓位,则准备新的多头交易。
    • 若存在空头仓位且启用了 允许空头平仓,则关闭空头。
  3. 当动量线先位于指数线下方、随后上穿指数线时:
    • 如果启用了 允许做空建仓,且当前没有空头仓位,则准备新的空头交易。
    • 若存在多头仓位且启用了 允许多头平仓,则关闭多头。
  4. 再入场(加仓)逻辑:
    • 持有多头时会跟踪最近一次多头成交价。当价格向有利方向移动至少 加仓步长(点),且多头成交次数未超过 最大入场次数 时,以基础手数追加多头。
    • 空头仓位使用同样的规则,比较最近一笔空头成交价。
  5. 当需要反手时,策略会发送一笔合并市价单,用以先平掉反向仓位,再以基础手数建立新方向仓位。
  6. 可选的止损和止盈通过 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;
		}
	}
}