1use crate::{fetch, DataError};
4use pine_core::{Data, DataProvider, Ohlcv, ProviderError, SymInfo, Timeframe};
5use serde::{de, Deserialize, Deserializer};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
18pub struct KrakenSource;
19
20impl KrakenSource {
21 pub fn new() -> Self {
22 Self
23 }
24}
25
26#[derive(Debug, Deserialize)]
27struct HttpResult {
28 #[serde(default)]
29 error: Vec<String>,
30 result: Option<OhlcResult>,
31}
32
33#[derive(Debug, Deserialize)]
36struct OhlcResult {
37 #[allow(dead_code)]
38 last: i64,
39 #[serde(flatten)]
40 pairs: HashMap<String, Vec<Candle>>,
41}
42
43#[derive(Debug, Deserialize)]
45struct Candle(
46 i64,
47 #[serde(deserialize_with = "quoted")] f64,
48 #[serde(deserialize_with = "quoted")] f64,
49 #[serde(deserialize_with = "quoted")] f64,
50 #[serde(deserialize_with = "quoted")] f64,
51 #[serde(deserialize_with = "quoted")] f64,
52 #[serde(deserialize_with = "quoted")] f64,
53 i64,
54);
55
56impl From<Candle> for Ohlcv {
57 fn from(candle: Candle) -> Self {
58 let Candle(time, open, high, low, close, _vwap, volume, _count) = candle;
59 Ohlcv {
60 time: time * 1000,
62 open,
63 high,
64 low,
65 close,
66 volume,
67 }
68 }
69}
70
71fn quoted<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
72 <&str>::deserialize(deserializer)?
73 .parse()
74 .map_err(de::Error::custom)
75}
76
77impl DataProvider for KrakenSource {
78 fn request(&self, symbol: &str, timeframe: Timeframe) -> Result<Data, ProviderError> {
79 let pair = symbol.to_uppercase();
80 let minutes = timeframe.as_minutes().unwrap_or(60);
84
85 let url = format!("https://api.kraken.com/0/public/OHLC?pair={pair}&interval={minutes}");
86 let body = fetch(&url)?;
87
88 let bad = |message: String| DataError::Provider {
89 provider: "kraken",
90 message,
91 };
92
93 let response: HttpResult =
94 serde_json::from_str(&body).map_err(|e| bad(format!("{e}: {body:.200}")))?;
95
96 if !response.error.is_empty() {
97 return Err(bad(response.error.join(", ")).into());
98 }
99
100 let candles = response
101 .result
102 .and_then(|result| result.pairs.into_values().next())
103 .ok_or_else(|| bad(format!("no candles for {pair}")))?;
104
105 let rows = candles.into_iter().map(Ohlcv::from);
106
107 Ok(Data::from_ohlcv(rows).with_syminfo(SymInfo {
108 ticker: pair.clone(),
109 tickerid: format!("KRAKEN:{pair}"),
110 prefix: "KRAKEN".to_string(),
111 type_: "crypto".to_string(),
112 ..SymInfo::default()
113 }))
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_kraken() {
123 let data = KrakenSource::new()
124 .request("XBTUSD", "60".parse().unwrap())
125 .unwrap();
126
127 assert_ne!(data.bars.len(), 0);
128 }
129}