Ver en GitHub

Estrategia de cuadrícula de pisada pendiente

Descripción general

La Estrategia de cuadrícula pendiente es una fiel StockSharp versión del MetaTrader 4 asesor experto Pending_tread.mq4. El EA original reconstruye constantemente dos escaleras de órdenes pendientes: una escalera por encima del mercado y otra por debajo. Cada escalera se puede configurar para utilizar órdenes de compra o venta, y el espaciado se define en pips. La implementación StockSharp reproduce el mismo comportamiento a través del API de alto nivel sin introducir indicadores o colecciones adicionales.

Lógica de trading

  1. Mantenimiento basado en oferta/demanda: la estrategia se suscribe a cotizaciones de nivel 1 (SubscribeLevel1) y mantiene los precios de oferta y demanda más recientes. Cada vez que llegan nuevos datos, se ejecuta la rutina de mantenimiento (con un acelerador configurable) y compara las órdenes pendientes existentes con el tamaño de la red configurada.
  2. Escalera por encima del mercado: dependiendo de AboveMarketSide, el algoritmo coloca órdenes de límite de compra o de venta en incrementos de PipStep pips por encima del mercado. Cada nuevo pedido recibe su propio nivel de toma de ganancias, compensado por TakeProfitPips pips.
  3. Escalera por debajo del mercado: el parámetro BelowMarketSide selecciona entre órdenes de límite de compra y órdenes de parada de venta apiladas por debajo del mercado. Se aplica la misma lógica de separación de pips y obtención de beneficios.
  4. Guardia de nivel de parada: el parámetro MinStopDistancePoints emula la comprobación MetaTrader MODE_STOPLEVEL. Las órdenes se omiten cuando la distancia entre el precio y el ancla de oferta/demanda relevante es menor que el límite proporcionado.
  5. Acelerador: ThrottleSeconds refleja el acelerador original de cinco segundos que evitó errores TRADE_CONTEXT_BUSY. Sólo se ejecuta un ciclo de mantenimiento durante ese intervalo, independientemente de cuántos ticks lleguen.

Todas las entradas basadas en pips (PipStep, TakeProfitPips) se convierten en compensaciones de precios absolutos utilizando el instrumento PriceStep y Decimals. Las comillas de cinco dígitos multiplican automáticamente el paso por diez para que coincida con la lógica de "punto ajustado" MetaTrader.

Parámetros

Parámetro Predeterminado Descripción
OrderVolume 0,01 Volumen utilizado al realizar cada orden pendiente. Redondeado al paso de volumen del instrumento antes del registro.
PipStep 12 Espaciado entre órdenes consecutivas en el ladder, expresado en pips.
TakeProfitPips 10 Distancia en pips utilizada para realizar la toma de ganancias de cada orden pendiente.
OrdersPerSide 10 Número máximo de órdenes activas mantenidas por encima y por debajo del mercado.
AboveMarketSide comprar Tipo de orden utilizada por encima del mercado. Buy crea órdenes de parada de compra, Sell crea órdenes de límite de venta.
BelowMarketSide Vender Tipo de orden utilizada por debajo del mercado. Buy crea órdenes de límite de compra, Sell crea órdenes de parada de venta.
MinStopDistancePoints 0 Distancia mínima (en puntos brutos) permitida entre la oferta/demanda y el precio pendiente. Establezca esto en el corredor MODE_STOPLEVEL si es necesario.
ThrottleSeconds 5 Periodo de enfriamiento entre ciclos de mantenimiento de la red.
SlippagePoints 3 Preservado para la paridad de documentación; StockSharp órdenes pendientes no utilizan este valor.

Notas de implementación

  • Utiliza solo los ayudantes de alto nivel StockSharp (SubscribeLevel1, BuyLimit, SellLimit, BuyStop, SellStop).
  • Los precios se normalizan a través de Security.ShrinkPrice para que el corredor reciba valores válidos alineados con ticks.
  • El volumen se ajusta para respetar VolumeStep, MinVolume y MaxVolume antes de enviar cada pedido.
  • Todos los mensajes de diagnóstico se enrutan a través de AddInfoLog/AddWarningLog, reflejando la salida detallada del script MetaTrader.
  • La implementación de Python se omite intencionalmente, según lo solicitado.

Consejos de uso

  1. Asigne un instrumento líquido y una cartera, luego comience la estrategia. Las escaleras pendientes aparecerán instantáneamente después de la primera actualización de nivel 1.
  2. Aumente OrdersPerSide con precaución: cada escalón adicional da como resultado otra orden pendiente activa del lado del corredor.
  3. Para imitar el EA original con precisión, mantenga el acelerador predeterminado en cinco segundos y configure MinStopDistancePoints con el requisito de nivel de parada del corredor.
  4. Recuerde que StockSharp maneja posiciones netas; Si se activan escaleras opuestas simultáneamente, los rellenos resultantes se compensarán parcialmente entre sí en lugar de crear subposiciones cubiertas.
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;
	}
}