挂单网格策略
该策略复刻了 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()提供的盘口数据来驱动网格和追踪逻辑。
使用建议
- 根据账户保证金和风险承受能力谨慎设置 Starting Volume 与 Volume Increase %,避免在大波动行情中急剧放大持仓。
- 确保交易通道支持触发型止损单,否则挂出的保护订单可能无法执行。
- 注意大量挂单会占用保证金或冻结资金,必要时调整挂单数量。
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from System.Collections.Generic import HashSet
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class pending_order_grid_strategy(Strategy):
def __init__(self):
super(pending_order_grid_strategy, self).__init__()
self._grid_spacing = self.Param("GridSpacing", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Grid Spacing %", "Percentage spacing between grid levels", "Grid")
self._grid_levels = self.Param("GridLevels", 3) \
.SetGreaterThanZero() \
.SetDisplay("Grid Levels", "Number of grid levels per side", "Grid")
self._take_profit_percent = self.Param("TakeProfitPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit %", "Take profit as percentage of entry", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 3.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop loss as percentage of entry", "Risk")
self._trade_long = self.Param("TradeLong", True) \
.SetDisplay("Enable Long", "Enable buy grid levels", "Grid")
self._trade_short = self.Param("TradeShort", True) \
.SetDisplay("Enable Short", "Enable sell grid levels", "Grid")
self._initial_price = 0
self._entry_price = 0
self._initialized = False
self._triggered_buy_levels = HashSet[int]()
self._triggered_sell_levels = HashSet[int]()
self._trade_count = 0
def OnReseted(self):
super(pending_order_grid_strategy, self).OnReseted()
self._initial_price = 0
self._entry_price = 0
self._initialized = False
self._triggered_buy_levels = HashSet[int]()
self._triggered_sell_levels = HashSet[int]()
self._trade_count = 0
def OnStarted2(self, time):
super(pending_order_grid_strategy, self).OnStarted2(time)
tf = DataType.TimeFrame(TimeSpan.FromMinutes(5))
sub = self.SubscribeCandles(tf)
sub.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
# Initialize grid around the first candle's close price
if not self._initialized:
self._initial_price = close
self._initialized = True
self._triggered_buy_levels.Clear()
self._triggered_sell_levels.Clear()
return
# Check if we have a position that needs TP/SL management
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
tp_price = self._entry_price * (1 + self._take_profit_percent.Value / 100)
sl_price = self._entry_price * (1 - self._stop_loss_percent.Value / 100)
if close >= tp_price or close <= sl_price:
self.SellMarket()
self.ResetGrid(close)
return
elif self.Position < 0:
tp_price = self._entry_price * (1 - self._take_profit_percent.Value / 100)
sl_price = self._entry_price * (1 + self._stop_loss_percent.Value / 100)
if close <= tp_price or close >= sl_price:
self.BuyMarket()
self.ResetGrid(close)
return
# Check grid levels for new entries
spacing = self._grid_spacing.Value / 100
# Buy levels below initial price
if self._trade_long.Value:
for i in range(1, self._grid_levels.Value + 1):
if self._triggered_buy_levels.Contains(i):
continue
level = self._initial_price * (1 - i * spacing)
if close <= level and self.Position <= 0:
# Close any short first
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._triggered_buy_levels.Add(i)
self._trade_count += 1
return
# Sell levels above initial price
if self._trade_short.Value:
for i in range(1, self._grid_levels.Value + 1):
if self._triggered_sell_levels.Contains(i):
continue
level = self._initial_price * (1 + i * spacing)
if close >= level and self.Position >= 0:
# Close any long first
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._triggered_sell_levels.Add(i)
self._trade_count += 1
return
def ResetGrid(self, new_price):
self._initial_price = new_price
self._entry_price = 0
self._triggered_buy_levels.Clear()
self._triggered_sell_levels.Clear()
def OnOwnTradeReceived(self, trade):
super(pending_order_grid_strategy, self).OnOwnTradeReceived(trade)
if trade is not None and trade.Trade is not None:
self._entry_price = trade.Trade.Price
def CreateClone(self):
return pending_order_grid_strategy()