Ver en GitHub

Estrategia Multi Arbitración

Descripción general

La Estrategia Multi Arbitración es un port StockSharp del asesor experto MetaTrader "Multi_arbitration 1.000". El script original evalúa continuamente las posiciones de compra y venta existentes, añade nuevas operaciones en la dirección con menor beneficio flotante, y realiza una liquidación global una vez que se alcanzan los objetivos generales de beneficio. Esta implementación en C# mantiene la lógica de decisión central adaptándola al modelo de portfolio de compensación de StockSharp y a la API de estrategias de alto nivel.

La estrategia:

  • Abre una posición larga inicial tan pronto como llega la primera vela finalizada.
  • Compara el beneficio no realizado de la dirección activa con la dirección alternativa para decidir si se requiere un giro.
  • Fuerza una posición plana cuando se supera el objetivo de beneficio configurado o cuando la presión de posición crece más allá de un límite configurable.
  • Utiliza solo órdenes de mercado (BuyMarket / SellMarket) para mantener simplicidad y ejecución rápida.

Lógica de Trading

  1. Orden inicial – La primera vela finalizada activa una orden de mercado larga con el volumen de operación configurado. Esto reproduce la entrada inmediata al mercado del experto MetaTrader.
  2. Comparación de beneficios – En cada vela finalizada, la estrategia calcula el PnL flotante de la dirección actual:
    • Beneficio largo = (close - entry) * volume
    • Beneficio corto = (entry - close) * volume
  3. Selección de posición – Si la dirección alternativa funcionaría mejor actualmente que la activa, la estrategia gira la posición enviando una orden de mercado dimensionada para cubrir la exposición existente y abrir una nueva posición en la nueva dirección. Cuando no hay posición abierta, el algoritmo por defecto realiza una entrada larga, coincidiendo con el asesor experto original.
  4. Guardia de límite de posiciones – Un parámetro configurable MaxOpenPositions refleja la comprobación MetaTrader contra LimitOrders(). Cuando la exposición combinada larga/corta alcanza este límite y la estrategia es rentable, aplana el libro para evitar el apalancamiento excesivo.
  5. Salida por objetivo de beneficio – Cuando el PnL de la cuenta (realizado + no realizado) supera el umbral ProfitForClose, la estrategia cierra todas las posiciones, exactamente como la comprobación original Equity - Balance.

Parámetros

Nombre Descripción Por defecto
TradeVolume Volumen utilizado para cada orden de mercado. Representa el tamaño mínimo de lote en el EA original. 1
ProfitForClose Umbral de beneficio que activa una salida global una vez superado. 300
MaxOpenPositions Número máximo de posiciones simultáneas permitidas antes de que la estrategia fuerce un aplanamiento. Actúa como equivalente a limit - 15. 15
CandleType Tipo de datos de vela que sincroniza las decisiones de operación. Por defecto es marco temporal de 1 minuto. velas de 1 minuto

Notas de Implementación

  • StockSharp usa un modelo de posición de compensación, por lo que la estrategia solo puede mantener una dirección neta a la vez. Los giros se manejan dimensionando las órdenes de mercado para cerrar la exposición existente y abrir una nueva posición en la dirección opuesta.
  • La llamada StartProtection() se utiliza para heredar el manejo de riesgo incorporado (por ejemplo, stop-out en posiciones distintas de cero cuando se detiene la estrategia).
  • Todas las variables de estado (_entryPrice, _currentSide, _initialOrderPlaced) se reinician en OnReseted para soportar reinicios y simulaciones repetidas sin datos obsoletos.
  • La estrategia solo reacciona a velas finalizadas para evitar el doble conteo de beneficios en barras parcialmente formadas.

Recomendaciones de Uso

  • Alinee el parámetro TradeVolume con el tamaño de lote del instrumento o el multiplicador del contrato.
  • El valor ProfitForClose debe establecerse usando la misma moneda que el PnL de la cuenta (por ejemplo, USD para cuentas FX).
  • Aumente o disminuya MaxOpenPositions dependiendo de cuán agresivamente quiera que la estrategia acumule exposición antes de forzar un aplanamiento.
  • Debido a que la estrategia siempre comienza con una operación larga, considere iniciarla manualmente cuando las entradas largas sean aceptables para el instrumento negociado.

Diferencias con la Versión MetaTrader

  • El modo de cobertura de MetaTrader permite posiciones largas y cortas simultáneas, mientras que este port opera en un entorno de compensación. La lógica de decisión aún compara la rentabilidad direccional, pero solo se mantiene una posición neta en cualquier momento.
  • Las comprobaciones específicas de la plataforma (permisos de trading del terminal, selección del tipo de relleno, números mágicos de cuenta) se reemplazan con equivalentes StockSharp como StartProtection() y suscripciones de velas.
  • Los diagnósticos comentados del archivo MQL no se reproducen; confíe en el registro StockSharp si se requiere información en tiempo de ejecución.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-direction arbitration strategy adapted from MetaTrader logic.
/// </summary>
public class MultiArbitrationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitForClose;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private bool _initialOrderPlaced;
	private decimal _entryPrice;
	private Sides? _currentSide;

	/// <summary>
	/// Target profit that triggers a full position exit.
	/// </summary>
	public decimal ProfitForClose
	{
		get => _profitForClose.Value;
		set => _profitForClose.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous positions allowed before forcing a flatten.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Candle type used for synchronization and decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MultiArbitrationStrategy"/> class.
	/// </summary>
	public MultiArbitrationStrategy()
	{
		_profitForClose = Param(nameof(ProfitForClose), 300m)
			.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume used when opening new positions.", "Trading");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 15)
			.SetGreaterThanZero()
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_initialOrderPlaced = false;
		_entryPrice = 0m;
		_currentSide = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_initialOrderPlaced)
		{
			OpenLong(candle);
			_initialOrderPlaced = true;
		}

		var longCount = _currentSide == Sides.Buy ? 1 : 0;
		var shortCount = _currentSide == Sides.Sell ? 1 : 0;

		var longProfit = _currentSide == Sides.Buy ? (candle.ClosePrice - _entryPrice) * Volume : 0m;
		var shortProfit = _currentSide == Sides.Sell ? (_entryPrice - candle.ClosePrice) * Volume : 0m;

		if (longCount + shortCount < MaxOpenPositions)
		{
			if (longProfit < shortProfit && _currentSide != Sides.Buy)
			{
				OpenLong(candle);
			}
			else if (shortProfit < longProfit && _currentSide != Sides.Sell)
			{
				OpenShort(candle);
			}
			else if (longProfit == 0m && shortProfit == 0m && Position == 0 && _currentSide is null)
			{
				OpenLong(candle);
			}
		}
		else if (PnL > 0m && Position != 0)
		{
			FlattenPosition(candle);
		}

		if (PnL > ProfitForClose && Position != 0)
		{
			FlattenPosition(candle);
		}
	}

	private void OpenLong(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Already holding a long position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Buy;
			return;
		}

		BuyMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Buy;
	}

	private void OpenShort(ICandleMessage candle)
	{
		if (Position < 0)
		{
			// Already holding a short position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Sell;
			return;
		}

		SellMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Sell;
	}

	private void FlattenPosition(ICandleMessage candle)
	{
		if (_currentSide is null)
			return;

		if (Position > 0)
		{
			SellMarket();
		}
		else if (Position < 0)
		{
			BuyMarket();
		}

		_currentSide = null;
		_entryPrice = 0m;
	}
}