DEV Community

pickuma
pickuma

Posted on • Originally published at pickuma.com

Building a Crypto Trading Bot With CCXT in Python: From API Keys to Live Orders

The first crypto bot I ever wrote made money for three days and then quietly lost it all back over the next two weeks. The code worked perfectly. That was the problem — I had confused "the code runs without errors" with "the strategy is profitable," and those are completely different claims. CCXT, the library this guide is built around, makes the first claim almost trivial to satisfy. It does nothing at all for the second.

That distinction is worth holding onto, because CCXT is genuinely excellent at the thing it does. It gives you a single, consistent Python interface to dozens of crypto exchanges — Binance, Kraken, Coinbase, OKX, Bybit, and many more — so that fetch_ohlcv and create_order mean the same thing whether you're talking to one exchange or another. You write your logic once and switch venues by changing a string. For the plumbing of a trading bot, that's exactly what you want, and it's why CCXT has been the de-facto standard in this space for years.

What follows is a working walkthrough on my M2 MacBook Air: install the library, get testnet keys, pull market data, build a dead-simple signal, and place orders — first as a dry run, then on a sandbox. I'll be blunt about the parts that bite. The goal is not to hand you a bot to run with real money tomorrow. It's to get you to the point where the only thing standing between you and a live deployment is the work that actually matters: testing.

Installing CCXT and getting testnet keys

Start in a virtual environment. Crypto exchange libraries pull in a chain of HTTP and crypto dependencies, and you do not want those leaking into your system Python.

python -m venv .venv
source .venv/bin/activate
pip install ccxt
Enter fullscreen mode Exit fullscreen mode

CCXT ships both a synchronous client and an async one (ccxt.async_support). For learning and for low-frequency bots, the synchronous client is simpler to reason about, and that's what I'll use here. If you later need to poll many symbols concurrently, the async client is a drop-in conceptual swap.

Before you write a line of trading logic, get testnet credentials. Most major exchanges run a sandbox or testnet environment with fake balances and a real-ish matching engine. Binance has its Spot Testnet and a separate Futures Testnet; Bybit, OKX, and others offer demo trading accounts. You generate API keys inside that sandbox exactly the way you would on the live exchange, but the money isn't real.

Two rules from the start. First, when you create an API key, do not enable withdrawal permissions — a trading bot never needs to move funds off the exchange, and a leaked key without withdrawal rights can't drain your account. Second, never hard-code keys in source. Read them from environment variables:

import os
import ccxt

exchange = ccxt.binance({
    "apiKey": os.environ["BINANCE_API_KEY"],
    "secret": os.environ["BINANCE_SECRET"],
    "enableRateLimit": True,
})
exchange.set_sandbox_mode(True)  # routes calls to the testnet
Enter fullscreen mode Exit fullscreen mode

set_sandbox_mode(True) is the line people forget. It tells CCXT to point at the exchange's test endpoints instead of production. Forgetting it is how a "harmless test" ends up placing a real order. enableRateLimit: True turns on CCXT's built-in throttle, which I'll come back to because it's one of the most important flags in the library.

Not every exchange in CCXT implements set_sandbox_mode, and a few implement it partially — some endpoints route to testnet while others silently fall through to production. Before trusting it, place one tiny order and confirm it shows up in your testnet account UI, not your live one. I treat the first confirmed testnet order as the real "hello world" of any new exchange integration. Until you've seen that order land where you expect, assume nothing.

Fetching market data: OHLCV and order books

A bot needs two kinds of data: historical candles to compute signals, and current book/price state to make decisions about execution. CCXT gives you both through unified methods.

OHLCV (open, high, low, close, volume) candles come from fetch_ohlcv:

import pandas as pd

# 1h candles for BTC/USDT, most recent 200
raw = exchange.fetch_ohlcv("BTC/USDT", timeframe="1h", limit=200)
df = pd.DataFrame(raw, columns=["ts", "open", "high", "low", "close", "volume"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms")
df = df.set_index("ts")
print(df.tail())
Enter fullscreen mode Exit fullscreen mode

The return value is a list of [timestamp_ms, open, high, low, close, volume] rows — the same shape on every exchange CCXT supports, which is the whole point. Note limit=200: exchanges cap how many candles you get per request (often a few hundred to ~1000). If you need years of history for backtesting, you paginate by passing a since timestamp and walking forward, sleeping between calls to respect rate limits.

For execution decisions, you want the current top of book rather than a stale candle close:

book = exchange.fetch_order_book("BTC/USDT", limit=5)
best_bid = book["bids"][0][0] if book["bids"] else None
best_ask = book["asks"][0][0] if book["asks"] else None
spread = (best_ask - best_bid) if best_bid and best_ask else None
print(f"bid={best_bid} ask={best_ask} spread={spread}")
Enter fullscreen mode Exit fullscreen mode

The spread between best bid and best ask is your first honest look at trading cost. On liquid pairs like BTC/USDT it's tiny; on thin altcoin pairs it can be a meaningful fraction of a percent, and that gap is money you lose the instant you cross it with a market order.

A simple signal: SMA crossover

The "hello world" of trading strategies is the simple moving average (SMA) crossover: when a fast average crosses above a slow one, you go long; when it crosses below, you exit. It is famous, it is easy to code, and — I want to be honest — it is not a reliable money-maker on its own. We use it here because it's transparent, not because you should trade it.

def sma_signal(df, fast=20, slow=50):
    df = df.copy()
    df["sma_fast"] = df["close"].rolling(fast).mean()
    df["sma_slow"] = df["close"].rolling(slow).mean()
    df = df.dropna()
    if len(df) < 2:
        return "hold"
    prev, last = df.iloc[-2], df.iloc[-1]
    crossed_up = prev["sma_fast"] <= prev["sma_slow"] and last["sma_fast"] > last["sma_slow"]
    crossed_down = prev["sma_fast"] >= prev["sma_slow"] and last["sma_fast"] < last["sma_slow"]
    if crossed_up:
        return "buy"
    if crossed_down:
        return "sell"
    return "hold"
Enter fullscreen mode Exit fullscreen mode

The detail that separates a toy from a bug is the crossover event. You don't want to buy on every bar where fast happens to be above slow — that would re-trigger constantly. You want the bar where the relationship flips, which is why the function compares the previous bar to the current one. The same look-ahead discipline you'd apply in a backtest applies here: only act on closed candles, never on the still-forming current bar, or you'll be trading on data that doesn't exist yet.

Placing orders, and the dry-run discipline

Here is the entire reason most people start this project — and it's the smallest part of the code. CCXT exposes unified create_market_order and create_limit_order methods:

# Market buy: spend by base amount
order = exchange.create_market_buy_order("BTC/USDT", 0.001)

# Limit buy: 0.001 BTC at a price you set
order = exchange.create_limit_buy_order("BTC/USDT", 0.001, 58000)
Enter fullscreen mode Exit fullscreen mode

Before any of that runs against even a testnet, gate it behind a dry-run flag. The pattern I use on every bot:

DRY_RUN = True

def execute(signal, symbol, amount):
    if signal == "hold":
        return
    side = "buy" if signal == "buy" else "sell"
    if DRY_RUN:
        print(f"[DRY RUN] would {side} {amount} {symbol}")
        return
    fn = exchange.create_market_buy_order if side == "buy" else exchange.create_market_sell_order
    return fn(symbol, amount)
Enter fullscreen mode Exit fullscreen mode

With DRY_RUN = True, you can run the full loop — fetch data, compute the signal, "place" orders — and watch it print decisions for days without touching a balance. This is where you catch the embarrassing bugs: the off-by-one in your candle indexing, the symbol you typo'd, the signal that fires every single bar. Only after the dry run behaves do you flip to testnet, and only after weeks of testnet do you even consider real money.

A non-negotiable detail: respect the exchange's minimum order size and precision. Every exchange has a minimum notional (often a few dollars) and rounds amounts to a fixed number of decimals. CCXT exposes these in exchange.markets[symbol]["limits"] and gives you exchange.amount_to_precision(symbol, amount) and exchange.price_to_precision(...). Skip them and the exchange rejects your order with an error that, on a bad day, you won't see until you're live.

Rate limits and exchange-specific quirks

The fastest way to get your bot temporarily banned is to hammer an exchange's REST API. Every venue publishes rate limits, and crossing them earns you HTTP 429s or a timed IP ban. Setting enableRateLimit: True (as above) makes CCXT space out requests automatically based on each exchange's documented weights. Leave it on. For anything beyond occasional polling, also consider the exchange's WebSocket feed (CCXT Pro / ccxt.pro) for streaming data instead of polling REST in a tight loop.

The unified API is a beautiful abstraction that leaks. CCXT papers over most differences, but exchanges genuinely disagree on things: how symbols are named, what order types they support, how they report balances, whether they want quote-amount or base-amount for market buys, and the structure of fields under the info key (which is the raw, un-normalized exchange response). The practical consequence is that "switch venues by changing a string" is true for the happy path and aspirational for the edge cases. Whenever you add a new exchange, re-test order placement and balance parsing specifically — don't assume your Binance code transfers cleanly to Kraken.

Wiring CCXT to place orders is a weekend. The parts that determine whether you make or lose money are nowhere in the library: a backtest honest enough to model fees and slippage, position sizing that survives a losing streak, and the gap between backtest and live execution. Backtests assume you fill at the close with zero market impact; live, you cross the spread, pay maker/taker fees (commonly around 0.1% per side on spot, varying by venue and volume tier as of mid-2026), and occasionally get worse fills than you modeled. A strategy that looks like a 0.8 Sharpe on paper can be flat or negative once those frictions land. Build and validate the backtest before you wire up live orders — see our companion guide on backtesting a first quant strategy in Python.

How CCXT fits the landscape

CCXT isn't the only way to talk to an exchange, and it's worth knowing when it's the right tool. The honest comparison is between a unified multi-exchange library, a single exchange's official SDK, and a full framework that bundles strategy, backtesting, and execution.

The pattern is straightforward. If you want full control and might use more than one exchange, CCXT is the foundation — you build the strategy and risk layer yourself. If you want to skip the plumbing entirely and just write strategy code, Freqtrade (which itself uses CCXT underneath) gives you backtesting and live trading in one package and is a reasonable place to start if you don't want to assemble parts. Hummingbot targets market making and arbitrage specifically. An official SDK makes sense only when you've committed hard to one exchange and need an endpoint CCXT doesn't expose. For most people learning to build a bot, CCXT plus your own thin strategy layer is the right amount of control without reinventing the network layer.

Who should build on CCXT directly

Build directly on CCXT if you're a developer who wants to understand and own every layer of your bot, expects to trade across more than one exchange, or has a strategy idea that doesn't fit the assumptions of an off-the-shelf framework. The unified API saves you enormous integration time while leaving the trading logic — the part that actually matters — entirely in your hands.

Reach for a framework like Freqtrade instead if your goal is to test and run strategies fast and you'd rather not hand-roll a backtester, an order manager, and a config system. There's no shame in it; a good framework encodes hard-won lessons about the live/backtest gap that you'd otherwise learn the expensive way.

And if you're new to markets, do neither yet. Build the dry-run loop in this guide, watch it make paper decisions for a couple of weeks, then build a proper backtest, and only then think about testnet money. The technology is the easy part. Your job is to find out — cheaply, before any real capital is at risk — whether your idea actually has an edge once fees and slippage take their cut. Most don't, and discovering that on testnet is a win, not a failure.

FAQ


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)