Skip to main content

pine_data/
lib.rs

1//! Loading market data for a script to run over.
2//!
3//! The data itself is [`pine_core::Data`] — bars plus the symbol and timeframe
4//! they belong to. This crate is the ways of getting one: a CSV file today, an
5//! exchange later.
6//!
7//! Series history and stateful builtins accumulate as bars execute, so a script
8//! must be replayed from its first bar. Handing over the whole series at once is
9//! what makes that the only thing a caller *can* do.
10
11use pine_core::{Data, Ohlcv};
12
13mod static_provider;
14
15mod binance;
16mod kraken;
17mod yahoo;
18
19pub use binance::BinanceSource;
20pub use kraken::KrakenSource;
21pub use static_provider::{resample, StaticProvider};
22pub use yahoo::YahooSource;
23
24pub(crate) fn fetch(url: &str) -> Result<String, DataError> {
25    // This call blocks on purpose such that a Script execution
26    // stays sync all the way down
27    ureq::get(url)
28        .set("User-Agent", "pinecone/0.1")
29        .call()
30        .map_err(|source| DataError::Http {
31            url: url.to_string(),
32            message: source.to_string(),
33        })?
34        .into_string()
35        .map_err(|source| DataError::Http {
36            url: url.to_string(),
37            message: source.to_string(),
38        })
39}
40
41/// Read a price that a provider sends as a JSON string rather than a number.
42pub(crate) fn quoted(value: &serde_json::Value) -> Option<f64> {
43    match value {
44        serde_json::Value::String(text) => text.parse().ok(),
45        other => other.as_f64(),
46    }
47}
48
49pub fn synthetic(count: usize) -> Data {
50    Data::from_ohlcv((0..count).map(|i| {
51        let close = 100.0 + i as f64;
52        Ohlcv {
53            time: i as i64 * 60_000,
54            open: close - 1.0,
55            high: close + 1.0,
56            low: close - 2.0,
57            close,
58            volume: 1000.0,
59        }
60    }))
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum DataError {
65    /// A file could not be opened, or its contents did not parse. The inner
66    /// error carries the row and line a bad record was on.
67    #[error("{path}: {source}")]
68    Read {
69        path: String,
70        #[source]
71        source: ::csv::Error,
72    },
73
74    /// The request itself failed — unreachable host, non-2xx status, bad body.
75    #[error("{url}: {message}")]
76    Http { url: String, message: String },
77
78    /// The provider answered, but not with the bars we asked for: an in-band
79    /// error, an unknown symbol, or a shape we cannot read.
80    #[error("{provider}: {message}")]
81    Provider {
82        provider: &'static str,
83        message: String,
84    },
85}