Estratégia de Arbitragem

Visão Geral

ArbitrageStrategy é uma estratégia de arbitragem entre um contrato de futuros e o seu ativo subjacente. Acompanha spreads entre instrumentos e abre posições quando surgem oportunidades de arbitragem.

Componentes Principais

A estratégia herda de Strategy e utiliza parâmetros para configuração:

public class ArbitrageStrategy : Strategy
{
	private enum ArbitrageState
	{
		Contango,        // O preço dos futuros é superior ao ativo subjacente
		Backwardation,   // O preço do ativo subjacente é superior aos futuros
		None,            // Sem posição
		OrderRegistration // Em processo de registo de ordens
	}

	// Parâmetros da estratégia
	private readonly StrategyParam<Security> _futureSecurity;
	private readonly StrategyParam<Security> _stockSecurity;
	private readonly StrategyParam<Portfolio> _futurePortfolio;
	private readonly StrategyParam<Portfolio> _stockPortfolio;
	private readonly StrategyParam<decimal> _stockMultiplicator;
	private readonly StrategyParam<decimal> _futureVolume;
	private readonly StrategyParam<decimal> _stockVolume;
	private readonly StrategyParam<decimal> _profitToExit;
	private readonly StrategyParam<decimal> _spreadToGenerateSignal;
}

Parâmetros da Estratégia

A estratégia permite personalizar os seguintes parâmetros:

  • FutureSecurity - instrumento de futuros
  • StockSecurity - instrumento do ativo subjacente
  • FuturePortfolio - portefólio para negociação de futuros
  • StockPortfolio - portefólio para negociação do ativo subjacente
  • StockMultiplicator - multiplicador do ativo subjacente (por exemplo, tamanho do lote)
  • FutureVolume - volume para negociação de futuros
  • StockVolume - volume para negociação do ativo subjacente
  • ProfitToExit - limiar de lucro para saída da posição
  • SpreadToGenerateSignal - limiar de spread para geração do sinal de entrada

Inicialização da Estratégia

No método OnStarted2, os parâmetros são validados e são criadas subscrições aos livros de ordens e às transações próprias:

protected override void OnStarted2(DateTime time)
{
	base.OnStarted2(time);

	if (FutureSecurity == null)
		throw new InvalidOperationException("O instrumento futuro não foi especificado.");

	if (StockSecurity == null)
		throw new InvalidOperationException("O instrumento de ações não foi especificado.");

	if (FuturePortfolio == null)
		throw new InvalidOperationException("O portfólio de futuros não foi especificado.");

	if (StockPortfolio == null)
		throw new InvalidOperationException("O portfólio de ações não foi especificado.");

	_futId = FutureSecurity.ToSecurityId();
	_stockId = StockSecurity.ToSecurityId();

	// Subscrição de atualizações do livro de ordens para ambos os instrumentos
	var futureDepthSubscription = new Subscription(DataType.MarketDepth, FutureSecurity);
	var stockDepthSubscription = new Subscription(DataType.MarketDepth, StockSecurity);

	futureDepthSubscription.WhenOrderBookReceived(this).Do(ProcessMarketDepth).Apply(this);
	stockDepthSubscription.WhenOrderBookReceived(this).Do(ProcessMarketDepth).Apply(this);

	// Subscrição de transações próprias para acompanhar preços de execução
	this
		.WhenOwnTradeReceived()
		.Do(OnOwnTradeReceived)
		.Apply(this);

	// Enviar pedidos de subscrição de dados de mercado
	Subscribe(futureDepthSubscription);
	Subscribe(stockDepthSubscription);
}

Processamento de Dados de Mercado

O método ProcessMarketDepth é chamado quando um livro de ordens é atualizado e implementa a lógica principal:

private void ProcessMarketDepth(IOrderBookMessage depth)
{
	// Atualizar o último livro de ordens de cada instrumento
	if (depth.SecurityId == _futId)
		_lastFut = depth;
	else if (depth.SecurityId == _stockId)
		_lastSt = depth;

	// Aguardar dados para ambos os instrumentos
	if (_lastFut is null || _lastSt is null)
		return;

	// Calcular preços médios ponderados por volume para volumes específicos
	_futBid = GetAveragePrice(_lastFut, Sides.Sell, FutureVolume);
	_futAck = GetAveragePrice(_lastFut, Sides.Buy, FutureVolume);
	_stBid = GetAveragePrice(_lastSt, Sides.Sell, StockVolume) * StockMultiplicator;
	_stAsk = GetAveragePrice(_lastSt, Sides.Buy, StockVolume) * StockMultiplicator;

	// Validar preços
	if (_futBid == 0 || _futAck == 0 || _stBid == 0 || _stAsk == 0)
		return;

	// Calcular spreads
	var contangoSpread = _futBid - _stAsk;        // Preço dos futuros > preço do ativo subjacente
	var backwardationSpread = _stBid - _futAck;   // Preço do ativo subjacente > preço dos futuros

	decimal spread;
	ArbitrageState arbitrageSignal;

	// Determinar a melhor oportunidade de arbitragem
	if (backwardationSpread > contangoSpread)
	{
		arbitrageSignal = ArbitrageState.Backwardation;
		spread = backwardationSpread;
	}
	else
	{
		arbitrageSignal = ArbitrageState.Contango;
		spread = contangoSpread;
	}

	// Registar estado atual e spreads
	LogInfo($"Current state {_currentState}, enter spread = {_enterSpread}");
	LogInfo($"{ArbitrageState.Backwardation} spread = {backwardationSpread}");
	LogInfo($"{ArbitrageState.Contango}        spread = {contangoSpread}");
	LogInfo($"Entrada pelo spread:{SpreadToGenerateSignal}. Saída pelo lucro:{ProfitToExit}");

	// Recalcular lucro com base nas condições atuais de mercado
	if (_currentState != ArbitrageState.None && _currentState != ArbitrageState.OrderRegistration)
	{
		CalculateProfit();
		LogInfo($"Profit: {_profit}");
	}

	// Processar sinais com base no estado atual e nas condições de mercado
	ProcessSignals(arbitrageSignal, spread);
}

Lógica de Negociação

O processamento de sinais e a tomada de decisões de entrada/saída são implementados no método ProcessSignals:

private void ProcessSignals(ArbitrageState arbitrageSignal, decimal spread)
{
	// Entrar numa nova posição quando não existe posição aberta e o spread excede o limiar
	if (_currentState == ArbitrageState.None && spread > SpreadToGenerateSignal)
	{
		_currentState = ArbitrageState.OrderRegistration;

		if (arbitrageSignal == ArbitrageState.Backwardation)
		{
			ExecuteBackwardation();
		}
		else
		{
			ExecuteContango();
		}
	}
	// Sair da posição de Backwardation quando o limiar de lucro é atingido
	else if (_currentState == ArbitrageState.Backwardation && _profit >= ProfitToExit)
	{
		_currentState = ArbitrageState.OrderRegistration;
		CloseBackwardationPosition();
	}
	// Sair da posição de Contango quando o limiar de lucro é atingido
	else if (_currentState == ArbitrageState.Contango && _profit >= ProfitToExit)
	{
		_currentState = ArbitrageState.OrderRegistration;
		CloseContangoPosition();
	}
}

Cálculo de Lucro

O método CalculateProfit calcula o lucro atual com base nos preços de entrada e nos preços atuais:

private void CalculateProfit()
{
	switch (_currentState)
	{
		case ArbitrageState.Backwardation:
			// Comprar futuros, vender ativo subjacente - lucro quando o preço dos futuros sobe e o preço do ativo subjacente desce
			_profit = (_stockExitPrice * StockMultiplicator - _stAsk) + (_futBid - _futureBuyPrice);
			break;

		case ArbitrageState.Contango:
			// Vender futuros, comprar ativo subjacente - lucro quando o preço dos futuros desce e o preço do ativo subjacente sobe
			_profit = (_futureExitPrice - _futAck) + (_stBid - _stockBuyPrice * StockMultiplicator);
			break;

		default:
			_profit = 0;
			break;
	}
}

Geração de Ordens

Para executar estratégias de arbitragem, são utilizados métodos para gerar ordens:

private (Order buy, Order sell) GenerateOrdersBackwardation()
{
	var futureBuy = CreateOrder(Sides.Buy, FutureVolume);
	futureBuy.Portfolio = FuturePortfolio;
	futureBuy.Security = FutureSecurity;
	futureBuy.Type = OrderTypes.Market;

	var stockSell = CreateOrder(Sides.Sell, StockVolume);
	stockSell.Portfolio = StockPortfolio;
	stockSell.Security = StockSecurity;
	stockSell.Type = OrderTypes.Market;

	return (futureBuy, stockSell);
}

private (Order sell, Order buy) GenerateOrdersContango()
{
	var futureSell = CreateOrder(Sides.Sell, FutureVolume);
	futureSell.Portfolio = FuturePortfolio;
	futureSell.Security = FutureSecurity;
	futureSell.Type = OrderTypes.Market;

	var stockBuy = CreateOrder(Sides.Buy, StockVolume);
	stockBuy.Portfolio = StockPortfolio;
	stockBuy.Security = StockSecurity;
	stockBuy.Type = OrderTypes.Market;

	return (futureSell, stockBuy);
}

Funcionalidades

  • A estratégia suporta trabalhar com dois instrumentos diferentes e dois portefólios
  • São utilizadas ordens de mercado para execução rápida
  • Regras (IMarketRule) são utilizadas para acompanhar a execução das ordens
  • O preço médio ponderado por volume é calculado com base no volume para preços mais precisos
  • A lógica de arbitragem considera spreads diretos (contango) e inversos (backwardation)
  • Suporta cálculo automático de lucro e saída quando o limiar alvo é atingido