eodhistoricaldata_api/
lib.rs

1/*!
2# eodhistoricaldata API
3
4This project provides a set of functions to receive data from the
5the eodhistoricaldata website via their [API](https://eodhistoricaldata.com/knowledgebase/). This project
6is licensed under Apache 2.0 or MIT license (see files LICENSE-Apache2.0 and LICENSE-MIT).
7
8# Usage
9Please note that you need to have a registered account with eodhistoricaldata to
10receive an individual API token. The most basic account is free but limited to
11EOD Historical Data and LIVE/Realtime Data only and allows only 20 requests per day.
12
13*/
14
15use reqwest::StatusCode;
16use serde::Deserialize;
17use serde_json::Value;
18use thiserror::Error;
19use time::Date;
20
21#[derive(Deserialize, Debug)]
22pub struct RealTimeQuote {
23    /// Ticker name
24    pub code: String,
25    /// UNIX timestamp convention, seconds passed sind 1st January 1970
26    pub timestamp: u64,
27    pub gmtoffset: i32,
28    pub open: f64,
29    pub high: f64,
30    pub low: f64,
31    pub close: f64,
32    pub volume: usize,
33    #[serde(rename = "previousClose")]
34    pub previous_close: f64,
35    pub change: f64,
36    pub change_p: f64,
37}
38
39#[derive(Deserialize, Debug)]
40pub struct HistoricQuote {
41    /// Quote date as string using the format `%Y-%m-%d`
42    pub date: String,
43    pub open: Option<f64>,
44    pub high: Option<f64>,
45    pub low: Option<f64>,
46    pub close: Option<f64>,
47    pub adjusted_close: f64,
48    pub volume: Option<usize>,
49}
50
51#[derive(Deserialize, Debug)]
52#[serde(rename_all = "camelCase")]
53pub struct Dividend {
54    pub currency: String,
55    /// Quote date as string using the format `%Y-%m-%d`
56    pub date: String,
57    pub declaration_date: Option<String>,
58    pub payment_date: String,
59    pub period: String,
60    pub record_date: String,
61    pub unadjusted_value: f64,
62    pub value: f64,
63}
64
65#[derive(Error, Debug)]
66pub enum EodHistDataError {
67    #[error("fetching the data from eodhistoricaldata failed with status code {0}")]
68    FetchFailed(StatusCode),
69    #[error("deserializing response from eodhistoricaldata failed")]
70    DeserializeFailed(#[from] reqwest::Error),
71    #[error("connection to eodhistoricaldata server failed")]
72    ConnectionFailed(#[from] serde_json::Error),
73}
74
75pub struct EodHistConnector {
76    url: &'static str,
77    api_token: String,
78}
79
80impl EodHistConnector {
81    /// Constructor for a new instance of EodHistConnector.
82    /// token is the API token you got from eodhistoricaldata
83    pub fn new(token: String) -> EodHistConnector {
84        EodHistConnector {
85            url: "https://eodhistoricaldata.com/api",
86            api_token: token,
87        }
88    }
89
90    /// Retrieve the latest quote for the given ticker
91    pub async fn get_latest_quote(&self, ticker: &str) -> Result<RealTimeQuote, EodHistDataError> {
92        let url: String = format!(
93            "{}/real-time/{}?api_token={}&fmt=json",
94            self.url, ticker, self.api_token
95        );
96        let resp = self.send_request(&url).await?;
97        let quote: RealTimeQuote = serde_json::from_value(resp)?;
98        Ok(quote)
99    }
100
101    /// Retrieve the quote history for the given ticker form date start to end (inklusive), if available
102    pub async fn get_quote_history(
103        &self,
104        ticker: &str,
105        start: Date,
106        end: Date,
107    ) -> Result<Vec<HistoricQuote>, EodHistDataError> {
108        let url: String = format!(
109            "{}/eod/{}?from={}&to={}&api_token={}&period=d&fmt=json",
110            self.url, ticker, start, end, self.api_token
111        );
112        let resp = self.send_request(&url).await?;
113        let quotes: Vec<HistoricQuote> = serde_json::from_value(resp)?;
114        Ok(quotes)
115    }
116
117    /// Retrieve the quote history for the given ticker form date start to end (inklusive), if available
118    pub async fn get_dividend_history(
119        &self,
120        ticker: &str,
121        start: Date,
122    ) -> Result<Vec<Dividend>, EodHistDataError> {
123        let url: String = format!(
124            "{}/div/{}?from={}&api_token={}&fmt=json",
125            self.url, ticker, start, self.api_token
126        );
127        let resp = self.send_request(&url).await?;
128        let dividends: Vec<Dividend> = serde_json::from_value(resp)?;
129        Ok(dividends)
130    }
131
132    /// Send request to eodhistoricaldata server and transform response to JSON value
133    async fn send_request(&self, url: &str) -> Result<Value, EodHistDataError> {
134        let resp = reqwest::get(url).await?;
135        match resp.status() {
136            StatusCode::OK => Ok(resp.json().await?),
137            status => Err(EodHistDataError::FetchFailed(status)),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use tokio_test;
146
147    #[test]
148    fn test_get_single_quote() {
149        // Use the official test token
150        let token = "OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX".to_string();
151        let provider = EodHistConnector::new(token);
152        let quote = tokio_test::block_on(provider.get_latest_quote("AAPL.US")).unwrap();
153
154        assert_eq!(&quote.code, "AAPL.US");
155    }
156
157    #[test]
158    fn test_get_quote_history() {
159        // Use the official test token
160        let token = "OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX".to_string();
161        let provider = EodHistConnector::new(token);
162        let start = Date::from_calendar_date(2020, time::Month::January, 1).unwrap();
163        let end = Date::from_calendar_date(2020, time::Month::January, 31).unwrap();
164        let quotes =
165            tokio_test::block_on(provider.get_quote_history("AAPL.US", start, end)).unwrap();
166
167        assert_eq!(quotes.len(), 21);
168        assert_eq!(quotes[0].date, "2020-01-02");
169        assert_eq!(quotes[quotes.len() - 1].date, "2020-01-31");
170    }
171
172    #[test]
173    fn test_get_dividend_history() {
174        // Use the official test token
175        let token = "OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX".to_string();
176        let provider = EodHistConnector::new(token);
177        let start = Date::from_calendar_date(2020, time::Month::January, 1).unwrap();
178        let dividends =
179            tokio_test::block_on(provider.get_dividend_history("AAPL.US", start)).unwrap();
180
181        assert!(dividends.len() >= 4);
182    }
183}