GitHub で見る

Volatility HFT EA戦略

この戦略は、Volatility HFT EA MetaTrader 5エキスパートアドバイザーをStockSharpの高レベルAPIに移植します。クローズ価格が高速なシンプル移動平均を大きく上回ったときに買い、その平均へのプルバックを待つという元のロジックを再現します。注文生成、インジケーター管理、保護的なエグジットはすべてAGENTS.mdのガイドラインに従い、MQLスクリプトの動作を維持します。

動作方法

  1. インジケーター設定CandleTypeで指定された作業タイムフレームに単一のシンプル移動平均(デフォルト長さ:5)が計算されます。
  2. 新しいバーの検出 – ローソク足が完了したとき(CandleStates.Finished)にのみ処理が行われ、EAのIsNewBarチェックを模倣します。
  3. ウォームアップ要件 – MQLで使用されるBars < 60ガードと一致するように、戦略はトレードを評価する前に60本の完了したローソク足を待ちます。
  4. エントリーフィルター – 最新のクローズがSMAより少なくともMaDifferencePips高く(差は計器のpipサイズを使用して価格に変換)、SMAの値が2バー前よりも高い場合にロング設定が現れます。元のEAはval[0] < -0.0015MA_Val1[0] > MA_Val1[2]を使用していました;このポートは配列を手動で保存せずに同じ条件を確認します。
  5. 一度に1ポジション – セルブランチがソースファイルでコメントアウトされていたため、ロングトレードのみサポートされます。ポジションが開いている間は新しいシグナルは無視されます。

リスク管理

  • ストップロス – pipで表されたオプションの保護ストップ。コードはSecurity.PriceStepからpipサイズを導出し、計器が3または5桁の小数を持つ場合は10を乗算して、MetaTraderからの_Digitsスケーリングを再現します。
  • テイクプロフィット – エグジットターゲットはエントリー時に取得されたSMA値にアンカーされており、mrequest.tp = MA_Val1[0];呼び出しを模倣します。戦略はローソク足の安値が保存されたSMAレベルに触れると、平均での指値注文をエミュレートしてポジションをクローズします。

パラメーター

パラメーター 説明
OrderVolume 各成行注文に使用するボリューム。
FastMaLength 高速シンプル移動平均の期間(デフォルト5)。
StopLossPips pipでのストップロス距離;無効にするには0に設定。
MaDifferencePips ロングエントリーに必要なクローズとSMAの最小距離(pip単位)。
CandleType ローソク足サブスクリプションとインジケーター計算に使用するタイムフレーム。

MinimumBars60に等しい固定内部定数で、十分な履歴に対するEAの要件を反映しています。

使用方法

  1. 戦略をセキュリティにアタッチし、希望のCandleTypeを選択します(高頻度動作には例えば1分バーなど)。
  2. FastMaLengthMaDifferencePipsStopLossPipsを計器のボラティリティに合わせて調整します。pipベースの入力は検出されたpipサイズを使用して自動的に変換されるため、同じデフォルトが4桁および5桁のFXシンボルで機能します。
  3. ポートフォリオのサイジングルールに合わせてOrderVolumeを設定します。戦略は成行注文のみを送信し、ポジションをピラミッドしません。
  4. 戦略を開始します。選択されたローソク足を購読し、SMAを構築し、60本のウォームアップバーを待ってから、完了した各ローソク足でエントリーを評価します。
  5. トレード管理を監視します:エグジットはSMAタッチまたはエントリー時に導出されたストップロス価格によってトリガーされます。

元のEAとの注意事項と違い

  • MQLバージョンはSymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN)を通じて最小ロットサイズを要求していました;ここではブローカーや資産クラス全体で戦略を柔軟に保つためにボリュームをパラメーターとして公開しています。
  • Volatility_HFT_EA.mq5でコメントアウトされていたため、売り条件は省略されています。したがって、動作はロングブランチのみを実行した公開されたスクリプトと一致します。
  • テイクプロフィットの処理は、指値注文を登録するのではなく、ローソク足の安値を使用して移動平均へのタッチを検出します。これにより、StockSharpワークフロー内で同じ意図が確実に機能します。
  • 手動の配列管理(CopyRatesCopyBufferArraySetAsSeries)はStockSharpインジケーターバインディングに置き換えられます。これにより、元の閾値とスロープ比較を保持しながらボイラープレートを削減します。
  • すべての計算はローソク足ベースのまま;戦略は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;
	}
}