Стратегия Pending tread Grid
Обзор
Pending tread Grid — это перенос эксперта MetaTrader 4 Pending_tread.mq4 на платформу StockSharp. Изначальный советник постоянно поддерживает две решётки отложенных ордеров: одну выше текущей цены и одну ниже. Каждую решётку можно настроить на работу либо с покупками, либо с продажами, а расстояние между ступенями задаётся в пунктах. Реализация на StockSharp повторяет эту логику, опираясь исключительно на высокоуровневый API.
Логика торговли
- Поддержка по bid/ask – стратегия подписывается на котировки первого уровня (
SubscribeLevel1) и сохраняет актуальные bid/ask. При поступлении новых данных (с учётом настраиваемого троттлинга) запускается процедура синхронизации сетки с текущим количеством ордеров.
- Решётка выше рынка – параметр
AboveMarketSide определяет тип ордеров, размещаемых выше цены: buy stop либо sell limit. Каждая ступень располагается на расстоянии PipStep пунктов и получает индивидуальный тейк-профит величиной TakeProfitPips.
- Решётка ниже рынка – параметр
BelowMarketSide задаёт buy limit либо sell stop-ордера, расположенные ниже текущей цены, с теми же интервалами и тейк-профитами.
- Контроль минимальной дистанции – параметр
MinStopDistancePoints имитирует MT4-поле MODE_STOPLEVEL. Если расстояние между ценой заявки и соответствующим bid/ask меньше указанного порога, ордер не ставится.
- Троттлинг –
ThrottleSeconds воспроизводит оригинальную паузу в пять секунд, защищающую от ошибки «TRADE_CONTEXT_BUSY». В течение указанного интервала выполняется не более одного цикла обслуживания, независимо от числа тиков.
Параметры, выраженные в пунктах (PipStep, TakeProfitPips), преобразуются в абсолютные ценовые смещения на основе PriceStep и Decimals инструмента. Для пяти- и трёхзначных котировок шаг автоматически умножается на десять, что соответствует mql-понятию «adjusted point».
Параметры
| Параметр |
Значение по умолчанию |
Описание |
OrderVolume |
0.01 |
Объём каждой заявки. Перед отправкой приводится к биржевому шагу объёма. |
PipStep |
12 |
Интервал между соседними ордерами в пунктах. |
TakeProfitPips |
10 |
Величина тейк-профита для каждого отложенного ордера, в пунктах. |
OrdersPerSide |
10 |
Максимальное количество активных заявок выше и ниже рынка. |
AboveMarketSide |
Buy |
Тип ордеров выше рынка: Buy — buy stop, Sell — sell limit. |
BelowMarketSide |
Sell |
Тип ордеров ниже рынка: Buy — buy limit, Sell — sell stop. |
MinStopDistancePoints |
0 |
Минимально допустимая дистанция (в пунктах) между рынком и отложенной заявкой. Укажите брокерский MODE_STOPLEVEL, если требуется. |
ThrottleSeconds |
5 |
Пауза между циклами обслуживания решётки, секунды. |
SlippagePoints |
3 |
Параметр сохранён для совместимости с MT4; в StockSharp на отложенные заявки не влияет. |
Особенности реализации
- Используются только высокоуровневые методы StockSharp:
SubscribeLevel1, BuyLimit, SellLimit, BuyStop, SellStop.
- Цены нормализуются через
Security.ShrinkPrice, чтобы соответствовать шагу котирования.
- Объёмы корректируются с учётом
VolumeStep, MinVolume и MaxVolume инструмента.
- Сообщения выводятся через
AddInfoLog и AddWarningLog, что соответствует детальному логированию оригинального эксперта.
- По требованию задачи Python-версия отсутствует.
Рекомендации по использованию
- Назначьте инструмент и портфель, затем запустите стратегию — после первого обновления Level 1 решётки появятся автоматически.
- Увеличивайте
OrdersPerSide осторожно: каждая дополнительная ступень — это новая реальная заявка у брокера.
- Чтобы максимально точно повторить MT4-эксперта, оставьте паузу в пять секунд и настройте
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()