Skip to main content

pine_data/
binance.rs

1//! Bars from Binance's public klines endpoint.
2
3use crate::{fetch, quoted, DataError};
4use pine_core::{Data, DataProvider, Ohlcv, ProviderError, SymInfo, Timeframe};
5
6/// Binance's public klines endpoint, as a [`DataProvider`]: it fetches whatever
7/// symbol and timeframe are asked for.
8///
9/// ```no_run
10/// # use pine_data::BinanceSource;
11/// # use pine_core::DataProvider;
12/// let data = BinanceSource::new().limit(500).request("BTCUSDT", "60".parse()?)?;
13/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
14/// ```
15#[derive(Debug, Clone)]
16pub struct BinanceSource {
17    limit: usize,
18}
19
20impl Default for BinanceSource {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl BinanceSource {
27    pub fn new() -> Self {
28        Self { limit: 500 }
29    }
30
31    /// How many of the most recent candles to ask for. Binance caps this at 1000.
32    pub fn limit(mut self, limit: usize) -> Self {
33        self.limit = limit;
34        self
35    }
36
37    /// A timeframe as Binance spells its kline intervals. Binance takes whole
38    /// hours as `"1h"` rather than `"60m"`, and writes a month `"1M"`.
39    fn interval(tf: &Timeframe) -> String {
40        match tf.as_minutes() {
41            Some(minutes) if tf.is_minutes() && minutes % 60 == 0 => format!("{}h", minutes / 60),
42            _ if tf.is_minutes() => format!("{}m", tf.multiplier),
43            _ if tf.is_daily() => format!("{}d", tf.multiplier),
44            _ if tf.is_weekly() => format!("{}w", tf.multiplier),
45            _ if tf.is_monthly() => format!("{}M", tf.multiplier),
46            _ => format!("{}m", tf.multiplier),
47        }
48    }
49}
50
51impl DataProvider for BinanceSource {
52    fn request(&self, symbol: &str, timeframe: Timeframe) -> Result<Data, ProviderError> {
53        let symbol = symbol.to_uppercase();
54
55        let url = format!(
56            "https://api.binance.com/api/v3/klines?symbol={}&interval={}&limit={}",
57            symbol,
58            Self::interval(&timeframe),
59            self.limit
60        );
61        let body = fetch(&url)?;
62
63        let bad = |message: String| DataError::Provider {
64            provider: "binance",
65            message,
66        };
67
68        // Each kline is an array: [openTime, open, high, low, close, volume, …]
69        // with the prices sent as strings.
70        let klines: Vec<serde_json::Value> =
71            serde_json::from_str(&body).map_err(|e| bad(format!("{e}: {body:.200}")))?;
72
73        let rows = klines
74            .iter()
75            .map(|k| {
76                Some(Ohlcv {
77                    time: k.get(0)?.as_i64()?,
78                    open: quoted(k.get(1)?)?,
79                    high: quoted(k.get(2)?)?,
80                    low: quoted(k.get(3)?)?,
81                    close: quoted(k.get(4)?)?,
82                    volume: quoted(k.get(5)?)?,
83                })
84            })
85            .collect::<Option<Vec<_>>>()
86            .ok_or_else(|| bad("unexpected kline shape".to_string()))?;
87
88        let data = Data::from_ohlcv(rows).with_syminfo(SymInfo {
89            ticker: symbol.clone(),
90            tickerid: format!("BINANCE:{symbol}"),
91            prefix: "BINANCE".to_string(),
92            type_: "crypto".to_string(),
93            ..SymInfo::default()
94        });
95
96        // The requested timeframe is authoritative, not one guessed from the
97        // spacing between bars.
98        Ok(data)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    #[ignore = "it does not work in CI"]
108    fn test_binance() {
109        let data = BinanceSource::new()
110            .limit(500)
111            .request("BTCUSDT", "60".parse().unwrap())
112            .unwrap();
113
114        assert_eq!(data.bars.len(), 500);
115    }
116}