在 GitHub 上查看

Vortex Indicator System 策略

概览

  • 来源:从 MetaTrader 5 专家顾问 "Vortex Indicator System"(MQL ID 19137)转换而来。
  • 核心思想:利用 Vortex 指标判断 VI+ 与 VI- 的金叉或死叉,并在发生交叉的蜡烛上设置突破触发价。
  • 交易风格:突破跟随,只有当价格突破触发价后才会真正进场。
  • 适用市场:只要能够提供 Vortex 指标和蜡烛数据的品种与周期均可使用,无需额外的经纪商特性。
  • 订单类型:通过 BuyMarketSellMarket 提交市价单;在设置新触发价前会先平掉反向仓位。

交易流程

  1. 订阅设定好的蜡烛类型,并按照参数长度计算 Vortex 指标。
  2. 当出现金叉(VI+ 向上穿越 VI-,且前一根蜡烛仍在下方)时:
    • 调用 ClosePosition() 平掉所有空头仓位。
    • 将交叉蜡烛的最高价保存为多头触发价。
    • 清除可能存在的空头触发价。
  3. 当出现死叉(VI- 向上穿越 VI+,且前一根蜡烛仍在下方)时:
    • 平掉所有多头仓位。
    • 将交叉蜡烛的最低价保存为空头触发价。
    • 清除可能存在的多头触发价。
  4. 在触发价有效期间监控后续蜡烛:
    • 若最高价突破多头触发价,且当前仓位为空或为空头,则以市价买入并覆盖任何空头仓位。
    • 若最低价跌破空头触发价,且当前仓位为空或为多头,则以市价卖出并覆盖任何多头仓位。
  5. 订单执行后相应触发价会被清除,同时只会保留一个方向的触发价。

参数说明

参数 默认值 描述
Length 14 Vortex 指标的周期长度,对应原始 MQL 输入 VI_Length
CandleType 60 分钟蜡烛 用于计算指标和检测触发价的蜡烛类型,可根据需要改成任意支持的周期。
Volume 继承自 Strategy.Volume 市价单的下单数量,启动前根据账户情况自行设置。

参数对策略的影响

  • 增大 Length 可降低噪音与信号频率,但信号更稳定。
  • 减小 Length 会提高灵敏度,产生更多的交叉与潜在交易。
  • CandleType 应尽量与原始脚本的图表周期保持一致,短周期更激进,长周期更偏趋势。

风险控制

  • 原脚本未设置止损或止盈,本策略保持相同行为。需要额外风险控制时,可在外部添加或自行扩展策略。
  • 一旦出现反向信号,会先平仓再等待突破确认,避免持有双向仓位。
  • 策略不会加仓或网格,仅保持最多一侧的单一仓位。

使用步骤

  1. 将策略加入 StockSharp 项目,并确保引用了 StockSharp.Algo.Indicators 库。
  2. 配置交易连接与标的证券。
  3. 在启动前设定 CandleTypeLengthVolume 参数,必要时可通过优化器寻找更优组合。
  4. 启动策略;当指标形成且数据处于实时状态时开始生成信号。

实现要点

  • 采用高阶的 SubscribeCandles API,并通过 Bind 与 Vortex 指标绑定,逻辑简洁清晰。
  • 通过保存上一根蜡烛的 Vortex 值实现与 MQL 相同的交叉检测方式。
  • 触发价使用可空的 decimal 字段实现,避免重复下单并保持代码易读。
  • C# 文件内包含英文注释,便于理解每个步骤并符合项目规范。

可选拓展

  • 根据 ATR 或其它指标添加止损/止盈规则。
  • 给触发价增加有效期,超过一定时间仍未触发时自动取消。
  • 引入波动率或趋势过滤器,以减少震荡市中的假突破。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy based on the Vortex indicator crossover system.
/// Replicates the logic of the original MQL expert by arming entry triggers
/// on the candle where VI+ and VI- lines cross and executing when price breaks the trigger.
/// </summary>
public class VortexIndicatorSystemStrategy : Strategy
{
	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<DataType> _candleType;

	private VortexIndicator _vortex = null!;
	private decimal _previousPlus;
	private decimal _previousMinus;
	private bool _hasPrevious;
	private decimal? _pendingBuyTrigger;
	private decimal? _pendingSellTrigger;

	/// <summary>
	/// Length of the Vortex indicator.
	/// </summary>
	public int Length
	{
		get => _length.Value;
		set => _length.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes parameters for the strategy.
	/// </summary>
	public VortexIndicatorSystemStrategy()
	{
		_length = Param(nameof(Length), 14)
			.SetDisplay("Vortex Length", "Period for the Vortex indicator", "General")
			.SetGreaterThanZero()
			
			.SetOptimize(7, 28, 7);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for analysis", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousPlus = 0m;
		_previousMinus = 0m;
		_hasPrevious = false;
		_pendingBuyTrigger = null;
		_pendingSellTrigger = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_vortex = new VortexIndicator
		{
			Length = Length
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.BindEx(_vortex, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue vortexValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_vortex.IsFormed)
			return;

		if (vortexValue is not VortexIndicatorValue typed)
			return;

		var viPlusN = typed.PlusVi;
		var viMinusN = typed.MinusVi;
		if (viPlusN is not decimal viPlus || viMinusN is not decimal viMinus)
			return;

		if (_pendingBuyTrigger is decimal buyTrigger && candle.HighPrice > buyTrigger)
		{
			if (Position <= 0)
			{
				// Reverse existing short if present and open a new long position when price breaks the trigger.
				BuyMarket();
			}

			_pendingBuyTrigger = null;
		}
		else if (_pendingSellTrigger is decimal sellTrigger && candle.LowPrice < sellTrigger)
		{
			if (Position >= 0)
			{
				// Reverse existing long if present and open a new short position when price breaks the trigger.
				SellMarket();
			}

			_pendingSellTrigger = null;
		}

		if (!_hasPrevious)
		{
			_previousPlus = viPlus;
			_previousMinus = viMinus;
			_hasPrevious = true;
			return;
		}

		var crossedUp = _previousPlus <= _previousMinus && viPlus > viMinus;
		var crossedDown = _previousPlus >= _previousMinus && viPlus < viMinus;

		if (crossedUp)
		{
			if (Position < 0)
			{
				// Flatten existing short positions when a bullish crossover appears.
				BuyMarket();
			}

			_pendingBuyTrigger = candle.HighPrice;
			_pendingSellTrigger = null;
		}
		else if (crossedDown)
		{
			if (Position > 0)
			{
				// Flatten existing long positions when a bearish crossover appears.
				SellMarket();
			}

			_pendingSellTrigger = candle.LowPrice;
			_pendingBuyTrigger = null;
		}

		_previousPlus = viPlus;
		_previousMinus = viMinus;
	}
}