kucoin 0.7.4

A robust and asynchronous Rust client for the KuCoin exchange API.
Documentation
#[derive(Debug)]
pub struct Candle {
    pub time: i64,
    pub open: f64,
    pub close: f64,
    pub high: f64,
    pub low: f64,
    pub volume: f64,
    pub turnover: f64,
}

use chrono::Utc;
use reqwest::Client;

pub async fn get_candles(
    client: &Client,
    symbol: &str,
    interval: &str,
    count: usize,
) -> Result<Vec<Candle>, Box<dyn std::error::Error>> {
    // Estimate duration per candle
    let seconds_per_candle = match interval {
        "1min" => 60,
        "5min" => 300,
        "15min" => 900,
        "1hour" => 3600,
        "4hour" => 14400,
        "1day" => 86400,
        "1week" => 604800,
        _ => return Err("Unsupported interval".into()),
    };

    // End at last fully closed candle
    let end_at = Utc::now().timestamp() - seconds_per_candle;
    let start_at = end_at - (seconds_per_candle * count as i64);

    let url = "https://api.kucoin.com/api/v1/market/candles";
    let res = client
        .get(url)
        .query(&[
            ("symbol", symbol),
            ("type", interval),
            ("startAt", &start_at.to_string()),
        ])
        .send()
        .await?
        .json::<serde_json::Value>()
        .await?;

    let data = res["data"]
        .as_array()
        .ok_or(format!("Invalid KuCoin response for symbol {}", symbol))?;

    let mut candles = Vec::with_capacity(count);

    // KuCoin returns newest → oldest
    for entry in data.iter().take(count) {
        let c = Candle {
            time: entry[0].as_str().unwrap().parse()?,
            open: entry[1].as_str().unwrap().parse()?,
            close: entry[2].as_str().unwrap().parse()?,
            high: entry[3].as_str().unwrap().parse()?,
            low: entry[4].as_str().unwrap().parse()?,
            volume: entry[5].as_str().unwrap().parse()?,
            turnover: entry[6].as_str().unwrap().parse()?,
        };
        candles.push(c);
    }

    Ok(candles)
}

#[cfg(test)]
mod tests {
    use super::*;
    use reqwest::Client;

    #[tokio::test]
    async fn test_get_weekly_candles_btc() {
        let client = Client::new();

        let count = 5;
        let candles = get_candles(&client, "BTC-USDT", "1week", count)
            .await
            .expect("Failed to fetch candles");

        // 1. Correct number of candles
        assert_eq!(
            candles.len(),
            count,
            "Expected {} candles, got {}",
            count,
            candles.len()
        );

        // 2. Candles are ordered oldest → newest
        for i in 1..candles.len() {
            assert!(
                candles[i].time > candles[i - 1].time,
                "Candles are not time-ordered"
            );
        }

        // 3. Candle values are sane
        for candle in &candles {
            assert!(candle.open > 0.0);
            assert!(candle.close > 0.0);
            assert!(candle.high >= candle.low);
            assert!(candle.volume >= 0.0);
        }

        // 4. Last candle is fully closed (not in the future)
        let now = chrono::Utc::now().timestamp();
        assert!(
            candles.last().unwrap().time < now,
            "Last candle appears to be unfinished"
        );
    }
}