Problem Description
Solution
Here is a complete Python script for a Spot-Future Crypto Arbitrage Bot using the QuantConnect Lean API.
Strategy Overview
This algorithm implements a Basis Trading (Convergence) strategy.
- Assets: It trades BTCUSDT Spot against BTCUSDT Futures.
- Entry Logic:
- Contango (Future > Spot): If the spread exceeds the
entry_threshold(e.g., 1%), the bot buys Spot and shorts the Future. - Backwardation (Spot > Future): If the spread drops below negative
entry_threshold, the bot shorts Spot and buys the Future.
- Contango (Future > Spot): If the spread exceeds the
- Exit Logic: When the spread converges back to 0 (or a small
exit_threshold), the bot liquidates all positions to capture the profit. - Execution: Uses
set_holdingsto maintain a delta-neutral portfolio (50% capital in Spot, 50% in Futures).
Python Code
# region imports
from AlgorithmImports import *
# endregion
class CryptoSpotFutureArbitrage(QCAlgorithm):
def initialize(self):
# 1. Set Setup and Strategy Parameters
self.set_start_date(2023, 1, 1)
self.set_end_date(2024, 1, 1)
self.set_cash(100000) # Set Strategy Cash (USDT)
# 2. Brokerage Setup
# We use Binance as it supports both Spot and Futures well in LEAN
self.set_brokerage_model(BrokerageName.BINANCE, AccountType.MARGIN)
# 3. Add Assets
# Add Spot BTCUSDT
self.spot_symbol = self.add_crypto("BTCUSDT", Resolution.MINUTE).symbol
# Add Future BTCUSDT (Perpetual or Front Month)
# LEAN automatically handles the continuous contract mapping if configured
future = self.add_crypto_future("BTCUSDT", Resolution.MINUTE)
self.future_symbol = future.symbol
# 4. Strategy Variables
self.entry_threshold = 0.01 # Enter when spread is 1%
self.exit_threshold = 0.001 # Exit when spread is 0.1%
self.spread_window = RollingWindow[float](10) # For logging/analysis
# Warm up period to ensure data is ready
self.set_warm_up(timedelta(minutes=5))
def on_data(self, data: Slice):
# Ensure we have data for both symbols
if not (data.contains_key(self.spot_symbol) and data.contains_key(self.future_symbol)):
return
# Get current prices
spot_price = data[self.spot_symbol].close
future_price = data[self.future_symbol].close
# Calculate Spread: (Future - Spot) / Spot
# Positive Spread = Contango (Future is more expensive)
# Negative Spread = Backwardation (Spot is more expensive)
spread = (future_price - spot_price) / spot_price
self.spread_window.add(spread)
# --- Execution Logic ---
# Check if we are currently invested
if not self.portfolio.invested:
# ENTRY: Contango Arbitrage (Buy Spot, Sell Future)
if spread > self.entry_threshold:
self.debug(f"Entry Long Arb | Spread: {spread:.4f} | Spot: {spot_price} | Fut: {future_price}")
# Allocate 50% to Spot (Long) and 50% to Future (Short)
self.set_holdings(self.spot_symbol, 0.45)
self.set_holdings(self.future_symbol, -0.45)
# ENTRY: Backwardation Arbitrage (Sell Spot, Buy Future)
# Note: Requires margin to short Spot
elif spread < -self.entry_threshold:
self.debug(f"Entry Short Arb | Spread: {spread:.4f} | Spot: {spot_price} | Fut: {future_price}")
# Allocate 50% to Spot (Short) and 50% to Future (Long)
self.set_holdings(self.spot_symbol, -0.45)
self.set_holdings(self.future_symbol, 0.45)
else:
# EXIT: Convergence
# We check the absolute value of the spread. If it is near zero, we exit.
if abs(spread) < self.exit_threshold:
self.debug(f"Exit Convergence | Spread: {spread:.4f}")
self.liquidate()
def on_order_event(self, order_event: OrderEvent):
# Log filled orders for debugging
if order_event.status == OrderStatus.FILLED:
self.log(f"Order Filled: {order_event.symbol} - {order_event.direction} @ {order_event.fill_price}")
def on_end_of_algorithm(self):
self.log(f"Final Portfolio Value: {self.portfolio.total_portfolio_value}")
Key Implementation Details
-
Brokerage Model (
BrokerageName.BINANCE):- Crypto arbitrage requires a brokerage model that supports both Spot and Futures. Binance or Bybit are the standard choices in QuantConnect.
AccountType.MARGINis selected to allow shorting (selling assets you don't own), which is required for the futures leg of the trade.
-
Data Handling (
on_data):- The script checks
data.contains_keyto ensure both price feeds are available before calculating the spread. - The spread is calculated as a percentage relative to the spot price.
- The script checks
-
Position Sizing (
set_holdings):- We use
0.45(45%) instead of0.50(50%) to leave a small buffer for fees and price fluctuations. This prevents the algorithm from being rejected due to "Insufficient Buying Power."
- We use
-
Directionality:
- Long Arb: If Future > Spot, we buy Spot and Short Future. We profit if the Future price drops or Spot price rises (convergence).
- Short Arb: If Spot > Future, we Short Spot and Buy Future.
Q&A: Crypto Arbitrage in QuantConnect
Q: Why do we use set_holdings instead of market_order?
A: set_holdings automatically calculates the quantity needed to reach a specific percentage of your portfolio value. In arbitrage, balancing the dollar value of the Spot and Future legs is crucial to remain "Delta Neutral" (immune to market direction). set_holdings handles this math for you.
Q: How does this bot handle funding rates?
A: This specific script focuses on price convergence. However, in a real Perpetual Futures trade, you would pay or receive a funding rate every 8 hours. In QuantConnect, funding rates are automatically deducted/added to your cash balance by the Lean engine when using supported brokerages like Binance or Bitfinex.
Q: Can I trade this on other pairs like ETHUSDT?
A: Yes. Simply change the ticker in the initialize method:
self.spot_symbol = self.add_crypto("ETHUSDT", Resolution.MINUTE).symbol
self.future_symbol = self.add_crypto_future("ETHUSDT", Resolution.MINUTE).symbol