Открыть на GitHub

Стратегия Pending tread Grid

Обзор

Pending tread Grid — это перенос эксперта MetaTrader 4 Pending_tread.mq4 на платформу StockSharp. Изначальный советник постоянно поддерживает две решётки отложенных ордеров: одну выше текущей цены и одну ниже. Каждую решётку можно настроить на работу либо с покупками, либо с продажами, а расстояние между ступенями задаётся в пунктах. Реализация на StockSharp повторяет эту логику, опираясь исключительно на высокоуровневый API.

Логика торговли

  1. Поддержка по bid/ask – стратегия подписывается на котировки первого уровня (SubscribeLevel1) и сохраняет актуальные bid/ask. При поступлении новых данных (с учётом настраиваемого троттлинга) запускается процедура синхронизации сетки с текущим количеством ордеров.
  2. Решётка выше рынка – параметр AboveMarketSide определяет тип ордеров, размещаемых выше цены: buy stop либо sell limit. Каждая ступень располагается на расстоянии PipStep пунктов и получает индивидуальный тейк-профит величиной TakeProfitPips.
  3. Решётка ниже рынка – параметр BelowMarketSide задаёт buy limit либо sell stop-ордера, расположенные ниже текущей цены, с теми же интервалами и тейк-профитами.
  4. Контроль минимальной дистанции – параметр MinStopDistancePoints имитирует MT4-поле MODE_STOPLEVEL. Если расстояние между ценой заявки и соответствующим bid/ask меньше указанного порога, ордер не ставится.
  5. Троттлинг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-версия отсутствует.

Рекомендации по использованию

  1. Назначьте инструмент и портфель, затем запустите стратегию — после первого обновления Level 1 решётки появятся автоматически.
  2. Увеличивайте OrdersPerSide осторожно: каждая дополнительная ступень — это новая реальная заявка у брокера.
  3. Чтобы максимально точно повторить MT4-эксперта, оставьте паузу в пять секунд и настройте MinStopDistancePoints согласно требованиям брокера.
  4. 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;
	}
}