Data·Data Analysis·Beginner

Funding Rate Comparison

Compare perpetual futures funding rates across all major cryptocurrency exchanges to identify funding rate arbitrage opportunities, gauge relative market sentiment through the cost of leverage, and anticipate cross-exchange capital flows driven by funding differentials.

data-engineeringperpetual-futures

Funding Rate Comparison

This notebook analyzes perpetual futures funding rates across various cryptocurrency exchanges to identify potential funding arbitrage signals. The strategy involves simultaneously shorting on an exchange with a high funding rate (to receive funding payments) and longing on an exchange with a low funding rate (to minimize funding costs).

Setup

Install Dependencies

Required libraries are installed for data acquisition, processing, and visualization.

[ ]
pip install ccxt pandas numpy matplotlib seaborn --quiet
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 153.3/153.3 kB 3.4 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.6/6.6 MB 34.0 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 33.9 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 223.8/223.8 kB 7.9 MB/s eta 0:00:00
[?25h

Import Libraries

Standard and third-party libraries are imported to facilitate data handling, API interaction, and plotting.

[ ]
import time
import warnings

import ccxt
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import seaborn as sns

Display Configuration

Notebook display options and plotting styles are configured for improved readability and presentation of results. Warnings are suppressed to maintain a clean output.

[ ]
warnings.filterwarnings('ignore')
pd.set_option('display.float_format', '{:.6f}'.format)
pd.set_option('display.max_columns', 20)
pd.set_option('display.width', 120)
sns.set_theme(style='darkgrid', palette='muted')

print(f"ccxt version  : {ccxt.__version__}")
print(f"pandas version: {pd.__version__}")
print("Environment ready.")
ccxt version  : 4.5.56
pandas version: 2.2.2
Environment ready.

Configuration

Key parameters for data collection and arbitrage signal detection are defined, including the target exchanges, perpetual future symbols, request timeout, and the funding rate differential threshold.

[ ]
EXCHANGES = ['binance', 'bybit', 'okx', 'kraken', 'kucoin']

# Perpetual futures symbols (ccxt unified format for linear perps)
PERP_SYMBOLS = [
    'BTC/USDT:USDT',
    'ETH/USDT:USDT',
    'SOL/USDT:USDT',
]

REQUEST_TIMEOUT_MS    = 10_000  # 10 seconds per request
FUNDING_THRESHOLD_PCT = 0.01    # 1% annualized minimum differential

print("Configuration loaded.")
print(f"  Exchanges         : {EXCHANGES}")
print(f"  Perp symbols      : {PERP_SYMBOLS}")
print(f"  Funding threshold : {FUNDING_THRESHOLD_PCT * 100:.2f}% annualized")
Configuration loaded.
  Exchanges         : ['binance', 'bybit', 'okx', 'kraken', 'kucoin']
  Perp symbols      : ['BTC/USDT:USDT', 'ETH/USDT:USDT', 'SOL/USDT:USDT']
  Funding threshold : 1.00% annualized

Exchange Initialization

This section initializes ccxt exchange objects for each specified exchange. It verifies connectivity and loads market data for each exchange, reporting any failures.

[ ]
def initialize_exchanges(exchange_ids: list) -> dict:
    """Instantiate and verify ccxt exchange objects."""
    active_exchanges = {}
    for ex_id in exchange_ids:
        try:
            exchange_class = getattr(ccxt, ex_id)
            exchange = exchange_class({
                'timeout': REQUEST_TIMEOUT_MS,
                'enableRateLimit': True,
            })
            exchange.load_markets()
            active_exchanges[ex_id] = exchange
            print(f"  [OK]   {ex_id:<12}{len(exchange.markets):>5} markets loaded")
        except Exception as e:
            print(f"  [FAIL] {ex_id:<12}{type(e).__name__}: {e}")
    return active_exchanges


print("\nInitializing exchanges...")
exchanges = initialize_exchanges(EXCHANGES)
print(f"\nActive exchanges: {list(exchanges.keys())}")

Initializing exchanges...
  [FAIL] binance      — ExchangeNotAvailable: binance GET https://api.binance.com/api/v3/exchangeInfo 451  {
  "code": 0,
  "msg": "Service unavailable from a restricted location according to 'b. Eligibility' in https://www.binance.com/en/terms. Please contact customer service if you believe you received this message in error."
}
  [FAIL] bybit        — RateLimitExceeded: bybit GET https://api.bybit.com/v5/market/instruments-info?category=spot 403 Forbidden {
    error:The Amazon CloudFront distribution is configured to block access from your country
}
  [OK]   okx          —  4123 markets loaded
  [OK]   kraken       —  1551 markets loaded
  [OK]   kucoin       —  1712 markets loaded

Active exchanges: ['okx', 'kraken', 'kucoin']

Funding Rate Collection

Functions are defined to fetch and aggregate current funding rates for specified perpetual futures symbols across all initialized exchanges. The funding rates are annualized for comparison purposes. A small delay is introduced between requests to respect API rate limits.

[ ]
def fetch_funding_rate(exchange_obj, symbol: str) -> dict | None:
    """
    Fetch the current funding rate for a perpetual futures contract.

    The 8-hour funding rate is annualized using the formula: `rate_8h × 3 periods/day × 365 days`.
    Returns `None` if the exchange does not support the specified symbol or market type.
    """
    try:
        if symbol not in exchange_obj.markets:
            return None
        market_info = exchange_obj.markets[symbol]
        if not market_info.get('swap', False):
            return None  # Not a perpetual futures market

        funding_data = exchange_obj.fetch_funding_rate(symbol)
        rate_8h = funding_data.get('fundingRate') or 0.0
        rate_annualized = rate_8h * 3 * 365  # 3 intervals/day × 365 days

        return {
            'symbol'                 : symbol,
            'funding_rate_8h'        : rate_8h,
            'funding_rate_annualized': rate_annualized,
            'next_funding_time'      : funding_data.get('fundingDatetime', 'N/A'),
        }
    except Exception: # Broad exception catch for API call issues
        return None


def funding_rate_comparison(exchange_dict: dict, perp_symbols: list) -> pd.DataFrame:
    """
    Aggregates current funding rates across all configured exchanges and symbols.

    Returns
    -------
    pd.DataFrame
        A DataFrame containing funding rate details, including exchange, symbol,
        8-hour funding rate, annualized funding rate, and next funding time.
        The data is sorted by symbol and then by annualized funding rate in descending order.
    """
    records = []
    for ex_id, ex_obj in exchange_dict.items():
        for symbol in perp_symbols:
            data = fetch_funding_rate(ex_obj, symbol)
            if data:
                data['exchange'] = ex_id
                records.append(data)
            time.sleep(0.15) # Introduce a small delay to respect API rate limits

    if not records:
        print("No funding rate data retrieved. Verify that selected exchanges support perpetual futures markets.")
        return pd.DataFrame()

    df = pd.DataFrame(records)
    col_order = ['exchange', 'symbol', 'funding_rate_8h', 'funding_rate_annualized',
                 'next_funding_time']
    df = df[[c for c in col_order if c in df.columns]] # Ensure column order
    df = df.sort_values(['symbol', 'funding_rate_annualized'], ascending=[True, False])
    return df.reset_index(drop=True)


print("\nFetching perpetual futures funding rates...")
funding_df = funding_rate_comparison(exchanges, PERP_SYMBOLS)

if not funding_df.empty:
    print(f"\nFunding rate records: {len(funding_df)}")
    print(funding_df.to_string(index=False))
else:
    print("No funding data available to display.")

Fetching perpetual futures funding rates...

Funding rate records: 6
exchange        symbol  funding_rate_8h  funding_rate_annualized        next_funding_time
     okx BTC/USDT:USDT         0.000074                 0.081445 2026-06-04T16:00:00.000Z
  kucoin BTC/USDT:USDT        -0.000007                -0.007665 2026-06-04T16:00:00.000Z
     okx ETH/USDT:USDT         0.000074                 0.081173 2026-06-04T16:00:00.000Z
  kucoin ETH/USDT:USDT         0.000053                 0.058035 2026-06-04T16:00:00.000Z
  kucoin SOL/USDT:USDT        -0.000055                -0.060225 2026-06-04T16:00:00.000Z
     okx SOL/USDT:USDT        -0.000055                -0.060678 2026-06-04T16:00:00.000Z

Funding Arbitrage Signal Detection

This section outlines the methodology for detecting funding rate arbitrage opportunities. The core strategy is to:

  1. Short Position: Establish a short position on the exchange offering the highest annualized funding rate for a given perpetual future symbol. This position aims to receive funding payments.
  2. Long Position: Simultaneously establish a long position on the exchange offering the lowest annualized funding rate for the same symbol. This position aims to pay the minimum possible funding (or receive if the rate is negative).

An arbitrage signal is generated if the annualized funding rate differential between the highest and lowest rate exchanges exceeds a predefined FUNDING_THRESHOLD_PCT. This differential represents the potential profit from the arbitrage strategy, assuming all other factors (e.g., execution fees, slippage, price movements) are managed or negligible.

[ ]
def detect_funding_arbitrage(
    funding_dataframe: pd.DataFrame,
    threshold_pct: float = FUNDING_THRESHOLD_PCT,
) -> pd.DataFrame:
    """
    Identifies funding arbitrage signals from cross-exchange funding rate data.

    The strategy involves shorting on the highest-rate exchange to receive funding
    and longing on the lowest-rate exchange to minimize funding costs.

    Returns
    -------
    pd.DataFrame
        A DataFrame detailing potential arbitrage signals, sorted by the
        annualized differential in descending order.
    """
    signals = []

    for symbol in funding_dataframe['symbol'].unique():
        sym_data = funding_dataframe[funding_dataframe['symbol'] == symbol].copy()
        if len(sym_data) < 2:
            # Requires at least two exchanges to identify a differential
            continue

        highest_row = sym_data.loc[sym_data['funding_rate_annualized'].idxmax()]
        lowest_row  = sym_data.loc[sym_data['funding_rate_annualized'].idxmin()]

        differential = (
            highest_row['funding_rate_annualized'] -
            lowest_row['funding_rate_annualized']
        )
        is_signal = differential > threshold_pct

        signals.append({
            'symbol'                  : symbol,
            'short_exchange'          : highest_row['exchange'],  # Short position taken here (funding received)
            'long_exchange'           : lowest_row['exchange'],   # Long position taken here (funding cost minimized)
            'short_rate_annualized'   : highest_row['funding_rate_annualized'],
            'long_rate_annualized'    : lowest_row['funding_rate_annualized'],
            'differential_annualized' : differential,
            'signal'                  : is_signal, # True if differential exceeds threshold
        })

    result = pd.DataFrame(signals)
    if not result.empty:
        result = result.sort_values('differential_annualized', ascending=False)
    return result.reset_index(drop=True)


if not funding_df.empty:
    funding_signals = detect_funding_arbitrage(funding_df)
    active_signals  = funding_signals[funding_signals['signal'] == True]

    print(f"\nFunding Arbitrage Signals (threshold = {FUNDING_THRESHOLD_PCT * 100:.2f}% annualized):")
    print("=" * 70)
    print(f"Total symbols evaluated : {len(funding_signals)}")
    print(f"Active signals          : {len(active_signals)}")
    print()
    print(funding_signals.to_string(index=False))
else:
    print("Funding rate data unavailable — signal detection skipped.")
    funding_signals = pd.DataFrame()

Funding Arbitrage Signals (threshold = 1.00% annualized):
======================================================================
Total symbols evaluated : 3
Active signals          : 2

       symbol short_exchange long_exchange  short_rate_annualized  long_rate_annualized  differential_annualized  signal
BTC/USDT:USDT            okx        kucoin               0.081445             -0.007665                 0.089110    True
ETH/USDT:USDT            okx        kucoin               0.081173              0.058035                 0.023138    True
SOL/USDT:USDT         kucoin           okx              -0.060225             -0.060678                 0.000453   False

Funding Rate Visualization

This section visualizes the collected funding rate data and any detected arbitrage signals. Two primary plots are generated:

  1. Heatmap of Annualized Funding Rates: Displays the annualized funding rate for each symbol across different exchanges. This provides a quick overview of which exchanges have higher or lower rates for specific assets.
  2. Bar Chart of Funding Rate Differentials: Illustrates the annualized funding rate differential for each symbol. This plot highlights the magnitude of potential arbitrage opportunities and indicates which symbols have active signals (where the differential exceeds the defined threshold). The bar colors distinguish between signals above and below the threshold, and annotations indicate the optimal short and long exchanges.
[ ]
if not funding_df.empty:
    fig, axes = plt.subplots(1, 2, figsize=(18, 6))
    fig.suptitle('Cross-Exchange Funding Rate Analysis', fontsize=14, fontweight='bold')

    # Panel 1: Heatmap — annualized funding rate per (exchange, symbol)
    ax1 = axes[0]
    funding_pivot = funding_df.pivot_table(
        index='exchange', columns='symbol', values='funding_rate_annualized'
    )
    sns.heatmap(
        funding_pivot * 100,     # Convert to percentage for display
        annot=True, fmt='.3f', cmap='RdBu_r', center=0,
        linewidths=0.5, ax=ax1,
        cbar_kws={'label': 'Annualized Funding Rate (%)'},
    )
    ax1.set_title('Annualized Funding Rate by Exchange and Symbol (%)')
    ax1.set_xlabel('Symbol')
    ax1.set_ylabel('Exchange')
    plt.setp(ax1.get_xticklabels(), rotation=30, ha='right')

    # Panel 2: Bar chart — differential per symbol with threshold line
    ax2 = axes[1]
    if not funding_signals.empty:
        bar_colors = [
            'crimson' if sig else 'steelblue' # Color bars based on signal status
            for sig in funding_signals['signal']
        ]
        ax2.bar(
            funding_signals['symbol'],
            funding_signals['differential_annualized'] * 100, # Convert to percentage
            color=bar_colors, edgecolor='white', linewidth=0.5,
        )
        ax2.axhline(
            FUNDING_THRESHOLD_PCT * 100, color='orange', linestyle='--',
            linewidth=1.5, label=f'Threshold ({FUNDING_THRESHOLD_PCT * 100:.1f}%)',
        )
        for i, row in funding_signals.iterrows():
            label = f"{row['short_exchange']}{row['long_exchange']}"
            ax2.text(
                i, row['differential_annualized'] * 100 + 0.002, # Position text slightly above bar
                label, ha='center', va='bottom', fontsize=7, rotation=15,
            )
        ax2.set_title('Funding Rate Differential by Symbol (Annualized %)')
        ax2.set_xlabel('Symbol')
        ax2.set_ylabel('Rate Differential (%)')
        ax2.yaxis.set_major_formatter(mtick.PercentFormatter(decimals=2))
        ax2.legend()
        ax2.grid(axis='y', alpha=0.4)
        ax2.tick_params(axis='x', rotation=20)
    else:
        ax2.text(0.5, 0.5, 'No signal data available',
                 ha='center', va='center', transform=ax2.transAxes, fontsize=12)

    plt.tight_layout() # Adjust layout to prevent overlapping elements
    plt.savefig('94_funding_rate_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("Chart saved: 94_funding_rate_comparison.png")
else:
    print("Funding data unavailable — visualization skipped.")
cell output
Chart saved: 94_funding_rate_comparison.png

Conclusion

This notebook demonstrates a methodology for identifying potential funding rate arbitrage opportunities across cryptocurrency exchanges. By analyzing annualized funding rates, we can detect significant differentials between exchanges for the same perpetual futures symbols. These differentials indicate situations where simultaneously shorting on a high-rate exchange and longing on a low-rate exchange could yield a profit from funding payments, assuming other trading costs and risks are managed.

The visualization clearly highlights which symbols currently present arbitrage signals above the defined threshold, along with the optimal exchanges for executing the short and long legs of the trade. This systematic approach can help in formulating data-driven arbitrage strategies in the perpetual futures market.