在 GitHub 上查看

Volatility HFT EA 策略

该策略将 Volatility HFT EA MetaTrader 5 专家顾问移植到 StockSharp 的高级 API。策略复现原始脚本的思路:当收盘价远高于快速简单移动平均线时买入,并持仓等待价格回落到该均线。报单流程、指标管理以及风控逻辑均遵循 AGENTS.md 的要求,同时保留 MQL 脚本的核心行为。

工作原理

  1. 指标初始化 – 在 CandleType 指定的主图周期上计算一个简单移动平均线(默认周期:5)。
  2. 新K线检测 – 只在蜡烛收盘 (CandleStates.Finished) 后处理数据,对应 EA 中的 IsNewBar 判定。
  3. 历史数据要求 – 策略会等待 60 根已完成的K线后才开始寻找信号,与原脚本中的 Bars < 60 判断保持一致。
  4. 入场条件 – 当最新收盘价至少高于 SMA MaDifferencePips 个点(通过合约的点值换算为价格),且当前 SMA 值高于两根K线之前的 SMA 时生成多头信号。原脚本的 val[0] < -0.0015MA_Val1[0] > MA_Val1[2] 在此通过指标绑定实现,无需手动维护数组。
  5. 单向持仓 – 由于源文件中的做空分支被注释掉,此移植版本仅执行多头逻辑;持仓未平仓时会忽略新的信号。

风险控制

  • 止损 – 以点数表示的可选保护性止损。程序根据 Security.PriceStep 推导点值;当价格精度为 3 或 5 位小数时会乘以 10,复刻 MetaTrader 中基于 _Digits 的缩放方式。
  • 止盈 – 入场时记录 SMA 数值作为止盈价(对应 mrequest.tp = MA_Val1[0];),当后续蜡烛的最低价触及该水平时平仓,相当于在均线位置挂出限价单。

参数

参数 说明
OrderVolume 每次下单的成交量。
FastMaLength 快速简单移动平均线的周期(默认 5)。
StopLossPips 止损距离(点数),填 0 表示不使用止损。
MaDifferencePips 收盘价与 SMA 之间所需的最小点差,用于触发多头信号。
CandleType 用于订阅K线并计算指标的时间框架。

MinimumBars 为内部常量,固定为 60,与 EA 对历史深度的要求一致。

使用方法

  1. 将策略附加到目标标的上,设置合适的 CandleType(例如高频场景可使用 1 分钟K线)。
  2. 根据品种波动调整 FastMaLengthMaDifferencePipsStopLossPips。点数类参数会依据自动识别的点值转换,因此默认值可兼容 4 位和 5 位报价的外汇品种。
  3. 设置 OrderVolume 以匹配资金管理需求。策略仅发送市价单,不会加仓分批建仓。
  4. 启动策略。系统会订阅所选K线,建立 SMA,等待 60 根预热K线,然后在每根收盘K线上评估入场条件。
  5. 观察持仓管理:当价格回落触及 SMA 或下破止损价时即会平仓。

与原始 EA 的差异

  • MQL 版本通过 SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN) 获取最小手数;此处将手数暴露为参数,以方便在不同经纪商与资产类别上复用。
  • 因源代码中的空头部分被注释,本移植仅包含多头逻辑,与公开的 EA 行为保持一致。
  • 止盈采用检测蜡烛最低价触及 SMA 的方式,而不是注册实际的限价单,这样可以在 StockSharp 架构下可靠地重现“回归均线即离场”的设计。
  • 原脚本中大量的数组操作(CopyRatesCopyBufferArraySetAsSeries)被高层 API 的指标绑定所取代,既减少了样板代码,又保留了原有阈值与斜率判断。
  • 所有计算基于已完成的蜡烛,未使用 GetValue 访问指标历史,这与仓库的编程规范一致。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Mean-reversion expert advisor that buys volatility spikes when price stretches far above a fast moving average.
/// Converted from the MetaTrader 5 "Volatility HFT EA" script.
/// </summary>
public class VolatilityHftEaStrategy : Strategy
{
	private readonly StrategyParam<int> _minimumBars;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _maDifferencePips;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _fastMa = null!;

	private decimal _pipSize = 1m;
	private decimal? _previousSma;
	private decimal? _smaTwoBarsAgo;
	private int _processedCandles;
	private int _cooldownLeft;

	private decimal _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	public VolatilityHftEaStrategy()
	{
		_minimumBars = Param(nameof(MinimumBars), 60)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Bars", "Minimum completed candles before signal evaluation", "Signal");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume applied to market orders", "Trading");

		_fastMaLength = Param(nameof(FastMaLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Length", "Period of the fast simple moving average", "Signal");

		_stopLossPips = Param(nameof(StopLossPips), 15m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk");

		_maDifferencePips = Param(nameof(MaDifferencePips), 15m)
			.SetGreaterThanZero()
			.SetDisplay("MA Difference (pips)", "Minimum distance between price and the moving average", "Signal");

		_cooldownBars = Param(nameof(CooldownBars), 24)
			.SetNotNegative()
			.SetDisplay("Cooldown Bars", "Bars to wait after entry or exit", "Signal");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for signal detection", "General");
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int FastMaLength
	{
		get => _fastMaLength.Value;
		set => _fastMaLength.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal MaDifferencePips
	{
		get => _maDifferencePips.Value;
		set => _maDifferencePips.Value = value;
	}

	public int MinimumBars
	{
		get => _minimumBars.Value;
		set => _minimumBars.Value = value;
	}

	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

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

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_fastMa = null!;
		_previousSma = null;
		_smaTwoBarsAgo = null;
		_processedCandles = 0;
		_cooldownLeft = 0;
		ResetPositionState();
	}

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

		_pipSize = CalculatePipSize();
		Volume = OrderVolume;

		_fastMa = new SMA
		{
			Length = FastMaLength
		};

		_previousSma = null;
		_smaTwoBarsAgo = null;
		_processedCandles = 0;
		_cooldownLeft = 0;
		ResetPositionState();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastMa, ProcessCandle)
			.Start();
	}

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

		ManageActivePosition(candle);

		if (_cooldownLeft > 0)
			_cooldownLeft--;

		if (!_fastMa.IsFormed)
		{
			UpdateSmaHistory(smaValue);
			_processedCandles++;
			return;
		}

		if (_processedCandles < MinimumBars)
		{
			UpdateSmaHistory(smaValue);
			_processedCandles++;
			return;
		}

		var threshold = Math.Max(MaDifferencePips, 10m) * _pipSize;

		if (_smaTwoBarsAgo.HasValue && _cooldownLeft == 0)
		{
			var distance = candle.ClosePrice - smaValue;
			var isBreakout = distance >= threshold;
			var isSlopePositive = _previousSma.HasValue && _previousSma.Value > _smaTwoBarsAgo.Value && smaValue > _previousSma.Value;
			var isBullishBar = candle.ClosePrice > candle.OpenPrice;

			if (isBreakout && isSlopePositive && isBullishBar && Position == 0)
			{
				EnterLong(candle, smaValue);
			}
		}

		UpdateSmaHistory(smaValue);
		_processedCandles++;
	}

	private void EnterLong(ICandleMessage candle, decimal smaValue)
	{
		// Strategy holds only one long position at a time.
		if (Position != 0)
			return;

		Volume = OrderVolume;
		BuyMarket();
		_cooldownLeft = CooldownBars;

		_entryPrice = candle.ClosePrice;

		var stopDistance = StopLossPips * _pipSize;
		_stopLossPrice = stopDistance > 0m ? _entryPrice - stopDistance : null;
		_takeProfitPrice = smaValue;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		var exitVolume = Math.Abs(Position);

		if (Position > 0)
		{
			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				SellMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
				return;
			}

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				BuyMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
				return;
			}

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
			}
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private void UpdateSmaHistory(decimal smaValue)
	{
		_smaTwoBarsAgo = _previousSma;
		_previousSma = smaValue;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
			return 1m;

		var decimals = GetDecimalPlaces(step);

		return decimals is 3 or 5
			? step * 10m
			: step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var text = Math.Abs(value).ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.');

		return separatorIndex < 0 ? 0 : text.Length - separatorIndex - 1;
	}
}