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}