Pending tread 网格策略
概述
Pending tread 网格策略 是 MetaTrader 4 专家顾问 Pending_tread.mq4 的 StockSharp 版本。原始 EA 会在行情上方与下方持续维护两组挂单梯形,每一组都可以选择使用买单或卖单,并以点数控制间距。本移植完全使用 StockSharp 高阶 API 实现同样的逻辑,没有引入额外指标或自建数据集合。
交易逻辑
- 买卖价驱动的维护 – 通过
SubscribeLevel1订阅一级行情,缓存最新的买价与卖价。每当接收到新报价时(受可配置的节流限制),维护流程会检查当前挂单数量与目标网格是否一致。 - 上方挂单梯形 –
AboveMarketSide决定在市场上方放置买入止损或卖出限价单。每一个阶梯相距PipStep点,并附带TakeProfitPips点的止盈。 - 下方挂单梯形 –
BelowMarketSide控制在市场下方堆叠买入限价或卖出止损单,点距与止盈计算与上方梯形相同。 - 止损距离保护 –
MinStopDistancePoints用来模拟 MetaTrader 的MODE_STOPLEVEL限制。如果挂单价格与对应的买价/卖价之间的距离小于限制,挂单会被跳过。 - 节流机制 –
ThrottleSeconds复刻了原程序中防止 “TRADE_CONTEXT_BUSY” 的 5 秒节流。在该时间窗口内只会执行一次维护,即使行情频繁更新。
所有以点数表示的输入(PipStep、TakeProfitPips)都会根据品种的 PriceStep 与 Decimals 转换为绝对价格偏移。对于五位或三位报价会自动乘以十,以匹配 MetaTrader 的 “adjusted point” 处理方式。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
OrderVolume |
0.01 | 每个挂单的下单数量,下单前会根据品种的最小步长进行修正。 |
PipStep |
12 | 相邻挂单之间的点数间隔。 |
TakeProfitPips |
10 | 每个挂单对应的止盈距离,单位为点。 |
OrdersPerSide |
10 | 市场上方与下方各维护的最大挂单数量。 |
AboveMarketSide |
Buy | 市场上方使用的挂单类型。Buy 表示买入止损,Sell 表示卖出限价。 |
BelowMarketSide |
Sell | 市场下方使用的挂单类型。Buy 表示买入限价,Sell 表示卖出止损。 |
MinStopDistancePoints |
0 | 买卖价与挂单价格之间允许的最小距离(点)。如有需要,可填写经纪商提供的 MODE_STOPLEVEL。 |
ThrottleSeconds |
5 | 每次维护之间的冷却时间,单位为秒。 |
SlippagePoints |
3 | 为与 MT4 输入保持一致而保留;在 StockSharp 中对挂单不起作用。 |
实现说明
- 仅使用 StockSharp 高阶接口(
SubscribeLevel1、BuyLimit、SellLimit、BuyStop、SellStop)。 - 所有价格通过
Security.ShrinkPrice归一化,确保满足交易所最小跳动要求。 - 下单数量会根据
VolumeStep、MinVolume、MaxVolume自动调整。 - 日志信息使用
AddInfoLog/AddWarningLog输出,保留原 EA 的详细提示风格。 - 根据任务要求,本目录未提供 Python 版本。
使用提示
- 绑定好品种与投资组合后启动策略。收到首个一级行情后两组挂单会立即生成。
- 调整
OrdersPerSide时需注意风险,每增加一个阶梯就会在经纪商侧新增一张挂单。 - 若要完全复刻原 EA,请保持 5 秒节流并设置
MinStopDistancePoints为经纪商要求的止损距离。 - StockSharp 采用净头寸模型,若上下两侧同时触发,成交会互相对冲,而不会形成 MT4 式的双向持仓。
using System;
using System.Linq;
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 grid strategy converted from the MetaTrader 4 expert advisor "Pending_tread".
/// Maintains two independent ladders of limit orders above and below the market with configurable direction and spacing.
/// When price reaches a grid level, a market order is placed in the configured direction.
/// </summary>
public class PendingTreadStrategy : Strategy
{
private readonly StrategyParam<decimal> _pipStep;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _ordersPerSide;
private readonly StrategyParam<Sides> _aboveMarketSide;
private readonly StrategyParam<Sides> _belowMarketSide;
private readonly StrategyParam<DataType> _candleType;
private decimal _pipSize;
private decimal _anchorPrice;
private bool _initialized;
private readonly List<decimal> _triggeredLevelsAbove = new();
private readonly List<decimal> _triggeredLevelsBelow = new();
private decimal _entryPrice;
public PendingTreadStrategy()
{
_pipStep = Param(nameof(PipStep), 200000m)
.SetGreaterThanZero()
.SetDisplay("Grid step (pips)", "Distance between adjacent pending orders expressed in pips", "Trading");
_takeProfitPips = Param(nameof(TakeProfitPips), 150000m)
.SetGreaterThanZero()
.SetDisplay("Take profit (pips)", "Individual take-profit distance assigned to every pending order", "Trading");
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order volume", "Volume sent with each pending order", "Trading");
_ordersPerSide = Param(nameof(OrdersPerSide), 2)
.SetGreaterThanZero()
.SetDisplay("Orders per side", "Maximum number of grid levels maintained above and below the anchor", "Trading");
_aboveMarketSide = Param(nameof(AboveMarketSide), Sides.Buy)
.SetDisplay("Above market side", "Type of orders triggered above the current price", "Orders");
_belowMarketSide = Param(nameof(BelowMarketSide), Sides.Sell)
.SetDisplay("Below market side", "Type of orders triggered below the current price", "Orders");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle type", "Candle timeframe", "General");
}
public decimal PipStep
{
get => _pipStep.Value;
set => _pipStep.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
public int OrdersPerSide
{
get => _ordersPerSide.Value;
set => _ordersPerSide.Value = value;
}
public Sides AboveMarketSide
{
get => _aboveMarketSide.Value;
set => _aboveMarketSide.Value = value;
}
public Sides BelowMarketSide
{
get => _belowMarketSide.Value;
set => _belowMarketSide.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_anchorPrice = 0m;
_initialized = false;
_triggeredLevelsAbove.Clear();
_triggeredLevelsBelow.Clear();
_entryPrice = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = GetPipSize();
this
.SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (!_initialized)
{
_anchorPrice = close;
_initialized = true;
return;
}
var distance = PipStep * _pipSize;
if (distance <= 0m)
return;
var tpOffset = TakeProfitPips * _pipSize;
// Check above-market grid levels
for (var i = 1; i <= OrdersPerSide; i++)
{
var level = _anchorPrice + distance * i;
if (_triggeredLevelsAbove.Contains(level))
continue;
if (close >= level)
{
_triggeredLevelsAbove.Add(level);
ExecuteGridOrder(AboveMarketSide, close, tpOffset);
return; // one order per candle
}
}
// Check below-market grid levels
for (var i = 1; i <= OrdersPerSide; i++)
{
var level = _anchorPrice - distance * i;
if (_triggeredLevelsBelow.Contains(level))
continue;
if (close <= level)
{
_triggeredLevelsBelow.Add(level);
ExecuteGridOrder(BelowMarketSide, close, tpOffset);
return; // one order per candle
}
}
// Check take-profit for existing position
CheckTakeProfit(close, tpOffset);
}
private void ExecuteGridOrder(Sides side, decimal price, decimal tpOffset)
{
// Close existing opposite position first
if (Position != 0)
{
if ((Position > 0 && side == Sides.Sell) || (Position < 0 && side == Sides.Buy))
{
ClosePosition(side);
}
}
var vol = OrderVolume;
if (side == Sides.Buy)
{
BuyMarket(vol);
_entryPrice = price;
}
else
{
SellMarket(vol);
_entryPrice = price;
}
}
private void ClosePosition(Sides newSide)
{
var absPos = Position.Abs();
if (absPos <= 0)
return;
if (Position > 0)
SellMarket(absPos);
else
BuyMarket(absPos);
}
private void CheckTakeProfit(decimal close, decimal tpOffset)
{
if (Position == 0 || _entryPrice == 0 || tpOffset <= 0)
return;
if (Position > 0 && close >= _entryPrice + tpOffset)
{
SellMarket(Position.Abs());
_entryPrice = 0;
// Reset grid to re-establish levels around current price
ResetGrid(close);
}
else if (Position < 0 && close <= _entryPrice - tpOffset)
{
BuyMarket(Position.Abs());
_entryPrice = 0;
ResetGrid(close);
}
}
private void ResetGrid(decimal newAnchor)
{
_anchorPrice = newAnchor;
_triggeredLevelsAbove.Clear();
_triggeredLevelsBelow.Clear();
}
private decimal GetPipSize()
{
var security = Security;
if (security == null)
return 0.01m;
var step = security.PriceStep ?? 0.01m;
return step > 0m ? step : 0.01m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from System.Collections.Generic import List
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class pending_tread_strategy(Strategy):
"""
Pending grid strategy converted from the MetaTrader 4 expert advisor 'Pending_tread'.
Maintains two independent ladders of limit orders above and below the market with configurable direction and spacing.
When price reaches a grid level, a market order is placed in the configured direction.
"""
def __init__(self):
super(pending_tread_strategy, self).__init__()
self._pip_step = self.Param("PipStep", 200000.0) \
.SetGreaterThanZero() \
.SetDisplay("Grid step (pips)", "Distance between adjacent pending orders expressed in pips", "Trading")
self._take_profit_pips = self.Param("TakeProfitPips", 150000.0) \
.SetGreaterThanZero() \
.SetDisplay("Take profit (pips)", "Individual take-profit distance assigned to every pending order", "Trading")
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Order volume", "Volume sent with each pending order", "Trading")
self._orders_per_side = self.Param("OrdersPerSide", 2) \
.SetGreaterThanZero() \
.SetDisplay("Orders per side", "Maximum number of grid levels maintained above and below the anchor", "Trading")
self._above_market_side = self.Param("AboveMarketSide", Sides.Buy) \
.SetDisplay("Above market side", "Type of orders triggered above the current price", "Orders")
self._below_market_side = self.Param("BelowMarketSide", Sides.Sell) \
.SetDisplay("Below market side", "Type of orders triggered below the current price", "Orders")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle type", "Candle timeframe", "General")
self._pip_size = 0.0
self._anchor_price = 0.0
self._initialized = False
self._triggered_levels_above = []
self._triggered_levels_below = []
self._entry_price = 0.0
@property
def PipStep(self):
return self._pip_step.Value
@PipStep.setter
def PipStep(self, value):
self._pip_step.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
@property
def OrdersPerSide(self):
return self._orders_per_side.Value
@OrdersPerSide.setter
def OrdersPerSide(self, value):
self._orders_per_side.Value = value
@property
def AboveMarketSide(self):
return self._above_market_side.Value
@AboveMarketSide.setter
def AboveMarketSide(self, value):
self._above_market_side.Value = value
@property
def BelowMarketSide(self):
return self._below_market_side.Value
@BelowMarketSide.setter
def BelowMarketSide(self, value):
self._below_market_side.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(pending_tread_strategy, self).OnReseted()
self._pip_size = 0.0
self._anchor_price = 0.0
self._initialized = False
self._triggered_levels_above = []
self._triggered_levels_below = []
self._entry_price = 0.0
def OnStarted2(self, time):
super(pending_tread_strategy, self).OnStarted2(time)
self._pip_size = self._get_pip_size()
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if not self._initialized:
self._anchor_price = close
self._initialized = True
return
distance = float(self.PipStep) * self._pip_size
if distance <= 0:
return
tp_offset = float(self.TakeProfitPips) * self._pip_size
# Check above-market grid levels
for i in range(1, int(self.OrdersPerSide) + 1):
level = self._anchor_price + distance * i
if level in self._triggered_levels_above:
continue
if close >= level:
self._triggered_levels_above.append(level)
self._execute_grid_order(self.AboveMarketSide, close, tp_offset)
return # one order per candle
# Check below-market grid levels
for i in range(1, int(self.OrdersPerSide) + 1):
level = self._anchor_price - distance * i
if level in self._triggered_levels_below:
continue
if close <= level:
self._triggered_levels_below.append(level)
self._execute_grid_order(self.BelowMarketSide, close, tp_offset)
return # one order per candle
# Check take-profit for existing position
self._check_take_profit(close, tp_offset)
def _execute_grid_order(self, side, price, tp_offset):
# Close existing opposite position first
if self.Position != 0:
if (self.Position > 0 and side == Sides.Sell) or (self.Position < 0 and side == Sides.Buy):
self._close_position(side)
vol = float(self.OrderVolume)
if side == Sides.Buy:
self.BuyMarket(vol)
self._entry_price = price
else:
self.SellMarket(vol)
self._entry_price = price
def _close_position(self, new_side):
abs_pos = abs(float(self.Position))
if abs_pos <= 0:
return
if self.Position > 0:
self.SellMarket(abs_pos)
else:
self.BuyMarket(abs_pos)
def _check_take_profit(self, close, tp_offset):
if self.Position == 0 or self._entry_price == 0 or tp_offset <= 0:
return
if self.Position > 0 and close >= self._entry_price + tp_offset:
self.SellMarket(abs(float(self.Position)))
self._entry_price = 0
# Reset grid to re-establish levels around current price
self._reset_grid(close)
elif self.Position < 0 and close <= self._entry_price - tp_offset:
self.BuyMarket(abs(float(self.Position)))
self._entry_price = 0
self._reset_grid(close)
def _reset_grid(self, new_anchor):
self._anchor_price = new_anchor
self._triggered_levels_above = []
self._triggered_levels_below = []
def _get_pip_size(self):
security = self.Security
if security is None:
return 0.01
step = security.PriceStep
if step is not None:
step = float(step)
if step > 0:
return step
return 0.01
def CreateClone(self):
return pending_tread_strategy()