use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::Value;
use thiserror::Error;
use time::Date;
#[derive(Deserialize, Debug)]
pub struct RealTimeQuote {
pub code: String,
pub timestamp: u64,
pub gmtoffset: i32,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: usize,
#[serde(rename = "previousClose")]
pub previous_close: f64,
pub change: f64,
pub change_p: f64,
}
#[derive(Deserialize, Debug)]
pub struct HistoricQuote {
pub date: String,
pub open: Option<f64>,
pub high: Option<f64>,
pub low: Option<f64>,
pub close: Option<f64>,
pub adjusted_close: f64,
pub volume: Option<usize>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Dividend {
pub currency: String,
pub date: String,
pub declaration_date: Option<String>,
pub payment_date: String,
pub period: String,
pub record_date: String,
pub unadjusted_value: f64,
pub value: f64,
}
#[derive(Error, Debug)]
pub enum EodHistDataError {
#[error("fetching the data from eodhistoricaldata failed with status code {0}")]
FetchFailed(StatusCode),
#[error("deserializing response from eodhistoricaldata failed")]
DeserializeFailed(#[from] reqwest::Error),
#[error("connection to eodhistoricaldata server failed")]
ConnectionFailed(#[from] serde_json::Error),
}
pub struct EodHistConnector {
url: &'static str,
api_token: String,
}
impl EodHistConnector {
pub fn new(token: String) -> EodHistConnector {
EodHistConnector {
url: "https://eodhistoricaldata.com/api",
api_token: token,
}
}
pub async fn get_latest_quote(&self, ticker: &str) -> Result<RealTimeQuote, EodHistDataError> {
let url: String = format!(
"{}/real-time/{}?api_token={}&fmt=json",
self.url, ticker, self.api_token
);
let resp = self.send_request(&url).await?;
let quote: RealTimeQuote = serde_json::from_value(resp)?;
Ok(quote)
}
pub async fn get_quote_history(
&self,
ticker: &str,
start: Date,
end: Date,
) -> Result<Vec<HistoricQuote>, EodHistDataError> {
let url: String = format!(
"{}/eod/{}?from={}&to={}&api_token={}&period=d&fmt=json",
self.url, ticker, start, end, self.api_token
);
let resp = self.send_request(&url).await?;
let quotes: Vec<HistoricQuote> = serde_json::from_value(resp)?;
Ok(quotes)
}
pub async fn get_dividend_history(
&self,
ticker: &str,
start: Date,
) -> Result<Vec<Dividend>, EodHistDataError> {
let url: String = format!(
"{}/div/{}?from={}&api_token={}&fmt=json",
self.url, ticker, start, self.api_token
);
let resp = self.send_request(&url).await?;
let dividends: Vec<Dividend> = serde_json::from_value(resp)?;
Ok(dividends)
}
async fn send_request(&self, url: &str) -> Result<Value, EodHistDataError> {
let resp = reqwest::get(url).await?;
match resp.status() {
StatusCode::OK => Ok(resp.json().await?),
status => Err(EodHistDataError::FetchFailed(status)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_test;
#[test]
fn test_get_single_quote() {
let token = "OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX".to_string();
let provider = EodHistConnector::new(token);
let quote = tokio_test::block_on(provider.get_latest_quote("AAPL.US")).unwrap();
assert_eq!("e.code, "AAPL.US");
}
#[test]
fn test_get_quote_history() {
let token = "OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX".to_string();
let provider = EodHistConnector::new(token);
let start = Date::from_calendar_date(2020, time::Month::January, 1).unwrap();
let end = Date::from_calendar_date(2020, time::Month::January, 31).unwrap();
let quotes =
tokio_test::block_on(provider.get_quote_history("AAPL.US", start, end)).unwrap();
assert_eq!(quotes.len(), 21);
assert_eq!(quotes[0].date, "2020-01-02");
assert_eq!(quotes[quotes.len() - 1].date, "2020-01-31");
}
#[test]
fn test_get_dividend_history() {
let token = "OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX".to_string();
let provider = EodHistConnector::new(token);
let start = Date::from_calendar_date(2020, time::Month::January, 1).unwrap();
let dividends =
tokio_test::block_on(provider.get_dividend_history("AAPL.US", start)).unwrap();
assert!(dividends.len() >= 4);
}
}