在 GitHub 上查看

Vortex Indicator System 策略

本策略将 MetaTrader 专家顾问 Vortex Indicator System.mq4 移植到 StockSharp 的高级 API。原始构想发表于 Technic al Analysis of Stocks & Commodities(2010 年 1 月),通过 Vortex 指标的交叉来设置突破订单:当 VI+ 与 VI- 出现交叉 时,交叉蜡烛的高点或低点被记录下来,之后的蜡烛一旦突破该价位就触发市价单。移植版本完整保留了这一 流程——出现反向信号时立即平掉已有仓位,记录突破价格,并等待下一根突破蜡烛执行交易。

工作流程

  1. 根据 CandleType 订阅一组蜡烛数据,并通过 Bind 将其绑定到一个 VortexIndicator 指标实例,从而在每根完 成的蜡烛上同时获得 VI+ 与 VI- 数值。
  2. 指标形成后,策略保存上一根蜡烛的 Vortex 数值,以检测与 MQL 原版相同的交叉条件:在最近两根已收蜡烛之间 ,VI+ 向上穿越 VI- 或反之。
  3. 准备阶段 – 一旦检测到多头交叉,立即平掉所有空头仓位,并将该交叉蜡烛的最高价记录为多头触发价。空 头交叉同理:平掉多头仓位,并将该蜡烛最低价记录为做空触发价。
  4. 触发阶段 – 在之后每根收盘的蜡烛中检查是否触及触发价(HighPrice ≥ 多头触发价或 LowPrice ≤ 空头触发 价)。若条件满足,则发送市价单,数量为 TradeVolume 并加上尚未完全平仓的反向仓位数量。
  5. 市价单执行后相应的触发价被清空。如果没有发生突破,则保持当前准备状态,直到出现新的交叉信号覆盖之前的 设置。
  6. 平仓逻辑完全依靠交叉信号:出现相反交叉时立即平仓并重新记录突破价,与 MetaTrader 版本保持一致。

交易信号

  • 多头准备:上一根蜡烛 VI+VI-,当前收盘蜡烛 VI+ > VI-,触发价设为该蜡烛的最高价。
  • 多头执行:第一根最高价触及触发价的蜡烛会发送买入市价单,数量为 TradeVolume 加上尚未平掉的空头数量。
  • 空头准备:上一根蜡烛 VI-VI+,当前收盘蜡烛 VI- > VI+,触发价设为该蜡烛的最低价。
  • 空头执行:第一根最低价触及触发价的蜡烛会发送卖出市价单,数量为 TradeVolume 加上尚未平掉的多头数量。

参数

参数 默认值 说明
VortexLength 14 Vortex 指标的周期。
CandleType 1 小时 用于计算的蜡烛周期与指标时间框架。
TradeVolume 1 每次开仓使用的市价单数量。

实现说明

  • 为符合转换规范,策略只处理 收盘 蜡烛。只要某根蜡烛的高/低突破了记录的触发价,就会在收盘时识别到突破。
  • OnStopped 中会清除所有未触发的价格,以便下次启动时状态干净。
  • 执行突破订单时会在基础仓位上加上反向仓位的绝对值,从而达到与原版相同的效果:先平旧仓,再建立新仓。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Vortex Indicator Breakout: Dual EMA crossover breakout with ATR stops.
/// </summary>
public class VortexIndicatorBreakoutStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;

	public VortexIndicatorBreakoutStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_fastEmaLength = Param(nameof(FastEmaLength), 14)
			.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators");

		_slowEmaLength = Param(nameof(SlowEmaLength), 28)
			.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;

		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastEma, slowEma, atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastEma);
			DrawIndicator(area, slowEma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0)
		{
			_prevFast = fastVal;
			_prevSlow = slowVal;
			return;
		}

		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (fastVal < slowVal && _prevFast >= _prevSlow)
			{
				SellMarket();
				_entryPrice = 0;
			}
			else if (close <= _entryPrice - atrVal * 2m)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow)
			{
				BuyMarket();
				_entryPrice = 0;
			}
			else if (close >= _entryPrice + atrVal * 2m)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (fastVal < slowVal && _prevFast >= _prevSlow)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevFast = fastVal;
		_prevSlow = slowVal;
	}
}