GitHub で見る

未決注文グリッド戦略

この戦略は、MetaTraderの「AntiFragile」未決注文グリッドエキスパートアドバイザーの動作を再現します。現在の市場価格を中心とした対称的なストップ注文のグリッドを継続的に構築し、ポジションが開かれると保護的な終了を適用します。

コアロジック

  • 起動時に、戦略はレベル1/注文書データから最良のビッドとアスクをキャッシュし、価格の上に買いストップ注文、価格の下に売りストップ注文を配置します。
  • 注文価格はDistanceパラメーターによって市場からオフセットされ、各後続レベルはインストゥルメントの価格ステップで乗算された*Spacing (ticks)*でスペースが取られます。
  • 各新しいグリッドレベルは、開始サイズに対して*Volume Increase %*分の注文ボリュームを増加させ、MQLバージョンのマーチンゲールスタイルのスケーリングを実装します。
  • 注文が約定すると、結果のネットポジションはストップロス注文とテイクプロフィット注文で保護されます。オプションのトレーリングストップロジックは最新のビッド/アスクを再利用して、未実現利益がトレーリング距離を超えたときにストップを締めます。
  • すべての未決注文が約定またはキャンセルされ、ポジションがフラットに戻った後、グリッドは自動的に再構築されます。

パラメーター

  • Starting Volume – 最初の未決注文のロット/コントラクトサイズ。後続注文は*Volume Increase %*でスケールされます。
  • Volume Increase % – 各新しいグリッドレベルに追加されるパーセンテージの増分(0.1は+0.1%/レベル)。
  • Distance – 最初の注文の前に追加される絶対価格オフセット(インストゥルメント通貨で解釈)。
  • Spacing (ticks) – 連続するグリッド注文間の価格ステップ数。
  • Orders per side – ロングとショートそれぞれのグリッド注文の最大数。
  • Take Profit (ticks) – 平均エントリーからの利益目標距離、価格ステップで表現。
  • Stop Loss (ticks) – 平均エントリーからのストップ距離。ゼロに設定して初期ストップを無効化。
  • Trailing Stop (ticks) – トレーリング距離。ゼロに設定してトレーリング調整を無効化。
  • Enable Long Grid / Enable Short Grid – 買いストップまたは売りストップ注文を配置するためのトグル。

実装ノート

  • StockSharp戦略はネットポジションを使用するため、反対方向の約定はMT4のようにヘッジされたバスケットを作成する代わりに互いを相殺します。グリッドは対称のままですが、ネットエクスポージャーのみが追跡されます。
  • ボリュームと価格は注文を送信する前にインストゥルメントのステップサイズに丸められます。
  • トレーリングストップは、利益がトレーリング距離を超えたときに、前のストップ注文をキャンセルしてより締まったレベルで新しいものを送ることで再作成されます。
  • 戦略は価格追跡とトレーリングロジックを駆動するために注文書データ(SubscribeOrderBook)を必要とします。

使用上のヒント

  1. Starting Volumeと*Volume Increase %*を保守的に設定してください;元のデフォルトはFXロットサイジングを前提としており、急速に成長する可能性があります。
  2. ポートフォリオが対象場所のストップ注文をサポートしていることを確認してください。すべてのグリッドエントリーはストップマーケット注文です。
  3. 大量の未決注文が予約資本を消費する可能性があるため、証拠金要件を監視してください。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pending order grid strategy that mirrors the classic AntiFragile EA behavior.
/// Places layered virtual grid levels above and below the initial price.
/// When price reaches a grid level, a market order is executed.
/// Applies take profit and stop loss management based on entry price.
/// </summary>
public class PendingOrderGridStrategy : Strategy
{
	private readonly StrategyParam<decimal> _gridSpacing;
	private readonly StrategyParam<int> _gridLevels;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<bool> _tradeLong;
	private readonly StrategyParam<bool> _tradeShort;

	private decimal _initialPrice;
	private decimal _entryPrice;
	private bool _initialized;
	private HashSet<int> _triggeredBuyLevels;
	private HashSet<int> _triggeredSellLevels;
	private int _tradeCount;

	/// <summary>
	/// Spacing between grid levels as a percentage of price.
	/// </summary>
	public decimal GridSpacing
	{
		get => _gridSpacing.Value;
		set => _gridSpacing.Value = value;
	}

	/// <summary>
	/// Number of grid levels per side.
	/// </summary>
	public int GridLevels
	{
		get => _gridLevels.Value;
		set => _gridLevels.Value = value;
	}

	/// <summary>
	/// Take profit as percentage of entry price.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	/// <summary>
	/// Stop loss as percentage of entry price.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	/// <summary>
	/// Enables buying on grid levels below price.
	/// </summary>
	public bool TradeLong
	{
		get => _tradeLong.Value;
		set => _tradeLong.Value = value;
	}

	/// <summary>
	/// Enables selling on grid levels above price.
	/// </summary>
	public bool TradeShort
	{
		get => _tradeShort.Value;
		set => _tradeShort.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="PendingOrderGridStrategy"/> class.
	/// </summary>
	public PendingOrderGridStrategy()
	{
		_gridSpacing = Param(nameof(GridSpacing), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Grid Spacing %", "Percentage spacing between grid levels", "Grid");

		_gridLevels = Param(nameof(GridLevels), 3)
			.SetGreaterThanZero()
			.SetDisplay("Grid Levels", "Number of grid levels per side", "Grid");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 2.0m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit %", "Take profit as percentage of entry", "Risk");

		_stopLossPercent = Param(nameof(StopLossPercent), 3.0m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Stop loss as percentage of entry", "Risk");

		_tradeLong = Param(nameof(TradeLong), true)
			.SetDisplay("Enable Long", "Enable buy grid levels", "Grid");

		_tradeShort = Param(nameof(TradeShort), true)
			.SetDisplay("Enable Short", "Enable sell grid levels", "Grid");

		_triggeredBuyLevels = new HashSet<int>();
		_triggeredSellLevels = new HashSet<int>();
	}

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

		_initialPrice = 0m;
		_entryPrice = 0m;
		_initialized = false;
		_triggeredBuyLevels = new HashSet<int>();
		_triggeredSellLevels = new HashSet<int>();
		_tradeCount = 0;
	}

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

		var tf = TimeSpan.FromMinutes(5).TimeFrame();

		SubscribeCandles(tf)
			.Bind(ProcessCandle)
			.Start();
	}

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

		var close = candle.ClosePrice;

		// Initialize grid around the first candle's close price
		if (!_initialized)
		{
			_initialPrice = close;
			_initialized = true;
			_triggeredBuyLevels.Clear();
			_triggeredSellLevels.Clear();
			return;
		}

		// Check if we have a position that needs TP/SL management
		if (Position != 0m && _entryPrice > 0m)
		{
			if (Position > 0m)
			{
				var tpPrice = _entryPrice * (1m + TakeProfitPercent / 100m);
				var slPrice = _entryPrice * (1m - StopLossPercent / 100m);

				if (close >= tpPrice || close <= slPrice)
				{
					SellMarket();
					ResetGrid(close);
					return;
				}
			}
			else if (Position < 0m)
			{
				var tpPrice = _entryPrice * (1m - TakeProfitPercent / 100m);
				var slPrice = _entryPrice * (1m + StopLossPercent / 100m);

				if (close <= tpPrice || close >= slPrice)
				{
					BuyMarket();
					ResetGrid(close);
					return;
				}
			}
		}

		// Check grid levels for new entries
		var spacing = GridSpacing / 100m;

		// Buy levels below initial price
		if (TradeLong)
		{
			for (var i = 1; i <= GridLevels; i++)
			{
				if (_triggeredBuyLevels.Contains(i))
					continue;

				var level = _initialPrice * (1m - i * spacing);

				if (close <= level && Position <= 0m)
				{
					// Close any short first
					if (Position < 0m)
						BuyMarket();

					BuyMarket();
					_triggeredBuyLevels.Add(i);
					_tradeCount++;
					return;
				}
			}
		}

		// Sell levels above initial price
		if (TradeShort)
		{
			for (var i = 1; i <= GridLevels; i++)
			{
				if (_triggeredSellLevels.Contains(i))
					continue;

				var level = _initialPrice * (1m + i * spacing);

				if (close >= level && Position >= 0m)
				{
					// Close any long first
					if (Position > 0m)
						SellMarket();

					SellMarket();
					_triggeredSellLevels.Add(i);
					_tradeCount++;
					return;
				}
			}
		}
	}

	private void ResetGrid(decimal newPrice)
	{
		_initialPrice = newPrice;
		_entryPrice = 0m;
		_triggeredBuyLevels.Clear();
		_triggeredSellLevels.Clear();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade?.Trade != null)
			_entryPrice = trade.Trade.Price;
	}
}