🚀 AI One-Click Generation of quantconnect Strategy Code

How to Build a Crypto Spot-Futures Arbitrage Bot in Python (with Code)

quantconnect | Published on: 2025/11/27
The following content is generated by EasyQuant .

Problem Description

Write a Python script for a crypto arbitrage bot that trades between spot and futures.

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.

  1. Assets: It trades BTCUSDT Spot against BTCUSDT Futures.
  2. 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.
  3. Exit Logic: When the spread converges back to 0 (or a small exit_threshold), the bot liquidates all positions to capture the profit.
  4. Execution: Uses set_holdings to 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

  1. 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.MARGIN is selected to allow shorting (selling assets you don't own), which is required for the futures leg of the trade.
  2. Data Handling (on_data):

    • The script checks data.contains_key to ensure both price feeds are available before calculating the spread.
    • The spread is calculated as a percentage relative to the spot price.
  3. Position Sizing (set_holdings):

    • We use 0.45 (45%) instead of 0.50 (50%) to leave a small buffer for fees and price fluctuations. This prevents the algorithm from being rejected due to "Insufficient Buying Power."
  4. 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