Volatility HFT EA 策略
该策略将 Volatility HFT EA MetaTrader 5 专家顾问移植到 StockSharp 的高级 API。策略复现原始脚本的思路:当收盘价远高于快速简单移动平均线时买入,并持仓等待价格回落到该均线。报单流程、指标管理以及风控逻辑均遵循 AGENTS.md 的要求,同时保留 MQL 脚本的核心行为。
工作原理
- 指标初始化 – 在
CandleType指定的主图周期上计算一个简单移动平均线(默认周期:5)。 - 新K线检测 – 只在蜡烛收盘 (
CandleStates.Finished) 后处理数据,对应 EA 中的IsNewBar判定。 - 历史数据要求 – 策略会等待 60 根已完成的K线后才开始寻找信号,与原脚本中的
Bars < 60判断保持一致。 - 入场条件 – 当最新收盘价至少高于 SMA
MaDifferencePips个点(通过合约的点值换算为价格),且当前 SMA 值高于两根K线之前的 SMA 时生成多头信号。原脚本的val[0] < -0.0015与MA_Val1[0] > MA_Val1[2]在此通过指标绑定实现,无需手动维护数组。 - 单向持仓 – 由于源文件中的做空分支被注释掉,此移植版本仅执行多头逻辑;持仓未平仓时会忽略新的信号。
风险控制
- 止损 – 以点数表示的可选保护性止损。程序根据
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 对历史深度的要求一致。
使用方法
- 将策略附加到目标标的上,设置合适的
CandleType(例如高频场景可使用 1 分钟K线)。 - 根据品种波动调整
FastMaLength、MaDifferencePips与StopLossPips。点数类参数会依据自动识别的点值转换,因此默认值可兼容 4 位和 5 位报价的外汇品种。 - 设置
OrderVolume以匹配资金管理需求。策略仅发送市价单,不会加仓分批建仓。 - 启动策略。系统会订阅所选K线,建立 SMA,等待 60 根预热K线,然后在每根收盘K线上评估入场条件。
- 观察持仓管理:当价格回落触及 SMA 或下破止损价时即会平仓。
与原始 EA 的差异
- MQL 版本通过
SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN)获取最小手数;此处将手数暴露为参数,以方便在不同经纪商与资产类别上复用。 - 因源代码中的空头部分被注释,本移植仅包含多头逻辑,与公开的 EA 行为保持一致。
- 止盈采用检测蜡烛最低价触及 SMA 的方式,而不是注册实际的限价单,这样可以在 StockSharp 架构下可靠地重现“回归均线即离场”的设计。
- 原脚本中大量的数组操作(
CopyRates、CopyBuffer、ArraySetAsSeries)被高层 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from System.Globalization import CultureInfo
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class volatility_hft_ea_strategy(Strategy):
def __init__(self):
super(volatility_hft_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Primary timeframe", "General")
self._minimum_bars = self.Param("MinimumBars", 60) \
.SetGreaterThanZero() \
.SetDisplay("Minimum Bars", "Minimum completed candles before signal evaluation", "Signal")
self._order_volume = self.Param("OrderVolume", Decimal(1)) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Volume applied to market orders", "Trading")
self._fast_ma_length = self.Param("FastMaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA Length", "Period of the fast SMA", "Signal")
self._stop_loss_pips = self.Param("StopLossPips", Decimal(15)) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._ma_difference_pips = self.Param("MaDifferencePips", Decimal(15)) \
.SetGreaterThanZero() \
.SetDisplay("MA Difference (pips)", "Minimum distance between price and MA", "Signal")
self._cooldown_bars = self.Param("CooldownBars", 24) \
.SetDisplay("Cooldown Bars", "Bars to wait after entry or exit", "Signal")
self._pip_size = Decimal(1)
self._previous_sma = None
self._sma_two_bars_ago = None
self._processed_candles = 0
self._cooldown_left = 0
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def MinimumBars(self):
return self._minimum_bars.Value
@property
def FastMaLength(self):
return self._fast_ma_length.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def MaDifferencePips(self):
return self._ma_difference_pips.Value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
def OnReseted(self):
super(volatility_hft_ea_strategy, self).OnReseted()
self._previous_sma = None
self._sma_two_bars_ago = None
self._processed_candles = 0
self._cooldown_left = 0
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
def OnStarted2(self, time):
super(volatility_hft_ea_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self.Volume = self.OrderVolume
self._previous_sma = None
self._sma_two_bars_ago = None
self._processed_candles = 0
self._cooldown_left = 0
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
self._sma_ind = SimpleMovingAverage()
self._sma_ind.Length = self.FastMaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._sma_ind, self._on_process).Start()
def _on_process(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
sv = Decimal(float(sma_value))
self._manage_active_position(candle)
if self._cooldown_left > 0:
self._cooldown_left -= 1
if not self._sma_ind.IsFormed:
self._update_sma_history(sv)
self._processed_candles += 1
return
if self._processed_candles < self.MinimumBars:
self._update_sma_history(sv)
self._processed_candles += 1
return
threshold = Decimal.Multiply(Math.Max(self.MaDifferencePips, Decimal(10)), self._pip_size)
if self._sma_two_bars_ago is not None and self._cooldown_left == 0:
close = candle.ClosePrice
distance = close - sv
is_breakout = distance >= threshold
is_slope_positive = (self._previous_sma is not None
and self._previous_sma > self._sma_two_bars_ago
and sv > self._previous_sma)
is_bullish_bar = candle.ClosePrice > candle.OpenPrice
if is_breakout and is_slope_positive and is_bullish_bar and self.Position == 0:
self.Volume = self.OrderVolume
self.BuyMarket()
self._cooldown_left = self.CooldownBars
self._entry_price = candle.ClosePrice
stop_dist = Decimal.Multiply(self.StopLossPips, self._pip_size)
self._stop_loss_price = self._entry_price - stop_dist if stop_dist > Decimal(0) else None
self._take_profit_price = sv
self._update_sma_history(sv)
self._processed_candles += 1
def _manage_active_position(self, candle):
if self.Position == 0:
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
return
exit_volume = Math.Abs(self.Position)
if self.Position > 0:
if self._take_profit_price is not None and candle.LowPrice <= self._take_profit_price:
self.SellMarket(exit_volume)
self._cooldown_left = self.CooldownBars
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
return
if self._stop_loss_price is not None and candle.LowPrice <= self._stop_loss_price:
self.SellMarket(exit_volume)
self._cooldown_left = self.CooldownBars
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
elif self.Position < 0:
if self._take_profit_price is not None and candle.HighPrice >= self._take_profit_price:
self.BuyMarket(exit_volume)
self._cooldown_left = self.CooldownBars
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
return
if self._stop_loss_price is not None and candle.HighPrice >= self._stop_loss_price:
self.BuyMarket(exit_volume)
self._cooldown_left = self.CooldownBars
self._entry_price = Decimal(0)
self._stop_loss_price = None
self._take_profit_price = None
def _update_sma_history(self, sma_value):
self._sma_two_bars_ago = self._previous_sma
self._previous_sma = sma_value
def _calculate_pip_size(self):
sec = self.Security
step = sec.PriceStep if sec is not None and sec.PriceStep is not None else Decimal(1)
if step <= Decimal(0):
return Decimal(1)
decimals = self._get_decimal_places(step)
if decimals == 3 or decimals == 5:
return Decimal.Multiply(step, Decimal(10))
return step
def _get_decimal_places(self, value):
text = Math.Abs(value).ToString(CultureInfo.InvariantCulture)
sep = text.IndexOf('.')
if sep < 0:
return 0
return len(text) - sep - 1
def CreateClone(self):
return volatility_hft_ea_strategy()