Crypto Bots

How to Build a Multi-Exchange Crypto Trading Bot

A multi-exchange trading bot monitors prices across Binance, Coinbase, Kraken, and OKX simultaneously and exploits price differences. Learn how to build one using CCXT's unified API.

A
AI Agents Hubยท2026-03-26ยท5 min readยท875 words

Builder of AI agents, crypto trading bots, and open-source automation tools. Sharing practical guides on how to build, deploy, and profit from AI and DeFi technology.

Why Multi-Exchange Bots Are More Powerful

A single-exchange bot is limited to the liquidity and opportunities on one platform. A multi-exchange bot:

  • Finds arbitrage: Same asset, different prices across exchanges
  • Routes to best price: Get the best execution by comparing quotes
  • Reduces dependency: If one exchange has downtime, others continue
  • Manages risk: Spread capital across multiple custodians

The CCXT Unified Interface

CCXT (CryptoCurrency eXchange Trading Library) is the backbone of multi-exchange bots โ€” it provides one API for 100+ exchanges:

import ccxt
import asyncio
from typing import Optional

# Initialize multiple exchanges with the same interface
exchanges = {
    'binance': ccxt.binance({
        'apiKey': BINANCE_KEY,
        'secret': BINANCE_SECRET,
        'enableRateLimit': True,
    }),
    'coinbase': ccxt.coinbaseadvanced({
        'apiKey': COINBASE_KEY,
        'secret': COINBASE_SECRET,
        'enableRateLimit': True,
    }),
    'kraken': ccxt.kraken({
        'apiKey': KRAKEN_KEY,
        'secret': KRAKEN_SECRET,
        'enableRateLimit': True,
    }),
    'okx': ccxt.okx({
        'apiKey': OKX_KEY,
        'secret': OKX_SECRET,
        'passphrase': OKX_PASSPHRASE,
        'enableRateLimit': True,
    }),
}

# Same function works for ALL exchanges
async def get_ticker(exchange_id: str, symbol: str) -> Optional[dict]:
    try:
        exchange = exchanges[exchange_id]
        ticker = exchange.fetch_ticker(symbol)
        return {
            'exchange': exchange_id,
            'symbol': symbol,
            'bid': ticker['bid'],
            'ask': ticker['ask'],
            'last': ticker['last'],
            'volume': ticker['baseVolume'],
        }
    except Exception as e:
        print(f"Error fetching {exchange_id} {symbol}: {e}")
        return None

Fetching All Prices in Parallel

async def get_all_prices(symbol: str) -> dict:
    """Fetch price from all exchanges simultaneously"""
    
    tasks = [
        get_ticker(ex_id, symbol) 
        for ex_id in exchanges.keys()
    ]
    
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    prices = {}
    for result in results:
        if result and isinstance(result, dict):
            prices[result['exchange']] = result
    
    return prices

async def find_best_price(symbol: str, side: str = 'buy') -> dict:
    """Find the best price to buy or sell across all exchanges"""
    prices = await get_all_prices(symbol)
    
    if side == 'buy':
        # Best buy = lowest ask price
        best = min(prices.values(), key=lambda x: x['ask'])
    else:
        # Best sell = highest bid price
        best = max(prices.values(), key=lambda x: x['bid'])
    
    return best

# Example usage
loop = asyncio.get_event_loop()
all_prices = loop.run_until_complete(get_all_prices('BTC/USDT'))

print("BTC/USDT prices across exchanges:")
for ex, data in sorted(all_prices.items(), key=lambda x: x[1]['ask']):
    print(f"  {ex:12}: bid ${data['bid']:,.2f} | ask ${data['ask']:,.2f}")

Cross-Exchange Arbitrage Engine

MIN_PROFIT_PCT = 0.003  # 0.3% minimum after fees
TRADE_AMOUNT_USD = 500  # Amount to trade per arb opportunity

class CrossExchangeArbBot:
    def __init__(self, symbols: list[str]):
        self.symbols = symbols
        self.trade_log = []
    
    def estimate_fees(self, exchange_id: str, amount_usd: float) -> float:
        """Estimate trading fee for an exchange"""
        fee_rates = {
            'binance': 0.001,   # 0.1% maker/taker
            'coinbase': 0.005,  # 0.5% taker (Advanced lower)
            'kraken': 0.0026,   # 0.26% taker
            'okx': 0.001,       # 0.1%
        }
        return amount_usd * fee_rates.get(exchange_id, 0.002)
    
    def check_arb_opportunity(self, prices: dict, symbol: str) -> Optional[dict]:
        """Find the best arbitrage opportunity"""
        
        best_bid_ex = max(prices.items(), key=lambda x: x[1]['bid'])
        best_ask_ex = min(prices.items(), key=lambda x: x[1]['ask'])
        
        buy_exchange = best_ask_ex[0]
        sell_exchange = best_bid_ex[0]
        
        if buy_exchange == sell_exchange:
            return None  # Same exchange, no arb
        
        buy_price = best_ask_ex[1]['ask']
        sell_price = best_bid_ex[1]['bid']
        
        # Calculate gross profit
        gross_profit = (sell_price - buy_price) / buy_price
        
        # Calculate fees
        buy_fee = self.estimate_fees(buy_exchange, TRADE_AMOUNT_USD)
        sell_fee = self.estimate_fees(sell_exchange, TRADE_AMOUNT_USD)
        total_fees = buy_fee + sell_fee
        
        # Net profit
        net_profit_usd = TRADE_AMOUNT_USD * gross_profit - total_fees
        net_profit_pct = net_profit_usd / TRADE_AMOUNT_USD
        
        if net_profit_pct >= MIN_PROFIT_PCT:
            return {
                'symbol': symbol,
                'buy_exchange': buy_exchange,
                'sell_exchange': sell_exchange,
                'buy_price': buy_price,
                'sell_price': sell_price,
                'gross_spread_pct': gross_profit * 100,
                'estimated_fees_usd': total_fees,
                'net_profit_usd': net_profit_usd,
                'net_profit_pct': net_profit_pct * 100,
            }
        
        return None
    
    async def execute_arb(self, opportunity: dict):
        """Execute an arbitrage trade"""
        sym = opportunity['symbol']
        buy_ex = exchanges[opportunity['buy_exchange']]
        sell_ex = exchanges[opportunity['sell_exchange']]
        
        price = opportunity['buy_price']
        qty = TRADE_AMOUNT_USD / price
        
        print(f"\n๐Ÿ’ฐ ARB DETECTED: {sym}")
        print(f"  Buy on {opportunity['buy_exchange']} @ ${opportunity['buy_price']:,.2f}")
        print(f"  Sell on {opportunity['sell_exchange']} @ ${opportunity['sell_price']:,.2f}")
        print(f"  Net profit: ${opportunity['net_profit_usd']:.2f} ({opportunity['net_profit_pct']:.3f}%)")
        
        # Execute both sides simultaneously
        buy_task = asyncio.create_task(
            asyncio.to_thread(
                buy_ex.create_market_buy_order, sym, qty
            )
        )
        sell_task = asyncio.create_task(
            asyncio.to_thread(
                sell_ex.create_market_sell_order, sym, qty
            )
        )
        
        buy_result, sell_result = await asyncio.gather(buy_task, sell_task)
        print(f"โœ… Arb executed: buy #{buy_result['id']} | sell #{sell_result['id']}")
    
    async def run(self):
        """Main arbitrage loop"""
        print("๐Ÿค– Multi-exchange arb bot started...")
        
        while True:
            for symbol in self.symbols:
                prices = await get_all_prices(symbol)
                opportunity = self.check_arb_opportunity(prices, symbol)
                
                if opportunity:
                    await self.execute_arb(opportunity)
            
            await asyncio.sleep(5)  # Scan every 5 seconds

# Run bot
bot = CrossExchangeArbBot(['BTC/USDT', 'ETH/USDT', 'SOL/USDT'])
asyncio.run(bot.run())

Handling Balance Across Exchanges

The biggest operational challenge: keeping enough capital on each exchange:

class BalanceManager:
    def __init__(self, min_balance_usd: float = 200):
        self.min_balance = min_balance_usd
    
    async def check_all_balances(self) -> dict:
        """Check USDT balance on all exchanges"""
        balances = {}
        for ex_id, exchange in exchanges.items():
            try:
                bal = exchange.fetch_balance()
                balances[ex_id] = float(bal.get('USDT', {}).get('free', 0))
            except Exception as e:
                balances[ex_id] = 0
                print(f"Balance check failed for {ex_id}: {e}")
        return balances
    
    def flag_low_balances(self, balances: dict) -> list[str]:
        """Return list of exchanges with low balances"""
        return [ex for ex, bal in balances.items() if bal < self.min_balance]

Important: Transfer Times Matter

The biggest risk in cross-exchange arb is transfer time. If BTC is cheap on Kraken:

  1. You buy BTC on Kraken โœ…
  2. You need to sell BTC on Binance โœ…
  3. Transferring BTC from Kraken to Binance takes 20-60 minutes โŒ

Solution: Keep pre-funded balances on both sides. If you have BTC already on Binance and USDT on Kraken, you can execute both legs simultaneously without any transfer.

This means your bot needs to actively manage float across exchanges โ€” a significant operational overhead that most beginners underestimate.

Multi-exchange bots are more complex but more profitable. Master single-exchange strategies first, then scale to multi-exchange when you understand the operational requirements.

Related Articles