在 GitHub 上查看

VQ EA

概述

  • 将 MetaTrader 专家顾问“VQ_EA”迁移到 StockSharp 平台。
  • 使用平滑的中位价近似原始的 Volatility Quality (VQ) 指标曲线,从而保持高阶 API 的实现方式。
  • 当平滑曲线方向发生反转时开仓,可选地通过保护性订单管理仓位。

原始 MQL 行为

  1. 从自定义指标 VQ 的第 3、4 个缓冲区读取买入或卖出信号。
  2. 当出现新的信号且当前方向没有仓位时,以市价开立新仓。
  3. 出现相反信号时立即关闭对向仓位。
  4. 附带固定手数/分数手数、保本、移动止损、日志输出以及提醒/邮件等功能。

StockSharp 实现

  • 使用中位价的简单移动平均线以及可选的第二次平滑来模拟 VQ 曲线的变色效果。
  • 通过点值过滤避免微小波动触发信号。
  • 继续使用市价单进出场,以贴近原专家顾问的执行方式。

信号生成步骤

  1. 订阅指定类型的蜡烛并计算每根已完成蜡烛的中位价。
  2. 应用基础平滑周期 (Length),若 Smoothing 大于 1,则再次平滑。
  3. 将当前平滑值与前一值比较,若绝对变化超过 FilterPoints(转换为价格单位),判断方向上升或下降。
  4. 方向由下降转为上升时开多,反之开空;若持有反向仓位,则在开新仓时加上绝对仓位量以实现反向。

风险管理

  • StopLossPointsTakeProfitPointsTrailingStopPoints 会根据标的的最小价差 (PriceStep) 转换为绝对价格距离。
  • 至少启用一项保护功能时,会调用 StartProtection 并使用市价调整,模拟原始 EA 的保护单逻辑。
  • 只有在 UseTrailingtrue 且拖尾距离大于 0 时才启用移动止损。

参数说明

  • Length – 中位价基础平滑周期,默认 5。
  • Smoothing – 第二次平滑周期,默认 1(不启用)。
  • FilterPoints – 方向变化所需的最小点数,默认 5。
  • StopLossPoints – 止损点数,默认 60,设为 0 关闭。
  • TakeProfitPoints – 止盈点数,默认 0(关闭)。
  • UseTrailing – 是否启用移动止损,默认 false。
  • TrailingStopPoints – 移动止损点数,默认 0(UseTrailing 为 false 时忽略)。
  • CandleType – 使用的时间框架,默认 1 小时蜡烛。
  • Volume – 继承自 Strategy.Volume,默认 1,每次开仓使用相同的数量。

与原始专家的差异

  • 未直接移植 VQ 指标,而是用平滑的中位价近似,实现上存在差别。
  • 未复现保本移动、提醒与邮件调度、分数手数等高级资金管理功能。
  • 移动止损步长逻辑简化为 StockSharp 内置的拖尾管理器。

使用提示

  • 仅在蜡烛收盘后生成信号,等价于原策略的“收盘交易”模式。
  • 请确保标的设置了正确的 PriceStep;若不可用,将退化为 1.0 来换算点值。
  • 策略主要用于演示,可按需求扩展更多资金管理规则。
using System;



using StockSharp.Algo.Indicators;

using StockSharp.Algo.Strategies;

using StockSharp.BusinessEntities;

using StockSharp.Messages;



namespace StockSharp.Samples.Strategies;



public class VqEaStrategy : Strategy

{

	private readonly StrategyParam<int> _emaPeriod;

	private readonly StrategyParam<int> _momentumPeriod;

	private readonly StrategyParam<DataType> _candleType;



	private decimal _prevMom; private bool _hasPrev;

	private int _cooldown;



	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }

	public int MomentumPeriod { get => _momentumPeriod.Value; set => _momentumPeriod.Value = value; }

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



	public VqEaStrategy()

	{

		_emaPeriod = Param(nameof(EmaPeriod), 20).SetDisplay("EMA Period", "EMA filter", "Indicators");

		_momentumPeriod = Param(nameof(MomentumPeriod), 14).SetDisplay("Momentum", "Momentum period", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame()).SetDisplay("Candle Type", "Candle timeframe", "General");

	}



	/// <inheritdoc />

	protected override void OnReseted()

	{

		base.OnReseted();

		_prevMom = default;

		_hasPrev = default;

		_cooldown = default;

	}



	/// <inheritdoc />

	protected override void OnStarted2(DateTime time)

	{

		base.OnStarted2(time);

		_hasPrev = false;

		var ema = new ExponentialMovingAverage { Length = EmaPeriod };

		var mom = new Momentum { Length = MomentumPeriod };

		var subscription = SubscribeCandles(CandleType);

		subscription.Bind(ema, mom, ProcessCandle).Start();

	}



	private void ProcessCandle(ICandleMessage candle, decimal ema, decimal mom)

	{

		if (candle.State != CandleStates.Finished) return;

		if (!IsFormedAndOnlineAndAllowTrading()) return;

		var close = candle.ClosePrice;

		if (!_hasPrev) { _prevMom = mom; _hasPrev = true; return; }

		if (_cooldown > 0)

		{

			_cooldown--;

			_prevMom = mom;

			return;

		}



		if (close > ema && _prevMom <= 0 && mom > 0 && Position <= 0)

		{

			var volume = Volume + Math.Abs(Position);

			BuyMarket(volume);

			_cooldown = 2;

		}

		else if (close < ema && _prevMom >= 0 && mom < 0 && Position >= 0)

		{

			var volume = Volume + Math.Abs(Position);

			SellMarket(volume);

			_cooldown = 2;

		}

		_prevMom = mom;

	}

}