ZetCode

Python ccxt

last modified October 30, 2026 Python 3.15

In this article we show how to work with cryptocurrency exchanges in Python with the ccxt library.

ccxt is a JavaScript/Python/PHP/C#/Go library for cryptocurrency trading. It supports over one hundred exchanges behind one unified API, so the same code fetches prices from Binance, Kraken, or Coinbase. It provides quick access to market data for storage, analysis, visualization, indicator development, algorithmic trading, strategy backtesting, and bot programming.

Exchanges provide public and private APIs. The public API gives market data, such as prices, order books, and trades; it needs no account. The private API gives access to balances, orders, and trading; it requires API keys generated in our exchange account.

The library is installed with the following command. The examples use ccxt 4.5 and the rich library for tables. The outputs show live market data from the time the examples were run.

$ pip install ccxt rich

Exchanges

In the first example, we list the supported exchanges.

exchanges.py
import ccxt

print(ccxt.__version__)
print(len(ccxt.exchanges))
print(ccxt.exchanges[:12])

The exchanges attribute contains the ids of all supported exchanges. Each id is also the name of a class in the ccxt module.

$ python exchanges.py
4.5.91
104
['alpaca', 'apex', 'aster', 'backpack', 'bequant', 'bigone', 'binance', 'binancecoinm', 'binanceus', 'binanceusdm', 'bingx', 'bit2c']

Exchange capabilities

Not every exchange supports every method. The has dictionary tells which unified methods an exchange implements.

capabilities.py
import ccxt

features = ['fetchTicker', 'fetchOHLCV', 'fetchOrderBook', 'fetchTrades',
            'fetchBalance', 'createOrder', 'fetchStatus']

for name in ['binance', 'kraken', 'coinbase', 'bitstamp']:
    exchange = getattr(ccxt, name)()
    supported = [f for f in features if exchange.has.get(f) is True]
    print(f'{exchange.name:<18} rateLimit={exchange.rateLimit}ms')
    print(f'  {", ".join(supported)}')

We check seven features on four exchanges.

exchange = getattr(ccxt, name)()

We get the exchange class by its id and create an instance.

supported = [f for f in features if exchange.has.get(f) is True]

A value in has is True, False, or 'emulated' for features that ccxt implements on top of other endpoints.

print(f'{exchange.name:<18} rateLimit={exchange.rateLimit}ms')

The rateLimit attribute is the minimum delay between requests in milliseconds. The built-in rate limiter, enabled by default, waits automatically so that the exchange does not ban us.

$ python capabilities.py
Binance            rateLimit=50ms
  fetchTicker, fetchOHLCV, fetchOrderBook, fetchTrades, fetchBalance, createOrder, fetchStatus
Kraken             rateLimit=1000ms
  fetchTicker, fetchOHLCV, fetchOrderBook, fetchTrades, fetchBalance, createOrder, fetchStatus
Coinbase Advanced  rateLimit=34ms
  fetchTicker, fetchOHLCV, fetchOrderBook, fetchTrades, fetchBalance, createOrder
Bitstamp           rateLimit=75ms
  fetchTicker, fetchOHLCV, fetchOrderBook, fetchTrades, fetchBalance, createOrder

Fetching a ticker

The fetch_ticker method returns a ticker: the current price and statistics calculated over the past 24 hours for a symbol.

ticker.py
import ccxt

binance = ccxt.binance()

ticker = binance.fetch_ticker('BTC/USDT')

print(ticker['symbol'], ticker['datetime'])

for key in ['last', 'bid', 'ask', 'open', 'high', 'low', 'baseVolume',
            'quoteVolume', 'percentage']:
    print(f'{key:<12} {ticker[key]:>18,.2f}')

The program retrieves the ticker for the BTC/USDT symbol on Binance.

ticker = binance.fetch_ticker('BTC/USDT')

A symbol is a trading pair written as BASE/QUOTE. Here the base asset is bitcoin and the quote asset is the USDT stablecoin. Prices are expressed in the quote asset.

for key in ['last', 'bid', 'ask', 'open', 'high', 'low', 'baseVolume',
            'quoteVolume', 'percentage']:

The ticker is a dictionary with the same keys on every exchange. The bid is the highest buy price and the ask the lowest sell price. The baseVolume is the traded amount in BTC and quoteVolume in USDT. The percentage is the 24-hour change. The raw response of the exchange is available under the info key.

$ python ticker.py
BTC/USDT 2026-10-30T19:12:42.011Z
last                  76,662.57
bid                   76,662.56
ask                   76,662.57
open                  80,826.79
high                  80,954.65
low                   76,325.98
baseVolume            33,973.29
quoteVolume    2,654,292,955.28
percentage                -5.15

Comparing prices

The ccxt.async_support module provides asynchronous versions of all exchanges. We use it to query several exchanges concurrently.

compare_prices.py
import asyncio
import ccxt.async_support as ccxt

pairs = {
    'binance': 'BTC/USDT',
    'bybit': 'BTC/USDT',
    'okx': 'BTC/USDT',
    'kucoin': 'BTC/USDT',
    'kraken': 'BTC/USD',
    'coinbase': 'BTC/USD',
}

async def last_price(name, symbol):
    exchange = getattr(ccxt, name)()
    try:
        ticker = await exchange.fetch_ticker(symbol)
        return name, symbol, ticker['last']
    except ccxt.BaseError as e:
        return name, symbol, type(e).__name__
    finally:
        await exchange.close()

async def main():
    results = await asyncio.gather(*(last_price(n, s) for n, s in pairs.items()))

    ok = sorted((r for r in results if isinstance(r[2], float)), key=lambda r: r[2])
    failed = [r for r in results if not isinstance(r[2], float)]

    for name, symbol, price in ok:
        print(f'{name:<10} {symbol:<9} {price:>12,.2f}')

    for name, symbol, error in failed:
        print(f'{name:<10} {symbol:<9} {error}')

    prices = [r[2] for r in ok]
    spread = max(prices) - min(prices)
    print(f'spread: {spread:.2f} ({spread / min(prices):.3%})')

asyncio.run(main())

We compare the last price of bitcoin on six exchanges.

import ccxt.async_support as ccxt

The async classes have the same names and methods; the methods are coroutines.

try:
    ticker = await exchange.fetch_ticker(symbol)
    return name, symbol, ticker['last']
except ccxt.BaseError as e:
    return name, symbol, type(e).__name__
finally:
    await exchange.close()

An async exchange holds an HTTP session that must be closed. All ccxt exceptions derive from BaseError; catching it in the coroutine keeps one failing exchange from breaking the others.

results = await asyncio.gather(*(last_price(n, s) for n, s in pairs.items()))

The requests run concurrently. Prices differ slightly between exchanges and between the USD and USDT markets.

$ python compare_prices.py
coinbase   BTC/USD      76,443.84
kraken     BTC/USD      76,474.60
bybit      BTC/USDT     76,586.10
kucoin     BTC/USDT     76,587.00
okx        BTC/USDT     76,587.50
binance    BTC/USDT     76,639.65
spread: 195.81 (0.256%)

Exchange status

The fetch_status method reports whether an exchange is operational or under maintenance.

status.py
import asyncio
import ccxt.async_support as ccxt

names = ['binance', 'bybit', 'kraken', 'okx', 'kucoin', 'bitstamp']

async def status(name):
    exchange = getattr(ccxt, name)()
    try:
        data = await exchange.fetch_status()
        return exchange.name, data['status']
    except ccxt.BaseError as e:
        return exchange.name, f'error: {type(e).__name__}'
    finally:
        await exchange.close()

async def main():
    for name, stat in await asyncio.gather(*map(status, names)):
        print(f'{name:<10} {stat}')

asyncio.run(main())

We check the status of six exchanges.

except ccxt.BaseError as e:
    return exchange.name, f'error: {type(e).__name__}'

Bitstamp does not implement fetchStatus, as the capabilities example showed, so the call raises NotSupported.

$ python status.py
Binance    ok
Bybit      ok
Kraken     ok
OKX        ok
KuCoin     ok
Bitstamp   error: NotSupported

Markets

The load_markets method downloads the list of markets of an exchange with their symbols, ids, precision, and limits. ccxt caches the markets, so they are loaded only once per instance.

markets.py
import ccxt
from rich import box
from rich.console import Console
from rich.table import Table

kraken = ccxt.kraken()
markets = kraken.load_markets()

print(f'Kraken has {len(markets)} markets')

eur = [m for m in markets.values() if m['quote'] == 'EUR' and m['spot']]
print(f'{len(eur)} spot markets quoted in EUR')

table = Table(title='Kraken EUR markets', box=box.ASCII)

for col in ['id', 'symbol', 'base', 'quote', 'min amount']:
    table.add_column(col)

for m in sorted(eur, key=lambda m: m['symbol'])[:8]:
    table.add_row(m['id'], m['symbol'], m['base'], m['quote'],
                  str(m['limits']['amount']['min']))

Console(width=80).print(table)

We list spot markets quoted in euros on Kraken.

eur = [m for m in markets.values() if m['quote'] == 'EUR' and m['spot']]

Besides spot markets, exchanges list futures, swaps, and options. The spot flag selects ordinary buy/sell markets.

table.add_row(m['id'], m['symbol'], m['base'], m['quote'],
              str(m['limits']['amount']['min']))

The id is the exchange's own name of the market, while the symbol is the unified ccxt name. The limits give the minimum order amount.

$ python markets.py
Kraken has 1464 markets
546 spot markets quoted in EUR
                 Kraken EUR markets                  
+---------------------------------------------------+
| id       | symbol    | base  | quote | min amount |
|----------+-----------+-------+-------+------------|
| 0GEUR    | 0G/EUR    | 0G    | EUR   | 17.0       |
| 1INCHEUR | 1INCH/EUR | 1INCH | EUR   | 50.0       |
| 2ZEUR    | 2Z/EUR    | 2Z    | EUR   | 80.0       |
| AEUR     | A/EUR     | A     | EUR   | 50.0       |
| AAVEEUR  | AAVE/EUR  | AAVE  | EUR   | 0.03       |
| ABEUR    | AB/EUR    | AB    | EUR   | 8000.0     |
| ACAEUR   | ACA/EUR   | ACA   | EUR   | 12000.0    |
| ACHEUR   | ACH/EUR   | ACH   | EUR   | 800.0      |
+---------------------------------------------------+

Order book

An order book is the list of current buy orders (bids) and sell orders (asks) for a market, with their prices and amounts.

order_book.py
import ccxt

bitstamp = ccxt.bitstamp()

book = bitstamp.fetch_order_book('ETH/USD', limit=5)
bids = book['bids'][:5]
asks = book['asks'][:5]

print(f'{"price":>10} {"amount":>12}')

for price, amount in reversed(asks):
    print(f'{price:>10.2f} {amount:>12.5f}  ask')

print('-' * 28)

for price, amount in bids:
    print(f'{price:>10.2f} {amount:>12.5f}  bid')

best_bid, best_ask = bids[0][0], asks[0][0]
print(f'spread: {best_ask - best_bid:.2f}, mid: {(best_ask + best_bid) / 2:.3f}')

The program shows the top of the order book for ETH/USD on Bitstamp.

book = bitstamp.fetch_order_book('ETH/USD', limit=5)

The limit parameter asks for a number of price levels; some exchanges accept only certain values or return more. We slice the lists to be sure.

for price, amount in reversed(asks):

Bids are sorted from the highest price and asks from the lowest. We print the asks in reverse so that the best prices of both sides meet in the middle.

best_bid, best_ask = bids[0][0], asks[0][0]

The difference between the best ask and the best bid is the spread; their average is the mid price.

$ python order_book.py
     price       amount
   2230.80      0.12000  ask
   2230.78      0.20000  ask
   2230.77      0.22417  ask
   2230.71     14.56935  ask
   2230.69      2.26147  ask
----------------------------
   2230.68      0.28065  bid
   2230.67      0.20000  bid
   2230.64      2.25214  bid
   2230.63      0.14610  bid
   2230.60      0.01064  bid
spread: 0.01, mid: 2230.685

Candlestick data

The fetch_ohlcv method fetches historical candlestick data: the open, high, low, and close prices and the volume for each period.

candles.py
from datetime import datetime, UTC

import ccxt
from rich import box
from rich.console import Console
from rich.table import Table

binance = ccxt.binance()
data = binance.fetch_ohlcv('BTC/USDT', '1d', limit=7)

table = Table(title='Binance - BTC/USDT', box=box.ASCII)

table.add_column('Date', justify='center')
for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
    table.add_column(col, justify='right')

for ts, o, h, l, c, v in data:
    d = datetime.fromtimestamp(ts / 1000, UTC)
    table.add_row(f'{d:%Y-%m-%d}', f'{o:,.2f}', f'{h:,.2f}', f'{l:,.2f}',
                  f'{c:,.2f}', f'{v:,.2f}')

Console(width=90).print(table)

We retrieve daily candles for BTC/USDT on Binance for the last seven days.

data = binance.fetch_ohlcv('BTC/USDT', '1d', limit=7)

The timeframe is one day and the limit is seven candles. Each candle is a list of six values.

d = datetime.fromtimestamp(ts / 1000, UTC)

Timestamps in ccxt are in milliseconds since the Unix epoch. We convert them to aware UTC datetimes; the old utcfromtimestamp function is deprecated.

$ python candles.py
                            Binance - BTC/USDT                            
+------------------------------------------------------------------------+
|    Date    |      Open |      High |       Low |     Close |    Volume |
|------------+-----------+-----------+-----------+-----------+-----------|
| 2026-10-24 | 81,189.44 | 81,213.64 | 79,220.74 | 80,888.16 | 11,719.54 |
| 2026-10-25 | 80,888.15 | 81,033.69 | 79,845.86 | 80,154.09 |  6,252.92 |
| 2026-10-26 | 80,154.09 | 80,844.70 | 79,368.67 | 80,150.56 | 15,371.35 |
| 2026-10-27 | 80,150.56 | 81,300.00 | 79,506.00 | 80,909.77 | 13,230.65 |
| 2026-10-28 | 80,909.77 | 81,823.00 | 80,397.14 | 81,343.50 | 13,109.37 |
| 2026-10-29 | 81,343.51 | 81,971.00 | 77,910.00 | 77,991.89 | 23,832.18 |
| 2026-10-30 | 77,991.89 | 78,804.01 | 76,325.98 | 76,632.01 | 22,501.29 |
+------------------------------------------------------------------------+

Public trades

The fetch_trades method returns the most recent trades of a market.

trades.py
import ccxt

kraken = ccxt.kraken()
trades = kraken.fetch_trades('ETH/EUR', limit=6)

for t in trades[-6:]:
    print(f"{t['datetime']} {t['side']:<4} {t['amount']:>10.5f} @ {t['price']:.2f}")

We print the last trades of ETH/EUR on Kraken. The side is the side of the taker, the order that completed the trade.

$ python trades.py
2026-10-30T19:11:53.261Z buy     0.30010 @ 1999.35
2026-10-30T19:12:05.663Z buy     0.04290 @ 1999.34
2026-10-30T19:12:11.422Z buy     0.00408 @ 1999.79
2026-10-30T19:12:19.312Z buy     0.02388 @ 1998.91
2026-10-30T19:12:19.596Z sell    0.19621 @ 1998.78
2026-10-30T19:12:39.829Z sell    0.67095 @ 1999.00

Private API

Methods such as fetch_balance and fetch_my_trades access our account and require API keys. The keys should be read from environment variables, never written in the source code.

balance.py
import os
import ccxt

binance = ccxt.binance({
    'apiKey': os.getenv('BINANCE_API_KEY'),
    'secret': os.getenv('BINANCE_SECRET_KEY'),
})

try:
    balance = binance.fetch_balance()
except ccxt.AuthenticationError as e:
    print('AuthenticationError:', e)
else:
    for coin, amount in balance['total'].items():
        if amount > 0:
            print(f'{coin:<6} {amount:>18.8f}')

    since = binance.parse8601('2026-10-01T00:00:00Z')
    for trade in binance.fetch_my_trades('LTC/USDT', since):
        print(trade['datetime'], trade['side'], trade['amount'], trade['price'])

We print non-zero balances and our recent LTC/USDT trades on Binance.

binance = ccxt.binance({
    'apiKey': os.getenv('BINANCE_API_KEY'),
    'secret': os.getenv('BINANCE_SECRET_KEY'),
})

The credentials are passed in the configuration dictionary. For trading, the keys should have the minimum permissions needed, and withdrawals should be disabled.

since = binance.parse8601('2026-10-01T00:00:00Z')
for trade in binance.fetch_my_trades('LTC/USDT', since):

The parse8601 helper converts an ISO 8601 string into a millisecond timestamp. Only trades after this time are returned.

We run the program without the environment variables. ccxt checks the credentials before sending the request and raises an AuthenticationError.

$ python balance.py
AuthenticationError: binance requires "apiKey" credential

Error handling

ccxt has a hierarchy of exceptions. ExchangeError and its subclasses mean that the exchange rejected the request; NetworkError and its subclasses mean that the request failed in transport and can usually be retried.

errors.py
import ccxt

binance = ccxt.binance({'timeout': 10_000})

try:
    binance.fetch_ticker('FOO/BAR')
except ccxt.BadSymbol as e:
    print('BadSymbol:', e)

print(issubclass(ccxt.BadSymbol, ccxt.ExchangeError))
print(issubclass(ccxt.RequestTimeout, ccxt.NetworkError))

broken = ccxt.binance({'timeout': 1})

try:
    broken.fetch_ticker('BTC/USDT')
except ccxt.NetworkError as e:
    print(type(e).__name__)

We provoke two kinds of errors.

try:
    binance.fetch_ticker('FOO/BAR')
except ccxt.BadSymbol as e:

An unknown symbol raises BadSymbol, a subclass of ExchangeError. Retrying will not help.

broken = ccxt.binance({'timeout': 1})

The timeout option is in milliseconds. One millisecond is too short for any request, so the call raises RequestTimeout, a subclass of NetworkError.

$ python errors.py
BadSymbol: binance does not have market symbol FOO/BAR
True
True
RequestTimeout

Source

ccxt documentation

In this article we have shown how to work with the ccxt cryptocurrency library.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.