# CCXT Python: building a multi-exchange trading bot

URL: https://fincodelabs.com/en/blog/ccxt-python-trading-bot · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-10-25 · Updated: 2026-11-05

CCXT Python guide: unified markets, fetch_ticker, fetch_ohlcv, precision helpers, sandbox mode, rate limits, errors, CCXT Pro streams and a tested bot skeleton.

**Short answer:** **CCXT** is an MIT-licensed library that gives Python one unified API for more than 100 crypto exchanges. You load markets once, call methods such as fetch_ticker, fetch_ohlcv and create_order with unified symbols like BTC/USDT, and pass exchange-specific options in a params dictionary. As of 5 November 2026 the current release is 4.5.92 and WebSocket streaming (CCXT Pro) is included free. A bot built on it still needs testnet trials, precision checks, error handling and risk limits of its own.

CCXT puts one interface over each exchange's signing, symbols, rounding and error codes, which makes it a common starting point for a multi-exchange bot in Python, though differences that matter for real orders remain, as shown below.

Every sample was run on 5 November 2026 with ccxt 4.5.92 on Python 3.11.3 against public endpoints and the Binance Spot Testnet, without API keys, so no order was placed there; the lost-reply paths ran against a local mock server instead. This guide recommends no exchange or strategy; whether you may use an exchange depends on your jurisdiction and its terms. In Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign-based crypto-asset platforms directed at residents of Turkey as unauthorized service provision. Automated trading can lose money.

## How to install CCXT in Python: sync, async and Pro

Install it with pip install ccxt; one package provides sync REST, asyncio REST and WebSocket clients.

As of 5 November 2026 the current PyPI release is 4.5.92, published on 4 November 2026; it needs Python 3.10 or later and is MIT-licensed. The package lists 104 exchange classes in ccxt.exchanges, 76 with WebSocket support, and the Pro manual calls Pro a free part of CCXT. Releases are frequent: pin the tested version.

**The three CCXT entry points**

| Import | Style | Typical use |
| --- | --- | --- |
| import ccxt | Synchronous REST | Scripts, polling bots |
| import ccxt.async_support | asyncio REST | Many exchanges at once |
| import ccxt.pro | asyncio WebSocket | Streaming data and orders |

## load_markets: unified symbols and exchange market ids

load_markets() downloads an exchange's market list once and maps unified symbols such as BTC/USDT to the exchange's own ids, precision and limits.

```python
import ccxt

exchanges = {name: getattr(ccxt, name)() for name in ("binance", "kraken", "okx")}
for name, ex in exchanges.items():
    ex.load_markets()                        # one request; cached on the instance
    m = ex.market("BTC/USDT")                # same unified symbol everywhere
    print(f"{name:8} id={m['id']:9} tick={m['precision']['price']:<5} "
          f"step={m['precision']['amount']:<6} min_amount={m['limits']['amount']['min']} "
          f"min_cost={m['limits']['cost']['min']}")
```

```
binance  id=BTCUSDT   tick=0.01  step=1e-05  min_amount=1e-05 min_cost=5.0
kraken   id=XBTUSDT   tick=0.1   step=1e-08  min_amount=6e-05 min_cost=0.5
okx      id=BTC-USDT  tick=0.1   step=1e-08  min_amount=1e-05 min_cost=None
```

BTC/USDT is BTCUSDT on Binance, XBTUSDT on Kraken and BTC-USDT on OKX, with different tick sizes, step sizes and minimums. All three use the TICK_SIZE precision mode, which the manual says almost all exchanges use: precision values are increments, not decimal counts. Reuse one instance per exchange: the market cache and rate limiter live on it.

## fetch_ticker and fetch_ohlcv: CCXT Python examples

Public market data needs no keys: fetch_ticker returns the latest bid, ask and last price, and fetch_ohlcv returns candles as lists of a millisecond timestamp, open, high, low, close and volume.

```python
import ccxt

ex = ccxt.kraken()                           # public data: no keys needed
t = ex.fetch_ticker("BTC/USDT")
print(t["symbol"], t["bid"], t["ask"])

candles = ex.fetch_ohlcv("BTC/USDT", "1h", limit=5)   # [ms, open, high, low, close, volume]
closed = candles[:-1]                        # the last candle is still forming
print(ex.iso8601(closed[-1][0]), closed[-1][4])

amount = ex.amount_to_precision("BTC/USDT", 0.000123456789)   # truncates, returns str
price = ex.price_to_precision("BTC/USDT", t["bid"] * 0.95)     # rounds, returns str
lim = ex.market("BTC/USDT")["limits"]
print(amount, price, float(amount) >= lim["amount"]["min"],
      float(amount) * float(price) >= (lim["cost"]["min"] or 0))
```

```
BTC/USDT 80454.5 80454.6
2026-11-05T04:00:00.000Z 80550.0
0.00012345 76431.8 True True
```

Three rules from the CCXT manual: the last candle may be incomplete until the next one starts, so drop it before applying your rules; since is a UTC timestamp in milliseconds, and without it each exchange returns its own default range; and exchanges limit how far back short timeframes go, so store candles yourself.

## Precision and limits: amount_to_precision and price_to_precision

The last lines above use CCXT's formatting helpers, which round to the market's precision but do not enforce its limits.

- amount_to_precision() truncates and price_to_precision() rounds; both return strings and raise InvalidOrder if the value rounds to zero.
- Minimum and maximum amount, price and cost are in market['limits'], and staying inside them is your job. A None, like OKX's minimum cost above, means unknown, not unlimited.

> **Hypothetical example.** A bot spends 20 USDT on BTC at 76,000 USDT, with a 0.00001 step and a 5 USDT minimum cost: 0.000263… truncates to 0.00026 BTC, a cost of 19.76 USDT, which is valid. At 3 USDT it truncates to 0.00003 BTC, a cost of 2.28 USDT, below the minimum, so the bot must skip the order.

## Sandbox mode, rate limits and CCXT exceptions

Switch to an exchange's test network with set_sandbox_mode(True) right after creating the instance, keep the built-in rate limiter on, and handle errors by class: a rate limit can wait and be retried, an exchange error needs a fix first, and a timeout or 5XX on an order means look it up before anything else.

```python
import os
import ccxt

ex = ccxt.binance({"apiKey": os.environ.get("BINANCE_TESTNET_KEY"),   # testnet keys only
                   "secret": os.environ.get("BINANCE_TESTNET_SECRET")})
ex.set_sandbox_mode(True)                    # must come before any other call
print(ex.urls["api"]["public"])

def place(symbol, side, amount, price, client_id):
    try:
        return ex.create_order(symbol, "limit", side, amount, price,
                               {"clientOrderId": client_id})
    except (ccxt.RateLimitExceeded, ccxt.DDoSProtection, ccxt.InvalidNonce) as e:
        print("refused, retry later:", type(e).__name__)
    except ccxt.OperationFailed:             # timeout, 5XX: outcome unknown, look it up first
        try:                                 # open or already filled: both are found
            return ex.fetch_order(None, symbol, {"clientOrderId": client_id})
        except ccxt.BaseError as e:          # not found yet is not proof that it failed
            print("outcome unknown, do not resend:", type(e).__name__)
    except ccxt.ExchangeError as e:          # also AuthenticationError, InvalidOrder
        print("do not retry:", type(e).__name__, str(e))

place("BTC/USDT", "buy", 0.0002, 50000, "demo-001")
```

```
https://testnet.binance.vision/api/v3
do not retry: AuthenticationError binance requires "apiKey" credential
```

- **Sandbox.** Only where the exchange runs one, with separate keys. For Binance it points the spot API at testnet.binance.vision, covered in the [Binance API Python guide](https://fincodelabs.com/en/blog/binance-api-python-trading-bot); 4.5.92 also has enable_demo_trading() for Binance's separate demo environment, not combinable with sandbox mode.
- **Rate limits.** enableRateLimit is on by default and uses a leaky bucket (a rolling window is optional); rateLimit is a pause in milliseconds multiplied by each endpoint's cost (50 for Binance, 1,000 for Kraken in 4.5.92). It cannot see other processes sharing your IP or key.
- **Exceptions.** NetworkError, under OperationFailed, covers RequestTimeout, RateLimitExceeded, DDoSProtection, ExchangeNotAvailable and InvalidNonce, usually temporary. ExchangeError subclasses such as AuthenticationError, InsufficientFunds, InvalidOrder and BadSymbol fail again unchanged. Binance's -1000 and -1006 answers raised a bare OperationFailed on a local mock, which neither of those two parents catches, so place catches OperationFailed after the clear refusals. Catch a subclass before its parent.
- **Unknown outcomes.** A RequestTimeout on create_order does not mean the order failed; the manual says to check orders and balance before acting again. fetch_order with the client order ID finds the order whether it is open or already filled, while a search of fetch_open_orders misses a filled one. Not found is not proof of failure either, because the order may arrive late, so look again before sending anything (see [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration)). The parameter name is exchange-specific: in 4.5.92, fetch_order reads clientOrderId on Binance, OKX, KuCoin and Bitget and sends it as userref on Kraken, while Bybit's does not read it.

## CCXT Pro in Python: install, WebSocket streams and async

CCXT Pro needs no separate install: pip install ccxt includes it, and Python imports it as ccxt.pro. On 4 November 2026 the repository's ccxt.pro list held 76 exchanges, fewer than the 104 REST classes, so check ccxt.pro.exchanges before planning on streams. ccxt.async_support and ccxt.pro run requests to several exchanges concurrently with asyncio, and Pro's watch_\* methods wait for the next WebSocket update instead of polling.

```python
import asyncio
import ccxt.pro as ccxtpro

async def watch(ex, n=2):
    try:
        for _ in range(n):
            t = await ex.watch_ticker("BTC/USDT")   # waits for the next update
            print(f"{ex.id:6} bid={t['bid']} ask={t['ask']}")
    finally:
        await ex.close()                        # release the connection

async def main():                               # two exchanges, concurrently
    await asyncio.gather(watch(ccxtpro.kraken()), watch(ccxtpro.okx()))

asyncio.run(main())
```

```
kraken bid=80448.4 ask=80448.5
okx    bid=80446.5 ask=80446.6
okx    bid=80446.5 ask=80446.6
kraken bid=80454.4 ask=80454.5
```

Each await returns the next update from an open connection; Pro also has watch_trades, watch_ohlcv, watch_order_book and private streams such as watch_orders and watch_balance. Always call close(), or CCXT prints a warning. On 5 November 2026, with aiohttp 3.14.5, close() raised an AttributeError in asyncio's SSL layer on all 15 runs on Python 3.11.3 and on none of 9 on 3.10.11, the version behind the output above, so test shutdown on the Python you deploy. Keeping a synchronized depth book is covered in the [WebSocket order book](https://fincodelabs.com/en/blog/websocket-order-book) guide.

## CCXT alternatives: when not to use CCXT

Skip CCXT when you trade one venue and need all of its features, when you only need normalized market data, or when your exchange is not on its list. These CCXT alternatives solve different problems and are not ranked.

**Options other than CCXT, read on 4 November 2026**

| Need | Option | What to check |
| --- | --- | --- |
| Every feature of one exchange | Direct integration with its REST and WebSocket APIs, or its official SDK, such as binance-sdk-spot for Binance or pybit for Bybit; guides for [Bybit](https://fincodelabs.com/en/blog/bybit-api-python), [OKX](https://fincodelabs.com/en/blog/okx-api-python), [Kraken](https://fincodelabs.com/en/blog/kraken-api-python), [Coinbase](https://fincodelabs.com/en/blog/coinbase-advanced-trade-api-python) and [Hyperliquid](https://fincodelabs.com/en/blog/hyperliquid-api-python) | You own signing, retries and API upgrades |
| Normalized market data only | Cryptofeed, a Python feed handler for exchange data | Its README badge shows an AGPL license and it asks for Python 3.13 or later |
| Backtesting and live trading in one engine | NautilusTrader, with a Rust core and Python bindings | LGPL-3.0; a full engine to learn, with a shorter exchange list |
| An exchange CCXT does not list | Direct integration | The README lists BtcTurk but not Paribu, and no separate Binance TR or OKX TR |

Either way, the strategy and its losses stay yours. For readers in Turkey, an exchange appearing in a library's list says nothing about whether it may serve you.

## A tested CCXT Python trading bot skeleton

The skeleton acts once per closed candle, sizes orders to the market's precision and minimum, tags each order with a client order ID derived from the bar, sells on exit what the entry bought, and sends orders only to the Binance Spot Testnet, and only when explicitly enabled.

```python
import os
import ccxt
import pandas as pd

SYMBOL, TIMEFRAME, MAX_COST = "BTC/USDT", "1h", 20.0    # hypothetical cap in USDT
SEND = os.environ.get("BOT_MODE") == "testnet"           # default: dry run

ex = ccxt.binance({"apiKey": os.environ.get("BINANCE_TESTNET_KEY"),
                   "secret": os.environ.get("BINANCE_TESTNET_SECRET")})
ex.set_sandbox_mode(True)               # data and orders: Binance Spot Testnet only
ex.load_markets()
min_cost = ex.market(SYMBOL)["limits"]["cost"]["min"] or 0

def send(side, amount, cid):
    try:
        ex.create_order(SYMBOL, "market", side, float(amount), None, {"clientOrderId": cid})
    except ccxt.OperationFailed:        # timeout, 5XX: it may have gone through
        try:
            ex.fetch_order(None, SYMBOL, {"clientOrderId": cid})
        except ccxt.BaseError as e:     # missing or no answer: never resend on a guess
            raise SystemExit(f"status unknown for {cid} ({type(e).__name__}): reconcile first")

def step(state):
    rows = ex.fetch_ohlcv(SYMBOL, TIMEFRAME, limit=100)[:-1]   # closed bars only
    bar, close = rows[-1][0], pd.Series([r[4] for r in rows])
    if bar == state.get("bar"):
        return "no new closed bar"
    fast = close.ewm(span=12, adjust=False).mean().iloc[-1]   # hypothetical rule,
    slow = close.ewm(span=26, adjust=False).mean().iloc[-1]   # not a recommendation
    if (fast > slow) == state.get("long", False):
        state["bar"] = bar
        return f"hold (long={fast > slow})"
    if fast > slow:                     # entry: size from the cap
        side, amount = "buy", ex.amount_to_precision(SYMBOL, MAX_COST / close.iloc[-1])
        if float(amount) * close.iloc[-1] < min_cost:
            state["bar"] = bar
            return "below minimum cost, skipped"
    else:                               # exit: what the entry bought; fees can reduce it
        side, amount = "sell", state["amount"]
    if SEND:
        send(side, amount, f"bot-{bar}")
    state.update(bar=bar, long=fast > slow, amount=amount)   # only after the order went through
    return f"{'sent' if SEND else 'dry run'}: {side} {amount}"

state = {}
for _ in range(2):                      # a real bot loops forever and alerts on errors
    print(step(state))
```

Run with no keys on 5 November 2026, it printed dry run: buy 0.00024 on one run and hold (long=False) on a later one, because the first line depends on the latest candles. Each run then printed no new closed bar: the second call found no newer candle, the guard against acting twice on one bar. The bar is recorded only after the order went through. If the reply is lost, the bot looks the order up, and it stops for reconciliation if it cannot confirm the order, because a resend with the same ID could double a filled market order. With BOT_MODE=testnet and testnet keys set, it sends market orders to the testnet. No keys were used here, so the order and lost-reply paths ran only against a local mock server, through ccxt's real request and error code: buy 0.00024 and then sell 0.00024 on a scripted path, one order after a lost reply on a filled entry, and a stop with no resend when the order could not be found.

Before anything like it trades real money, it needs persistent state, an exit amount adjusted for fees, reconciliation with fetch_balance and fetch_open_orders after restarts, an independent risk layer with a kill switch, and alerting, as described in [trading bot development](https://fincodelabs.com/en/blog/trading-bot-development) and [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management). Keys belong in a secrets store with trade-only permissions, as in [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

### Where exchange-specific parameters are still needed

Everything outside the unified path goes through exchange-specific params. In 4.5.92, Binance's create_order maps the unified clientOrderId to newClientOrderId and accepts test: True for the spot order-test endpoint. Margin modes, position sides and endpoints without a unified method still need each exchange's documentation. Connecting several venues under one risk layer is the scope of the [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration) service.

## Frequently asked questions

### Is CCXT free for commercial use?

Yes. The MIT license allows commercial use, without warranty; each exchange's API terms still apply.

### Is CCXT Pro free, and does it need a separate install?

It is free, and no separate install is needed: the Pro manual calls it a free part of CCXT, pip install ccxt includes it and Python imports it as ccxt.pro. Fewer exchanges support it than the REST classes.

### Does CCXT support testnets?

Where the exchange runs one: call set_sandbox_mode(True) right after creating the instance, with keys from the test site.

### Why does fetch_ohlcv return fewer candles than I asked for?

Exchanges limit how many candles they return and how far back they go, and periods without trades leave gaps. Page through history with the since argument in milliseconds and store what you fetch.

### Does CCXT handle rate limits automatically?

Its built-in limiter is on by default and paces requests per exchange instance. Tune rateLimit to the exchange's published limits and still handle RateLimitExceeded and DDoSProtection.

## Sources

- [CCXT GitHub repository and README (supported exchanges, MIT license)](https://github.com/ccxt/ccxt)
- [CCXT Manual (markets, precision and limits, sandbox, rate limit, OHLCV, orders, errors)](https://github.com/ccxt/ccxt/wiki/Manual)
- [CCXT Pro Manual (WebSocket watch methods, Pro is free)](https://github.com/ccxt/ccxt/wiki/ccxt.pro.manual)
- [PyPI: ccxt (release 4.5.92, Python requirement)](https://pypi.org/project/ccxt/)
- [CCXT source: error hierarchy](https://github.com/ccxt/ccxt/blob/master/ts/src/base/errorHierarchy.ts)
- [CCXT source: Python Binance exchange class (create_order params, demo trading)](https://github.com/ccxt/ccxt/blob/master/python/ccxt/binance.py)
- [Binance Spot Testnet: General info](https://github.com/binance/binance-spot-api-docs/blob/master/testnet/general-info.md)
- [Capital Markets Law No. 6362 (consolidated text, Article 99/A, Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)
- [pybit on GitHub (official Python connector for Bybit)](https://github.com/bybit-exchange/pybit)
- [PyPI: binance-sdk-spot (official Binance Spot SDK)](https://pypi.org/project/binance-sdk-spot/)
- [Cryptofeed on GitHub (README, license badge)](https://github.com/bmoscon/cryptofeed)
- [NautilusTrader on GitHub (README, license)](https://github.com/nautechsystems/nautilus_trader)
- [CCXT source: python/ccxt/pro/__init__.py (Pro exchange list)](https://github.com/ccxt/ccxt/blob/master/python/ccxt/pro/__init__.py)
- [CCXT source: Python exchange classes (fetch_order parameters)](https://github.com/ccxt/ccxt/tree/master/python/ccxt)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/ccxt-python-trading-bot

Cite as: Fincode, "CCXT Python: building a multi-exchange trading bot", updated November 5, 2026, https://fincodelabs.com/en/blog/ccxt-python-trading-bot
