Ver no GitHub

Estratégia de grade de piso pendente

Visão geral

A Estratégia de grade de piso pendente é uma versão StockSharp fiel do MetaTrader 4 consultor especialista Pending_tread.mq4. O EA original reconstrói constantemente duas escadas de ordens pendentes: uma escada acima do mercado e outra abaixo. Cada escada pode ser configurada para usar ordens de compra ou venda e o espaçamento é definido em pips. A implementação StockSharp reproduz o mesmo comportamento por meio do API de alto nível sem introduzir indicadores ou coleções adicionais.

Lógica de negociação

  1. Manutenção orientada por ofertas/vendas – a estratégia assina cotações de nível 1 (SubscribeLevel1) e mantém os preços de compra e venda mais recentes. Cada vez que novos dados chegam, a rotina de manutenção é executada (com acelerador configurável) e compara as ordens pendentes existentes com o tamanho da grade configurado.
  2. Escada acima do mercado – dependendo de AboveMarketSide, o algoritmo coloca ordens de compra stop ou de venda com limite em incrementos de PipStep pips acima do mercado. Cada novo pedido recebe seu próprio nível de lucro, compensado em TakeProfitPips pips.
  3. Escada abaixo do mercado – o parâmetro BelowMarketSide seleciona entre ordens de limite de compra e ordens stop de venda empilhadas abaixo do mercado. Aplica-se o mesmo espaçamento de pip e lógica de lucro.
  4. Proteção de nível de parada – o parâmetro MinStopDistancePoints emula a verificação MetaTrader MODE_STOPLEVEL. As ordens são ignoradas quando a distância entre o preço e a âncora de compra/venda relevante é menor que o limite fornecido.
  5. AceleraçãoThrottleSeconds espelha a aceleração original de cinco segundos que evitou erros de TRADE_CONTEXT_BUSY. Apenas um ciclo de manutenção é executado durante esse intervalo, independentemente de quantos ticks chegam.

Todas as entradas baseadas em pip (PipStep, TakeProfitPips) são convertidas em compensações de preço absoluto usando os instrumentos PriceStep e Decimals. As cotações de cinco dígitos multiplicam automaticamente o passo por dez para corresponder à lógica do MetaTrader "ponto ajustado".

Parâmetros

Parâmetro Padrão Descrição
OrderVolume 0,01 Volume utilizado ao colocar todas as ordens pendentes. Arredondado para a etapa de volume do instrumento antes do registro.
PipStep 12 Espaçamento entre ordens consecutivas na escada, expresso em pips.
TakeProfitPips 10 Distância em pips usada para colocar o lucro para cada ordem pendente.
OrdersPerSide 10 Número máximo de ordens ativas mantidas acima e abaixo do mercado.
AboveMarketSide Comprar Tipo de pedido usado acima do mercado. Buy cria ordens de parada de compra, Sell cria ordens de limite de venda.
BelowMarketSide Vender Tipo de pedido usado abaixo do mercado. Buy cria ordens de limite de compra, Sell cria ordens de stop de venda.
MinStopDistancePoints 0 Distância mínima (em pontos brutos) permitida entre o preço de compra/venda e o preço pendente. Defina isso para o corretor MODE_STOPLEVEL se necessário.
ThrottleSeconds 5 Período de resfriamento entre os ciclos de manutenção da rede.
SlippagePoints 3 Preservado para paridade de documentação; StockSharp pedidos pendentes não usam esse valor.

Notas de implementação

  • Usa apenas os auxiliares de alto nível StockSharp (SubscribeLevel1, BuyLimit, SellLimit, BuyStop, SellStop).
  • Os preços são normalizados por meio de Security.ShrinkPrice para que a corretora receba valores válidos alinhados aos ticks.
  • O volume é ajustado para respeitar VolumeStep, MinVolume e MaxVolume antes de cada pedido ser enviado.
  • Todas as mensagens de diagnóstico são roteadas por meio de AddInfoLog / AddWarningLog, espelhando a saída detalhada do script MetaTrader.
  • A implementação do Python foi omitida intencionalmente, conforme solicitado.

Dicas de uso

  1. Atribua um instrumento e um portfólio líquidos e, em seguida, inicie a estratégia. As escadas pendentes aparecerão instantaneamente após a primeira atualização de nível 1.
  2. Aumente OrdersPerSide com cautela: cada degrau adicional resulta em outra ordem pendente ativa no lado do corretor.
  3. Para imitar o EA original com precisão, mantenha o acelerador padrão em cinco segundos e configure MinStopDistancePoints com o requisito de nível de stop do corretor.
  4. Lembre-se de que StockSharp lida com posições líquidas; se escadas opostas forem acionadas simultaneamente, os preenchimentos resultantes compensarão parcialmente uns aos outros, em vez de criar subposições protegidas.
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;
	}
}