Skip to main content

finance_query/models/calendar/
market.rs

1//! Market-wide calendar models.
2//!
3//! Returned by the [`Capability::CALENDAR`](crate::Capability::CALENDAR) route
4//! via [`Providers::calendar`](crate::Providers::calendar). These span the whole
5//! market over a date range, unlike [`CalendarEvent`](super::CalendarEvent),
6//! which builds a per-symbol timeline from already-fetched Yahoo quote data.
7
8use serde::{Deserialize, Serialize};
9
10/// Which market-wide calendar to fetch.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum CalendarKind {
14    /// Earnings releases.
15    Earnings,
16    /// Initial public offerings.
17    Ipo,
18    /// Dividend payments.
19    Dividend,
20    /// Stock splits.
21    Split,
22    /// Macro-economic releases.
23    Economic,
24    /// Market holidays and early closes.
25    MarketHoliday,
26    /// Live exchange open/closed status — a snapshot, not a dated event, so
27    /// providers serving it ignore the `from`/`to` range.
28    MarketStatus,
29}
30
31impl CalendarKind {
32    /// The [`Operation`](crate::providers::Operation) this kind dispatches as.
33    pub(crate) fn operation(self) -> crate::providers::Operation {
34        use crate::providers::Operation;
35        match self {
36            Self::Earnings => Operation::EarningsCalendar,
37            Self::Ipo => Operation::IpoCalendar,
38            Self::Dividend => Operation::DividendCalendar,
39            Self::Split => Operation::SplitCalendar,
40            Self::Economic => Operation::EconomicCalendar,
41            Self::MarketHoliday => Operation::HolidayCalendar,
42            Self::MarketStatus => Operation::MarketStatus,
43        }
44    }
45}
46
47/// One market-wide calendar entry.
48///
49/// The `kind`-specific payload lives in [`MarketCalendarEntry::detail`].
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[non_exhaustive]
52pub struct MarketCalendarEntry {
53    /// Ticker symbol. `None` for economic releases, which are market-wide.
54    pub symbol: Option<String>,
55    /// Event date as reported by the provider (`YYYY-MM-DD`, or a timestamp
56    /// string for economic releases).
57    pub date: Option<String>,
58    /// Event-specific payload.
59    pub detail: CalendarDetail,
60}
61
62/// The event-specific payload of a [`MarketCalendarEntry`].
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[non_exhaustive]
65#[serde(tag = "type", rename_all = "snake_case")]
66pub enum CalendarDetail {
67    /// An earnings release, with actuals once reported.
68    ///
69    /// Providers report different subsets; anything a provider lacks is `None`.
70    #[non_exhaustive]
71    Earnings {
72        /// Reported EPS, if already released.
73        eps: Option<f64>,
74        /// Consensus EPS estimate.
75        eps_estimated: Option<f64>,
76        /// Reported revenue, if already released.
77        revenue: Option<f64>,
78        /// Consensus revenue estimate.
79        revenue_estimated: Option<f64>,
80        /// End of the fiscal period being reported (`YYYY-MM-DD`).
81        fiscal_date_ending: Option<String>,
82        /// Time of day for the release (e.g. `"amc"`, `"bmo"`).
83        time: Option<String>,
84        /// Company name.
85        company: Option<String>,
86        /// Number of analyst estimates behind the consensus.
87        estimate_count: Option<u32>,
88        /// Reported EPS against the consensus, in percent, once released.
89        surprise_percent: Option<f64>,
90        /// EPS reported for the same quarter a year earlier.
91        previous_eps: Option<f64>,
92        /// Date of the same quarter's release a year earlier (`YYYY-MM-DD`).
93        previous_report_date: Option<String>,
94        /// Market capitalisation when the calendar was read.
95        market_cap: Option<f64>,
96    },
97    /// An initial public offering.
98    Ipo {
99        /// Company name.
100        company: Option<String>,
101        /// Listing exchange.
102        exchange: Option<String>,
103        /// Corporate action description (e.g. `"expected"`, `"priced"`).
104        actions: Option<String>,
105        /// Shares offered.
106        shares: Option<f64>,
107        /// Offering price range as reported (e.g. `"17.00-19.00"`).
108        price_range: Option<String>,
109        /// Market capitalisation at offering.
110        market_cap: Option<f64>,
111    },
112    /// A dividend payment.
113    Dividend {
114        /// Dividend amount per share.
115        dividend: Option<f64>,
116        /// Split-adjusted dividend amount per share.
117        adj_dividend: Option<f64>,
118        /// Record date (`YYYY-MM-DD`).
119        record_date: Option<String>,
120        /// Payment date (`YYYY-MM-DD`).
121        payment_date: Option<String>,
122        /// Declaration date (`YYYY-MM-DD`).
123        declaration_date: Option<String>,
124    },
125    /// A stock split.
126    Split {
127        /// Split ratio numerator (new shares).
128        numerator: Option<f64>,
129        /// Split ratio denominator (old shares).
130        denominator: Option<f64>,
131    },
132    /// A market holiday or early close.
133    MarketHoliday {
134        /// Holiday name (e.g. `"Thanksgiving"`).
135        name: Option<String>,
136        /// Exchange the holiday applies to (e.g. `"NYSE"`).
137        exchange: Option<String>,
138        /// Status (e.g. `"closed"`, `"early-close"`).
139        status: Option<String>,
140        /// Open time, when the exchange opens late or closes early.
141        open: Option<String>,
142        /// Close time, when the exchange closes early.
143        close: Option<String>,
144    },
145    /// A macro-economic release.
146    Economic {
147        /// Event name (e.g. `"CPI m/m"`).
148        event: Option<String>,
149        /// Country code or name.
150        country: Option<String>,
151        /// Reported value, if already released.
152        actual: Option<f64>,
153        /// Previous period's value.
154        previous: Option<f64>,
155        /// Consensus estimate.
156        estimate: Option<f64>,
157        /// Absolute change from the previous value.
158        change: Option<f64>,
159        /// Percentage change from the previous value.
160        change_percentage: Option<f64>,
161        /// Provider-assigned impact rating (e.g. `"High"`).
162        impact: Option<String>,
163    },
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn earnings_json_from_before_the_added_fields_still_deserializes() {
172        let entry: MarketCalendarEntry = serde_json::from_value(serde_json::json!({
173            "symbol": "AAPL",
174            "date": "2026-07-30",
175            "detail": {
176                "type": "earnings",
177                "eps": 1.62,
178                "eps_estimated": 1.5,
179                "revenue": null,
180                "revenue_estimated": null,
181                "fiscal_date_ending": "2026-06-30",
182                "time": "amc"
183            }
184        }))
185        .unwrap();
186        let CalendarDetail::Earnings {
187            eps,
188            time,
189            company,
190            market_cap,
191            ..
192        } = entry.detail
193        else {
194            panic!("expected Earnings detail");
195        };
196        assert_eq!((eps, time.as_deref()), (Some(1.62), Some("amc")));
197        assert_eq!((company, market_cap), (None, None));
198    }
199}