CCXT
Rust Examples

Multi Exchange Ohlcv Chart

Multi Exchange Ohlcv Chart — CCXT Rust code example.

use ccxt::types::OHLCV;
use ccxt::{from_id, Params, TypedExchange, TypedExchangeExt};


// to run it cargo run --manifest-path examples/rust/Cargo.toml --bin multi_exchange_ohlcv_chart

const TIMEFRAME: &str = "1h";
const LIMIT: i64 = 72;
const HEIGHT: usize = 18;
const MARKERS: [char; 4] = ['*', 'o', '+', 'x'];
const OVERLAP: char = '#';

struct Series {
    id: String,
    symbol: String,
    marker: char,
    closes: Vec<f64>,
}

async fn fetch_series(id: &str, symbol: &str, marker: char) -> Option<Series> {
    let mut exchange: Box<dyn TypedExchange> = match from_id(id, None) {
        Some(e) => e,
        None => {
            println!("{id:<18} unknown exchange id");
            return None;
        }
    };
    let started = std::time::Instant::now();
    let candles: Vec<OHLCV> = match exchange
        .fetch_ohlcv(symbol, Some(TIMEFRAME), None, Some(LIMIT), Params::none())
        .await
    {
        Ok(c) => c,
        Err(e) => {
            println!("{id:<18} [{}] {}", e.kind, e.message);
            return None;
        }
    };
    let closes: Vec<f64> = candles.iter().map(|candle| candle[4]).collect();
    if closes.is_empty() {
        println!("{id:<18} no candles returned");
        return None;
    }
    println!("{id:<18} {symbol:<10} {} candles in {} ms", closes.len(), started.elapsed().as_millis());
    Some(Series {
        id: id.to_string(),
        symbol: symbol.to_string(),
        marker,
        closes,
    })
}

fn tail(series: &Series, width: usize) -> &[f64] {
    &series.closes[series.closes.len() - width..]
}

fn plot(series: &[Series], width: usize) {
    let mut low = f64::MAX;
    let mut high = f64::MIN;
    for s in series {
        for close in tail(s, width) {
            low = low.min(*close);
            high = high.max(*close);
        }
    }
    if !(high > low) {
        println!("flat range, nothing to plot");
        return;
    }
    let mut grid = vec![vec![' '; width]; HEIGHT];
    for s in series {
        for (x, close) in tail(s, width).iter().enumerate() {
            let normalized = (close - low) / (high - low);
            let y = ((1.0 - normalized) * (HEIGHT as f64 - 1.0)).round() as usize;
            let y = y.min(HEIGHT - 1);
            grid[y][x] = match grid[y][x] {
                ' ' => s.marker,
                existing if existing == s.marker => existing,
                _ => OVERLAP,
            };
        }
    }
    for (row_index, row) in grid.iter().enumerate() {
        let price = high - (high - low) * (row_index as f64) / (HEIGHT as f64 - 1.0);
        println!("{price:>12.2} | {}", row.iter().collect::<String>());
    }
    println!("{:>12} +{}", "", "-".repeat(width));
    println!("{:>12}   oldest {} x {} newest", "", width, TIMEFRAME);
}

fn legend(series: &[Series], width: usize) {
    println!();
    println!(
        "{:<3} {:<18} {:<10} {:>12} {:>12} {:>9}",
        "", "exchange", "symbol", "first", "last", "change"
    );
    for s in series {
        let window = tail(s, width);
        let first = window[0];
        let last = window[window.len() - 1];
        let change = if first > 0.0 {
            (last - first) / first * 100.0
        } else {
            0.0
        };
        println!(
            "{:<3} {:<18} {:<10} {:>12.2} {:>12.2} {:>8.2}%",
            s.marker, s.id, s.symbol, first, last, change
        );
    }
    println!("{OVERLAP:<3} two or more exchanges on the same point");
}

async fn run() {
    let targets = [
        ("binance", "BTC/USDT"),
        ("okx", "BTC/USDT"),
        ("kraken", "BTC/USD"),
        ("bitstamp", "BTC/USD"),
    ];
    println!("fetching {} candles of {TIMEFRAME}\n", LIMIT);
    let started = std::time::Instant::now();
    let pending = targets
        .iter()
        .enumerate()
        .map(|(index, (id, symbol))| fetch_series(id, symbol, MARKERS[index % MARKERS.len()]));
    let series: Vec<Series> = futures::future::join_all(pending)
        .await
        .into_iter()
        .flatten()
        .collect();
    println!("\n{} of {} exchanges in {} ms", series.len(), targets.len(), started.elapsed().as_millis());
    if series.is_empty() {
        println!("\nno exchange returned candles");
        return;
    }
    let width = series
        .iter()
        .map(|s| s.closes.len())
        .min()
        .unwrap_or(0)
        .min(LIMIT as usize);
    if width == 0 {
        println!("\nno overlapping candles");
        return;
    }
    println!();
    plot(&series, width);
    legend(&series, width);
}

fn main() {
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .thread_stack_size(64 * 1024 * 1024)
        .enable_all()
        .build()
        .unwrap()
        .block_on(run());
}