finance_query/adapters/fmp/
estimates.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
17#[non_exhaustive]
18pub struct AnalystEstimate {
19 pub symbol: Option<String>,
21 pub date: Option<String>,
23 #[serde(rename = "estimatedRevenueLow")]
25 pub estimated_revenue_low: Option<f64>,
26 #[serde(rename = "estimatedRevenueHigh")]
28 pub estimated_revenue_high: Option<f64>,
29 #[serde(rename = "estimatedRevenueAvg")]
31 pub estimated_revenue_avg: Option<f64>,
32 #[serde(rename = "estimatedEbitdaLow")]
34 pub estimated_ebitda_low: Option<f64>,
35 #[serde(rename = "estimatedEbitdaHigh")]
37 pub estimated_ebitda_high: Option<f64>,
38 #[serde(rename = "estimatedEbitdaAvg")]
40 pub estimated_ebitda_avg: Option<f64>,
41 #[serde(rename = "estimatedEpsAvg")]
43 pub estimated_eps_avg: Option<f64>,
44 #[serde(rename = "estimatedEpsHigh")]
46 pub estimated_eps_high: Option<f64>,
47 #[serde(rename = "estimatedEpsLow")]
49 pub estimated_eps_low: Option<f64>,
50 #[serde(rename = "numberAnalystEstimatedRevenue")]
52 pub number_analyst_estimated_revenue: Option<i32>,
53 #[serde(rename = "numberAnalystsEstimatedEps")]
55 pub number_analysts_estimated_eps: Option<i32>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60#[non_exhaustive]
61pub struct AnalystRecommendation {
62 pub symbol: Option<String>,
64 pub date: Option<String>,
66 #[serde(rename = "analystRatingsBuy")]
68 pub analyst_ratings_buy: Option<i32>,
69 #[serde(rename = "analystRatingsHold")]
71 pub analyst_ratings_hold: Option<i32>,
72 #[serde(rename = "analystRatingsSell")]
74 pub analyst_ratings_sell: Option<i32>,
75 #[serde(rename = "analystRatingsStrongBuy")]
77 pub analyst_ratings_strong_buy: Option<i32>,
78 #[serde(rename = "analystRatingsStrongSell")]
80 pub analyst_ratings_strong_sell: Option<i32>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85#[non_exhaustive]
86pub struct EarningsSurprise {
87 pub date: Option<String>,
89 pub symbol: Option<String>,
91 #[serde(rename = "actualEarningResult")]
93 pub actual_earning_result: Option<f64>,
94 #[serde(rename = "estimatedEarning")]
96 pub estimated_earning: Option<f64>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[non_exhaustive]
102pub struct StockGrade {
103 pub symbol: Option<String>,
105 pub date: Option<String>,
107 #[serde(rename = "gradingCompany")]
109 pub grading_company: Option<String>,
110 #[serde(rename = "previousGrade")]
112 pub previous_grade: Option<String>,
113 #[serde(rename = "newGrade")]
115 pub new_grade: Option<String>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120#[non_exhaustive]
121pub struct EarningsTranscript {
122 pub symbol: Option<String>,
124 pub quarter: Option<i32>,
126 pub year: Option<i32>,
128 pub date: Option<String>,
130 pub content: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[non_exhaustive]
137pub struct EarningsTranscriptRef {
138 pub symbol: Option<String>,
140 pub quarter: Option<i32>,
142 pub year: Option<i32>,
144 pub date: Option<String>,
146}
147
148pub 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
169pub 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
179pub 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
186pub 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
194pub 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
213pub 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}