1use crate::{fetch, quoted, DataError};
4use pine_core::{Data, DataProvider, Ohlcv, ProviderError, SymInfo, Timeframe};
5
6#[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 pub fn limit(mut self, limit: usize) -> Self {
33 self.limit = limit;
34 self
35 }
36
37 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 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 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}