CCXT
Rust Examples

Multi Sub

Multi Sub — CCXT Rust code example.

// Multiple simultaneous WebSocket subscriptions.
//
// Part A — ONE exchange instance, TWO channels multiplexed over a single WS
//   connection. The client's socket reader fills every subscribed channel's
//   cache in the background, so alternating `watch_order_book` + `watch_trades`
//   in one loop keeps both live at once (the typed methods take `&mut self`, so
//   a single instance drives them in sequence, not via two parallel borrows).
//   Proves channel multiplexing on one connection.
//
// Part B — TWO independent instances (own connections) in concurrent
//   `tokio::spawn` tasks, one watching the book and one watching trades. Proves
//   parallel subscriptions coexist and the global per-book / per-side stores
//   (keyed by unique ids) don't collide across concurrent books.
//
//   cargo run --features ws --bin multi_sub -- [secs]   (default 20)
use ccxt::Params;
use ccxt_pro::{from_id, TypedExchange, TypedExchangeExt};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

const EXCHANGE: &str = "binance";
const SYMBOL: &str = "BTC/USDT";

fn build() -> Box<dyn TypedExchange> {
    from_id(EXCHANGE, None).expect("no pro wrapper for binance")
}

// ── Part A: one instance, two channels on a single connection ────────────────
async fn part_a(secs: u64) {
    println!("── Part A: one {EXCHANGE} instance, watchOrderBook + watchTrades on ONE connection ──");
    let mut ex = build();
    ex.load_markets(false).await;

    let deadline = Instant::now() + Duration::from_secs(secs);
    let (mut ob_updates, mut trade_events, mut trade_count) = (0u64, 0u64, 0u64);
    let (mut best_bid, mut best_ask, mut last_px) = (0.0f64, 0.0f64, 0.0f64);

    while Instant::now() < deadline {
        // Each await returns the next update for THAT channel; the other
        // channel keeps buffering in the background meanwhile.
        if let Ok(ob) = ex.watch_order_book(SYMBOL, Some(10), Params::none()).await {
            ob_updates += 1;
            if let Some(b) = ob.bids.first() { best_bid = b[0]; }
            if let Some(a) = ob.asks.first() { best_ask = a[0]; }
        }
        if let Ok(trades) = ex.watch_trades(SYMBOL, None, Some(50), Params::none()).await {
            if !trades.is_empty() {
                trade_events += 1;
                trade_count += trades.len() as u64;
                if let Some(p) = trades.last().and_then(|t| t.price) { last_px = p; }
            }
        }
    }

    println!(
        "Part A done: orderBook updates={ob_updates}  trade events={trade_events} (\
         {trade_count} trades)  | bestBid={best_bid} bestAsk={best_ask} lastTrade={last_px}"
    );
    let ok = ob_updates > 0 && trade_count > 0;
    println!("Part A: both channels delivered on one connection? {}\n", if ok { "YES ✓" } else { "NO ✗" });
}

// Drive a single channel to completion, counting updates, for `secs`. Each
// watch await is bounded by `timeout` so a quiet channel can't stall past the
// deadline (the loop only re-checks the clock between awaits).
async fn count_book(secs: u64, counter: Arc<AtomicU64>) {
    let mut ex = build();
    ex.load_markets(false).await;
    eprintln!("  [book task] markets loaded, subscribing…");
    let deadline = Instant::now() + Duration::from_secs(secs);
    let mut first = true;
    while Instant::now() < deadline {
        let remaining = deadline.saturating_duration_since(Instant::now());
        match tokio::time::timeout(remaining, ex.watch_order_book(SYMBOL, Some(10), Params::none())).await {
            Ok(Ok(ob)) => {
                if first { eprintln!("  [book task] first update: bids={} asks={}", ob.bids.len(), ob.asks.len()); first = false; }
                counter.fetch_add(1, Ordering::Relaxed);
            }
            // A binance spot sequence gap surfaces as a ChecksumError telling you
            // to reconnect — the next `watch_order_book` call re-seeds. Keep going.
            Ok(Err(_)) => continue,
            Err(_) => break, // deadline elapsed
        }
    }
}

async fn count_trades(secs: u64, counter: Arc<AtomicU64>) {
    let mut ex = build();
    ex.load_markets(false).await;
    eprintln!("  [trades task] markets loaded, subscribing…");
    let deadline = Instant::now() + Duration::from_secs(secs);
    let mut first = true;
    while Instant::now() < deadline {
        let remaining = deadline.saturating_duration_since(Instant::now());
        match tokio::time::timeout(remaining, ex.watch_trades(SYMBOL, None, Some(50), Params::none())).await {
            Ok(Ok(trades)) => {
                if first { eprintln!("  [trades task] first update: {} trades", trades.len()); first = false; }
                counter.fetch_add(trades.len() as u64, Ordering::Relaxed);
            }
            Ok(Err(e)) => { eprintln!("  [trades task] error: {e}"); break; }
            Err(_) => break, // deadline elapsed
        }
    }
}

// ── Part B: two independent instances, truly concurrent ──────────────────────
async fn part_b(secs: u64) {
    println!("── Part B: TWO {EXCHANGE} instances watching book & trades concurrently ──");
    let ob_count = Arc::new(AtomicU64::new(0));
    let tr_count = Arc::new(AtomicU64::new(0));

    let h1 = tokio::spawn(count_book(secs, ob_count.clone()));
    let h2 = tokio::spawn(count_trades(secs, tr_count.clone()));
    let _ = tokio::join!(h1, h2);

    let (ob, tr) = (ob_count.load(Ordering::Relaxed), tr_count.load(Ordering::Relaxed));
    println!("Part B done: concurrent orderBook updates={ob}  trades={tr}");
    let ok = ob > 0 && tr > 0;
    println!("Part B: both concurrent subscriptions delivered? {}\n", if ok { "YES ✓" } else { "NO ✗" });
}

async fn run() {
    let secs: u64 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(20);
    let part = std::env::args().nth(2).unwrap_or_else(|| "both".to_string());
    println!("multi_sub: {EXCHANGE} {SYMBOL} — {secs}s per part (part={part})\n");
    if part == "a" || part == "both" { part_a(secs).await; }
    if part == "b" || part == "both" { part_b(secs).await; }
    println!("multi_sub: done.");
}

fn main() {
    // The pro runtime signals errors by panicking across an internal
    // catch_unwind boundary (surfaced back as `Result`). Silence the default
    // hook so those caught panics don't print — same as the `ti-rust` harness.
    std::panic::set_hook(Box::new(|_| {}));
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(4)
        .thread_stack_size(64 * 1024 * 1024)
        .enable_all()
        .build()
        .unwrap();
    rt.block_on(run());
}