Skip to main content

finance_query/adapters/fmp/
estimates.rs

1//! Analyst estimates, recommendations, earnings surprises, grades, and transcripts.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9use super::models::Period;
10
11// ============================================================================
12// Response types
13// ============================================================================
14
15/// Analyst estimate entry.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[non_exhaustive]
18pub struct AnalystEstimate {
19    /// Ticker symbol.
20    pub symbol: Option<String>,
21    /// Date.
22    pub date: Option<String>,
23    /// Estimated revenue low.
24    #[serde(rename = "estimatedRevenueLow")]
25    pub estimated_revenue_low: Option<f64>,
26    /// Estimated revenue high.
27    #[serde(rename = "estimatedRevenueHigh")]
28    pub estimated_revenue_high: Option<f64>,
29    /// Estimated revenue avg.
30    #[serde(rename = "estimatedRevenueAvg")]
31    pub estimated_revenue_avg: Option<f64>,
32    /// Estimated EBITDA low.
33    #[serde(rename = "estimatedEbitdaLow")]
34    pub estimated_ebitda_low: Option<f64>,
35    /// Estimated EBITDA high.
36    #[serde(rename = "estimatedEbitdaHigh")]
37    pub estimated_ebitda_high: Option<f64>,
38    /// Estimated EBITDA avg.
39    #[serde(rename = "estimatedEbitdaAvg")]
40    pub estimated_ebitda_avg: Option<f64>,
41    /// Estimated EPS avg.
42    #[serde(rename = "estimatedEpsAvg")]
43    pub estimated_eps_avg: Option<f64>,
44    /// Estimated EPS high.
45    #[serde(rename = "estimatedEpsHigh")]
46    pub estimated_eps_high: Option<f64>,
47    /// Estimated EPS low.
48    #[serde(rename = "estimatedEpsLow")]
49    pub estimated_eps_low: Option<f64>,
50    /// Number of analysts for revenue.
51    #[serde(rename = "numberAnalystEstimatedRevenue")]
52    pub number_analyst_estimated_revenue: Option<i32>,
53    /// Number of analysts for EPS.
54    #[serde(rename = "numberAnalystsEstimatedEps")]
55    pub number_analysts_estimated_eps: Option<i32>,
56}
57
58/// Analyst recommendation entry.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[non_exhaustive]
61pub struct AnalystRecommendation {
62    /// Ticker symbol.
63    pub symbol: Option<String>,
64    /// Date.
65    pub date: Option<String>,
66    /// Analyst ratings buy count.
67    #[serde(rename = "analystRatingsBuy")]
68    pub analyst_ratings_buy: Option<i32>,
69    /// Analyst ratings hold count.
70    #[serde(rename = "analystRatingsHold")]
71    pub analyst_ratings_hold: Option<i32>,
72    /// Analyst ratings sell count.
73    #[serde(rename = "analystRatingsSell")]
74    pub analyst_ratings_sell: Option<i32>,
75    /// Analyst ratings strong buy count.
76    #[serde(rename = "analystRatingsStrongBuy")]
77    pub analyst_ratings_strong_buy: Option<i32>,
78    /// Analyst ratings strong sell count.
79    #[serde(rename = "analystRatingsStrongSell")]
80    pub analyst_ratings_strong_sell: Option<i32>,
81}
82
83/// Earnings surprise entry.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[non_exhaustive]
86pub struct EarningsSurprise {
87    /// Date.
88    pub date: Option<String>,
89    /// Ticker symbol.
90    pub symbol: Option<String>,
91    /// Actual earning result.
92    #[serde(rename = "actualEarningResult")]
93    pub actual_earning_result: Option<f64>,
94    /// Estimated earning.
95    #[serde(rename = "estimatedEarning")]
96    pub estimated_earning: Option<f64>,
97}
98
99/// Stock grade entry.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[non_exhaustive]
102pub struct StockGrade {
103    /// Ticker symbol.
104    pub symbol: Option<String>,
105    /// Date.
106    pub date: Option<String>,
107    /// Grading company.
108    #[serde(rename = "gradingCompany")]
109    pub grading_company: Option<String>,
110    /// Previous grade.
111    #[serde(rename = "previousGrade")]
112    pub previous_grade: Option<String>,
113    /// New grade.
114    #[serde(rename = "newGrade")]
115    pub new_grade: Option<String>,
116}
117
118/// Earnings call transcript entry.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[non_exhaustive]
121pub struct EarningsTranscript {
122    /// Ticker symbol.
123    pub symbol: Option<String>,
124    /// Quarter.
125    pub quarter: Option<i32>,
126    /// Year.
127    pub year: Option<i32>,
128    /// Date.
129    pub date: Option<String>,
130    /// Transcript content.
131    pub content: Option<String>,
132}
133
134/// Earnings transcript list entry (available transcripts).
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[non_exhaustive]
137pub struct EarningsTranscriptRef {
138    /// Ticker symbol.
139    pub symbol: Option<String>,
140    /// Quarter.
141    pub quarter: Option<i32>,
142    /// Year.
143    pub year: Option<i32>,
144    /// Date.
145    pub date: Option<String>,
146}
147
148// ============================================================================
149// Public API
150// ============================================================================
151
152/// Fetch analyst estimates for a symbol.
153///
154/// * `period` - Annual or Quarter
155/// * `limit` - Number of results
156pub async fn analyst_estimates(
157    symbol: &str,
158    period: Period,
159    limit: u32,
160) -> Result<Vec<AnalystEstimate>> {
161    let client = build_client()?;
162    let path = format!("/api/v3/analyst-estimates/{}", encode_path_segment(symbol));
163    let limit_str = limit.to_string();
164    client
165        .get(&path, &[("period", period.as_str()), ("limit", &limit_str)])
166        .await
167}
168
169/// Fetch analyst stock recommendations.
170pub async fn analyst_recommendations(symbol: &str) -> Result<Vec<AnalystRecommendation>> {
171    let client = build_client()?;
172    let path = format!(
173        "/api/v3/analyst-stock-recommendations/{}",
174        encode_path_segment(symbol)
175    );
176    client.get(&path, &[]).await
177}
178
179/// Fetch earnings surprises for a symbol.
180pub async fn earnings_surprises(symbol: &str) -> Result<Vec<EarningsSurprise>> {
181    let client = build_client()?;
182    let path = format!("/api/v3/earnings-surprises/{}", encode_path_segment(symbol));
183    client.get(&path, &[]).await
184}
185
186/// Fetch stock grade history for a symbol.
187pub async fn stock_grade(symbol: &str, limit: u32) -> Result<Vec<StockGrade>> {
188    let client = build_client()?;
189    let path = format!("/api/v3/grade/{}", encode_path_segment(symbol));
190    let limit_str = limit.to_string();
191    client.get(&path, &[("limit", &*limit_str)]).await
192}
193
194/// Fetch an earnings call transcript.
195///
196/// * `quarter` - Quarter number (1-4)
197/// * `year` - Year (e.g., 2024)
198pub async fn earnings_transcript(
199    symbol: &str,
200    quarter: u32,
201    year: u32,
202) -> Result<Vec<EarningsTranscript>> {
203    let client = build_client()?;
204    let path = format!(
205        "/api/v3/earning_call_transcript/{}",
206        encode_path_segment(symbol)
207    );
208    let q = quarter.to_string();
209    let y = year.to_string();
210    client.get(&path, &[("quarter", &*q), ("year", &*y)]).await
211}
212
213/// Fetch a list of available earnings transcripts for a symbol.
214pub async fn earnings_transcript_list(symbol: &str) -> Result<Vec<EarningsTranscriptRef>> {
215    let client = build_client()?;
216    client
217        .get("/api/v4/earning_call_transcript", &[("symbol", symbol)])
218        .await
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[tokio::test]
226    async fn test_analyst_estimates_mock() {
227        let mut server = mockito::Server::new_async().await;
228        let _mock = server
229            .mock("GET", "/api/v3/analyst-estimates/AAPL")
230            .match_query(mockito::Matcher::AllOf(vec![
231                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
232                mockito::Matcher::UrlEncoded("period".into(), "quarter".into()),
233                mockito::Matcher::UrlEncoded("limit".into(), "4".into()),
234            ]))
235            .with_status(200)
236            .with_body(
237                serde_json::json!([
238                    {
239                        "symbol": "AAPL",
240                        "date": "2024-03-31",
241                        "estimatedRevenueAvg": 90000000000.0,
242                        "estimatedEpsAvg": 1.50,
243                        "numberAnalystEstimatedRevenue": 30,
244                        "numberAnalystsEstimatedEps": 28
245                    }
246                ])
247                .to_string(),
248            )
249            .create_async()
250            .await;
251
252        let client = super::super::build_test_client(&server.url()).unwrap();
253        let resp: Vec<AnalystEstimate> = client
254            .get(
255                "/api/v3/analyst-estimates/AAPL",
256                &[("period", "quarter"), ("limit", "4")],
257            )
258            .await
259            .unwrap();
260        assert_eq!(resp.len(), 1);
261        assert_eq!(resp[0].symbol.as_deref(), Some("AAPL"));
262        assert!((resp[0].estimated_eps_avg.unwrap() - 1.50).abs() < 0.01);
263    }
264
265    #[tokio::test]
266    async fn test_earnings_surprises_mock() {
267        let mut server = mockito::Server::new_async().await;
268        let _mock = server
269            .mock("GET", "/api/v3/earnings-surprises/AAPL")
270            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
271                "apikey".into(),
272                "test-key".into(),
273            )]))
274            .with_status(200)
275            .with_body(
276                serde_json::json!([
277                    {
278                        "date": "2024-01-25",
279                        "symbol": "AAPL",
280                        "actualEarningResult": 2.18,
281                        "estimatedEarning": 2.10
282                    }
283                ])
284                .to_string(),
285            )
286            .create_async()
287            .await;
288
289        let client = super::super::build_test_client(&server.url()).unwrap();
290        let resp: Vec<EarningsSurprise> = client
291            .get("/api/v3/earnings-surprises/AAPL", &[])
292            .await
293            .unwrap();
294        assert_eq!(resp.len(), 1);
295        assert!((resp[0].actual_earning_result.unwrap() - 2.18).abs() < 0.01);
296    }
297}