diff --git a/src/quotes.rs b/src/quotes.rs index bb14aca..19099f8 100644 --- a/src/quotes.rs +++ b/src/quotes.rs @@ -8,371 +8,374 @@ use super::YahooError; #[cfg(not(feature = "decimal"))] pub mod decimal { - pub type Decimal = f64; - pub const ZERO: Decimal = 0.0; + pub type Decimal = f64; + pub const ZERO: Decimal = 0.0; } #[cfg(feature = "decimal")] pub mod decimal { - pub type Decimal = rust_decimal::Decimal; - pub const ZERO: Decimal = Decimal::ZERO; + pub type Decimal = rust_decimal::Decimal; + pub const ZERO: Decimal = Decimal::ZERO; } pub use decimal::*; #[derive(Deserialize, Debug)] pub struct YResponse { - pub chart: YChart, + pub chart: YChart, } impl YResponse { - fn check_consistency(&self) -> Result<(), YahooError> { - for stock in &self.chart.result { - let n = stock.timestamp.len(); - if n == 0 { - return Err(YahooError::EmptyDataSet); - } - let quote = &stock.indicators.quote[0]; - if quote.open.len() != n - || quote.high.len() != n - || quote.low.len() != n - || quote.volume.len() != n - || quote.close.len() != n - { - return Err(YahooError::DataInconsistency); - } - if let Some(ref adjclose) = stock.indicators.adjclose { - if adjclose[0].adjclose.len() != n { - return Err(YahooError::DataInconsistency); - } - } - } - Ok(()) - } - - pub fn from_json(json: serde_json::Value) -> Result { - Ok(serde_json::from_value(json)?) - } - - /// Return the latest valid quote - pub fn last_quote(&self) -> Result { - self.check_consistency()?; - let stock = &self.chart.result[0]; - let n = stock.timestamp.len(); - for i in (0..n).rev() { - let quote = stock.indicators.get_ith_quote(stock.timestamp[i], i); - if quote.is_ok() { - return quote; - } - } - Err(YahooError::EmptyDataSet) - } - - pub fn quotes(&self) -> Result, YahooError> { - self.check_consistency()?; - let stock: &YQuoteBlock = &self.chart.result[0]; - let mut quotes = Vec::new(); - let n = stock.timestamp.len(); - for i in 0..n { - let timestamp = stock.timestamp[i]; - let quote = stock.indicators.get_ith_quote(timestamp, i); - if let Ok(q) = quote { - quotes.push(q); - } - } - Ok(quotes) - } - - pub fn metadata(&self) -> Result { - self.check_consistency()?; - let stock = &self.chart.result[0]; - Ok(stock.meta.to_owned()) - } - - /// This method retrieves information about the splits that might have - /// occured during the considered time period - pub fn splits(&self) -> Result, YahooError> { - self.check_consistency()?; - let stock = &self.chart.result[0]; - if let Some(events) = &stock.events { - if let Some(splits) = &events.splits { - let mut data = splits.values().cloned().collect::>(); - data.sort_unstable_by_key(|d| d.date); - return Ok(data); - } - } - Ok(vec![]) - } - - /// This method retrieves information about the dividends that have - /// been recorded during the considered time period. - /// - /// Note: Date is the ex-dividend date) - pub fn dividends(&self) -> Result, YahooError> { - self.check_consistency()?; - let stock = &self.chart.result[0]; - if let Some(events) = &stock.events { - if let Some(dividends) = &events.dividends { - let mut data = dividends.values().cloned().collect::>(); - data.sort_unstable_by_key(|d| d.date); - return Ok(data); - } - } - Ok(vec![]) - } - - /// This method retrieves information about the capital gains that might have - /// occured during the considered time period (available only for Mutual Funds) - pub fn capital_gains(&self) -> Result, YahooError> { - self.check_consistency()?; - let stock = &self.chart.result[0]; - if let Some(events) = &stock.events { - if let Some(capital_gain) = &events.capital_gains { - let mut data = capital_gain.values().cloned().collect::>(); - data.sort_unstable_by_key(|d| d.date); - return Ok(data); - } - } - Ok(vec![]) - } + fn check_consistency(&self) -> Result<(), YahooError> { + for stock in &self.chart.result { + let n = stock.timestamp.len(); + if n == 0 { + return Err(YahooError::EmptyDataSet); + } + let quote = &stock.indicators.quote[0]; + if quote.open.len() != n + || quote.high.len() != n + || quote.low.len() != n + || quote.volume.len() != n + || quote.close.len() != n + { + return Err(YahooError::DataInconsistency); + } + if let Some(ref adjclose) = stock.indicators.adjclose { + if adjclose[0].adjclose.len() != n { + return Err(YahooError::DataInconsistency); + } + } + } + Ok(()) + } + + pub fn from_json(json: serde_json::Value) -> Result { + Ok(serde_json::from_value(json)?) + } + + /// Return the latest valid quote + pub fn last_quote(&self) -> Result { + self.check_consistency()?; + let stock = &self.chart.result[0]; + let n = stock.timestamp.len(); + for i in (0..n).rev() { + let quote = stock.indicators.get_ith_quote(stock.timestamp[i], i); + if quote.is_ok() { + return quote; + } + } + Err(YahooError::EmptyDataSet) + } + + pub fn quotes(&self) -> Result, YahooError> { + self.check_consistency()?; + let stock: &YQuoteBlock = &self.chart.result[0]; + let mut quotes = Vec::new(); + let n = stock.timestamp.len(); + for i in 0..n { + let timestamp = stock.timestamp[i]; + let quote = stock.indicators.get_ith_quote(timestamp, i); + if let Ok(q) = quote { + quotes.push(q); + } + } + Ok(quotes) + } + + pub fn metadata(&self) -> Result { + self.check_consistency()?; + let stock = &self.chart.result[0]; + Ok(stock.meta.to_owned()) + } + + /// This method retrieves information about the splits that might have + /// occured during the considered time period + pub fn splits(&self) -> Result, YahooError> { + self.check_consistency()?; + let stock = &self.chart.result[0]; + if let Some(events) = &stock.events { + if let Some(splits) = &events.splits { + let mut data = splits.values().cloned().collect::>(); + data.sort_unstable_by_key(|d| d.date); + return Ok(data); + } + } + Ok(vec![]) + } + + /// This method retrieves information about the dividends that have + /// been recorded during the considered time period. + /// + /// Note: Date is the ex-dividend date) + pub fn dividends(&self) -> Result, YahooError> { + self.check_consistency()?; + let stock = &self.chart.result[0]; + if let Some(events) = &stock.events { + if let Some(dividends) = &events.dividends { + let mut data = dividends.values().cloned().collect::>(); + data.sort_unstable_by_key(|d| d.date); + return Ok(data); + } + } + Ok(vec![]) + } + + /// This method retrieves information about the capital gains that might have + /// occured during the considered time period (available only for Mutual Funds) + pub fn capital_gains(&self) -> Result, YahooError> { + self.check_consistency()?; + let stock = &self.chart.result[0]; + if let Some(events) = &stock.events { + if let Some(capital_gain) = &events.capital_gains { + let mut data = capital_gain.values().cloned().collect::>(); + data.sort_unstable_by_key(|d| d.date); + return Ok(data); + } + } + Ok(vec![]) + } } /// Struct for single quote #[derive(Debug, Clone, PartialEq, PartialOrd, Deserialize, Serialize)] pub struct Quote { - pub timestamp: u64, - pub open: Decimal, - pub high: Decimal, - pub low: Decimal, - pub volume: u64, - pub close: Decimal, - pub adjclose: Decimal, + pub timestamp: u64, + pub open: Decimal, + pub high: Decimal, + pub low: Decimal, + pub volume: u64, + pub close: Decimal, + pub adjclose: Decimal, } #[derive(Deserialize, Debug)] pub struct YChart { - pub result: Vec, - pub error: Option, + pub result: Vec, + pub error: Option, } #[derive(Deserialize, Debug)] pub struct YQuoteBlock { - pub meta: YMetaData, - pub timestamp: Vec, - pub events: Option, - pub indicators: QuoteBlock, + pub meta: YMetaData, + pub timestamp: Vec, + pub events: Option, + pub indicators: QuoteBlock, } #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct YMetaData { - pub currency: Option, - pub symbol: String, - pub exchange_name: String, - pub instrument_type: String, - #[serde(default)] - pub first_trade_date: Option, - pub regular_market_time: u32, - pub gmtoffset: i32, - pub timezone: String, - pub exchange_timezone_name: String, - pub regular_market_price: Decimal, - pub chart_previous_close: Decimal, - pub previous_close: Option, - #[serde(default)] - pub scale: Option, - pub price_hint: i32, - pub current_trading_period: CurrentTradingPeriod, - #[serde(default)] - pub trading_periods: TradingPeriods, - pub data_granularity: String, - pub range: String, - pub valid_ranges: Vec, + pub currency: Option, + pub symbol: String, + pub exchange_name: String, + pub instrument_type: String, + #[serde(default)] + pub first_trade_date: Option, + pub regular_market_time: u32, + pub gmtoffset: i32, + pub timezone: String, + pub exchange_timezone_name: String, + pub regular_market_price: Decimal, + pub chart_previous_close: Decimal, + pub previous_close: Option, + #[serde(default)] + pub scale: Option, + pub price_hint: i32, + pub current_trading_period: CurrentTradingPeriod, + #[serde(default)] + 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>>, + 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 mut regular: Vec = Vec::new(); - - println!("visit_seq"); - while let Ok(Some(mut e)) = seq.next_element::>() { - regular.append(&mut e); - } - - 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) - } + 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 mut regular: Vec = Vec::new(); + + while let Ok(Some(mut e)) = seq.next_element::>() { + regular.append(&mut e); + } + + 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 CurrentTradingPeriod { - pub pre: PeriodInfo, - pub regular: PeriodInfo, - pub post: PeriodInfo, + pub pre: PeriodInfo, + pub regular: PeriodInfo, + pub post: PeriodInfo, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct PeriodInfo { - pub timezone: String, - pub start: u32, - pub end: u32, - pub gmtoffset: i32, + pub timezone: String, + pub start: u32, + pub end: u32, + pub gmtoffset: i32, } #[derive(Deserialize, Debug)] pub struct QuoteBlock { - quote: Vec, - #[serde(default)] - adjclose: Option>, + quote: Vec, + #[serde(default)] + adjclose: Option>, } impl QuoteBlock { - fn get_ith_quote(&self, timestamp: u64, i: usize) -> Result { - let adjclose = match &self.adjclose { - Some(adjclose) => adjclose[0].adjclose[i], - None => None, - }; - let quote = &self.quote[0]; - // reject if close is not set - if quote.close[i].is_none() { - return Err(YahooError::EmptyDataSet); - } - Ok(Quote { - timestamp, - open: quote.open[i].unwrap_or(ZERO), - high: quote.high[i].unwrap_or(ZERO), - low: quote.low[i].unwrap_or(ZERO), - volume: quote.volume[i].unwrap_or(0), - close: quote.close[i].unwrap(), - adjclose: adjclose.unwrap_or(ZERO), - }) - } + fn get_ith_quote(&self, timestamp: u64, i: usize) -> Result { + let adjclose = match &self.adjclose { + Some(adjclose) => adjclose[0].adjclose[i], + None => None, + }; + let quote = &self.quote[0]; + // reject if close is not set + if quote.close[i].is_none() { + return Err(YahooError::EmptyDataSet); + } + Ok(Quote { + timestamp, + open: quote.open[i].unwrap_or(ZERO), + high: quote.high[i].unwrap_or(ZERO), + low: quote.low[i].unwrap_or(ZERO), + volume: quote.volume[i].unwrap_or(0), + close: quote.close[i].unwrap(), + adjclose: adjclose.unwrap_or(ZERO), + }) + } } #[derive(Deserialize, Debug)] pub struct AdjClose { - adjclose: Vec>, + adjclose: Vec>, } #[derive(Deserialize, Debug)] pub struct QuoteList { - pub volume: Vec>, - pub high: Vec>, - pub close: Vec>, - pub low: Vec>, - pub open: Vec>, + pub volume: Vec>, + pub high: Vec>, + pub close: Vec>, + pub low: Vec>, + pub open: Vec>, } #[derive(Deserialize, Debug)] pub struct EventsBlock { - pub splits: Option>, - pub dividends: Option>, - #[serde(rename = "capitalGains")] - pub capital_gains: Option>, + pub splits: Option>, + pub dividends: Option>, + #[serde(rename = "capitalGains")] + pub capital_gains: Option>, } /// This structure simply models a split that has occured. #[derive(Deserialize, Debug, Clone)] pub struct Split { - /// This is the date (timestamp) when the split occured - pub date: u64, - /// Numerator of the split. For instance a 1:5 split means you get 5 share - /// wherever you had one before the split. (Here the numerator is 1 and - /// denom is 5). A reverse split is considered as nothing but a regular - /// split with a numerator > denom. - pub numerator: Decimal, - /// Denominator of the split. For instance a 1:5 split means you get 5 share - /// wherever you had one before the split. (Here the numerator is 1 and - /// denom is 5). A reverse split is considered as nothing but a regular - /// split with a numerator > denom. - pub denominator: Decimal, - /// A textual representation of the split. - #[serde(rename = "splitRatio")] - pub split_ratio: String, + /// This is the date (timestamp) when the split occured + pub date: u64, + /// Numerator of the split. For instance a 1:5 split means you get 5 share + /// wherever you had one before the split. (Here the numerator is 1 and + /// denom is 5). A reverse split is considered as nothing but a regular + /// split with a numerator > denom. + pub numerator: Decimal, + /// Denominator of the split. For instance a 1:5 split means you get 5 share + /// wherever you had one before the split. (Here the numerator is 1 and + /// denom is 5). A reverse split is considered as nothing but a regular + /// split with a numerator > denom. + pub denominator: Decimal, + /// A textual representation of the split. + #[serde(rename = "splitRatio")] + pub split_ratio: String, } /// This structure simply models a dividend which has been recorded. #[derive(Deserialize, Debug, Clone)] pub struct Dividend { - /// This is the price of the dividend - pub amount: Decimal, - /// This is the ex-dividend date - pub date: u64, + /// This is the price of the dividend + pub amount: Decimal, + /// This is the ex-dividend date + pub date: u64, } /// This structure simply models a capital gain which has been recorded. #[derive(Deserialize, Debug, Clone)] pub struct CapitalGain { - /// This is the amount of capital gain distributed by the fund - pub amount: f64, - /// This is the recorded date of the capital gain - pub date: u64, + /// This is the amount of capital gain distributed by the fund + pub amount: f64, + /// This is the recorded date of the capital gain + pub date: u64, } #[derive(Deserialize, Debug)] @@ -498,11 +501,11 @@ pub struct FinancialData { #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_deserialize_period_info() { - let period_info_json = r#" + #[test] + fn test_deserialize_period_info() { + let period_info_json = r#" { "timezone": "EST", "start": 1705501800, @@ -510,19 +513,19 @@ mod tests { "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#" + 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#" [ [ { @@ -535,24 +538,24 @@ mod tests { ] ] "#; - 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#" + 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": [ [ @@ -566,24 +569,24 @@ mod tests { ] } "#; - 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#" + 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": [ [ @@ -617,28 +620,28 @@ mod tests { ] } "#; - 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); - } + 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); + } }