yahoo_finance_api/
quotes.rs

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    /// Return the latest valid quote
79    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    /// This method retrieves information about the splits that might have
119    /// occured during the considered time period
120    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    /// This method retrieves information about the dividends that have
137    /// been recorded during the considered time period.
138    ///
139    /// Note: Date is the ex-dividend date)
140    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    /// This method retrieves information about the capital gains that might have
157    /// occured during the considered time period (available only for Mutual Funds)
158    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/// Struct for single quote
176#[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        // reject if close is not set
354
355        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/// This structure simply models a split that has occured.
419#[derive(Deserialize, Debug, Clone)]
420pub struct Split {
421    /// This is the date (timestamp) when the split occured
422    pub date: i64,
423    /// Numerator of the split. For instance a 1:5 split means you get 5 share
424    /// wherever you had one before the split. (Here the numerator is 1 and
425    /// denom is 5). A reverse split is considered as nothing but a regular
426    /// split with a numerator > denom.
427    pub numerator: Decimal,
428    /// Denominator of the split. For instance a 1:5 split means you get 5 share
429    /// wherever you had one before the split. (Here the numerator is 1 and
430    /// denom is 5). A reverse split is considered as nothing but a regular
431    /// split with a numerator > denom.
432    pub denominator: Decimal,
433    /// A textual representation of the split.
434    #[serde(rename = "splitRatio")]
435    pub split_ratio: String,
436}
437
438/// This structure simply models a dividend which has been recorded.
439#[derive(Deserialize, Debug, Clone)]
440pub struct Dividend {
441    /// This is the price of the dividend
442    pub amount: Decimal,
443    /// This is the ex-dividend date as UNIX timestamp
444    pub date: i64,
445}
446
447/// This structure simply models a capital gain which has been recorded.
448#[derive(Deserialize, Debug, Clone)]
449pub struct CapitalGain {
450    /// This is the amount of capital gain distributed by the fund
451    pub amount: f64,
452    /// This is the recorded date of the capital gain
453    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    /// The trailing_pe field may contain the string "Infinity" instead of f64, in which case we return f64::MAX
588    #[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// Структуры для earnings dates response
746#[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}