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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
In this article we have shown how to work with the ccxt cryptocurrency library.
Author
List all Python tutorials.