Ver en GitHub

Estrategia de Órdenes Pendientes OCO

Descripción general

La Estrategia de Órdenes Pendientes OCO replica el comportamiento del asesor experto MetaTrader4 OCO_EA.mq4 dentro de la API de alto nivel de StockSharp. El algoritmo permite a un trader armar hasta cuatro disparadores de precio independientes (buy limit, buy stop, sell limit, sell stop). Cuando el mejor bid o ask en vivo toca el nivel de precio configurado, la estrategia envía una orden de mercado inmediata, cancelando opcionalmente todos los demás disparadores pendientes de forma clásica "uno cancela los otros" (OCO).

La estrategia depende puramente de datos de mercado de nivel 1 – no se requieren indicadores históricos. Está pensada para flujos de trabajo de trading discrecional o semi-automatizado donde los traders definen manualmente los niveles de precio y quieren que la plataforma ejecute tan pronto como se toque el nivel, mientras también adjunta órdenes de salida protectoras.

Lógica de negociación

  1. El trader establece cualquier combinación de los cuatro precios disparadores y activa el parámetro Armed a true.
  2. La estrategia se suscribe a actualizaciones de nivel 1 y mantiene el último mejor bid y ask en memoria.
  3. En cada actualización compara los precios almacenados con los umbrales configurados:
    • Si el mejor ask es menor o igual que el precio Buy limit, se envía una orden de compra de mercado con el volumen configurado.
    • Si el mejor ask es mayor o igual que el precio Buy stop, se envía una orden de compra de mercado.
    • Si el mejor bid es mayor o igual que el precio Sell limit, se envía una orden de venta de mercado.
    • Si el mejor bid es menor o igual que el precio Sell stop, se envía una orden de venta de mercado.
  4. Después de cada disparador ejecutado, el nivel correspondiente se borra (se devuelve a cero). Cuando Use OCO link está habilitado, todos los demás niveles se borran inmediatamente, reflejando el comportamiento original de MT4. Cuando el enlace OCO está deshabilitado, los demás niveles permanecen activos hasta que disparan o se borran manualmente.
  5. Si todos los precios disparadores son cero, la estrategia se desarma automáticamente cambiando Armed de vuelta a false.

Todas las ejecuciones se realizan con llamadas BuyMarket y SellMarket para garantizar rellenos inmediatos que respeten el enrutamiento de intercambio configurado en el entorno StockSharp. Se producen entradas de registro informativas para cada disparador para simplificar el monitoreo.

Parámetros

  • Order volume – volumen enviado con cada orden de mercado. El valor debe ser positivo.
  • Buy limit price – umbral de precio ask que activa una entrada larga de estilo límite. Establecer en 0 para deshabilitar.
  • Buy stop price – umbral de precio ask que activa una entrada larga de estilo stop. Establecer en 0 para deshabilitar.
  • Sell limit price – umbral de precio bid que activa una entrada corta de estilo límite. Establecer en 0 para deshabilitar.
  • Sell stop price – umbral de precio bid que activa una entrada corta de estilo stop. Establecer en 0 para deshabilitar.
  • Stop loss (pips) – distancia en puntos del instrumento para el stop de protección. Convertido a precio multiplicando por Security.PriceStep (respaldo 1 cuando el instrumento no reporta un tamaño de tick).
  • Take profit (pips) – distancia en puntos del instrumento para el objetivo de beneficio. Se usa la misma lógica de conversión que para el stop loss.
  • Use OCO link – si es true, la primera orden rellena borra los niveles de precio restantes y desarma la estrategia. Si es false, los niveles restantes permanecen activos hasta que se disparen individualmente.
  • Armed – interruptor de seguridad que habilita o deshabilita la lógica de trading. La estrategia lo restablece automáticamente a false cuando no quedan niveles de disparadores activos.

Gestión de riesgos

StartProtection está habilitado durante OnStarted, adjuntando offsets de stop-loss y take-profit de precio absoluto a cada posición abierta. Los offsets se derivan de los parámetros Stop loss (pips) y Take profit (pips) usando el tamaño del tick del instrumento. Las órdenes de protección siempre se envían como órdenes de mercado para garantizar la ejecución de salida incluso cuando el instrumento subyacente es ilíquido.

Como la estrategia es puramente basada en eventos, no mantiene órdenes límite pendientes en el intercambio; reacciona a los datos del mercado y envía órdenes de mercado, igual que la versión MQL original que emitía órdenes inmediatas y luego las modificaba para aplicar las distancias de stop-loss y take-profit.

Consejos de uso

  1. Configurar el instrumento, portafolio y conexión dentro de StockSharp como de costumbre.
  2. Establecer Order volume para que coincida con el tamaño de lote deseado.
  3. Ingresar cualquier subconjunto de precios disparadores y cambiar Armed a true. Los valores dejados en 0 se ignoran.
  4. Opcionalmente deshabilitar Use OCO link para mantener los disparadores restantes activos después del primer relleno.
  5. Monitorear el registro para mensajes que confirmen cada disparador y el estado de restablecimiento automático.

Recuerde que la estrategia usa el paso de precio proporcionado por el broker. Si el instrumento de trading cotiza en pips fraccionarios o usa tamaños de tick no convencionales, ajuste las distancias basadas en pips en consecuencia antes de armar la estrategia.

Diferencias respecto al script MQL original

  • La estrategia usa el ayudante StartProtection de StockSharp en lugar de modificar manualmente las órdenes para aplicar niveles de stop-loss y take-profit.
  • Las suscripciones de datos de nivel 1 se manejan a través de enlaces de alto nivel en lugar de encuestas manuales de los valores Bid, Ask, High y Low.
  • Los parámetros se exponen a través de StrategyParam<T> para que puedan ajustarse y optimizarse directamente en la UI de StockSharp.
  • El registro reemplaza las notificaciones Comment y PlaySound de MT4, proporcionando transparencia de ejecución dentro de los registros de 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.");
	}
}