Ver no GitHub

Estratégia de Ordens Pendentes OCO

Visão geral

A Estratégia de Ordens Pendentes OCO replica o comportamento do consultor especialista MetaTrader4 OCO_EA.mq4 dentro da API de alto nível do StockSharp. O algoritmo permite que um trader ative até quatro gatilhos de preço independentes (buy limit, buy stop, sell limit, sell stop). Sempre que o melhor bid ou ask ao vivo toca o nível de preço configurado, a estratégia envia uma ordem de mercado imediata, cancelando opcionalmente todos os outros gatilhos pendentes de forma clássica "one-cancels-the-others" (OCO).

A estratégia depende puramente de dados de mercado de nível 1 – nenhum indicador histórico é necessário. É destinada a fluxos de trabalho de negociação discricionária ou semi-automatizada onde os traders definem manualmente os níveis de preço e querem que a plataforma execute assim que o nível seja atingido, enquanto também anexa ordens de saída protetoras.

Lógica de negociação

  1. O trader define qualquer combinação dos quatro preços de gatilho e muda o parâmetro Armed para true.
  2. A estratégia se inscreve em atualizações de nível 1 e mantém o último melhor bid e ask na memória.
  3. Em cada atualização compara os preços armazenados com os limiares configurados:
    • Se o melhor ask for menor ou igual ao preço Buy limit, uma ordem de compra de mercado com o volume configurado é enviada.
    • Se o melhor ask for maior ou igual ao preço Buy stop, uma ordem de compra de mercado é enviada.
    • Se o melhor bid for maior ou igual ao preço Sell limit, uma ordem de venda de mercado é enviada.
    • Se o melhor bid for menor ou igual ao preço Sell stop, uma ordem de venda de mercado é enviada.
  4. Após cada gatilho executado, o nível correspondente é apagado (redefinido para zero). Quando Use OCO link está habilitado, todos os outros níveis são apagados imediatamente, espelhando o comportamento original do MT4. Quando o link OCO está desabilitado, outros níveis permanecem ativos até que disparem ou sejam apagados manualmente.
  5. Se todos os preços de gatilho forem zero, a estratégia se desarma automaticamente mudando Armed de volta para false.

Todas as execuções são realizadas com chamadas BuyMarket e SellMarket para garantir preenchimentos imediatos que respeitem o roteamento de câmbio configurado no ambiente StockSharp. Entradas de registro informativas são produzidas para cada gatilho para simplificar o monitoramento.

Parâmetros

  • Order volume – volume enviado com cada ordem de mercado. O valor deve ser positivo.
  • Buy limit price – limiar de preço ask que ativa uma entrada comprada no estilo limite. Definir como 0 para desabilitar.
  • Buy stop price – limiar de preço ask que ativa uma entrada comprada no estilo stop. Definir como 0 para desabilitar.
  • Sell limit price – limiar de preço bid que ativa uma entrada vendida no estilo limite. Definir como 0 para desabilitar.
  • Sell stop price – limiar de preço bid que ativa uma entrada vendida no estilo stop. Definir como 0 para desabilitar.
  • Stop loss (pips) – distância em pontos do instrumento para o stop de proteção. Convertido para preço multiplicando por Security.PriceStep (fallback 1 quando o instrumento não reporta um tamanho de tick).
  • Take profit (pips) – distância em pontos do instrumento para o objetivo de lucro. A mesma lógica de conversão do stop loss é usada.
  • Use OCO link – se true, a primeira ordem preenchida apaga os níveis de preço restantes e desarma a estratégia. Se false, os níveis restantes permanecem ativos até que disparem individualmente.
  • Armed – interruptor de segurança que habilita ou desabilita a lógica de negociação. A estratégia o redefine automaticamente para false quando não há mais níveis de gatilho ativos.

Gestão de risco

StartProtection é habilitado durante OnStarted, anexando offsets de stop-loss e take-profit de preço absoluto a cada posição aberta. Os offsets são derivados dos parâmetros Stop loss (pips) e Take profit (pips) usando o tamanho do tick do instrumento. As ordens de proteção são sempre enviadas como ordens de mercado para garantir a execução da saída mesmo quando o instrumento subjacente é ilíquido.

Como a estratégia é puramente orientada a eventos, ela não mantém ordens limite pendentes na bolsa; ela reage a dados de mercado e envia ordens de mercado, assim como a versão MQL original que emitia ordens imediatas e depois as modificava para aplicar as distâncias de stop-loss e take-profit.

Dicas de uso

  1. Configurar o instrumento, portfólio e conexão dentro do StockSharp como de costume.
  2. Definir Order volume para corresponder ao tamanho de lote desejado.
  3. Inserir qualquer subconjunto de preços de gatilho e mudar Armed para true. Valores deixados em 0 são ignorados.
  4. Opcionalmente desabilitar Use OCO link para manter os gatilhos restantes ativos após o primeiro preenchimento.
  5. Monitorar o registro para mensagens confirmando cada gatilho e o estado de redefinição automática.

Lembre-se que a estratégia usa o passo de preço fornecido pelo corretor. Se o instrumento de negociação cota em pips fracionários ou usa tamanhos de tick não convencionais, ajuste as distâncias baseadas em pips de acordo antes de armar a estratégia.

Diferenças em relação ao script MQL original

  • A estratégia usa o auxiliar StartProtection do StockSharp em vez de modificar manualmente as ordens para aplicar níveis de stop-loss e take-profit.
  • As inscrições de dados de nível 1 são tratadas através de bindings de alto nível em vez de polling manual dos valores Bid, Ask, High e Low.
  • Os parâmetros são expostos através de StrategyParam<T> para que possam ser ajustados e otimizados diretamente na UI do StockSharp.
  • O registro substitui as notificações Comment e PlaySound do MT4, fornecendo transparência de execução dentro dos registros do StockSharp.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Manually armed OCO price triggers driven by Level1 bid/ask updates.
/// </summary>
public class OcoPendingOrdersStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _buyLimitPrice;
	private readonly StrategyParam<decimal> _buyStopPrice;
	private readonly StrategyParam<decimal> _sellLimitPrice;
	private readonly StrategyParam<decimal> _sellStopPrice;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<bool> _useOcoLink;
	private readonly StrategyParam<bool> _armed;
	private decimal[] _initialLevels;
	private bool _initialArmed;
	private decimal? _bestBid;
	private decimal? _bestAsk;

	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal BuyLimitPrice { get => _buyLimitPrice.Value; set => _buyLimitPrice.Value = value; }
	public decimal BuyStopPrice { get => _buyStopPrice.Value; set => _buyStopPrice.Value = value; }
	public decimal SellLimitPrice { get => _sellLimitPrice.Value; set => _sellLimitPrice.Value = value; }
	public decimal SellStopPrice { get => _sellStopPrice.Value; set => _sellStopPrice.Value = value; }
	public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
	public bool UseOcoLink { get => _useOcoLink.Value; set => _useOcoLink.Value = value; }
	public bool Armed { get => _armed.Value; set => _armed.Value = value; }

	public OcoPendingOrdersStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order volume", "Volume sent with each market order", "Trading");
		_buyLimitPrice = Param(nameof(BuyLimitPrice), 0m)
			.SetNotNegative()
			.SetDisplay("Buy limit price", "Ask price threshold that activates a limit-style long entry, 0 disables", "Levels");
		_buyStopPrice = Param(nameof(BuyStopPrice), 0m)
			.SetNotNegative()
			.SetDisplay("Buy stop price", "Ask price threshold that activates a stop-style long entry, 0 disables", "Levels");
		_sellLimitPrice = Param(nameof(SellLimitPrice), 0m)
			.SetNotNegative()
			.SetDisplay("Sell limit price", "Bid price threshold that activates a limit-style short entry, 0 disables", "Levels");
		_sellStopPrice = Param(nameof(SellStopPrice), 0m)
			.SetNotNegative()
			.SetDisplay("Sell stop price", "Bid price threshold that activates a stop-style short entry, 0 disables", "Levels");
		_stopLossPips = Param(nameof(StopLossPips), 0)
			.SetNotNegative()
			.SetDisplay("Stop loss (pips)", "Protective stop distance in instrument points, multiplied by the price step", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 0)
			.SetNotNegative()
			.SetDisplay("Take profit (pips)", "Profit target distance in instrument points, multiplied by the price step", "Risk");
		_useOcoLink = Param(nameof(UseOcoLink), true)
			.SetDisplay("Use OCO link", "The first filled order clears the remaining price levels and disarms the strategy", "Control");
		_armed = Param(nameof(Armed), false)
			.SetDisplay("Armed", "Safety switch for the triggers, reset to false when no active level remains", "Control");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, DataType.Level1)];

	protected override void OnReseted()
	{
		base.OnReseted();
		if (_initialLevels != null)
		{
			BuyLimitPrice = _initialLevels[0];
			BuyStopPrice = _initialLevels[1];
			SellLimitPrice = _initialLevels[2];
			SellStopPrice = _initialLevels[3];
			Armed = _initialArmed;
		}
		_initialLevels = null;
		_initialArmed = false;
		_bestBid = null;
		_bestAsk = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_initialLevels = [BuyLimitPrice, BuyStopPrice, SellLimitPrice, SellStopPrice];
		_initialArmed = Armed;

		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var take = TakeProfitPips > 0 ? new Unit(TakeProfitPips * step, UnitTypes.Absolute) : null;
		var stop = StopLossPips > 0 ? new Unit(StopLossPips * step, UnitTypes.Absolute) : null;
		if (take is not null || stop is not null)
			StartProtection(takeProfit: take, stopLoss: stop, useMarketOrders: true);

		// A Level1 binding only sees updates carrying its build field, so bid-only and ask-only updates need one binding each.
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ProcessLevel1).Start();
		}
	}

	private void ProcessLevel1(Level1ChangeMessage message)
	{
		if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid)
			_bestBid = bid;

		if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask)
			_bestAsk = ask;

		if (!Armed)
			return;

		if (_bestAsk is decimal bestAsk)
		{
			if (BuyLimitPrice > 0m && bestAsk <= BuyLimitPrice)
			{
				var level = BuyLimitPrice;
				BuyLimitPrice = 0m;
				Execute(Sides.Buy, $"Buy limit {level} hit by ask {bestAsk}");

				if (!Armed)
					return;
			}

			if (BuyStopPrice > 0m && bestAsk >= BuyStopPrice)
			{
				var level = BuyStopPrice;
				BuyStopPrice = 0m;
				Execute(Sides.Buy, $"Buy stop {level} hit by ask {bestAsk}");

				if (!Armed)
					return;
			}
		}

		if (_bestBid is decimal bestBid)
		{
			if (SellLimitPrice > 0m && bestBid >= SellLimitPrice)
			{
				var level = SellLimitPrice;
				SellLimitPrice = 0m;
				Execute(Sides.Sell, $"Sell limit {level} hit by bid {bestBid}");

				if (!Armed)
					return;
			}

			if (SellStopPrice > 0m && bestBid <= SellStopPrice)
			{
				var level = SellStopPrice;
				SellStopPrice = 0m;
				Execute(Sides.Sell, $"Sell stop {level} hit by bid {bestBid}");

				if (!Armed)
					return;
			}
		}

		DisarmIfEmpty();
	}

	private void Execute(Sides side, string trigger)
	{
		LogInfo($"{trigger}: {side} {OrderVolume} at market.");

		if (side == Sides.Buy)
			BuyMarket(OrderVolume);
		else
			SellMarket(OrderVolume);

		if (!UseOcoLink)
			return;

		ClearLevels();
		Armed = false;
		LogInfo("OCO link cleared all trigger levels; strategy disarmed.");
	}

	private void ClearLevels()
	{
		BuyLimitPrice = 0m;
		BuyStopPrice = 0m;
		SellLimitPrice = 0m;
		SellStopPrice = 0m;
	}

	private void DisarmIfEmpty()
	{
		if (BuyLimitPrice > 0m || BuyStopPrice > 0m || SellLimitPrice > 0m || SellStopPrice > 0m)
			return;

		Armed = false;
		LogInfo("No trigger levels remain; strategy disarmed.");
	}
}