Skip to main content

pine_data/
kraken.rs

1//! Bars from Kraken's public OHLC endpoint.
2
3use crate::{fetch, DataError};
4use pine_core::{Data, DataProvider, Ohlcv, ProviderError, SymInfo, Timeframe};
5use serde::{de, Deserialize, Deserializer};
6use std::collections::HashMap;
7
8/// Kraken's public OHLC endpoint, as a [`DataProvider`]: it fetches whatever pair
9/// and timeframe are asked for.
10///
11/// ```no_run
12/// # use pine_data::KrakenSource;
13/// # use pine_core::DataProvider;
14/// let data = KrakenSource::new().request("XBTUSD", "60".parse()?)?;
15/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
16/// ```
17#[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// The candles sit under Kraken's own name for the pair — "XBTUSD" comes back as
34// "XXBTZUSD" — so they are whichever key `last` is not.
35#[derive(Debug, Deserialize)]
36struct OhlcResult {
37    #[allow(dead_code)]
38    last: i64,
39    #[serde(flatten)]
40    pairs: HashMap<String, Vec<Candle>>,
41}
42
43// [time, open, high, low, close, vwap, volume, count], the prices as strings.
44#[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            // Kraken timestamps are seconds; a bar's time is in ms.
61            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        // Kraken asks for the interval as a number of minutes; a sub-minute or
81        // month timeframe has none, and Kraken serves neither, so it falls back
82        // to the hour its API defaults to.
83        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}