Skip to main content

finance_query/adapters/fmp/
calendars.rs

1//! Calendar endpoints: earnings, IPO, stock split, dividend, and economic calendars.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6
7use super::build_client;
8
9// ============================================================================
10// Response types
11// ============================================================================
12
13/// Earnings calendar entry.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[non_exhaustive]
16pub struct EarningsCalendarEntry {
17    /// Date (YYYY-MM-DD).
18    pub date: Option<String>,
19    /// Ticker symbol.
20    pub symbol: Option<String>,
21    /// Estimated EPS.
22    pub eps: Option<f64>,
23    /// Estimated EPS.
24    #[serde(rename = "epsEstimated")]
25    pub eps_estimated: Option<f64>,
26    /// Time of announcement (bmo = before market open, amc = after market close).
27    pub time: Option<String>,
28    /// Revenue.
29    pub revenue: Option<f64>,
30    /// Estimated revenue.
31    #[serde(rename = "revenueEstimated")]
32    pub revenue_estimated: Option<f64>,
33    /// Fiscal date ending.
34    #[serde(rename = "fiscalDateEnding")]
35    pub fiscal_date_ending: Option<String>,
36    /// Updated from date.
37    #[serde(rename = "updatedFromDate")]
38    pub updated_from_date: Option<String>,
39}
40
41/// IPO calendar entry.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[non_exhaustive]
44pub struct IpoCalendarEntry {
45    /// Date (YYYY-MM-DD).
46    pub date: Option<String>,
47    /// Company name.
48    pub company: Option<String>,
49    /// Ticker symbol.
50    pub symbol: Option<String>,
51    /// Exchange.
52    pub exchange: Option<String>,
53    /// Number of actions (shares offered).
54    pub actions: Option<String>,
55    /// Shares offered.
56    pub shares: Option<f64>,
57    /// Price range.
58    #[serde(rename = "priceRange")]
59    pub price_range: Option<String>,
60    /// Market cap.
61    #[serde(rename = "marketCap")]
62    pub market_cap: Option<f64>,
63}
64
65/// Stock split calendar entry.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[non_exhaustive]
68pub struct StockSplitCalendarEntry {
69    /// Date (YYYY-MM-DD).
70    pub date: Option<String>,
71    /// Ticker symbol.
72    pub symbol: Option<String>,
73    /// Numerator.
74    pub numerator: Option<f64>,
75    /// Denominator.
76    pub denominator: Option<f64>,
77}
78
79/// Dividend calendar entry.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct DividendCalendarEntry {
83    /// Date (YYYY-MM-DD).
84    pub date: Option<String>,
85    /// Ticker symbol.
86    pub symbol: Option<String>,
87    /// Dividend amount.
88    pub dividend: Option<f64>,
89    /// Adjusted dividend.
90    #[serde(rename = "adjDividend")]
91    pub adj_dividend: Option<f64>,
92    /// Record date.
93    #[serde(rename = "recordDate")]
94    pub record_date: Option<String>,
95    /// Payment date.
96    #[serde(rename = "paymentDate")]
97    pub payment_date: Option<String>,
98    /// Declaration date.
99    #[serde(rename = "declarationDate")]
100    pub declaration_date: Option<String>,
101}
102
103/// Economic calendar entry.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[non_exhaustive]
106pub struct EconomicCalendarEntry {
107    /// Event name.
108    pub event: Option<String>,
109    /// Date (YYYY-MM-DD).
110    pub date: Option<String>,
111    /// Country.
112    pub country: Option<String>,
113    /// Actual value.
114    pub actual: Option<f64>,
115    /// Previous value.
116    pub previous: Option<f64>,
117    /// Change value.
118    pub change: Option<f64>,
119    /// Change percentage.
120    #[serde(rename = "changePercentage")]
121    pub change_percentage: Option<f64>,
122    /// Estimate.
123    pub estimate: Option<f64>,
124    /// Impact level.
125    pub impact: Option<String>,
126}
127
128// ============================================================================
129// Public API
130// ============================================================================
131
132/// Fetch earnings calendar within a date range.
133///
134/// * `from` - Start date (YYYY-MM-DD)
135/// * `to` - End date (YYYY-MM-DD)
136pub async fn earnings_calendar(from: &str, to: &str) -> Result<Vec<EarningsCalendarEntry>> {
137    let client = build_client()?;
138    client
139        .get("/api/v3/earning_calendar", &[("from", from), ("to", to)])
140        .await
141}
142
143/// Fetch IPO calendar within a date range.
144pub async fn ipo_calendar(from: &str, to: &str) -> Result<Vec<IpoCalendarEntry>> {
145    let client = build_client()?;
146    client
147        .get("/api/v3/ipo_calendar", &[("from", from), ("to", to)])
148        .await
149}
150
151/// Fetch stock split calendar within a date range.
152pub async fn stock_split_calendar(from: &str, to: &str) -> Result<Vec<StockSplitCalendarEntry>> {
153    let client = build_client()?;
154    client
155        .get(
156            "/api/v3/stock_split_calendar",
157            &[("from", from), ("to", to)],
158        )
159        .await
160}
161
162/// Fetch dividend calendar within a date range.
163pub async fn dividend_calendar(from: &str, to: &str) -> Result<Vec<DividendCalendarEntry>> {
164    let client = build_client()?;
165    client
166        .get(
167            "/api/v3/stock_dividend_calendar",
168            &[("from", from), ("to", to)],
169        )
170        .await
171}
172
173/// Fetch economic calendar within a date range.
174pub async fn economic_calendar(from: &str, to: &str) -> Result<Vec<EconomicCalendarEntry>> {
175    let client = build_client()?;
176    client
177        .get("/api/v3/economic_calendar", &[("from", from), ("to", to)])
178        .await
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[tokio::test]
186    async fn test_earnings_calendar_mock() {
187        let mut server = mockito::Server::new_async().await;
188        let _mock = server
189            .mock("GET", "/api/v3/earning_calendar")
190            .match_query(mockito::Matcher::AllOf(vec![
191                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
192                mockito::Matcher::UrlEncoded("from".into(), "2024-01-01".into()),
193                mockito::Matcher::UrlEncoded("to".into(), "2024-01-31".into()),
194            ]))
195            .with_status(200)
196            .with_body(
197                serde_json::json!([
198                    {
199                        "date": "2024-01-25",
200                        "symbol": "MSFT",
201                        "eps": 2.93,
202                        "epsEstimated": 2.78,
203                        "time": "amc",
204                        "revenue": 62020000000.0,
205                        "revenueEstimated": 61100000000.0
206                    }
207                ])
208                .to_string(),
209            )
210            .create_async()
211            .await;
212
213        let client = super::super::build_test_client(&server.url()).unwrap();
214        let resp: Vec<EarningsCalendarEntry> = client
215            .get(
216                "/api/v3/earning_calendar",
217                &[("from", "2024-01-01"), ("to", "2024-01-31")],
218            )
219            .await
220            .unwrap();
221        assert_eq!(resp.len(), 1);
222        assert_eq!(resp[0].symbol.as_deref(), Some("MSFT"));
223        assert!((resp[0].eps.unwrap() - 2.93).abs() < 0.01);
224    }
225
226    #[tokio::test]
227    async fn test_economic_calendar_mock() {
228        let mut server = mockito::Server::new_async().await;
229        let _mock = server
230            .mock("GET", "/api/v3/economic_calendar")
231            .match_query(mockito::Matcher::AllOf(vec![
232                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
233                mockito::Matcher::UrlEncoded("from".into(), "2024-01-01".into()),
234                mockito::Matcher::UrlEncoded("to".into(), "2024-01-31".into()),
235            ]))
236            .with_status(200)
237            .with_body(
238                serde_json::json!([
239                    {
240                        "event": "CPI",
241                        "date": "2024-01-11",
242                        "country": "US",
243                        "actual": 3.4,
244                        "previous": 3.1,
245                        "estimate": 3.2,
246                        "impact": "High"
247                    }
248                ])
249                .to_string(),
250            )
251            .create_async()
252            .await;
253
254        let client = super::super::build_test_client(&server.url()).unwrap();
255        let resp: Vec<EconomicCalendarEntry> = client
256            .get(
257                "/api/v3/economic_calendar",
258                &[("from", "2024-01-01"), ("to", "2024-01-31")],
259            )
260            .await
261            .unwrap();
262        assert_eq!(resp.len(), 1);
263        assert_eq!(resp[0].event.as_deref(), Some("CPI"));
264    }
265}