Ver no GitHub

Estratégia Multi Arbitragem

Visão geral

A Estratégia Multi Arbitragem é um port StockSharp do consultor especialista MetaTrader "Multi_arbitration 1.000". O script original avalia continuamente as posições compradas e vendidas existentes, adiciona novos trades na direção com menor lucro flutuante, e realiza uma liquidação global assim que os objetivos gerais de lucro são atingidos. Esta implementação em C# mantém a lógica de decisão central, adaptando-a ao modelo de portfolio de compensação do StockSharp e à API de estratégias de alto nível.

A estratégia:

  • Abre uma posição comprada inicial assim que a primeira vela finalizada chega.
  • Compara o lucro não realizado da direção ativa com a direção alternativa para decidir se uma reversão é necessária.
  • Força uma posição plana quando o objetivo de lucro configurado é excedido ou quando a pressão de posição cresce além de um limite configurável.
  • Usa apenas ordens de mercado (BuyMarket / SellMarket) para manter simplicidade e execução rápida.

Lógica de Trading

  1. Ordem inicial – A primeira vela finalizada aciona uma ordem de mercado comprada com o volume de operação configurado. Isso reproduz a entrada imediata no mercado do consultor especialista MetaTrader.
  2. Comparação de lucros – Em cada vela finalizada, a estratégia calcula o PnL flutuante da direção atual:
    • Lucro comprado = (close - entry) * volume
    • Lucro vendido = (entry - close) * volume
  3. Seleção de posição – Se a direção alternativa funcionaria melhor atualmente que a ativa, a estratégia inverte a posição enviando uma ordem de mercado dimensionada para cobrir a exposição existente e abrir uma nova posição na nova direção. Quando nenhuma posição está aberta, o algoritmo padroniza para uma entrada comprada, correspondendo ao consultor especialista original.
  4. Guarda de limite de posições – Um parâmetro configurável MaxOpenPositions espelha a verificação MetaTrader contra LimitOrders(). Quando a exposição combinada comprada/vendida atinge esse limite e a estratégia é lucrativa, ela aplaina o livro para evitar alavancagem excessiva.
  5. Saída por objetivo de lucro – Quando o PnL da conta (realizado + não realizado) excede o limite ProfitForClose, a estratégia fecha todas as posições, exatamente como a verificação original Equity - Balance.

Parâmetros

Nome Descrição Padrão
TradeVolume Volume usado para cada ordem de mercado. Representa o tamanho mínimo de lote no EA original. 1
ProfitForClose Limite de lucro que aciona uma saída global quando excedido. 300
MaxOpenPositions Número máximo de posições simultâneas permitidas antes de a estratégia forçar um aplainamento. Equivale a limit - 15. 15
CandleType Tipo de dados de vela que sincroniza as decisões de operação. Padrão é período de 1 minuto. velas de 1 minuto

Notas de Implementação

  • StockSharp usa um modelo de posição de compensação, portanto a estratégia só pode manter uma direção líquida por vez. As reversões são tratadas dimensionando ordens de mercado para fechar a exposição existente e abrir uma nova posição na direção oposta.
  • A chamada StartProtection() é usada para herdar o gerenciamento de risco integrado (por exemplo, stop-out em posições diferentes de zero quando a estratégia é parada).
  • Todas as variáveis de estado (_entryPrice, _currentSide, _initialOrderPlaced) são redefinidas em OnReseted para suportar reinicializações e simulações repetidas sem dados obsoletos.
  • A estratégia só reage a velas finalizadas para evitar a contagem dupla de lucros em barras parcialmente formadas.

Recomendações de Uso

  • Alinhe o parâmetro TradeVolume com o tamanho do lote do instrumento ou o multiplicador do contrato.
  • O valor ProfitForClose deve ser definido usando a mesma moeda que o PnL da conta (por exemplo, USD para contas FX).
  • Aumente ou diminua MaxOpenPositions dependendo de quão agressivamente você quer que a estratégia acumule exposição antes de forçar um aplainamento.
  • Como a estratégia sempre começa com uma operação comprada, considere iniciá-la manualmente quando entradas compradas são aceitáveis para o instrumento negociado.

Diferenças da Versão MetaTrader

  • O modo de cobertura do MetaTrader permite posições compradas e vendidas simultâneas, enquanto este port opera em um ambiente de compensação. A lógica de decisão ainda compara a rentabilidade direcional, mas apenas uma posição líquida é mantida a qualquer momento.
  • Verificações específicas da plataforma (permissões de trading do terminal, seleção do tipo de preenchimento, números mágicos da conta) são substituídas por equivalentes StockSharp como StartProtection() e assinaturas de velas.
  • Diagnósticos comentados do arquivo MQL não são reproduzidos; confie no log do StockSharp se informações em tempo de execução forem necessárias.
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;
	}
}