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.
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 --quiet1.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 sns1.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')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.