Data·Data Analysis·Beginner

Compare Prices across Exchanges

Compare real-time and historical cryptocurrency prices across multiple major exchanges to identify persistent pricing discrepancies, exchange-specific premiums and discounts, and cross-exchange market structure differences that impact trading decisions.

data-analysisdata-engineering

Cross-Exchange Price Comparison

This notebook aggregates and compares spot prices (bid, ask, mid) across multiple cryptocurrency exchanges. The analysis includes statistical summaries, a grouped bar chart visualizing price deviations, and a pairwise spread heatmap. This methodology can identify potential arbitrage opportunities and market inefficiencies.

1. Environment Setup

1.1 Install Dependencies

This section ensures that all necessary Python libraries are installed. The ccxt library facilitates interaction with cryptocurrency exchanges, while pandas, numpy, matplotlib, and seaborn are utilized for data manipulation, numerical operations, visualization, and statistical plotting, respectively.

[ ]
# Install ccxt if not already installed
!pip install ccxt --quiet

1.2 Import Libraries

Essential Python libraries for data processing, analysis, and visualization are imported in this section.

[ ]
import time
import warnings
import ccxt
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import seaborn as sns

1.3 Configure Display Settings

Display options for Pandas DataFrames and Matplotlib/Seaborn plots are configured to enhance readability and presentation of analytical results. Warnings are suppressed to maintain 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')

1.4 Environment Verification

This subsection verifies the versions of critical libraries and confirms the environment is ready for execution.

[ ]
print(f"ccxt version  : {ccxt.__version__}")
print(f"pandas version: {pd.__version__}")
print("Environment status: Ready.")
ccxt version  : 4.5.56
pandas version: 2.2.2
Environment status: Ready.

2. Configuration Parameters

This section defines the global configuration parameters for the data retrieval process, including target exchanges, trading symbols, and request timeouts. These parameters are modifiable to customize data collection.

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

SYMBOLS = [
    'BTC/USDT',
    'ETH/USDT',
    'SOL/USDT',
    'BNB/USDT',
    'XRP/USDT',
]

REQUEST_TIMEOUT_MS = 10_000  # 10 seconds per request

print("Configuration status: Loaded.")
Configuration status: Loaded.

3. Data Collection

This section outlines the process for connecting to cryptocurrency exchanges and fetching real-time ticker data for specified trading pairs. It includes functions for initializing exchange clients and aggregating price information.

3.1 Exchange Initialization

The initialize_exchanges function instantiates ccxt exchange objects for a given list of exchange identifiers. Each exchange client is configured with a timeout and rate limiting. Market data is loaded to verify connectivity and support for target symbols. The function returns a dictionary of active exchange objects.

[ ]
def initialize_exchanges(exchange_ids: list) -> dict:
    """Instantiate and verify ccxt exchange objects.

    Args:
        exchange_ids (list): A list of exchange IDs (e.g., ['binance', 'bybit']).

    Returns:
        dict: A dictionary mapping exchange IDs to their initialized ccxt 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("Initializing exchanges...")
exchanges = initialize_exchanges(EXCHANGES)
print(f"Active exchanges count: {len(exchanges)}")
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 count: 3

3.2 Fetching Ticker Data

This subsection defines functions to retrieve ticker information (bid, ask, last price, volume) for specified symbols across all initialized exchanges. The compare_prices_across_exchanges function aggregates this data into a Pandas DataFrame for subsequent analysis. A brief delay is introduced between requests to comply with exchange rate limits.

[ ]
def fetch_ticker_data(exchange_obj, symbol: str) -> dict | None:
    """Fetch ticker data for a single symbol from a single exchange.

    Args:
        exchange_obj: Initialized ccxt exchange object.
        symbol (str): Trading pair symbol (e.g., 'BTC/USDT').

    Returns:
        dict | None: A dictionary containing ticker data or None if an error occurs.
    """
    try:
        if symbol not in exchange_obj.markets:
            return None
        ticker = exchange_obj.fetch_ticker(symbol)
        bid  = ticker.get('bid')  or 0.0
        ask  = ticker.get('ask')  or 0.0
        last = ticker.get('last') or 0.0
        mid_price = (bid + ask) / 2 if (bid and ask) else last
        return {
            'symbol'    : symbol,
            'bid'       : bid,
            'ask'       : ask,
            'last'      : last,
            'volume_24h': ticker.get('baseVolume') or 0.0,
            'mid_price' : mid_price,
        }
    except Exception:
        return None


def compare_prices_across_exchanges(exchange_dict: dict, symbols: list) -> pd.DataFrame:
    """Aggregate bid/ask/mid prices across all exchanges for all target symbols.

    Args:
        exchange_dict (dict): Dictionary of initialized exchange objects.
        symbols (list): List of trading pair symbols.

    Returns:
        pd.DataFrame: DataFrame containing aggregated ticker data.

    Raises:
        RuntimeError: If no ticker data is retrieved.
    """
    records = []
    for ex_id, ex_obj in exchange_dict.items():
        for symbol in symbols:
            data = fetch_ticker_data(ex_obj, symbol)
            if data:
                data['exchange'] = ex_id
                records.append(data)
            time.sleep(0.1)

    if not records:
        raise RuntimeError("No ticker data retrieved. Verify exchange connectivity.")

    df = pd.DataFrame(records)
    col_order = ['exchange', 'symbol', 'bid', 'ask', 'mid_price', 'last', 'volume_24h']
    df = df[[c for c in col_order if c in df.columns]]
    return df.reset_index(drop=True)


print("Fetching price data from all active exchanges...")
price_df = compare_prices_across_exchanges(exchanges, SYMBOLS)

print(f"Records retrieved : {len(price_df)}")
print(f"Exchanges covered : {price_df['exchange'].unique().tolist()}")
print(f"Symbols covered   : {price_df['symbol'].unique().tolist()}")
print("Sample records of fetched data:")
print(price_df.head(10).to_string(index=False))
Fetching price data from all active exchanges...
Records retrieved : 15
Exchanges covered : ['okx', 'kraken', 'kucoin']
Symbols covered   : ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
Sample records of fetched data:
exchange   symbol          bid          ask    mid_price         last      volume_24h
     okx BTC/USDT 62697.500000 62702.500000 62700.000000 62698.600000    16272.529748
     okx ETH/USDT  1759.090000  1759.100000  1759.095000  1759.420000   272826.264644
     okx SOL/USDT    68.440000    68.450000    68.445000    68.460000  1391415.767639
     okx BNB/USDT   593.400000   593.500000   593.450000   593.700000    29051.011991
     okx XRP/USDT     1.154800     1.154900     1.154850     1.154900 33317483.801664
  kraken BTC/USDT 62710.200000 62736.900000 62723.550000 62698.800000      379.962289
  kraken ETH/USDT  1759.190000  1759.780000  1759.485000  1760.040000     4439.880470
  kraken SOL/USDT    68.470000    68.480000    68.475000    68.510000    56206.172604
  kraken BNB/USDT   593.250000   593.980000   593.615000   592.520000      251.261000
  kraken XRP/USDT     1.154670     1.154900     1.154785     1.158040  1512124.272635

4. Data Analysis

This section performs statistical analysis on the collected price data to identify patterns and discrepancies across different exchanges. It calculates summary statistics and generates visualizations to compare prices.

4.1 Summary Statistics

Cross-exchange mid-price statistics are computed for each symbol, including count, mean, standard deviation, minimum, and maximum prices. Additional metrics such as price range, range in basis points (bps), and coefficient of variation (CV) are derived to quantify price dispersion.

[ ]
price_stats = (
    price_df
    .groupby('symbol')['mid_price']
    .agg(count='count', mean='mean', std='std', min_px='min', max_px='max')
)
price_stats['range']     = price_stats['max_px'] - price_stats['min_px']
price_stats['range_bps'] = (price_stats['range'] / price_stats['mean']) * 10_000
price_stats['cv_pct']    = (price_stats['std'] / price_stats['mean']) * 100

print("Cross-Exchange Price Summary (mid-price basis):")
print("=" * 75)
print(price_stats.round(4).to_string())
Cross-Exchange Price Summary (mid-price basis):
===========================================================================
          count         mean       std       min_px       max_px     range  range_bps   cv_pct
symbol                                                                                        
BNB/USDT      3   593.562800  0.097800   593.450000   593.623500  0.173500   2.923000 0.016500
BTC/USDT      3 62716.766700 14.608200 62700.000000 62726.750000 26.750000   4.265200 0.023300
ETH/USDT      3  1759.331700  0.207900  1759.095000  1759.485000  0.390000   2.216800 0.011800
SOL/USDT      3    68.448300  0.025200    68.425000    68.475000  0.050000   7.304800 0.036800
XRP/USDT      3     1.154800  0.000000     1.154800     1.154900  0.000100   0.562900 0.002800

4.2 Price Comparison Visualization

This subsection generates two visualizations to illustrate cross-exchange price dynamics. The first plot displays the mid-price for each trading pair across exchanges on a logarithmic scale. The second plot shows the percentage deviation of each exchange's mid-price from the cross-exchange mean, highlighting relative price differences. This chart is saved as 92_price_comparison.png.

[ ]
price_pivot = price_df.pivot_table(index='symbol', columns='exchange', values='mid_price')

price_normalized = price_pivot.apply(lambda row: (row / row.mean() - 1) * 100, axis=1)

fig, axes = plt.subplots(1, 2, figsize=(18, 6))
fig.suptitle('Cross-Exchange Price Comparison', fontsize=14, fontweight='bold', y=1.01)

ax1 = axes[0]
price_pivot.plot(kind='bar', ax=ax1, logy=True, width=0.7, edgecolor='white', linewidth=0.5)
ax1.set_title('Mid-Price by Exchange (Log Scale)', fontsize=12)
ax1.set_xlabel('Trading Pair')
ax1.set_ylabel('Mid-Price (USDT, log)')
ax1.legend(title='Exchange', bbox_to_anchor=(1.01, 1), loc='upper left')
ax1.tick_params(axis='x', rotation=30)
ax1.grid(axis='y', alpha=0.5)

ax2 = axes[1]
price_normalized.plot(kind='bar', ax=ax2, width=0.7, edgecolor='white', linewidth=0.5)
ax2.set_title('Price Deviation from Cross-Exchange Mean (%)', fontsize=12)
ax2.set_xlabel('Trading Pair')
ax2.set_ylabel('Deviation (%)')
ax2.axhline(0, color='red', linewidth=1.0, linestyle='--', label='Mean')
ax2.legend(title='Exchange', bbox_to_anchor=(1.01, 1), loc='upper left')
ax2.tick_params(axis='x', rotation=30)
ax2.yaxis.set_major_formatter(mtick.PercentFormatter(decimals=4))
ax2.grid(axis='y', alpha=0.5)

plt.tight_layout()
plt.savefig('92_price_comparison.png', dpi=150, bbox_inches='tight')
cell output

Conclusion

This analysis successfully aggregated and compared spot prices across multiple cryptocurrency exchanges, identifying key statistical insights and visualizing price deviations. The visualizations highlighted instances of price dispersion, which could indicate potential arbitrage opportunities or market inefficiencies. This framework provides a robust foundation for monitoring cross-exchange price dynamics and can be extended with more sophisticated arbitrage strategy simulations and real-time data feeds.