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.30000000000000004Binary 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' elsewhere4. 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:
NetworkErrorfamily —RequestTimeout,ExchangeNotAvailable,DDoSProtection,RateLimitExceeded. Transient. Back off and retry.ExchangeErrorfamily —InvalidOrder,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.