在 GitHub 上查看

挂单网格策略

该策略复刻了 MetaTrader 中“AntiFragile”挂单网格 EA 的逻辑。策略在行情上下方持续铺设对称的止损单网格,并在触发后提供风控处理。

核心机制

  • 启动后订阅盘口数据,记录最新买一/卖一价,并在价格上方挂出买入止损单、下方挂出卖出止损单。
  • 每个挂单价格先按 Distance 参数离当前价一定距离,再根据 Spacing (ticks) 乘以品种最小价格跳动生成网格间距。
  • 订单手数依据 Volume Increase % 逐级递增,复现原 MQL 程序的马丁加仓方式。
  • 当挂单成交形成净头寸后,策略会同步下达止损和止盈单;若启用 Trailing Stop (ticks),则根据实时买卖价在盈利达到设定阈值时上移/下移止损。
  • 所有挂单成交或被取消且仓位回到空仓后,会重新搭建完整网格。

参数说明

  • Starting Volume – 第一笔挂单的基础手数,之后的订单按百分比递增。
  • Volume Increase % – 每层网格的手数增幅(0.1 代表每层增加 0.1%)。
  • Distance – 首个挂单相对当前价的绝对偏移量(以标的货币计)。
  • Spacing (ticks) – 网格层之间的跳动数。
  • Orders per side – 多头与空头方向各自的最大挂单数量。
  • Take Profit (ticks) – 止盈相对均价的跳动距离,为 0 表示不下达止盈。
  • Stop Loss (ticks) – 初始止损的跳动距离,为 0 表示不下达止损。
  • Trailing Stop (ticks) – 启用追踪止损时的移动距离,为 0 表示关闭追踪。
  • Enable Long Grid / Enable Short Grid – 是否在上方放置买入止损单 / 在下方放置卖出止损单。

实现细节

  • StockSharp 使用净头寸模型,不支持 MT4 那样的对冲持仓;相反方向成交会相互抵消。
  • 所有价格与手数在下单前都会按照交易所最小跳动量进行取整。
  • 追踪止损通过取消旧止损单并以新价格重新下单来实现。
  • 策略依赖 SubscribeOrderBook() 提供的盘口数据来驱动网格和追踪逻辑。

使用建议

  1. 根据账户保证金和风险承受能力谨慎设置 Starting VolumeVolume Increase %,避免在大波动行情中急剧放大持仓。
  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;
	}
}