Rust Examples
Orderbook Diff
Orderbook Diff — CCXT Rust code example.
// CCXT Rust — concurrent order-book watcher example.
//
// Spawns two fetcher tasks that poll Binance order books IN PARALLEL —
// spot (BTC/USDT) and the USDT-margined perpetual (BTC/USDT:USDT) — every
// 15 seconds and publish the top-of-book into a shared map, while a third
// task reads that map on the same cadence and prints the spot/perp price
// difference (the perp basis).
//
// Usage:
// cargo run --bin orderbook_diff # every 15 s, until Ctrl+C
// cargo run --bin orderbook_diff -- 4 # 4 rounds, then exit
// cargo run --bin orderbook_diff -- 4 5 # 4 rounds, 5 s cadence
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use ccxt::Params;
use ccxt::Binance;
use tokio::sync::RwLock;
use tokio::time::MissedTickBehavior;
const SPOT: &str = "BTC/USDT";
const SWAP: &str = "BTC/USDT:USDT";
#[derive(Clone, Debug)]
struct TopOfBook {
bid: f64,
ask: f64,
exchange_ts: Option<i64>,
updates: u64,
}
impl TopOfBook {
fn mid(&self) -> f64 {
(self.bid + self.ask) / 2.0
}
}
/// Symbol → latest top-of-book, shared between the fetchers and the printer.
type SharedBooks = Arc<RwLock<HashMap<String, TopOfBook>>>;
fn wall_ms() -> u128 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis()
}
async fn run_fetcher(symbol: &'static str, books: SharedBooks, every: Duration, rounds: Option<u64>) {
let mut exchange = Binance::new(None);
let t0 = Instant::now();
exchange.load_markets(false).await;
println!("[{symbol}] markets loaded in {} ms", t0.elapsed().as_millis());
let mut tick = tokio::time::interval(every);
tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
let mut attempts: u64 = 0;
let mut updates: u64 = 0;
while rounds.map_or(true, |max| attempts < max) {
tick.tick().await;
attempts += 1;
let started = Instant::now();
let started_wall = wall_ms();
match exchange.fetch_order_book(symbol, Some(5), Params::none()).await {
Ok(ob) => {
if let (Some(bid), Some(ask)) = (ob.bids.first(), ob.asks.first()) {
updates += 1;
println!(
"[{symbol}] #{updates} bid={} ask={} (started @+{} ms, took {} ms)",
bid[0],
ask[0],
started_wall % 60_000,
started.elapsed().as_millis(),
);
books.write().await.insert(
symbol.to_string(),
TopOfBook { bid: bid[0], ask: ask[0], exchange_ts: ob.timestamp, updates },
);
} else {
eprintln!("[{symbol}] empty order book");
}
}
Err(e) => eprintln!("[{symbol}] fetch_order_book failed: [{}] {}", e.kind, e.message),
}
}
println!("[{symbol}] fetcher done: {updates}/{attempts} rounds succeeded");
}
async fn run_printer(books: SharedBooks, every: Duration) {
let mut tick = tokio::time::interval(every);
tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
tick.tick().await;
let map = books.read().await;
match (map.get(SPOT), map.get(SWAP)) {
(Some(spot), Some(swap)) => {
let diff = swap.mid() - spot.mid();
let bps = diff / spot.mid() * 10_000.0;
println!(
"== basis: spot mid={:.2} perp mid={:.2} diff={:+.2} USDT ({:+.2} bps) [spot upd #{} ts={:?} / perp upd #{} ts={:?}]",
spot.mid(),
swap.mid(),
diff,
bps,
spot.updates,
spot.exchange_ts,
swap.updates,
swap.exchange_ts,
);
}
_ => println!("== basis: waiting for both books ({} of 2 in the map)", map.len()),
}
}
}
#[tokio::main]
async fn main() {
let mut args = std::env::args().skip(1);
let rounds: Option<u64> = args.next().and_then(|s| s.parse().ok());
let every = Duration::from_secs(args.next().and_then(|s| s.parse().ok()).unwrap_or(15));
println!("=== CCXT Rust — concurrent {SPOT} vs {SWAP} order-book watcher ===");
match rounds {
Some(n) => println!("cadence: {} s, {} rounds\n", every.as_secs(), n),
None => println!("cadence: {} s, until Ctrl+C\n", every.as_secs()),
}
let books: SharedBooks = Arc::new(RwLock::new(HashMap::new()));
// The exchange futures carry a `Send` guarantee (the trait surface is
// emitted as `impl Future + Send` / `Pin<Box<dyn Future + Send>>`), so the
// fetchers go through plain `tokio::spawn` and run on the multi-threaded
// runtime — genuinely parallel tasks, one exchange instance per task.
let spot = tokio::spawn(run_fetcher(SPOT, books.clone(), every, rounds));
let swap = tokio::spawn(run_fetcher(SWAP, books.clone(), every, rounds));
let printer = tokio::spawn(run_printer(books.clone(), every));
let _ = spot.await;
let _ = swap.await;
// Bounded run: the fetchers finished their rounds — print one final
// reading (both books are guaranteed in the map by now), then stop.
printer.abort();
let map = books.read().await;
if let (Some(spot), Some(swap)) = (map.get(SPOT), map.get(SWAP)) {
let diff = swap.mid() - spot.mid();
println!(
"\nfinal: spot mid={:.2} perp mid={:.2} diff={:+.2} USDT",
spot.mid(),
swap.mid(),
diff,
);
}
}