CCXT for Rust

SkillSecurity

CCXT cryptocurrency exchange library for Rust developers. Covers both REST API (standard) and WebSocket API (real-time). Helps install CCXT, connect to exchanges, fetch market data, place orders, stream live tickers/orderbooks, handle authentication, and manage errors in Rust projects. Use when working with crypto exchanges in Rust applications, trading bots, or low-latency services. Async (tokio), typed wrappers returning Result<T, ExchangeError>.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the CCXT for Rust skill

What this skill tells your AI

The instructions your AI receives, as published by ccxt/ccxt in .claude/skills/ccxt-rust/SKILL.md and read by ahel’s review.

A comprehensive guide to using CCXT in Rust projects for cryptocurrency exchange integration.

Every exchange has a typed wrapper (ccxt::Binance, ccxt::Kraken, …) exposing the unified CCXT API with native Rust return types — Ticker, Order, OrderBook, Market — instead of a dynamic value. All methods are async and return Result<T, ExchangeError>.

Installation

REST API

cargo add ccxt tokio --features tokio/full

WebSocket API (ccxt.pro)

cargo add ccxt-pro

Prediction markets

cargo add ccxt-prediction

Cargo.toml

[dependencies]
ccxt = "4.5.75"                 # REST exchanges (typed) — required
ccxt-pro = "4.5.75"             # WebSocket (watch*) exchanges — only if you stream
ccxt-prediction = "4.5.75"      # prediction markets — only if you trade them
tokio = { version = "1", features = ["full"] }

Requirements

  • Rust stable, edition 2021 or later.
  • A tokio runtime — every unified method is async. There is no sync API.
  • ccxt alone is enough for REST. Add ccxt-pro only when you need watch_*; it is a separate crate so a REST-only build does not compile the whole WebSocket surface.

Quick Start

REST API

use ccxt::{Binance, Params};

#[tokio::main]
async fn main() -> Result<(), ccxt::ExchangeError> {
    let mut exchange = Binance::new(None);
    exchange.load_markets(false).await;

    let ticker = exchange.fetch_ticker("BTC/USDT", Params::none()).await?;
    println!("{} last={:?} bid={:?} ask={:?}", ticker.symbol, ticker.last, ticker.bid, ticker.ask);
    Ok(())
}

WebSocket API — real-time updates

use ccxt::Params;
use ccxt_pro::Binance;

#[tokio::main]
async fn main() -> Result<(), ccxt::ExchangeError> {
    let mut exchange = Binance::new(None);
    exchange.try_load_markets(false).await?;

    loop {
        let ticker = exchange.watch_ticker("BTC/USDT", Params::none()).await?;
        println!("{:?}", ticker.last); // live updates
    }
}

Each watch_* call resolves to one decoded update, so consuming a stream is just calling it in a loop.

Crate layout

CrateContainsUse when
ccxtTyped REST wrappers (ccxt::Binance, …), Params, Config, types::*, TypedExchange. Re-exports the whole engine at its root.Always
ccxt-proTyped WebSocket wrappers (ccxt_pro::Binance, …) with watch_*Streaming
ccxt-predictionTyped prediction-market wrappers (ccxt_prediction::Kalshi, …)Prediction markets
ccxt-baseThe untyped engine (Value, HTTP, crypto, rate limiter, Cores, WS infra)Rarely direct — ccxt re-exports it

ccxt re-exports ccxt-base at its root, so ccxt::Value, ccxt::runtime::…, ccxt::exchanges::binance::BinanceCore resolve alongside the typed ccxt::Binance.

Coverage today: 105 typed REST venues, 76 typed WebSocket venues, 7 prediction venues.

REST vs WebSocket

FeatureREST APIWebSocket API
Use forOne-time queries, placing ordersReal-time monitoring, live price feeds
Crateccxtccxt-pro
Importuse ccxt::Binance;use ccxt_pro::Binance;
Methodsfetch_* (fetch_ticker, fetch_order_book)watch_* (watch_ticker, watch_order_book)
SpeedSlower (HTTP request/response)Faster (persistent connection)
Rate limitsStrict (1–2 req/sec)More lenient (continuous stream)
Best forTrading, account managementPrice monitoring, arbitrage detection

Both crates expose a type named Binance. When you use both in one file, alias one of them:

use ccxt::Binance as BinanceRest;
use ccxt_pro::Binance as BinanceWs;

Runtime setup

Two settings matter in real programs and are easy to miss:

fn main() {
    // 1. The transpiled core signals errors by panicking across an internal
    //    catch_unwind; the typed layer turns that back into `Result`. Silencing
    //    the default hook stops caught panics from printing to stderr.
    //    Set CCXT_SHOW_PANICS=1 to see them while debugging.
    if std::env::var("CCXT_SHOW_PANICS").is_err() {
        std::panic::set_hook(Box::new(|_| {}));
    }

    // 2. The generated exchange code is deeply nested — give worker threads a
    //    large stack. The default 2 MB can overflow on some venues.
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .thread_stack_size(64 * 1024 * 1024)
        .enable_all()
        .build()
        .unwrap();

    rt.block_on(run());
}

#[tokio::main] is fine for short examples and scripts; use the explicit builder for anything long-running.

Creating an Exchange Instance

Public (no authentication)

use ccxt::Binance;

let mut exchange = Binance::new(None);

Private (with credentials), via the Config builder

use ccxt::{Binance, Config, Params};

let mut exchange = Binance::with_config(
    Config::new()
        .api_key("YOUR_API_KEY")
        .secret("YOUR_SECRET")
        .enable_rate_limit(true)       // on by default
        .timeout_ms(10_000)
        .option_str("defaultType", "swap")
        .option("fetchMarkets", Params::new().with_strs("types", &["spot", "linear"])),
);

Config covers every credential (api_key, secret, password, uid, wallet_address, private_key, token), plus sandbox, verbose, enable_rate_limit, rate_limit_ms, timeout_ms, and arbitrary properties via set_str / set_int / set_float / set_bool. options nests exactly as in the other bindings: option_str/option_int/option_bool/ option_strs for a flat key, option(key, Params) for a nested object, options(Params) for a whole block. Repeated calls deep-merge rather than replace.

Settings after construction

The core's fields are dynamic, and the wrapper derefs read-only, so exchange.verbose = true does not compile. Use the setters — they chain:

let mut exchange = Binance::new(None);
exchange.set_api_key("YOUR_API_KEY");
exchange.set_secret("YOUR_SECRET");
exchange.set_verbose(true).set_timeout_ms(10_000);
exchange.set_enable_rate_limit(true);
exchange.set_options(Params::new().with_str("defaultType", "spot"));

println!("{} {} {}", exchange.id(), exchange.is_verbose(), exchange.is_sandbox_mode_enabled());

Sandbox / testnet

// at construction
let mut a = Binance::with_config(Config::new().sandbox(true));

// or afterwards — returns Err(NotSupported) when the venue has no testnet
let mut b = Binance::new(None);
b.set_sandbox_mode(true)?;

Choosing an exchange at runtime

from_id / from_id_with_config build any supported venue from its id and hand back a trait object; the typed API stays available through TypedExchangeExt.

use ccxt::{from_id_with_config, Config, Params, TypedExchange, TypedExchangeExt};

for id in ["binance", "bybit", "okx"] {
    let Some(mut exchange) = from_id_with_config(id, Config::new().enable_rate_limit(true)) else {
        continue; // unknown id
    };
    exchange.load_markets(false).await;
    let ticker = exchange.fetch_ticker("BTC/USDT", Params::none()).await?;
    println!("{id} {:?}", ticker.last);
}

Box<dyn TypedExchange> works because TypedExchange is object-safe; the ergonomic typed methods live on the blanket TypedExchangeExt, so both traits must be in scope. The same pair exists in ccxt_pro (for watch_*) and ccxt_prediction.

Write generic code the same way:

async fn best_bid<E: TypedExchange + TypedExchangeExt>(ex: &mut E) -> Result<Option<f64>, ccxt::ExchangeError> {
    Ok(ex.fetch_ticker("BTC/USDT", Params::none()).await?.bid)
}

The params argument

Every unified method ends with params — the venue-specific knobs. On the typed layer that is a Params builder over Rust primitives, never a dynamic value:

use ccxt::Params;

// nothing extra — these three are equivalent
exchange.fetch_ticker("BTC/USDT", Params::none()).await?;
exchange.fetch_ticker("BTC/USDT", ()).await?;
exchange.fetch_ticker("BTC/USDT", [("recvWindow", "5000")]).await?;

// venue-specific extras
exchange.create_order(
    "BTC/USDT", "limit", "buy", 0.001, Some(50_000.0),
    Params::new()
        .with_str("clientOrderId", "my-order-1")
        .with_bool("postOnly", true)
        .with_str("timeInForce", "GTC")
        .with_float("triggerPrice", 49_000.0)
        .with_int("recvWindow", 5_000)
        .with_strs("clientOrderIds", &["a", "b"]),
).await?;

Builders: with_str, with_int, with_float, with_bool, with_strs, with_json, with_params (nested object). Entries keep insertion order, which some signing routines depend on. Unified params (clientOrderId, postOnly, timeInForce, reduceOnly, triggerPrice, …) mean the same thing on every venue — CCXT translates them into whatever the exchange wants on the wire.

Common REST Operations

Loading markets

// Untyped, panics on failure — fine for scripts
exchange.load_markets(false).await;

// Preferred: fallible and typed
let markets: Vec<ccxt::types::Market> = exchange.try_load_markets(false).await?;

Loading markets is required before any call that resolves a unified symbol.

Market metadata

Trading rules can be checked locally, before sending an order and without an extra request:

let market = exchange.market("BTC/USDT")?;          // Err(BadSymbol) when not listed
println!("{} {} active={}", market.symbol, market.market_type, market.active);
println!("min amount {:?}  min cost {:?}", market.limits.amount.min, market.limits.cost.min);
println!("amount step {:?}  price tick {:?}", market.precision.amount, market.precision.price);

let swaps: Vec<_> = exchange.markets().into_iter().filter(|m| m.swap && m.active).collect();
let symbols: Vec<String> = exchange.symbols();
let currencies = exchange.currencies();

Market fields: id, symbol, base, quote, settle, base_id, quote_id, market_type, spot, margin, swap, future, option, active, contract, linear, inverse, taker, maker, limits, precision, raw.

Fetching a ticker

let ticker = exchange.fetch_ticker("BTC/USDT", Params::none()).await?;
println!("{:?}", ticker.last);           // last price
println!("{:?}", ticker.bid);            // best bid
println!("{:?}", ticker.ask);            // best ask
println!("{:?}", ticker.base_volume);    // 24h volume
println!("{:?}", ticker.timestamp);

// Multiple tickers (if supported) -> HashMap<String, Ticker>
let tickers = exchange
    .fetch_tickers(Some(vec!["BTC/USDT".into(), "ETH/USDT".into()]), Params::none())
    .await?;
for (symbol, t) in &tickers {
    println!("{symbol} {:?}", t.last);
}

Numeric fields are Option<f64> — a venue that does not publish a field yields None, not 0.0.

Fetching an order book

let book = exchange.fetch_order_book("BTC/USDT", Some(5), Params::none()).await?;
if let Some(bid) = book.bids.first() {
    println!("top bid price={} amount={}", bid[0], bid[1]);
}
if let Some(ask) = book.asks.first() {
    println!("top ask price={} amount={}", ask[0], ask[1]);
}

bids / asks are Vec<[f64; 2]>[price, amount], best first. Pass None for full depth.

Fetching OHLCV (candlesticks)

use ccxt::types::OHLCV;   // = [f64; 6]

let candles: Vec<OHLCV> = exchange
    .fetch_ohlcv("BTC/USDT", Some("1h"), None, Some(100), Params::none())
    .await?;

for c in &candles {
    println!("ts={} o={} h={} l={} c={} v={}", c[0], c[1], c[2], c[3], c[4], c[5]);
}

Fetching trades

// Recent public trades
let trades = exchange.fetch_trades("BTC/USDT", None, Some(50), Params::none()).await?;

// Your trades (requires authentication)
let my_trades = exchange.fetch_my_trades(Some("BTC/USDT"), None, Some(50), Params::none()).await?;

Fetching balance

let balance = exchange.fetch_balance(()).await?;
println!("{:?}", balance.free.get("USDT"));    // available
println!("{:?}", balance.used.get("USDT"));    // held in orders
println!("{:?}", balance.total.get("USDT"));   // free + used

free / used / total are HashMap<String, f64> keyed by currency code; balance.info holds the raw venue payload.

Creating orders

// Generic
let order = exchange
    .create_order("BTC/USDT", "limit", "buy", 0.001, Some(50_000.0), Params::none())
    .await?;
println!("{:?} {:?} {:?}", order.id, order.status, order.filled);

// Market orders take `None` for price
let order = exchange
    .create_order("BTC/USDT", "market", "sell", 0.001, None, Params::none())
    .await?;

// Convenience constructors
let o = exchange.create_limit_buy_order("BTC/USDT", 0.001, 50_000.0, Params::none()).await?;
let o = exchange.create_limit_sell_order("BTC/USDT", 0.001, 60_000.0, Params::none()).await?;
let o = exchange.create_market_buy_order("BTC/USDT", 0.001, Params::none()).await?;
let o = exchange.create_market_sell_order("BTC/USDT", 0.001, Params::none()).await?;
let o = exchange.create_market_buy_order_with_cost("BTC/USDT", 100.0, Params::none()).await?;

// Trigger / conditional
//                                       symbol      side    amount  price     trigger
let o = exchange.create_stop_limit_order("BTC/USDT", "sell", 0.001, 47_900.0, 48_000.0, Params::none()).await?;
//                                        symbol      side    amount  trigger
let o = exchange.create_stop_market_order("BTC/USDT", "sell", 0.001, 48_000.0, Params::none()).await?;
//                                    symbol      type     side    amount  price           trigger
let o = exchange.create_trigger_order("BTC/USDT", "limit", "sell", 0.001, Some(47_900.0), Some(48_000.0), Params::none()).await?;

Managing orders

let open = exchange.fetch_open_orders(Some("BTC/USDT"), None, None, Params::none()).await?;
let closed = exchange.fetch_closed_orders(Some("BTC/USDT"), None, None, Params::none()).await?;
let all = exchange.fetch_orders(Some("BTC/USDT"), None, None, Params::none()).await?;
let one = exchange.fetch_order("12345", Some("BTC/USDT"), Params::none()).await?;

let edited = exchange
    .edit_order("12345", "BTC/USDT", "limit", "buy", Some(0.002), Some(49_000.0), Params::none())
    .await?;

let canceled = exchange.cancel_order("12345", Some("BTC/USDT"), Params::none()).await?;
let batch = exchange.cancel_orders(vec!["1".into(), "2".into()], Some("BTC/USDT"), Params::none()).await?;
let everything = exchange.cancel_all_orders(Some("BTC/USDT"), Params::none()).await?;

Order fields: id, client_order_id, symbol, timestamp, datetime, status ("open" | "closed" | "canceled" | "expired"), order_type, side, price, amount, filled, remaining, cost, fee, raw. Note order_typetype is a Rust keyword.

Positions (derivatives)

let positions = exchange.fetch_positions(None, Params::none()).await?;
for p in &positions {
    println!("{} {:?} contracts={:?} entry={:?} upnl={:?}",
        p.symbol, p.side, p.contracts, p.entry_price, p.unrealized_pnl);
}

let one = exchange.fetch_position("BTC/USDT:USDT", Params::none()).await?;
let closed = exchange.close_position("BTC/USDT:USDT", Some("long"), Params::none()).await?;

WebSocket Operations (Real-time)

All examples use ccxt_pro. Load markets once before watching.

Watching a ticker

use ccxt::Params;
use ccxt_pro::Binance;

let mut exchange = Binance::new(None);
exchange.try_load_markets(false).await?;

loop {
    match exchange.watch_ticker("BTC/USDT", Params::none()).await {
        Ok(t) => println!("{:?} {:?}", t.last, t.timestamp),
        Err(e) => { eprintln!("[{}] {}", e.kind, e.message); break; }
    }
}

Watching an order book

loop {
    let book = exchange.watch_order_book("BTC/USDT", Some(20), Params::none()).await?;
    println!("{:?} {:?}", book.bids.first(), book.asks.first());
}

limit is best-effort — some venues only publish a full book, so expect more levels than requested.

Watching trades

loop {
    let trades = exchange.watch_trades("BTC/USDT", None, Some(50), Params::none()).await?;
    for t in &trades {
        println!("{:?} {:?} {:?} {:?}", t.datetime, t.side, t.price, t.amount);
    }
}

One update carries the batch of trades the venue published, not a single trade.

Watching OHLCV

loop {
    let candles = exchange.watch_ohlcv("BTC/USDT", Some("1m"), None, None, Params::none()).await?;
    if let Some(c) = candles.last() {
        println!("close={} volume={}", c[4], c[5]);
    }
}

Watching multiple symbols on one connection

let symbols = vec!["BTC/USDT".to_string(), "ETH/USDT".to_string()];

let trades = exchange.watch_trades_for_symbols(symbols.clone(), None, None, Params::none()).await?;
let book = exchange.watch_order_book_for_symbols(symbols, Some(10), Params::none()).await?;
let tickers = exchange.watch_tickers(Some(vec!["BTC/USDT".into()]), Params::none()).await?;

These multiplex over a single WebSocket connection instead of opening one per symbol.

Watching your orders / trades / balance / positions (auth required)

use ccxt::{Config, Params};
use ccxt_pro::Binance;

let mut exchange = Binance::with_config(Config::new().api_key("KEY").secret("SECRET"));
exchange.try_load_markets(false).await?;

loop {
    let orders = exchange.watch_orders(Some("BTC/USDT"), None, None, Params::none()).await?;
    for o in &orders {
        println!("{:?} {:?} {:?}", o.id, o.status, o.filled);
    }
}
let my_trades = exchange.watch_my_trades(Some("BTC/USDT"), None, None, Params::none()).await?;
let balance = exchange.watch_balance(Params::none()).await?;
let positions = exchange.watch_positions(None, None, None, Params::none()).await?;

watch_orders and watch_my_trades share one user-data stream and one authentication, so subscribing to both costs a single connection.

Concurrent subscriptions

A watch_* call needs &mut self, so two streams from the same instance cannot be awaited concurrently. Either interleave them in one loop, or give each stream its own instance:

let mut a = ccxt_pro::Binance::new(None);
let mut b = ccxt_pro::Binance::new(None);
a.try_load_markets(false).await?;
b.try_load_markets(false).await?;

let (btc, eth) = tokio::join!(
    a.watch_ticker("BTC/USDT", Params::none()),
    b.watch_ticker("ETH/USDT", Params::none()),
);

For many symbols on one venue, prefer the *_for_symbols variants above — one connection, one task.

Closing connections

There is no typed close() and no typed un_watch_* yet. WebSocket clients live in a global registry keyed by URL, so dropping the exchange value does not by itself disconnect. To force a disconnect, drop the client for that URL:

ccxt::pro::ws_client::drop_client("wss://stream.binance.com:9443/ws");

For a process that streams until exit, doing nothing is fine.

Complete Method Reference

Rust method names are the snake_case form of the unified CCXT names — fetchOHLCV is fetch_ohlcv, createOrder is create_order, watchOrderBook is watch_order_book.

124 unified REST methods are available as typed wrappers (ccxt), and 25 watch_* methods on top of those in ccxt-pro. Anything outside that list is still reachable untyped — see Untyped escape hatch.

Market data

  • fetch_markets(params) — all markets
  • fetch_currencies(params) — all currencies
  • fetch_ticker(symbol, params) / fetch_tickers(symbols, params)
  • fetch_spot_tickers(symbols, params) / fetch_contract_tickers(symbols, params)
  • fetch_bids_asks(symbols, params) — best bid/ask for many symbols
  • fetch_mark_price(symbol, params) / fetch_mark_prices(symbols, params)
  • fetch_order_book(symbol, limit, params) / fetch_order_books(symbols, limit, params)
  • fetch_l3_order_book(symbol, limit, params)
  • fetch_trades(symbol, since, limit, params)
  • fetch_ohlcv(symbol, timeframe, since, limit, params)
  • fetch_spot_ohlcv / fetch_contract_ohlcv / fetch_index_ohlcv / fetch_mark_ohlcv / fetch_premium_index_ohlcv
  • fetch_time(params) — server time
  • fetch_status(params) — exchange status

Account & balance

  • fetch_balance(params) 🔒
  • fetch_free_balance / fetch_used_balance / fetch_total_balance / fetch_partial_balance 🔒
  • fetch_ledger(code, since, limit, params) / fetch_ledger_entry(id, code, params) 🔒
  • fetch_transactions / fetch_deposits / fetch_withdrawals / fetch_deposits_withdrawals 🔒
  • fetch_borrow_interest(code, symbol, since, limit, params) 🔒
  • fetch_cross_borrow_rate(code, params) / fetch_isolated_borrow_rate(symbol, params)

Trading

Creating:

  • create_order(symbol, type, side, amount, price, params) 🔒
  • create_limit_order / create_market_order 🔒
  • create_limit_buy_order / create_limit_sell_order 🔒
  • create_market_buy_order / create_market_sell_order 🔒
  • create_market_order_with_cost / create_market_buy_order_with_cost / create_market_sell_order_with_cost 🔒
  • create_stop_order / create_stop_limit_order / create_stop_market_order 🔒
  • create_stop_loss_order / create_take_profit_order / create_trigger_order 🔒
  • create_trailing_amount_order / create_trailing_percent_order 🔒
  • create_post_only_order / create_reduce_only_order / create_twap_order 🔒
  • create_order_with_take_profit_and_stop_loss(...) 🔒
  • create_orders(orders, params) — batch 🔒

Managing:

  • fetch_order(id, symbol, params) / fetch_order_with_client_order_id 🔒
  • fetch_orders / fetch_open_orders / fetch_closed_orders / fetch_canceled_orders / fetch_canceled_and_closed_orders 🔒
  • fetch_order_status(id, symbol, params) / fetch_order_trades(id, symbol, since, limit, params) 🔒
  • fetch_my_trades(symbol, since, limit, params) 🔒
  • edit_order(id, symbol, type, side, amount, price, params) / edit_orders / edit_order_with_client_order_id / edit_limit_order / edit_limit_buy_order / edit_limit_sell_order 🔒
  • cancel_order(id, symbol, params) / cancel_orders(ids, symbol, params) / cancel_all_orders(symbol, params) / cancel_orders_for_symbols(orders, params) / cancel_order_with_client_order_id / cancel_orders_with_client_order_ids 🔒

Derivatives & futures

  • fetch_position(symbol, params) / fetch_positions(symbols, params) / fetch_positions_for_symbol / fetch_positions_risk 🔒
  • fetch_position_history / fetch_positions_history 🔒
  • close_position(symbol, side, params) / close_all_positions(params) 🔒
  • fetch_leverage(symbol, params) / fetch_leverages(symbols, params) / fetch_market_leverage_tiers(symbol, params)
  • fetch_margin_mode(symbol, params) / fetch_margin_modes(symbols, params)
  • fetch_funding_rate(symbol, params) / fetch_funding_rates(symbols, params) / fetch_funding_interval / fetch_funding_intervals
  • fetch_open_interest(symbol, params) / fetch_open_interests / fetch_open_interest_history
  • fetch_liquidations(symbol, since, limit, params) / fetch_my_liquidations 🔒
  • fetch_greeks(symbol, params) / fetch_all_greeks(symbols, params) — options

Fees, deposits, withdrawals, transfers

  • fetch_trading_fee(symbol, params) / fetch_trading_fees(params) 🔒
  • fetch_deposit_address(code, params) / fetch_deposit_addresses(codes, params) / fetch_deposit_addresses_by_network(code, params) / create_deposit_address(code, params) 🔒
  • withdraw(code, amount, address, tag, params) 🔒
  • transfer(code, amount, from_account, to_account, params) / fetch_transfer / fetch_transfers 🔒
  • fetch_convert_currencies(params)

WebSocket (ccxt-pro)

Public:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
44k
Forks
9k
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
ccxt-rust
Source
github.com/ccxt/ccxt