Cryptocurrency markets are highly dynamic, with price discrepancies often appearing across exchanges for the same digital asset. These inefficiencies create opportunities for traders to execute cross-exchange arbitrage—buying low on one platform and selling high on another. In this guide, we’ll walk through how to build a real-time monitoring tool using Python and the ccxt library to detect such arbitrage opportunities between major exchanges like Binance and OKX.
Whether you're exploring algorithmic trading or simply want to understand market microstructure better, this system provides a solid foundation for identifying potential profits in near real time.
Understanding Cross-Exchange Arbitrage
Cross-exchange arbitrage involves exploiting price differences of the same cryptocurrency across different trading platforms. For instance, if Bitcoin (BTC) trades at $90,000 on Binance and $90,200 on OKX, a trader can buy BTC on Binance and simultaneously sell it on OKX, capturing the $200 spread—minus fees and slippage.
This strategy is considered low-risk compared to directional trading because it doesn’t rely on predicting market movement. Instead, it capitalizes on temporary market inefficiencies caused by varying liquidity, trading volume, or regional demand.
👉 Discover how real-time data can power your trading edge
While many forms of arbitrage exist—such as spot-futures (basis), inter-temporal, or statistical—the focus here is cross-exchange spot arbitrage. We'll develop a tool that continuously monitors price spreads between two exchanges and alerts when deviations exceed a set threshold.
Note: This is an educational project. No investment advice is provided. Always assess risks before deploying any live trading system.
Core Workflow Overview
Building an effective arbitrage monitor involves three key stages:
- Pair Matching – Identify tradable assets available on both exchanges.
- Real-Time Price Listening – Stream ticker data to compute live price differences.
- Opportunity Detection & Application – Flag meaningful spreads and consider execution strategies.
Let’s dive into each step with practical code examples.
Step 1: Matching Tradable Pairs Across Exchanges
To compare prices, we first need to find common trading pairs between two exchanges. A pair like BTC/USDT must be correctly mapped based on base (BTC) and quote (USDT) currencies.
We use the ccxt library to fetch market data from each exchange and match symbols by their underlying asset structure.
import ccxt
def load_pairs(exchange_a, exchange_b, type_a="spot", subtype_a=None, type_b="spot", subtype_b=None):
exchange_a.load_markets()
exchange_b.load_markets()
markets_a = {
(m['base'], m['quote']): m['symbol']
for m in exchange_a.markets.values()
if m['type'] == type_a and (subtype_a is None or m.get(subtype_a))
}
markets_b = {
(m['base'], m['quote']): m['symbol']
for m in exchange_b.markets.values()
if m['type'] == type_b and (subtype_b is None or m.get(subtype_b))
}
pair_keys = set(markets_a.keys()) & set(markets_b.keys())
return [
{
'base': base,
'quote': quote,
'symbol_a': markets_a[(base, quote)],
'symbol_b': markets_b[(base, quote)],
}
for base, quote in pair_keys
]Example Usage
binance = ccxt.binance({'enableRateLimit': True})
okx = ccxt.okx({'enableRateLimit': True})
# Match spot markets
pairs = load_pairs(binance, okx, type_a='spot', type_b='spot')You can also match derivatives—like pairing Binance spot with OKX perpetual contracts:
pairs = load_pairs(binance, okx, type_a='spot', type_b='swap', subtype_b='linear')⚠️ Watch Out for False Matches
Some tokens have identical tickers but represent different assets (e.g., NEIRO on OKX vs Bybit). Additionally, meme coins like PEPE may be scaled differently (e.g., 1000PEPE), causing mismatches. Manual validation of matched pairs is recommended.
Step 2: Detecting Abnormal Price Spreads
Even after matching pairs, some may show abnormal spreads due to misidentification or illiquidity. We implement a filter to flag these:
def detect_abnormal_pairs(exchange_a, exchange_b, pairs, threshold=0.05):
abnormal_pairs = []
normal_pairs = []
for pair in pairs:
try:
ticker_a = exchange_a.fetch_ticker(pair['symbol_a'])
ticker_b = exchange_b.fetch_ticker(pair['symbol_b'])
price_a = ticker_a.get('last')
price_b = ticker_b.get('last')
if None in [price_a, price_b]:
continue
min_price = min(price_a, price_b)
spread_pct = abs(price_a - price_b) / min_price
result = {
**pair,
'price_a': price_a,
'price_b': price_b,
'spread_pct': spread_pct,
'is_abnormal': spread_pct > threshold
}
if result['is_abnormal']:
abnormal_pairs.append(result)
else:
normal_pairs.append(pair)
except Exception as e:
print(f"Error processing {pair}: {e}")
return abnormal_pairs, normal_pairsSave results to CSV for review:
import pandas as pd
abnormal, normal = detect_abnormal_pairs(binance, okx, pairs, 0.05)
pd.DataFrame(normal).to_csv("normal_pairs.csv", index=False)
pd.DataFrame(abnormal).to_csv("abnormal_pairs.csv", index=False)This step ensures only valid, liquid pairs enter the monitoring pipeline.
Step 3: Real-Time Spread Monitoring with WebSockets
For low-latency updates, we use ccxt.pro, which supports WebSocket streaming via watch_tickers.
Here’s a streamlined version of the Monitor class:
import asyncio
import ccxt.pro as ccxtpro
from collections import defaultdict
class Monitor:
def __init__(self, exchange_a, exchange_b, pairs):
self.exchange_a = exchange_a
self.exchange_b = exchange_b
self.symbol_map = self._build_symbol_map(pairs)
self.pair_data = {}
self.running = False
def _build_symbol_map(self, pairs):
symbol_map = defaultdict(dict)
for p in pairs:
key = (p['base'], p['quote'])
symbol_map['a'][p['symbol_a']] = {'index': 'a', 'pair_key': key}
symbol_map['b'][p['symbol_b']] = {'index': 'b', 'pair_key': key}
return symbol_map
async def monitor(self, exchange, index):
symbols = list(self.symbol_map[index])
while self.running:
try:
tickers = await exchange.watch_tickers(symbols)
await self.process_tickers(tickers, index)
except Exception as e:
print(f"Monitoring error ({index}): {e}")
await asyncio.sleep(5)
async def process_tickers(self, tickers, index):
for symbol, ticker in tickers.items():
if symbol not in self.symbol_map[index]:
continue
pair_key = self.symbol_map[index][symbol]['pair_key']
if pair_key not in self.pair_data:
self.pair_data[pair_key] = {'price_a': None, 'price_b': None}
self.pair_data[pair_key][f'price_{index}'] = ticker['last']
await self.calculate_spread(pair_key)
async def calculate_spread(self, pair_key):
data = self.pair_data[pair_key]
if data['price_a'] and data['price_b']:
try:
min_price = min(data['price_a'], data['price_b'])
spread_pct = abs(data['price_a'] - data['price_b']) / min_price
data['spread_pct'] = spread_pct
if spread_pct > 0.01: # 1% threshold
await self.trigger_alert(pair_key)
except (TypeError, ZeroDivisionError):
pass
async def trigger_alert(self, pair_key):
data = self.pair_data[pair_key]
print(f"🚨 Arbitrage Opportunity! {pair_key}: {data['spread_pct']:.2%}")Run the Monitor
async def main():
pairs_df = pd.read_csv("normal_pairs.csv")
pairs = pairs_df[['base', 'quote', 'symbol_a', 'symbol_b']].to_dict('records')
exchange_a = ccxtpro.binance({'enableRateLimit': True})
exchange_b = ccxtpro.okx({'enableRateLimit': True})
monitor = Monitor(exchange_a, exchange_b, pairs)
monitor.running = True
tasks = [
asyncio.create_task(monitor.monitor(exchange_a, 'a')),
asyncio.create_task(monitor.monitor(exchange_b, 'b'))
]
try:
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
monitor.running = False
await asyncio.gather(*tasks)
asyncio.run(main())Sample output:
🚨 Arbitrage Opportunity! ('GLM', 'USDT'): 2.14%
🚨 Arbitrage Opportunity! ('GLM', 'USDT'): 1.99%👉 Turn alerts into action with advanced trading tools
Practical Considerations for Live Trading
Detecting spreads is just the beginning. Turning signals into profit requires addressing several real-world challenges:
- Slippage: The last traded price may not reflect available order book depth.
- Fees: Taker/maker fees reduce net gains.
- Transfer Delays: Moving funds between exchanges introduces latency.
- Liquidity Risk: Small-cap coins may lack sufficient volume to execute large trades.
For accurate modeling, integrate order book data (watch_order_book) instead of relying solely on last prices.
Frequently Asked Questions (FAQ)
Q: Is cross-exchange arbitrage still profitable in 2025?
A: Opportunities exist but are fleeting—especially for major coins. Smaller altcoins or sudden market events (like flash crashes) offer better chances.
Q: Can I automate this strategy safely?
A: Automation increases speed but also risk. Start with paper trading or small allocations. Always include circuit breakers and risk limits.
Q: Why use WebSockets instead of REST APIs?
A: WebSockets provide real-time streaming with lower latency and reduced API rate limit pressure—critical for fast-moving markets.
Q: How do I handle token mismatches like PEPE vs 1000PEPE?
A: Implement custom mapping rules or manually curate your watchlist to avoid false positives.
Q: What’s the ideal spread threshold?
A: Depends on fees and volatility. A common starting point is 0.5%–1%, but adjust based on historical backtesting.
Q: Which exchanges work best for arbitrage?
A: High-volume platforms like Binance, OKX, Bybit, and Kraken often show temporary divergences due to regional demand imbalances.
Final Thoughts
While true risk-free arbitrage is rare in mature markets, crypto’s fragmented landscape still offers exploitable inefficiencies—especially during volatile periods. This monitoring tool gives you the foundation to explore those opportunities with precision.
Always remember: what looks like free profit might hide hidden costs. Test thoroughly, validate assumptions, and never risk more than you can afford to lose.