CCXT
Rust Examples

Binance Active Swap Prices

Binance Active Swap Prices — CCXT Rust code example.

// binance-active-swap-prices — swap markets → active ones → first 10 → prices,
// entirely on the typed wrapper.
//
// The point of the typed layer is that none of this needs the dynamic `Value`
// enum: `markets()` hands back `Vec<Market>` with real Rust fields (`swap`,
// `active`, `linear`, `limits`, `precision`), and `fetch_tickers` hands back
// `Tickers` — a `HashMap<String, Ticker>` of decoded prices.
//
//   cargo run --release --bin binance-active-swap-prices
//   cargo run --release --bin binance-active-swap-prices -- 25    # first 25
use ccxt::types::{Market, Ticker};
use ccxt::{Binance, Config, Params};

fn f(v: Option<f64>) -> String {
    v.map(|x| format!("{x}")).unwrap_or_else(|| "—".to_string())
}

/// `linear`/`inverse` as a label, straight off the typed market.
fn contract_kind(m: &Market) -> &'static str {
    match (m.linear, m.inverse) {
        (Some(true), _) => "linear",
        (_, Some(true)) => "inverse",
        _ => "?",
    }
}

async fn run() {
    let take: usize = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(10);

    // 1. Build the client. No `Value` anywhere — `Config` takes primitives and
    //    nests `options` the same way every other ccxt binding does.
    let mut binance = Binance::with_config(
        Config::new().option("fetchMarkets", Params::new().with_strs("types", &["linear", "inverse"])),
    );

    // 2. Load the market table once.
    binance.load_markets(false).await;
    let all: Vec<Market> = binance.markets();
    println!("loaded {} markets", all.len());

    // 3. Swap markets only — `Market::swap` is a plain `bool`.
    let swaps: Vec<Market> = all.into_iter().filter(|m| m.swap).collect();
    println!("  of which swap:   {}", swaps.len());

    // 4. Active only — the `active` flag on the market, no request needed.
    let (active, inactive): (Vec<Market>, Vec<Market>) =
        swaps.into_iter().partition(|m| m.active);
    println!("  of which active: {} (skipping {} inactive)", active.len(), inactive.len());
    if let Some(m) = inactive.first() {
        println!("  e.g. inactive:   {} ({})", m.symbol, contract_kind(m));
    }

    // 5. First N. Sort by symbol so the selection is reproducible — the order
    //    the venue lists markets in is not stable.
    let mut chosen = active;
    chosen.sort_by(|a, b| a.symbol.cmp(&b.symbol));
    chosen.truncate(take);
    if chosen.is_empty() {
        println!("\nno active swap markets found");
        return;
    }
    println!("\ntaking the first {} by symbol:", chosen.len());

    // 6. Prices for exactly those symbols — one batched call rather than N.
    let symbols: Vec<String> = chosen.iter().map(|m| m.symbol.clone()).collect();
    let tickers = match binance.fetch_tickers(Some(symbols.clone()), Params::none()).await {
        Ok(t) => t,
        Err(e) => {
            // Unified error hierarchy: retry the OperationFailed subtree only.
            println!("fetch_tickers failed [{}]: {}", e.kind, e.message);
            println!("retryable: {}", e.is("OperationFailed"));
            return;
        }
    };

    // binance's futures 24h-ticker endpoint carries last/high/low/volume but
    // NOT bid/ask, so those come back as `None`. The top of book is a separate
    // endpoint — `fetch_bids_asks` — also batched. Merge the two so the table
    // below has a full quote.
    let books = binance.fetch_bids_asks(Some(symbols), Params::none()).await.unwrap_or_default();

    println!(
        "\n{:<24} {:>6} {:>14} {:>14} {:>14} {:>16}",
        "symbol", "kind", "bid", "ask", "last", "24h base vol"
    );
    println!("{}", "-".repeat(94));
    for m in &chosen {
        match tickers.get(&m.symbol) {
            Some(t) => {
                let book = books.get(&m.symbol);
                let bid = t.bid.or_else(|| book.and_then(|b| b.bid));
                let ask = t.ask.or_else(|| book.and_then(|b| b.ask));
                println!(
                    "{:<24} {:>6} {:>14} {:>14} {:>14} {:>16}",
                    m.symbol, contract_kind(m), f(bid), f(ask), f(t.last), f(t.base_volume)
                );
            }
            // A listed, active market can still have no ticker (never traded).
            None => println!("{:<24} {:>6} {:>14}", m.symbol, contract_kind(m), "no ticker"),
        }
    }

    // 7. The typed market carries its trading rules too — useful before sizing
    //    an order, and free (already loaded, no extra request).
    if let Some(m) = chosen.first() {
        println!(
            "\n{} — settle {} · min amount {} · min cost {} · amount step {} · price step {}",
            m.symbol,
            m.settle.clone().unwrap_or_else(|| "—".to_string()),
            f(m.limits.amount.min),
            f(m.limits.cost.min),
            f(m.precision.amount),
            f(m.precision.price),
        );
        let cheapest: Option<&Ticker> = tickers.get(&m.symbol);
        if let (Some(min), Some(px)) = (m.limits.amount.min, cheapest.and_then(|t| t.last)) {
            println!("  smallest order there ≈ {:.2} {}", min * px, m.quote);
        }
    }
}

fn main() {
    // The transpiled core signals errors by panicking across an internal
    // catch_unwind; the typed layer turns that back into `Result`. Silence the
    // default hook so caught errors do not print twice.
    if std::env::var("CCXT_SHOW_PANICS").is_err() {
        std::panic::set_hook(Box::new(|_| {}));
    }
    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());
}