◈   ⌘ api · Intermediate

Spot Futures Arbitrage API: Build an Automated Bot

A practical guide to automating spot-futures arbitrage using exchange APIs. Covers Python authentication, spread detection, order execution, and risk controls with real code examples.

Uncle Solieditor · voc · 06.05.2026 ·views 22
◈   Contents
  1. → The Spot-Futures Basis: Where the Profit Actually Lives
  2. → Setting Up API Authentication on Binance
  3. → Detecting Arbitrage Opportunities via the REST API
  4. → Executing Both Legs of the Arbitrage Trade
  5. → Rate Limits, Risk Controls, and Position Monitoring
  6. → Frequently Asked Questions

Spot-futures arbitrage is one of the few strategies in crypto where the edge has nothing to do with predicting price direction. It exploits a structural gap: when a perpetual futures contract trades at a premium to the spot price, the funding mechanism that should correct it becomes your income stream. Buy spot BTC on Binance, short the same quantity in BTC perpetual futures, and you collect funding payments every eight hours for as long as the premium persists. The net position is market-neutral — price moves up or down cancel out between the two legs. The challenge is execution. Manually watching a spread across Binance, Bybit, and OKX simultaneously, then clicking through order screens fast enough to capture the entry before the gap closes, is impractical beyond trivial position sizes. Latency kills the trade as capital grows. This guide walks through building a functional spot-futures arbitrage system using Python and exchange REST APIs — with real endpoints, authenticated requests, and error handling that protects you when things inevitably go wrong.

The Spot-Futures Basis: Where the Profit Actually Lives

The basis is the difference between a futures contract price and the spot price of the same asset. In crypto, because perpetual futures never expire, this gap is managed through the funding rate: every eight hours, traders on the losing side of the basis pay traders on the winning side. When futures trade above spot — called contango — long holders pay short holders. The cash-and-carry trade captures this: hold spot long, hold futures short, collect payments. On Binance and Bybit, funding rates settle every 8 hours, meaning three payments per day. Annualized, even modest rates compound significantly. The catch is fees. You pay taker fees entering and exiting both legs. On Binance, spot taker is 0.1%, futures taker is 0.04% — a round trip costs roughly 0.28% in fees alone. Your captured basis must clear this hurdle before the trade makes money. On OKX, a unified account lets you post the same margin across both legs, reducing capital requirements. Understanding this fee math precisely is what separates profitable runs from expensive lessons.

Funding rate scenarios and estimated annualized yields (before fees)
Funding Rate (8h)Payments/DayAnnualized YieldMarket Condition
0.01%3~10.95%Quiet / neutral
0.03%3~32.85%Moderately bullish
0.10%3~109.5%Strong bull run
-0.01%3~-10.95%Bearish (reverse trade)
Never enter a position based on a single funding rate snapshot. Track the 7-day moving average. Rates during the FTX collapse hit -0.3% per period then snapped back within 48 hours — positioning on that signal would have cost you significantly in fees and slippage.

Setting Up API Authentication on Binance

Binance uses HMAC-SHA256 signatures to authenticate private requests. You need an API key with both spot trading and futures trading permissions. Create keys in your Binance account under API Management and whitelist your server's IP immediately — this single step eliminates a large class of key-theft attacks. The spot REST base URL is https://api.binance.com and the USDM futures base URL is https://fapi.binance.com. All signed requests require a timestamp parameter within 1000ms of server time, plus a signature computed from the complete query string. The code below sets up the signing utility, fetches server time to avoid clock-skew errors, and queries your futures wallet balance as a connectivity test.

import hmac
import hashlib
import time
import requests

API_KEY = 'your_api_key_here'
API_SECRET = 'your_api_secret_here'
BASE_SPOT = 'https://api.binance.com'
BASE_FUTURES = 'https://fapi.binance.com'

def sign_params(params: dict) -> str:
    query_string = '&'.join([f'{k}={v}' for k, v in params.items()])
    return hmac.new(
        API_SECRET.encode('utf-8'),
        query_string.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

def api_headers() -> dict:
    return {'X-MBX-APIKEY': API_KEY}

def get_server_time() -> int:
    resp = requests.get(BASE_SPOT + '/api/v3/time')
    resp.raise_for_status()
    return resp.json()['serverTime']

def get_futures_balance(asset: str = 'USDT') -> float:
    params = {'timestamp': get_server_time()}
    params['signature'] = sign_params(params)
    resp = requests.get(
        BASE_FUTURES + '/fapi/v2/balance',
        params=params,
        headers=api_headers()
    )
    resp.raise_for_status()
    for item in resp.json():
        if item['asset'] == asset:
            return float(item['availableBalance'])
    return 0.0

balance = get_futures_balance()
print('Available USDT in futures wallet: ' + str(round(balance, 2)))

Detecting Arbitrage Opportunities via the REST API

The three data points you need for every entry decision are: spot price, futures mark price, and current funding rate. Binance exposes all three through public endpoints — no authentication required. The spot price lives at /api/v3/ticker/price, and the mark price plus funding data at /fapi/v1/premiumIndex. Fetching both concurrently cuts your polling latency roughly in half. The example below uses asyncio with aiohttp for parallel requests and computes the net annualized yield after accounting for round-trip fee costs. The same approach works for Bybit (GET /v5/market/tickers) and OKX (GET /api/v5/public/funding-rate) with minor endpoint differences. A signal flag is set only when net yield clears your configured minimum — this prevents entering during periods when the spread barely covers execution costs.

import asyncio
import aiohttp

SYMBOL = 'BTCUSDT'
MIN_NET_YIELD_PCT = 15.0  # annualized, after fees
ROUND_TRIP_FEE = (0.001 + 0.0004) * 2  # spot + futures, both legs

async def fetch_arb_data(session: aiohttp.ClientSession) -> dict:
    spot_url = 'https://api.binance.com/api/v3/ticker/price?symbol=' + SYMBOL
    premium_url = 'https://fapi.binance.com/fapi/v1/premiumIndex?symbol=' + SYMBOL

    async with session.get(spot_url) as s, session.get(premium_url) as fr:
        spot_data = await s.json()
        funding_data = await fr.json()

    spot_price = float(spot_data['price'])
    mark_price = float(funding_data['markPrice'])
    funding_rate = float(funding_data['lastFundingRate'])

    basis_pct = (mark_price - spot_price) / spot_price * 100
    annualized_yield = funding_rate * 3 * 365 * 100
    net_yield = annualized_yield - (ROUND_TRIP_FEE * 365 * 100)

    return {
        'spot': spot_price,
        'mark': mark_price,
        'basis_pct': round(basis_pct, 4),
        'funding_rate': funding_rate,
        'annualized_yield': round(annualized_yield, 2),
        'net_yield': round(net_yield, 2),
        'signal': net_yield >= MIN_NET_YIELD_PCT
    }

async def monitor(interval: int = 10):
    async with aiohttp.ClientSession() as session:
        while True:
            data = await fetch_arb_data(session)
            tag = '[SIGNAL]' if data['signal'] else '[WATCH] '
            print('{} Spot={:.2f} Mark={:.2f} Basis={:.4f}% Net={:.1f}%/yr'.format(
                tag, data['spot'], data['mark'], data['basis_pct'], data['net_yield']
            ))
            await asyncio.sleep(interval)

asyncio.run(monitor())

Executing Both Legs of the Arbitrage Trade

Leg sequencing is the highest-risk moment in the entire strategy. If you place the spot buy but the futures short fails — due to insufficient margin, a rate limit breach, or a network timeout — you're holding an unintended directional long. The correct sequence is futures short first: this leg defines your hedge. If it fails, abort and never touch spot. If it succeeds, immediately place the spot buy. Use market orders on both legs — limit orders risk partial fills that leave you unhedged. Set a 5-second timeout on each request. On Binance, an order placed at the API layer but not confirmed within the timeout window may still have executed server-side; always query the order status before retrying to avoid accidental double-fills. The code below handles both legs with explicit error handling for the critical failure mode: futures succeeds, spot fails.

import hmac
import hashlib
import time
import requests

API_KEY = 'your_api_key'
API_SECRET = 'your_api_secret'

def signed_post(url: str, params: dict) -> dict:
    params['timestamp'] = int(time.time() * 1000)
    qs = '&'.join([f'{k}={v}' for k, v in params.items()])
    params['signature'] = hmac.new(
        API_SECRET.encode('utf-8'), qs.encode('utf-8'), hashlib.sha256
    ).hexdigest()
    resp = requests.post(
        url, params=params,
        headers={'X-MBX-APIKEY': API_KEY},
        timeout=5
    )
    resp.raise_for_status()
    return resp.json()

def open_arb_position(symbol: str, quantity: float):
    # Leg 1: short futures first — defines the hedge
    try:
        fut = signed_post(
            'https://fapi.binance.com/fapi/v1/order',
            {'symbol': symbol, 'side': 'SELL', 'type': 'MARKET', 'quantity': quantity}
        )
        print('Futures SHORT placed: orderId=' + str(fut['orderId']))
    except requests.exceptions.HTTPError as e:
        print('Futures FAILED (' + str(e.response.status_code) + '): aborting both legs')
        return None, None
    except requests.exceptions.Timeout:
        print('Futures TIMEOUT — verify position via GET /fapi/v1/openOrders before retrying!')
        raise

    # Leg 2: spot buy only after futures hedge is confirmed
    try:
        spot = signed_post(
            'https://api.binance.com/api/v3/order',
            {'symbol': symbol, 'side': 'BUY', 'type': 'MARKET', 'quantity': quantity}
        )
        print('Spot BUY placed: orderId=' + str(spot['orderId']))
        return fut, spot
    except Exception as e:
        print('Spot FAILED: ' + str(e))
        print('CRITICAL: futures leg is open — close it via API or manually!')
        raise

# Open ~$5000 position at $65,000 BTC spot price
open_arb_position('BTCUSDT', 0.077)

Rate Limits, Risk Controls, and Position Monitoring

Binance enforces weight-based rate limits: 1200 request-weight per minute for spot, 2400 for USDM futures. Price ticker requests cost 2 weight each. A 10-second polling loop hitting the premium index endpoint uses roughly 12 weight per minute — well within limits. Order placement adds 1 weight. Watch for 429 responses; on a second consecutive 429 within the same window, Binance may issue a temporary IP ban. Implement exponential backoff starting at 2 seconds, capping at 60. Bybit uses a token-bucket model with similar practical limits. OKX rate limits are per-endpoint and slightly more generous on market data. Whatever exchange you're on, set a conservative self-imposed limit at 60% of the published ceiling — this gives headroom for burst traffic during volatility spikes when you most need fast execution.

Risk management at the strategy level matters more than any individual trade. Set hard caps on total deployed capital — for most operators, 20-30% of portfolio in market-neutral strategies is reasonable. Track the time until next funding settlement: avoid opening new positions within 15 minutes of an 8-hour window close, since funding rates can reverse sharply post-settlement. For real-time signal data to layer on top of your basis detection, platforms like VoiceOfChain publish live funding rate alerts and basis readings across Binance, Bybit, and OKX — integrating these as a secondary confirmation filter helps avoid entering during periods when rates are already compressing rather than expanding.

Split capital across at least two exchanges — Binance and Bybit are the standard pair for this strategy. Running independent positions on each reduces both exchange concentration risk and the impact of one platform's downtime on your overall yield.

Frequently Asked Questions

Is spot-futures arbitrage truly market neutral?
Yes, when both legs are correctly sized and fully filled. Your spot long and futures short offset each other so directional moves cancel out. The residual exposure is basis risk — the spread between spot and futures widening against you before exit. Precise size matching and fast dual-leg execution minimize this.
How much capital do I need to run this profitably?
At minimum, funding income must exceed round-trip fees. With Binance fees around 0.28% per round trip and typical funding rates of 0.03% per 8 hours, you need to hold positions across multiple funding periods. Most practitioners find $10,000+ necessary to make the fee math comfortable; below that, fees eat too large a percentage of gross yield.
What happens if one leg gets stuck or partially filled?
This is the main operational risk. If the futures short executes but the spot buy fails, you have an unintended directional short. Your error handling should immediately attempt to close the futures position and alert you for manual review. Never leave a mismatched position open — the directional loss can easily exceed months of funding income.
Which exchanges have the best APIs for this strategy?
Binance has the deepest liquidity for major pairs and well-documented REST and WebSocket APIs. Bybit is a strong second with fast derivatives execution and a clean API design. OKX offers a unified account that lets you share margin across spot and futures, simplifying capital management. All three have Python client libraries.
Can I run this on multiple trading pairs simultaneously?
Yes. Many practitioners run across 5-15 pairs like BTC, ETH, SOL, and BNB to diversify funding rate risk. The architecture scales cleanly — one async coroutine per symbol for monitoring, shared order executor with rate-limit tracking. Watch total capital allocation across all open positions and make sure your margin covers worst-case scenarios on all pairs.
How do I handle the position when funding rates turn negative?
When funding flips negative, longs receive payments instead of paying them. At that point your cash-and-carry position is paying fees, not collecting them. Set a yield threshold below which you automatically unwind: close the futures short first, then sell spot. Build this exit logic into your monitoring loop from day one, not as an afterthought.

Spot-futures arbitrage via API is one of the most tractable systematic strategies in crypto: the edge is structural, the data is public, and the execution path automates cleanly. The code above gives you a working foundation — HMAC authentication against Binance's REST API, async spread detection with real premium index endpoints, and dual-leg order execution with proper sequencing and error handling. The next steps are building position tracking that persists across restarts, adding Bybit and OKX as secondary venues for when Binance spreads compress, and wiring the kill switch into your monitoring loop. For timing entries, pull real-time funding rate signals from platforms like VoiceOfChain rather than acting purely on instantaneous snapshots. The strategy rewards operational discipline more than raw cleverness — keep the codebase simple, monitor positions actively, and build for the edge cases before they happen.

◈   more on this topic
◉ basics Mastering the ccxt library documentation for crypto traders ⌂ exchanges Mastering the Binance CCXT Library for Crypto Traders ⌬ bots Best Crypto Trading Bots 2025: Profitable AI-Powered Strategies