1use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt;
5use time::OffsetDateTime;
6
7use super::YahooError;
8
9#[cfg(not(feature = "decimal"))]
10pub mod decimal {
11 pub type Decimal = f64;
12 pub const ZERO: Decimal = 0.0;
13}
14
15#[cfg(feature = "decimal")]
16pub mod decimal {
17 pub type Decimal = rust_decimal::Decimal;
18 pub const ZERO: Decimal = Decimal::ZERO;
19}
20
21pub use decimal::*;
22
23#[derive(Deserialize, Debug)]
24pub struct YResponse {
25 pub chart: YChart,
26}
27
28impl YResponse {
29 pub(crate) fn map_error_msg(self) -> Result<YResponse, YahooError> {
30 if self.chart.result.is_none() {
31 if let Some(y_error) = self.chart.error {
32 return Err(YahooError::ApiError(y_error));
33 }
34 }
35 Ok(self)
36 }
37
38 fn check_historical_consistency(&self) -> Result<&Vec<YQuoteBlock>, YahooError> {
39 let Some(result) = &self.chart.result else {
40 return Err(YahooError::NoResult);
41 };
42
43 for stock in result {
44 let n = stock.timestamp.as_ref().map_or(0, |v| v.len());
45
46 if n == 0 {
47 return Err(YahooError::NoQuotes);
48 }
49
50 let quote = &stock.indicators.quote[0];
51
52 if quote.open.is_none()
53 || quote.high.is_none()
54 || quote.low.is_none()
55 || quote.volume.is_none()
56 || quote.close.is_none()
57 {
58 return Err(YahooError::DataInconsistency);
59 }
60
61 let open_len = quote.open.as_ref().map_or(0, |v| v.len());
62 let high_len = quote.high.as_ref().map_or(0, |v| v.len());
63 let low_len = quote.low.as_ref().map_or(0, |v| v.len());
64 let volume_len = quote.volume.as_ref().map_or(0, |v| v.len());
65 let close_len = quote.close.as_ref().map_or(0, |v| v.len());
66
67 if open_len != n || high_len != n || low_len != n || volume_len != n || close_len != n {
68 return Err(YahooError::DataInconsistency);
69 }
70 }
71 Ok(result)
72 }
73
74 pub fn from_json(json: serde_json::Value) -> Result<YResponse, YahooError> {
75 Ok(serde_json::from_value(json)?)
76 }
77
78 pub fn last_quote(&self) -> Result<Quote, YahooError> {
80 let stock = &self.check_historical_consistency()?[0];
81
82 let n = stock.timestamp.as_ref().map_or(0, |v| v.len());
83
84 for i in (0..n).rev() {
85 let quote = stock
86 .indicators
87 .get_ith_quote(stock.timestamp.as_ref().unwrap()[i], i);
88 if quote.is_ok() {
89 return quote;
90 }
91 }
92 Err(YahooError::NoQuotes)
93 }
94
95 pub fn quotes(&self) -> Result<Vec<Quote>, YahooError> {
96 let stock = &self.check_historical_consistency()?[0];
97
98 let mut quotes = Vec::new();
99 let n = stock.timestamp.as_ref().map_or(0, |v| v.len());
100 for i in 0..n {
101 let timestamp = stock.timestamp.as_ref().unwrap()[i];
102 let quote = stock.indicators.get_ith_quote(timestamp, i);
103 if let Ok(q) = quote {
104 quotes.push(q);
105 }
106 }
107 Ok(quotes)
108 }
109
110 pub fn metadata(&self) -> Result<YMetaData, YahooError> {
111 let Some(result) = &self.chart.result else {
112 return Err(YahooError::NoResult);
113 };
114 let stock = &result[0];
115 Ok(stock.meta.to_owned())
116 }
117
118 pub fn splits(&self) -> Result<Vec<Split>, YahooError> {
121 let Some(result) = &self.chart.result else {
122 return Err(YahooError::NoResult);
123 };
124 let stock = &result[0];
125
126 if let Some(events) = &stock.events {
127 if let Some(splits) = &events.splits {
128 let mut data = splits.values().cloned().collect::<Vec<Split>>();
129 data.sort_unstable_by_key(|d| d.date);
130 return Ok(data);
131 }
132 }
133 Ok(vec![])
134 }
135
136 pub fn dividends(&self) -> Result<Vec<Dividend>, YahooError> {
141 let Some(result) = &self.chart.result else {
142 return Err(YahooError::NoResult);
143 };
144 let stock = &result[0];
145
146 if let Some(events) = &stock.events {
147 if let Some(dividends) = &events.dividends {
148 let mut data = dividends.values().cloned().collect::<Vec<Dividend>>();
149 data.sort_unstable_by_key(|d| d.date);
150 return Ok(data);
151 }
152 }
153 Ok(vec![])
154 }
155
156 pub fn capital_gains(&self) -> Result<Vec<CapitalGain>, YahooError> {
159 let Some(result) = &self.chart.result else {
160 return Err(YahooError::NoResult);
161 };
162 let stock = &result[0];
163
164 if let Some(events) = &stock.events {
165 if let Some(capital_gain) = &events.capital_gains {
166 let mut data = capital_gain.values().cloned().collect::<Vec<CapitalGain>>();
167 data.sort_unstable_by_key(|d| d.date);
168 return Ok(data);
169 }
170 }
171 Ok(vec![])
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, PartialOrd, Deserialize, Serialize)]
177pub struct Quote {
178 pub timestamp: i64,
179 pub open: Decimal,
180 pub high: Decimal,
181 pub low: Decimal,
182 pub volume: u64,
183 pub close: Decimal,
184 pub adjclose: Decimal,
185}
186
187#[derive(Deserialize, Debug)]
188pub struct YChart {
189 pub result: Option<Vec<YQuoteBlock>>,
190 pub error: Option<YErrorMessage>,
191}
192
193#[derive(Deserialize, Debug)]
194pub struct YQuoteBlock {
195 pub meta: YMetaData,
196 pub timestamp: Option<Vec<i64>>,
197 pub events: Option<EventsBlock>,
198 pub indicators: QuoteBlock,
199}
200
201#[derive(Deserialize, Debug, Clone)]
202#[serde(rename_all = "camelCase")]
203pub struct YMetaData {
204 pub currency: Option<String>,
205 pub symbol: String,
206 pub long_name: Option<String>,
207 pub short_name: Option<String>,
208 pub instrument_type: String,
209 pub exchange_name: String,
210 pub full_exchange_name: String,
211 #[serde(default)]
212 pub first_trade_date: Option<i32>,
213 pub regular_market_time: Option<u32>,
214 pub gmtoffset: i32,
215 pub timezone: String,
216 pub exchange_timezone_name: String,
217 pub regular_market_price: Option<Decimal>,
218 pub chart_previous_close: Option<Decimal>,
219 pub previous_close: Option<Decimal>,
220 pub has_pre_post_market_data: bool,
221 pub fifty_two_week_high: Option<Decimal>,
222 pub fifty_two_week_low: Option<Decimal>,
223 pub regular_market_day_high: Option<Decimal>,
224 pub regular_market_day_low: Option<Decimal>,
225 pub regular_market_volume: Option<Decimal>,
226 #[serde(default)]
227 pub scale: Option<i32>,
228 pub price_hint: i32,
229 pub current_trading_period: CurrentTradingPeriod,
230 #[serde(default)]
231 pub trading_periods: TradingPeriods,
232 pub data_granularity: String,
233 pub range: String,
234 pub valid_ranges: Vec<String>,
235}
236
237#[derive(Default, Debug, Clone, PartialEq, Eq)]
238pub struct TradingPeriods {
239 pub pre: Option<Vec<Vec<PeriodInfo>>>,
240 pub regular: Option<Vec<Vec<PeriodInfo>>>,
241 pub post: Option<Vec<Vec<PeriodInfo>>>,
242}
243
244impl<'de> Deserialize<'de> for TradingPeriods {
245 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246 where
247 D: Deserializer<'de>,
248 {
249 #[derive(Deserialize)]
250 #[serde(field_identifier, rename_all = "lowercase")]
251 enum Field {
252 Regular,
253 Pre,
254 Post,
255 }
256
257 struct TradingPeriodsVisitor;
258
259 impl<'de> Visitor<'de> for TradingPeriodsVisitor {
260 type Value = TradingPeriods;
261
262 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
263 formatter.write_str("struct (or array) TradingPeriods")
264 }
265
266 fn visit_seq<V>(self, mut seq: V) -> Result<TradingPeriods, V::Error>
267 where
268 V: SeqAccess<'de>,
269 {
270 let mut regular: Vec<PeriodInfo> = Vec::new();
271
272 while let Ok(Some(mut e)) = seq.next_element::<Vec<PeriodInfo>>() {
273 regular.append(&mut e);
274 }
275
276 Ok(TradingPeriods {
277 pre: None,
278 regular: Some(vec![regular]),
279 post: None,
280 })
281 }
282
283 fn visit_map<V>(self, mut map: V) -> Result<TradingPeriods, V::Error>
284 where
285 V: MapAccess<'de>,
286 {
287 let mut pre = None;
288 let mut post = None;
289 let mut regular = None;
290 while let Some(key) = map.next_key()? {
291 match key {
292 Field::Pre => {
293 if pre.is_some() {
294 return Err(de::Error::duplicate_field("pre"));
295 }
296 pre = Some(map.next_value()?);
297 }
298 Field::Post => {
299 if post.is_some() {
300 return Err(de::Error::duplicate_field("post"));
301 }
302 post = Some(map.next_value()?);
303 }
304 Field::Regular => {
305 if regular.is_some() {
306 return Err(de::Error::duplicate_field("regular"));
307 }
308 regular = Some(map.next_value()?);
309 }
310 }
311 }
312 Ok(TradingPeriods { pre, post, regular })
313 }
314 }
315
316 deserializer.deserialize_any(TradingPeriodsVisitor)
317 }
318}
319
320#[derive(Deserialize, Debug, Clone)]
321pub struct CurrentTradingPeriod {
322 pub pre: PeriodInfo,
323 pub regular: PeriodInfo,
324 pub post: PeriodInfo,
325}
326
327#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
328pub struct PeriodInfo {
329 pub timezone: String,
330 pub start: u32,
331 pub end: u32,
332 pub gmtoffset: i32,
333}
334
335#[derive(Deserialize, Debug)]
336pub struct QuoteBlock {
337 quote: Vec<QuoteList>,
338 #[serde(default)]
339 adjclose: Option<Vec<AdjClose>>,
340}
341
342impl QuoteBlock {
343 fn get_ith_quote(&self, timestamp: i64, i: usize) -> Result<Quote, YahooError> {
344 let adjclose = match &self.adjclose {
345 Some(vec_of_adjclose) => match vec_of_adjclose[0].adjclose {
346 Some(ref adjclose) => adjclose[i],
347 None => None,
348 },
349 None => None,
350 };
351
352 let quote = &self.quote[0];
353 let open = match quote.open {
356 Some(ref open) => open[i],
357 None => None,
358 };
359
360 let high = match quote.high {
361 Some(ref high) => high[i],
362 None => None,
363 };
364
365 let low = match quote.low {
366 Some(ref low) => low[i],
367 None => None,
368 };
369
370 let volume = match quote.volume {
371 Some(ref volume) => volume[i],
372 None => None,
373 };
374
375 let close = match quote.close {
376 Some(ref close) => close[i],
377 None => None,
378 };
379
380 if close.is_none() {
381 return Err(YahooError::NoQuotes);
382 }
383
384 Ok(Quote {
385 timestamp,
386 open: open.unwrap_or(ZERO),
387 high: high.unwrap_or(ZERO),
388 low: low.unwrap_or(ZERO),
389 volume: volume.unwrap_or(0),
390 close: close.unwrap(),
391 adjclose: adjclose.unwrap_or(ZERO),
392 })
393 }
394}
395
396#[derive(Deserialize, Debug)]
397pub struct AdjClose {
398 adjclose: Option<Vec<Option<Decimal>>>,
399}
400
401#[derive(Deserialize, Debug)]
402pub struct QuoteList {
403 pub volume: Option<Vec<Option<u64>>>,
404 pub high: Option<Vec<Option<Decimal>>>,
405 pub close: Option<Vec<Option<Decimal>>>,
406 pub low: Option<Vec<Option<Decimal>>>,
407 pub open: Option<Vec<Option<Decimal>>>,
408}
409
410#[derive(Deserialize, Debug)]
411pub struct EventsBlock {
412 pub splits: Option<HashMap<i64, Split>>,
413 pub dividends: Option<HashMap<i64, Dividend>>,
414 #[serde(rename = "capitalGains")]
415 pub capital_gains: Option<HashMap<i64, CapitalGain>>,
416}
417
418#[derive(Deserialize, Debug, Clone)]
420pub struct Split {
421 pub date: i64,
423 pub numerator: Decimal,
428 pub denominator: Decimal,
433 #[serde(rename = "splitRatio")]
435 pub split_ratio: String,
436}
437
438#[derive(Deserialize, Debug, Clone)]
440pub struct Dividend {
441 pub amount: Decimal,
443 pub date: i64,
445}
446
447#[derive(Deserialize, Debug, Clone)]
449pub struct CapitalGain {
450 pub amount: f64,
452 pub date: i64,
454}
455
456#[derive(Deserialize, Debug)]
457#[serde(rename_all = "camelCase")]
458pub struct YQuoteSummary {
459 #[serde(rename = "quoteSummary")]
460 pub quote_summary: Option<ExtendedQuoteSummary>,
461 pub finance: Option<YFinance>,
462}
463
464#[derive(Deserialize, Debug)]
465pub struct YFinance {
466 pub result: Option<serde_json::Value>,
467 pub error: Option<YErrorMessage>,
468}
469
470#[derive(Deserialize, Debug)]
471pub struct YErrorMessage {
472 pub code: Option<String>,
473 pub description: Option<String>,
474}
475
476#[derive(Deserialize, Debug)]
477pub struct ExtendedQuoteSummary {
478 pub result: Option<Vec<YSummaryData>>,
479 pub error: Option<YErrorMessage>,
480}
481
482impl YQuoteSummary {
483 pub fn from_json(json: serde_json::Value) -> Result<YQuoteSummary, YahooError> {
484 Ok(serde_json::from_value(json)?)
485 }
486}
487
488#[derive(Deserialize, Debug)]
489#[serde(rename_all = "camelCase")]
490pub struct YSummaryData {
491 pub asset_profile: Option<AssetProfile>,
492 pub summary_detail: Option<SummaryDetail>,
493 pub default_key_statistics: Option<DefaultKeyStatistics>,
494 pub quote_type: Option<QuoteType>,
495 pub financial_data: Option<FinancialData>,
496}
497
498#[derive(Deserialize, Debug)]
499#[serde(rename_all = "camelCase")]
500pub struct AssetProfile {
501 pub address1: Option<String>,
502 pub city: Option<String>,
503 pub state: Option<String>,
504 pub zip: Option<String>,
505 pub country: Option<String>,
506 pub phone: Option<String>,
507 pub website: Option<String>,
508 pub industry: Option<String>,
509 pub sector: Option<String>,
510 pub long_business_summary: Option<String>,
511 pub full_time_employees: Option<u32>,
512 pub company_officers: Vec<CompanyOfficer>,
513 pub audit_risk: Option<u16>,
514 pub board_risk: Option<u16>,
515 pub compensation_risk: Option<u16>,
516 pub share_holder_rights_risk: Option<u16>,
517 pub overall_risk: Option<u16>,
518 pub governance_epoch_date: Option<u32>,
519 pub compensation_as_of_epoch_date: Option<u32>,
520 pub ir_website: Option<String>,
521 pub max_age: Option<u32>,
522}
523
524#[derive(Deserialize, Debug)]
525#[serde(rename_all = "camelCase")]
526pub struct CompanyOfficer {
527 pub name: String,
528 pub age: Option<u32>,
529 pub title: String,
530 pub year_born: Option<u32>,
531 pub fiscal_year: Option<u32>,
532 pub total_pay: Option<ValueWrapper>,
533}
534
535#[derive(Deserialize, Debug)]
536#[serde(rename_all = "camelCase")]
537pub struct ValueWrapper {
538 pub raw: Option<i64>,
539 pub fmt: Option<String>,
540 pub long_fmt: Option<String>,
541}
542
543fn deserialize_f64_special<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
544where
545 D: Deserializer<'de>,
546{
547 let s: serde_json::Value = Deserialize::deserialize(deserializer)?;
548 match s {
549 serde_json::Value::String(ref v) if v.eq_ignore_ascii_case("infinity") => {
550 Ok(Some(f64::INFINITY))
551 }
552 serde_json::Value::String(ref v) if v.eq_ignore_ascii_case("-infinity") => {
553 Ok(Some(f64::NEG_INFINITY))
554 }
555 serde_json::Value::String(ref v) if v.eq_ignore_ascii_case("nan") => Ok(Some(f64::NAN)),
556 serde_json::Value::Number(n) => n
557 .as_f64()
558 .ok_or_else(|| serde::de::Error::custom("Invalid number"))
559 .map(Some),
560 serde_json::Value::Null => Ok(None),
561 _ => Err(serde::de::Error::custom(format!(
562 "Invalid type for f64: {:?}",
563 s
564 ))),
565 }
566}
567
568#[derive(Deserialize, Debug)]
569#[serde(rename_all = "camelCase")]
570pub struct SummaryDetail {
571 pub max_age: Option<i64>,
572 pub price_hint: Option<i64>,
573 pub previous_close: Option<f64>,
574 pub open: Option<f64>,
575 pub day_low: Option<f64>,
576 pub day_high: Option<f64>,
577 pub regular_market_previous_close: Option<f64>,
578 pub regular_market_open: Option<f64>,
579 pub regular_market_day_low: Option<f64>,
580 pub regular_market_day_high: Option<f64>,
581 pub dividend_rate: Option<f64>,
582 pub dividend_yield: Option<f64>,
583 pub ex_dividend_date: Option<i64>,
584 pub payout_ratio: Option<f64>,
585 pub five_year_avg_dividend_yield: Option<f64>,
586 pub beta: Option<f64>,
587 #[serde(
589 default,
590 deserialize_with = "deserialize_f64_special",
591 rename = "trailingPE"
592 )]
593 pub trailing_pe: Option<f64>,
594 #[serde(
595 default,
596 rename = "forwardPE",
597 deserialize_with = "deserialize_f64_special"
598 )]
599 pub forward_pe: Option<f64>,
600 pub volume: Option<u64>,
601 pub regular_market_volume: Option<u64>,
602 pub average_volume: Option<u64>,
603 #[serde(rename = "averageVolume10days")]
604 pub average_volume_10days: Option<u64>,
605 #[serde(rename = "averageDailyVolume10Day")]
606 pub average_daily_volume_10day: Option<u64>,
607 pub bid: Option<f64>,
608 pub ask: Option<f64>,
609 pub bid_size: Option<i64>,
610 pub ask_size: Option<i64>,
611 pub market_cap: Option<u64>,
612 pub fifty_two_week_low: Option<f64>,
613 pub fifty_two_week_high: Option<f64>,
614 #[serde(
615 default,
616 rename = "priceToSalesTrailing12Months",
617 deserialize_with = "deserialize_f64_special"
618 )]
619 pub price_to_sales_trailing12months: Option<f64>,
620 pub fifty_day_average: Option<f64>,
621 pub two_hundred_day_average: Option<f64>,
622 pub trailing_annual_dividend_rate: Option<f64>,
623 #[serde(default, deserialize_with = "deserialize_f64_special")]
624 pub trailing_annual_dividend_yield: Option<f64>,
625 pub currency: Option<String>,
626 pub from_currency: Option<String>,
627 pub to_currency: Option<String>,
628 pub last_market: Option<String>,
629 pub coin_market_cap_link: Option<String>,
630 pub algorithm: Option<String>,
631 pub tradeable: Option<bool>,
632 pub expire_date: Option<u32>,
633 pub strike_price: Option<u32>,
634 pub open_interest: Option<Decimal>,
635}
636
637#[derive(Deserialize, Debug)]
638#[serde(rename_all = "camelCase")]
639pub struct DefaultKeyStatistics {
640 pub max_age: Option<i64>,
641 pub price_hint: Option<u64>,
642 pub enterprise_value: Option<i64>,
643 #[serde(
644 default,
645 rename = "forwardPE",
646 deserialize_with = "deserialize_f64_special"
647 )]
648 pub forward_pe: Option<f64>,
649 pub profit_margins: Option<f64>,
650 pub float_shares: Option<u64>,
651 pub shares_outstanding: Option<u64>,
652 pub shares_short: Option<u64>,
653 pub shares_short_prior_month: Option<u64>,
654 pub shares_short_previous_month_date: Option<u64>,
655 pub date_short_interest: Option<i64>,
656 pub shares_percent_shares_out: Option<f64>,
657 pub held_percent_insiders: Option<f64>,
658 pub held_percent_institutions: Option<f64>,
659 pub short_ratio: Option<f64>,
660 pub short_percent_of_float: Option<f64>,
661 pub beta: Option<f64>,
662 pub implied_shares_outstanding: Option<u64>,
663 pub category: Option<String>,
664 pub book_value: Option<f64>,
665 pub price_to_book: Option<f64>,
666 pub fund_family: Option<String>,
667 pub fund_inception_date: Option<u32>,
668 pub legal_type: Option<String>,
669 pub last_fiscal_year_end: Option<i64>,
670 pub next_fiscal_year_end: Option<i64>,
671 pub most_recent_quarter: Option<i64>,
672 pub earnings_quarterly_growth: Option<f64>,
673 pub net_income_to_common: Option<i64>,
674 pub trailing_eps: Option<f64>,
675 pub forward_eps: Option<f64>,
676 pub last_split_factor: Option<String>,
677 pub last_split_date: Option<i64>,
678 pub enterprise_to_revenue: Option<f64>,
679 pub enterprise_to_ebitda: Option<f64>,
680 #[serde(rename = "52WeekChange")]
681 pub fifty_two_week_change: Option<f64>,
682 #[serde(rename = "SandP52WeekChange")]
683 pub sand_p_fifty_two_week_change: Option<f64>,
684 pub last_dividend_value: Option<f64>,
685 pub last_dividend_date: Option<i64>,
686 pub latest_share_class: Option<String>,
687 pub lead_investor: Option<String>,
688}
689
690#[derive(Deserialize, Debug)]
691#[serde(rename_all = "camelCase")]
692pub struct QuoteType {
693 pub exchange: Option<String>,
694 pub quote_type: Option<String>,
695 pub symbol: Option<String>,
696 pub underlying_symbol: Option<String>,
697 pub short_name: Option<String>,
698 pub long_name: Option<String>,
699 pub first_trade_date_epoch_utc: Option<i64>,
700 #[serde(rename = "timeZoneFullName")]
701 pub timezone_full_name: Option<String>,
702 #[serde(rename = "timeZoneShortName")]
703 pub timezone_short_name: Option<String>,
704 pub uuid: Option<String>,
705 pub message_board_id: Option<String>,
706 pub gmt_off_set_milliseconds: Option<i64>,
707 pub max_age: Option<i64>,
708}
709
710#[derive(Deserialize, Debug)]
711#[serde(rename_all = "camelCase")]
712pub struct FinancialData {
713 pub max_age: Option<i64>,
714 pub current_price: Option<f64>,
715 pub target_high_price: Option<f64>,
716 pub target_low_price: Option<f64>,
717 pub target_mean_price: Option<f64>,
718 pub target_median_price: Option<f64>,
719 pub recommendation_mean: Option<f64>,
720 pub recommendation_key: Option<String>,
721 pub number_of_analyst_opinions: Option<u64>,
722 pub total_cash: Option<u64>,
723 pub total_cash_per_share: Option<f64>,
724 pub ebitda: Option<i64>,
725 pub total_debt: Option<u64>,
726 pub quick_ratio: Option<f64>,
727 pub current_ratio: Option<f64>,
728 pub total_revenue: Option<i64>,
729 pub debt_to_equity: Option<f64>,
730 pub revenue_per_share: Option<f64>,
731 pub return_on_assets: Option<f64>,
732 pub return_on_equity: Option<f64>,
733 pub gross_profits: Option<i64>,
734 pub free_cashflow: Option<i64>,
735 pub operating_cashflow: Option<i64>,
736 pub earnings_growth: Option<f64>,
737 pub revenue_growth: Option<f64>,
738 pub gross_margins: Option<f64>,
739 pub ebitda_margins: Option<f64>,
740 pub operating_margins: Option<f64>,
741 pub profit_margins: Option<f64>,
742 pub financial_currency: Option<String>,
743}
744
745#[derive(Deserialize, Debug, Clone)]
747pub struct YEarningsResponse {
748 pub finance: YEarningsFinance,
749}
750
751#[derive(Deserialize, Debug, Clone)]
752pub struct YEarningsFinance {
753 pub result: Vec<YEarningsResult>,
754 pub error: Option<serde_json::Value>,
755}
756
757#[derive(Deserialize, Debug, Clone)]
758pub struct YEarningsResult {
759 pub documents: Vec<YEarningsDocument>,
760}
761
762#[derive(Deserialize, Debug, Clone)]
763pub struct YEarningsDocument {
764 pub columns: Vec<YEarningsColumn>,
765 pub rows: Vec<Vec<serde_json::Value>>,
766}
767
768#[derive(Deserialize, Debug, Clone)]
769pub struct YEarningsColumn {
770 pub label: String,
771}
772
773#[derive(Debug, Clone, PartialEq)]
774pub struct FinancialEvent {
775 pub earnings_date: OffsetDateTime,
776 pub event_type: String,
777 pub eps_estimate: Option<f64>,
778 pub reported_eps: Option<f64>,
779 pub surprise_percent: Option<f64>,
780 pub timezone: Option<String>,
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[test]
788 fn test_deserialize_period_info() {
789 let period_info_json = r#"
790 {
791 "timezone": "EST",
792 "start": 1705501800,
793 "end": 1705525200,
794 "gmtoffset": -18000
795 }
796 "#;
797 let period_info_expected = PeriodInfo {
798 timezone: "EST".to_string(),
799 start: 1705501800,
800 end: 1705525200,
801 gmtoffset: -18000,
802 };
803 let period_info_deserialized: PeriodInfo = serde_json::from_str(period_info_json).unwrap();
804 assert_eq!(&period_info_deserialized, &period_info_expected);
805 }
806
807 #[test]
808 fn test_deserialize_trading_periods_simple() {
809 let trading_periods_json = r#"
810 [
811 [
812 {
813 "timezone": "EST",
814 "start": 1705501800,
815 "end": 1705525200,
816 "gmtoffset": -18000
817 }
818
819 ]
820 ]
821 "#;
822 let trading_periods_expected = TradingPeriods {
823 pre: None,
824 regular: Some(vec![vec![PeriodInfo {
825 timezone: "EST".to_string(),
826 start: 1705501800,
827 end: 1705525200,
828 gmtoffset: -18000,
829 }]]),
830 post: None,
831 };
832 let trading_periods_deserialized: TradingPeriods =
833 serde_json::from_str(trading_periods_json).unwrap();
834 assert_eq!(&trading_periods_expected, &trading_periods_deserialized);
835 }
836
837 #[test]
838 fn test_deserialize_trading_periods_complex_regular_only() {
839 let trading_periods_json = r#"
840 {
841 "regular": [
842 [
843 {
844 "timezone": "EST",
845 "start": 1705501800,
846 "end": 1705525200,
847 "gmtoffset": -18000
848 }
849 ]
850 ]
851 }
852 "#;
853 let trading_periods_expected = TradingPeriods {
854 pre: None,
855 regular: Some(vec![vec![PeriodInfo {
856 timezone: "EST".to_string(),
857 start: 1705501800,
858 end: 1705525200,
859 gmtoffset: -18000,
860 }]]),
861 post: None,
862 };
863 let trading_periods_deserialized: TradingPeriods =
864 serde_json::from_str(trading_periods_json).unwrap();
865 assert_eq!(&trading_periods_expected, &trading_periods_deserialized);
866 }
867
868 #[test]
869 fn test_deserialize_trading_periods_complex() {
870 let trading_periods_json = r#"
871 {
872 "pre": [
873 [
874 {
875 "timezone": "EST",
876 "start": 1705482000,
877 "end": 1705501800,
878 "gmtoffset": -18000
879 }
880 ]
881 ],
882 "post": [
883 [
884 {
885 "timezone": "EST",
886 "start": 1705525200,
887 "end": 1705539600,
888 "gmtoffset": -18000
889 }
890 ]
891 ],
892 "regular": [
893 [
894 {
895 "timezone": "EST",
896 "start": 1705501800,
897 "end": 1705525200,
898 "gmtoffset": -18000
899 }
900 ]
901 ]
902 }
903 "#;
904 let trading_periods_expected = TradingPeriods {
905 pre: Some(vec![vec![PeriodInfo {
906 timezone: "EST".to_string(),
907 start: 1705482000,
908 end: 1705501800,
909 gmtoffset: -18000,
910 }]]),
911 regular: Some(vec![vec![PeriodInfo {
912 timezone: "EST".to_string(),
913 start: 1705501800,
914 end: 1705525200,
915 gmtoffset: -18000,
916 }]]),
917 post: Some(vec![vec![PeriodInfo {
918 timezone: "EST".to_string(),
919 start: 1705525200,
920 end: 1705539600,
921 gmtoffset: -18000,
922 }]]),
923 };
924 let trading_periods_deserialized: TradingPeriods =
925 serde_json::from_str(trading_periods_json).unwrap();
926 assert_eq!(&trading_periods_expected, &trading_periods_deserialized);
927 }
928
929 #[test]
930 fn test_deserialize_f64_special() {
931 #[derive(Debug, Deserialize)]
932 #[allow(dead_code)]
933 struct MyStruct {
934 #[serde(default, deserialize_with = "deserialize_f64_special")]
935 bad: Option<f64>,
936 good: Option<f64>,
937 }
938
939 let json_data = r#"{ "bad": "Infinity", "good": 999.999 }"#;
940 let _: MyStruct = serde_json::from_str(json_data).unwrap();
941
942 let json_data = r#"{ "bad": 123.45 }"#;
943 let _: MyStruct = serde_json::from_str(json_data).unwrap();
944
945 let json_data = r#"{ "bad": null }"#;
946 let _: MyStruct = serde_json::from_str(json_data).unwrap();
947
948 let json_data = r#"{ "bad": "NaN" }"#;
949 let _: MyStruct = serde_json::from_str(json_data).unwrap();
950
951 let json_data = r#"{ "bad": "-Infinity" }"#;
952 let _: MyStruct = serde_json::from_str(json_data).unwrap();
953
954 let json_data = r#"{ }"#;
955 let _: MyStruct = serde_json::from_str(json_data).unwrap();
956 }
957}