From 389271cad620aa96cc340b968b73c9a608fb8447 Mon Sep 17 00:00:00 2001 From: Mark Beinker Date: Sat, 27 Jan 2024 19:56:02 +0100 Subject: [PATCH] implement custom deserializer for TradingPeriods --- examples/get_quote_period_interval.rs | 22 +++ src/async_impl.rs | 22 ++- src/blocking_impl.rs | 4 +- src/lib.rs | 7 +- src/quotes.rs | 243 +++++++++++++++++++++++++- 5 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 examples/get_quote_period_interval.rs diff --git a/examples/get_quote_period_interval.rs b/examples/get_quote_period_interval.rs new file mode 100644 index 0000000..42a7804 --- /dev/null +++ b/examples/get_quote_period_interval.rs @@ -0,0 +1,22 @@ +#[cfg(not(feature = "blocking"))] +use tokio_test; +use yahoo_finance_api as yahoo; + +#[cfg(not(feature = "blocking"))] +fn get_history() -> Result { + let provider = yahoo::YahooConnector::new(); + tokio_test::block_on(provider.get_quote_period_interval("AAPL", "1d", "1m", true)) +} + +#[cfg(feature = "blocking")] +fn get_history() -> Result { + let provider = yahoo::YahooConnector::new(); + provider.get_quote_history_interval("AAPL", "1d", "1m", true) +} + +fn main() { + let quote_history = get_history().unwrap(); + //let times = quote_history.chart.result.timestamp; + //let quotes = quote_history.indicators.quote.q + println!("Quote history of VTI:\n{:#?}", quote_history); +} diff --git a/src/async_impl.rs b/src/async_impl.rs index b1de9d6..e9c474f 100644 --- a/src/async_impl.rs +++ b/src/async_impl.rs @@ -52,8 +52,28 @@ impl YahooConnector { symbol = ticker, start = start.unix_timestamp(), end = end.unix_timestamp(), - interval = interval + interval = interval, + ); + YResponse::from_json(self.send_request(&url).await?) + } + + /// Retrieve the quote history for the given ticker form date start to end (inclusive), if available; specifying the interval of the ticker. + pub async fn get_quote_period_interval( + &self, + ticker: &str, + period: &str, + interval: &str, + prepost: bool + ) -> Result { + let url = format!( + YCHART_PERIOD_INTERVAL_QUERY!(), + url = self.url, + symbol = ticker, + period = period, + interval = interval, + prepost = prepost, ); + println!("result: {:?}", self.send_request(&url).await?); YResponse::from_json(self.send_request(&url).await?) } diff --git a/src/blocking_impl.rs b/src/blocking_impl.rs index cba9268..afcaaaf 100644 --- a/src/blocking_impl.rs +++ b/src/blocking_impl.rs @@ -40,6 +40,7 @@ impl YahooConnector { start: OffsetDateTime, end: OffsetDateTime, interval: &str, + prepost: bool, ) -> Result { let url = format!( YCHART_PERIOD_QUERY!(), @@ -47,7 +48,8 @@ impl YahooConnector { symbol = ticker, start = start.unix_timestamp(), end = end.unix_timestamp(), - interval = interval + interval = interval, + prepost = prepost ); YResponse::from_json(self.send_request(&url)?) } diff --git a/src/lib.rs b/src/lib.rs index 5c8de84..d123290 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -178,7 +178,7 @@ mod quote_summary; mod search_result; mod yahoo_error; pub use quotes::{ - AdjClose, Dividend, CapitalGain, PeriodInfo, Quote, QuoteBlock, QuoteList, Split, TradingPeriod, YChart, + AdjClose, Dividend, CapitalGain, PeriodInfo, Quote, QuoteBlock, QuoteList, Split, TradingPeriods, YChart, YMetaData, YQuoteBlock, YResponse, }; pub use quote_summary::{YQuoteResponse, YQuoteSummary}; @@ -202,6 +202,11 @@ macro_rules! YCHART_RANGE_QUERY { "{url}/{symbol}?symbol={symbol}&interval={interval}&range={range}&events=div|split|capitalGains" }; } +macro_rules! YCHART_PERIOD_INTERVAL_QUERY { + () => { + "{url}/{symbol}?symbol={symbol}&period={period}&interval={interval}&includePrePost={prepost}" + }; +} macro_rules! YTICKER_QUERY { () => { "{url}?q={name}" diff --git a/src/quotes.rs b/src/quotes.rs index b917dc6..6e415a0 100644 --- a/src/quotes.rs +++ b/src/quotes.rs @@ -1,6 +1,11 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + fmt +}; -use serde::Deserialize; +use serde::{Deserialize, + de::{self, Deserializer, Visitor, SeqAccess, MapAccess} +}; use super::YahooError; @@ -166,22 +171,103 @@ pub struct YMetaData { #[serde(default)] pub scale: Option, pub price_hint: i32, - pub current_trading_period: TradingPeriod, + pub current_trading_period: CurrentTradingPeriod, #[serde(default)] - pub trading_periods: Option>>, + pub trading_periods: TradingPeriods, pub data_granularity: String, pub range: String, pub valid_ranges: Vec, } +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct TradingPeriods { + pub pre: Option>>, + pub regular: Option>>, + pub post: Option>>, +} + +impl<'de> Deserialize<'de> for TradingPeriods +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { Regular, Pre, Post } + + struct TradingPeriodsVisitor; + + impl<'de> Visitor<'de> for TradingPeriodsVisitor { + type Value = TradingPeriods; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct (or array) TradingPeriods") + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let regular: Vec = seq.next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + Ok(TradingPeriods{ + pre: None, + regular: Some(vec![regular]), + post: None + }) + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut pre = None; + let mut post = None; + let mut regular = None; + while let Some(key) = map.next_key()? { + match key { + Field::Pre => { + if pre.is_some() { + return Err(de::Error::duplicate_field("pre")); + } + pre = Some(map.next_value()?); + }, + Field::Post => { + if post.is_some() { + return Err(de::Error::duplicate_field("post")); + } + post = Some(map.next_value()?); + }, + Field::Regular => { + if regular.is_some() { + return Err(de::Error::duplicate_field("regular")); + } + regular = Some(map.next_value()?); + }, + + } + } + Ok(TradingPeriods{ + pre, + post, + regular, + }) + } + } + + deserializer.deserialize_any(TradingPeriodsVisitor) + } +} + #[derive(Deserialize, Debug, Clone)] -pub struct TradingPeriod { +pub struct CurrentTradingPeriod { pub pre: PeriodInfo, pub regular: PeriodInfo, pub post: PeriodInfo, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct PeriodInfo { pub timezone: String, pub start: u32, @@ -278,3 +364,148 @@ pub struct CapitalGain { /// This is the recorded date of the capital gain pub date: u64, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_period_info() { + let period_info_json = r#" + { + "timezone": "EST", + "start": 1705501800, + "end": 1705525200, + "gmtoffset": -18000 + } + "#; + let period_info_expected = PeriodInfo{ + timezone: "EST".to_string(), + start: 1705501800, + end: 1705525200, + gmtoffset: -18000, + }; + let period_info_deserialized: PeriodInfo = serde_json::from_str(period_info_json).unwrap(); + assert_eq!(&period_info_deserialized, &period_info_expected); + } + + #[test] + fn test_deserialize_trading_periods_simple() { + let trading_periods_json = r#" + [ + [ + { + "timezone": "EST", + "start": 1705501800, + "end": 1705525200, + "gmtoffset": -18000 + } + + ] + ] + "#; + let trading_periods_expected = TradingPeriods{ + pre: None, + regular: Some(vec![vec![PeriodInfo{ + timezone: "EST".to_string(), + start: 1705501800, + end: 1705525200, + gmtoffset: -18000, + }]]), + post: None, + }; + let trading_periods_deserialized: TradingPeriods = serde_json::from_str(trading_periods_json).unwrap(); + assert_eq!(&trading_periods_expected, &trading_periods_deserialized); + } + + #[test] + fn test_deserialize_trading_periods_complex_regular_only() { + let trading_periods_json = r#" + { + "regular": [ + [ + { + "timezone": "EST", + "start": 1705501800, + "end": 1705525200, + "gmtoffset": -18000 + } + ] + ] + } + "#; + let trading_periods_expected = TradingPeriods{ + pre: None, + regular: Some(vec![vec![PeriodInfo{ + timezone: "EST".to_string(), + start: 1705501800, + end: 1705525200, + gmtoffset: -18000, + }]]), + post: None + }; + let trading_periods_deserialized: TradingPeriods = serde_json::from_str(trading_periods_json).unwrap(); + assert_eq!(&trading_periods_expected, &trading_periods_deserialized); + } + + #[test] + fn test_deserialize_trading_periods_complex() { + let trading_periods_json = r#" + { + "pre": [ + [ + { + "timezone": "EST", + "start": 1705482000, + "end": 1705501800, + "gmtoffset": -18000 + } + ] + ], + "post": [ + [ + { + "timezone": "EST", + "start": 1705525200, + "end": 1705539600, + "gmtoffset": -18000 + } + ] + ], + "regular": [ + [ + { + "timezone": "EST", + "start": 1705501800, + "end": 1705525200, + "gmtoffset": -18000 + } + ] + ] + } + "#; + let trading_periods_expected = TradingPeriods{ + pre: Some(vec![vec![PeriodInfo{ + timezone: "EST".to_string(), + start: 1705482000, + end: 1705501800, + gmtoffset: -18000, + }]]), + regular: Some(vec![vec![PeriodInfo{ + timezone: "EST".to_string(), + start: 1705501800, + end: 1705525200, + gmtoffset: -18000, + }]]), + post: Some(vec![vec![PeriodInfo{ + timezone: "EST".to_string(), + start: 1705525200, + end: 1705539600, + gmtoffset: -18000, + }]]), + }; + let trading_periods_deserialized: TradingPeriods = serde_json::from_str(trading_periods_json).unwrap(); + assert_eq!(&trading_periods_expected, &trading_periods_deserialized); + } +} +