Seven mistakes that blow up crypto trading bots (you're probably making #1 right now)

Floating-point money, rate-limit bans, symbol mixups, blind retries — the classic failure modes of trading code, and the library machinery that exists because of them.

By CCXT Team

Trading code fails in predictable ways. After a decade of maintaining integrations with 100+ exchanges, we've seen the same seven mistakes take down bots over and over — many of CCXT's design decisions exist precisely because of them. Here they are, roughly in the order they'll bite you.

1. Doing money math with floats

console.log(0.1 + 0.2);
// 0.30000000000000004

Binary floats cannot represent most decimal fractions — in every language. In trading code that's not a trivia point: it produces order amounts the exchange rejects, or worse, quietly wrong position sizes that compound over thousands of trades.

Internally, all CCXT arithmetic runs through Precise — decimal string math with no float in sight, in every language. For your own code the important tools are the precision helpers, which round any value to exactly what the exchange accepts:

const amount = exchange.amountToPrecision('BTC/USDT', rawAmount);
const price = exchange.priceToPrecision('BTC/USDT', rawPrice);

Every market also carries its precision and limits (minimum amount, minimum cost) in the market structure — check them before ordering instead of parsing rejection errors after.

2. Ignoring rate limits until the ban

Every exchange enforces request-rate limits; exceed them and you get HTTP 429s, then a temporary IP ban — usually mid-trade, when your loop is at its busiest.

CCXT's built-in token-bucket rate limiter is enabled by default and knows each exchange's per-endpoint costs: heavy calls consume a bigger share of the budget than light ones. Keep it on, and design for it: cache loadMarkets() at startup instead of re-fetching, batch with fetchTickers(symbols) instead of many fetchTicker calls, and use WebSockets for anything you poll more than a few times a minute.

3. Hardcoding exchange-specific symbols

BTCUSDT, XBTUSDT, BTC-USDT, tBTCUSD — every exchange names the same market differently, and code with raw IDs sprinkled through it dies the day you add a second exchange.

CCXT's rule: unified symbols in your code, exchange IDs at the wire, never mixed. You write BTC/USDT everywhere (derivatives have unified forms too: BTC/USDT:USDT for the linear perpetual swap), and the library maps to the exchange's ID internally. If you ever need the raw ID — say, for an exchange-specific endpoint — derive it, don't hardcode it:

const market = exchange.market('BTC/USDT');
console.log(market.id);   // 'BTCUSDT' on binance, 'XBTUSDT' elsewhere

4. Treating all errors the same

The two deadly variants: crash on any error (bot dies at the first hiccup at 2 a.m.), or blindly retry everything (bot re-submits an order the exchange already rejected — or worse, already accepted).

CCXT normalizes every exchange's error zoo into one typed hierarchy, so your handler can be policy, not string matching:

  • NetworkError familyRequestTimeout, ExchangeNotAvailable, DDoSProtection, RateLimitExceeded. Transient. Back off and retry.
  • ExchangeError familyInvalidOrder, InsufficientFunds, AuthenticationError, BadSymbol. Your request is wrong or your state is. Retrying identical input gives an identical failure — fix the cause instead.
try {
    const order = await exchange.createOrder(symbol, 'limit', side, amount, price);
} catch (e) {
    if (e instanceof ccxt.NetworkError) {
        checkIfItWentThrough();   // see below, then maybe retry
    } else if (e instanceof ccxt.ExchangeError) {
        logAndStop(e);            // do NOT blind-retry these
    }
}

The subtle case is a timeout on createOrder: the exchange may have accepted the order even though your request timed out. Retrying naively doubles your position. The fix is idempotency — send your own clientOrderId in params, and on timeout query for it before retrying.

5. Assuming pagination just works

fetchOHLCV, fetchTrades, and fetchMyTrades return one page, capped at an exchange-specific maximum (often 500–1000 rows). Code that calls them once and assumes it got "the history" silently computes indicators on partial data.

Fetching a range means looping: request with since, take the last row's timestamp, request again from there, stop when nothing new arrives. Respect each exchange's page cap (the limit argument), and remember the rate limiter is spending budget on every page.

6. Letting your clock drift

Signed requests embed a timestamp, and exchanges reject requests whose timestamp is too far from their server time. A clock a few seconds off produces baffling authentication failures that come and go — classically on a VPS or Raspberry Pi without NTP.

Run NTP. And most exchanges wired for it in CCXT accept an option that measures and corrects the offset for you:

const exchange = new ccxt.binance({
    apiKey: KEY,
    secret: SECRET,
    options: {
        adjustForTimeDifference: true,
    },
});

7. Testing with real money

Nobody plans to test in production; people just don't know the alternatives exist:

  • exchange.setSandboxMode(true) switches CCXT to the exchange's testnet or demo environment where one exists (Binance, Bybit, OKX, and many others). Play money, real API.
  • Keys with withdrawals disabled + IP allowlist — a trading bot never needs withdrawal permission. This turns a leaked key from a catastrophe into an inconvenience.
  • Minimum order sizes on the first live runs, with a kill switch you've actually tested.

None of these mistakes is exotic — that's the point. They're the standard tax every trading-bot developer pays once. CCXT can't stop you from paying it, but it can make the correct thing the default thing: string math, rate limiting on by default, unified symbols, typed errors, sandbox mode one call away.

New to the library? Start with Build your first crypto trading bot.