---
name: ccxt-rust
description: 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>.
---

# CCXT for Rust

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

```bash
cargo add ccxt tokio --features tokio/full
```

### WebSocket API (ccxt.pro)

```bash
cargo add ccxt-pro
```

### Prediction markets

```bash
cargo add ccxt-prediction
```

### Cargo.toml

```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

```rust
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

```rust
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:

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

## Runtime setup

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

```rust
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)

```rust
use ccxt::Binance;

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

### Private (with credentials), via the `Config` builder

```rust
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:

```rust
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

```rust
// 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`.

```rust
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:

```rust
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:

```rust
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

```rust
// 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:

```rust
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

```rust
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

```rust
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)

```rust
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

```rust
// 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

```rust
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

```rust
// 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

```rust
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)

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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)

```rust
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);
    }
}
```

```rust
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:

```rust
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:

```rust
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
