CCXT for Rust
SkillSecurityCCXT 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.
No other account needed.
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. ccxtalone is enough for REST. Addccxt-proonly when you needwatch_*; 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
| Crate | Contains | Use when |
|---|---|---|
ccxt | Typed REST wrappers (ccxt::Binance, …), Params, Config, types::*, TypedExchange. Re-exports the whole engine at its root. | Always |
ccxt-pro | Typed WebSocket wrappers (ccxt_pro::Binance, …) with watch_* | Streaming |
ccxt-prediction | Typed prediction-market wrappers (ccxt_prediction::Kalshi, …) | Prediction markets |
ccxt-base | The 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
| Feature | REST API | WebSocket API |
|---|---|---|
| Use for | One-time queries, placing orders | Real-time monitoring, live price feeds |
| Crate | ccxt | ccxt-pro |
| Import | use ccxt::Binance; | use ccxt_pro::Binance; |
| Methods | fetch_* (fetch_ticker, fetch_order_book) | watch_* (watch_ticker, watch_order_book) |
| Speed | Slower (HTTP request/response) | Faster (persistent connection) |
| Rate limits | Strict (1–2 req/sec) | More lenient (continuous stream) |
| Best for | Trading, account management | Price 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_type — type 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 marketsfetch_currencies(params)— all currenciesfetch_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 symbolsfetch_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_ohlcvfetch_time(params)— server timefetch_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_intervalsfetch_open_interest(symbol, params)/fetch_open_interests/fetch_open_interest_historyfetch_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