Arbitrage Strategy
Overview
ArbitrageStrategy
is an arbitrage strategy between a futures contract and its underlying asset. It tracks spreads between instruments and opens positions when arbitrage opportunities arise.
Main Components
The strategy inherits from Strategy and uses parameters for configuration:
public class ArbitrageStrategy : Strategy
{
private enum ArbitrageState
{
Contango, // Futures price is higher than the underlying asset
Backvordation, // Underlying asset price is higher than the futures
None, // No position
OrderRegistration // In the process of registering orders
}
// Strategy parameters
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;
}
Strategy Parameters
The strategy allows customizing the following parameters:
- FutureSecurity - futures instrument
- StockSecurity - underlying asset instrument
- FuturePortfolio - portfolio for futures trading
- StockPortfolio - portfolio for underlying asset trading
- StockMultiplicator - multiplier for the underlying asset (e.g., lot size)
- FutureVolume - volume for futures trading
- StockVolume - volume for underlying asset trading
- ProfitToExit - profit threshold for position exit
- SpreadToGenerateSignal - spread threshold for entry signal generation
Strategy Initialization
In the OnStarted method, parameters are validated, subscriptions to order books and own trades are created:
protected override void OnStarted(DateTimeOffset time)
{
base.OnStarted(time);
if (FutureSecurity == null)
throw new InvalidOperationException("Future security is not specified.");
if (StockSecurity == null)
throw new InvalidOperationException("Stock security is not specified.");
if (FuturePortfolio == null)
throw new InvalidOperationException("Future portfolio is not specified.");
if (StockPortfolio == null)
throw new InvalidOperationException("Stock portfolio is not specified.");
_futId = FutureSecurity.ToSecurityId();
_stockId = StockSecurity.ToSecurityId();
// Subscription to order book updates for both instruments
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);
// Subscription to own trades to track execution prices
this
.WhenOwnTradeReceived()
.Do(OnNewMyTrade)
.Apply(this);
// Sending requests for market data subscription
Subscribe(futureDepthSubscription);
Subscribe(stockDepthSubscription);
}
Processing Market Data
The ProcessMarketDepth
method is called when an order book is updated and implements the main logic:
private void ProcessMarketDepth(IOrderBookMessage depth)
{
// Update the last order book for each instrument
if (depth.SecurityId == _futId)
_lastFut = depth;
else if (depth.SecurityId == _stockId)
_lastSt = depth;
// Wait for data for both instruments
if (_lastFut is null || _lastSt is null)
return;
// Calculate volume-weighted average prices for specific volumes
_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;
// Validate prices
if (_futBid == 0 || _futAck == 0 || _stBid == 0 || _stAsk == 0)
return;
// Calculate spreads
var contangoSpread = _futBid - _stAsk; // Futures price > underlying asset price
var backvordationSpread = _stBid - _futAck; // Underlying asset price > futures price
decimal spread;
ArbitrageState arbitrageSignal;
// Determine the best arbitrage opportunity
if (backvordationSpread > contangoSpread)
{
arbitrageSignal = ArbitrageState.Backvordation;
spread = backvordationSpread;
}
else
{
arbitrageSignal = ArbitrageState.Contango;
spread = contangoSpread;
}
// Log current state and spreads
LogInfo($"Current state {_currentState}, enter spread = {_enterSpread}");
LogInfo($"{ArbitrageState.Backvordation} spread = {backvordationSpread}");
LogInfo($"{ArbitrageState.Contango} spread = {contangoSpread}");
LogInfo($"Entry from spread:{SpreadToGenerateSignal}. Exit from profit:{ProfitToExit}");
// Recalculate profit based on current market conditions
if (_currentState != ArbitrageState.None && _currentState != ArbitrageState.OrderRegistration)
{
CalculateProfit();
LogInfo($"Profit: {_profit}");
}
// Process signals based on current state and market conditions
ProcessSignals(arbitrageSignal, spread);
}
Trading Logic
Signal processing and entry/exit decision making are implemented in the ProcessSignals
method:
private void ProcessSignals(ArbitrageState arbitrageSignal, decimal spread)
{
// Enter a new position when there's no open position and spread exceeds threshold
if (_currentState == ArbitrageState.None && spread > SpreadToGenerateSignal)
{
_currentState = ArbitrageState.OrderRegistration;
if (arbitrageSignal == ArbitrageState.Backvordation)
{
ExecuteBackvardation();
}
else
{
ExecuteContango();
}
}
// Exit from Backvordation position when profit threshold is reached
else if (_currentState == ArbitrageState.Backvordation && _profit >= ProfitToExit)
{
_currentState = ArbitrageState.OrderRegistration;
CloseBackvardationPosition();
}
// Exit from Contango position when profit threshold is reached
else if (_currentState == ArbitrageState.Contango && _profit >= ProfitToExit)
{
_currentState = ArbitrageState.OrderRegistration;
CloseContangoPosition();
}
}
Profit Calculation
The CalculateProfit
method calculates current profit based on entry prices and current prices:
private void CalculateProfit()
{
switch (_currentState)
{
case ArbitrageState.Backvordation:
// Buy futures, sell underlying asset - profit when futures price rises and underlying asset price falls
_profit = (_stockExitPrice * StockMultiplicator - _stAsk) + (_futBid - _futureBuyPrice);
break;
case ArbitrageState.Contango:
// Sell futures, buy underlying asset - profit when futures price falls and underlying asset price rises
_profit = (_futureExitPrice - _futAck) + (_stBid - _stockBuyPrice * StockMultiplicator);
break;
default:
_profit = 0;
break;
}
}
Order Generation
For executing arbitrage strategies, methods for generating orders are used:
private (Order buy, Order sell) GenerateOrdersBackvardation()
{
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);
}
Features
- The strategy supports working with two different instruments and two portfolios
- Market orders are used for fast execution
- Rules (IMarketRule) are used to track order execution
- Volume-weighted average price is calculated based on volume for more accurate prices
- Arbitrage logic considers both direct (contango) and reverse (backwardation) spreads
- Supports automatic profit calculation and exit when target threshold is reached