fmp_rs/endpoints/
analyst.rs1use crate::Result;
4use crate::client::FmpClient;
5use crate::models::analyst::{AnalystEstimates, AnalystGrade, ConsensusSummary, PriceTarget};
6use crate::models::common::Period;
7use serde::Serialize;
8
9pub struct Analyst {
11 client: FmpClient,
12}
13
14#[derive(Debug, Clone, Serialize)]
15struct AnalystQuery {
16 #[serde(skip_serializing_if = "Option::is_none")]
17 period: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 limit: Option<u32>,
20}
21
22impl Analyst {
23 pub(crate) fn new(client: FmpClient) -> Self {
24 Self { client }
25 }
26
27 pub async fn get_estimates(
49 &self,
50 symbol: &str,
51 period: Option<Period>,
52 limit: Option<u32>,
53 ) -> Result<Vec<AnalystEstimates>> {
54 let query = AnalystQuery {
55 period: period.map(|p| p.to_string()),
56 limit,
57 };
58 self.client
59 .get_with_query(&format!("v3/analyst-estimates/{}", symbol), &query)
60 .await
61 }
62
63 pub async fn get_price_targets(&self, symbol: &str) -> Result<Vec<PriceTarget>> {
82 self.client
83 .get_with_query(&format!("v4/price-target?symbol={}", symbol), &())
84 .await
85 }
86
87 pub async fn get_price_target_consensus(
106 &self,
107 symbol: &str,
108 ) -> Result<Vec<PriceTargetConsensus>> {
109 self.client
110 .get_with_query(&format!("v4/price-target-consensus?symbol={}", symbol), &())
111 .await
112 }
113
114 pub async fn get_grades(&self, symbol: &str) -> Result<Vec<AnalystGrade>> {
137 self.client
138 .get_with_query(&format!("v3/grade/{}", symbol), &())
139 .await
140 }
141
142 pub async fn get_recommendation_consensus(
163 &self,
164 symbol: &str,
165 ) -> Result<Vec<ConsensusSummary>> {
166 self.client
167 .get_with_query(&format!("v3/rating/{}", symbol), &())
168 .await
169 }
170}
171
172#[derive(Debug, Clone, Serialize, serde::Deserialize, PartialEq)]
174#[serde(rename_all = "camelCase")]
175pub struct PriceTargetConsensus {
176 pub symbol: String,
177 pub target_high: f64,
178 pub target_low: f64,
179 pub target_consensus: f64,
180 pub target_median: f64,
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_new() {
189 let client = FmpClient::builder().api_key("test_key").build().unwrap();
190 let analyst = Analyst::new(client);
191 assert!(std::ptr::addr_of!(analyst.client).is_null() == false);
192 }
193
194 #[tokio::test]
195 #[ignore] async fn test_get_estimates() {
197 let client = FmpClient::new().unwrap();
198 let result = client
199 .analyst()
200 .get_estimates("AAPL", Some(Period::Annual), Some(5))
201 .await;
202 assert!(result.is_ok());
203 }
204
205 #[tokio::test]
206 #[ignore] async fn test_get_price_targets() {
208 let client = FmpClient::new().unwrap();
209 let result = client.analyst().get_price_targets("AAPL").await;
210 assert!(result.is_ok());
211 }
212
213 #[tokio::test]
214 #[ignore] async fn test_get_grades() {
216 let client = FmpClient::new().unwrap();
217 let result = client.analyst().get_grades("AAPL").await;
218 assert!(result.is_ok());
219 }
220
221 #[tokio::test]
222 #[ignore] async fn test_get_recommendation_consensus() {
224 let client = FmpClient::new().unwrap();
225 let result = client.analyst().get_recommendation_consensus("AAPL").await;
226 assert!(result.is_ok());
227 }
228
229 #[tokio::test]
231 #[ignore] async fn test_get_price_target_consensus() {
233 let client = FmpClient::new().unwrap();
234 let result = client.analyst().get_price_target_consensus("AAPL").await;
235 assert!(result.is_ok());
236 let consensus = result.unwrap();
237
238 if !consensus.is_empty() {
239 let target = &consensus[0];
240 assert_eq!(target.symbol, "AAPL".to_string());
241 assert!(target.target_high >= target.target_low); assert!(target.target_high > 0.0 && target.target_low > 0.0); }
245 }
246
247 #[tokio::test]
249 #[ignore] async fn test_analyst_data_with_invalid_symbol() {
251 let client = FmpClient::new().unwrap();
252
253 let invalid_symbol = "INVALID_STOCK_12345";
254
255 let estimates_result = client
257 .analyst()
258 .get_estimates(invalid_symbol, Some(Period::Annual), Some(5))
259 .await;
260 let targets_result = client.analyst().get_price_targets(invalid_symbol).await;
261 let consensus_result = client
262 .analyst()
263 .get_price_target_consensus(invalid_symbol)
264 .await;
265 let grades_result = client.analyst().get_grades(invalid_symbol).await;
266 let rec_consensus_result = client
267 .analyst()
268 .get_recommendation_consensus(invalid_symbol)
269 .await;
270
271 match estimates_result {
273 Ok(data) => assert!(data.is_empty()),
274 Err(_) => {} }
276
277 match targets_result {
278 Ok(data) => assert!(data.is_empty()),
279 Err(_) => {} }
281
282 match consensus_result {
283 Ok(data) => assert!(data.is_empty()),
284 Err(_) => {} }
286
287 match grades_result {
288 Ok(data) => assert!(data.is_empty()),
289 Err(_) => {} }
291
292 match rec_consensus_result {
293 Ok(data) => assert!(data.is_empty()),
294 Err(_) => {} }
296 }
297
298 #[tokio::test]
299 #[ignore] async fn test_estimates_with_different_periods() {
301 let client = FmpClient::new().unwrap();
302
303 let annual_result = client
305 .analyst()
306 .get_estimates("AAPL", Some(Period::Annual), Some(3))
307 .await;
308 let quarterly_result = client
309 .analyst()
310 .get_estimates("AAPL", Some(Period::Quarter), Some(3))
311 .await;
312
313 assert!(annual_result.is_ok());
314 assert!(quarterly_result.is_ok());
315
316 let annual_estimates = annual_result.unwrap();
317 let quarterly_estimates = quarterly_result.unwrap();
318
319 if !annual_estimates.is_empty() && !quarterly_estimates.is_empty() {
321 assert_eq!(annual_estimates[0].symbol, "AAPL".to_string());
323 assert_eq!(quarterly_estimates[0].symbol, "AAPL".to_string());
324 }
325 }
326
327 #[tokio::test]
328 #[ignore] async fn test_estimates_limit_parameter() {
330 let client = FmpClient::new().unwrap();
331
332 let small_limit_result = client
334 .analyst()
335 .get_estimates("AAPL", Some(Period::Annual), Some(2))
336 .await;
337 let large_limit_result = client
338 .analyst()
339 .get_estimates("AAPL", Some(Period::Annual), Some(10))
340 .await;
341
342 assert!(small_limit_result.is_ok());
343 assert!(large_limit_result.is_ok());
344
345 let small_estimates = small_limit_result.unwrap();
346 let large_estimates = large_limit_result.unwrap();
347
348 assert!(small_estimates.len() <= 2);
350
351 if !small_estimates.is_empty() && !large_estimates.is_empty() {
353 assert!(large_estimates.len() >= small_estimates.len());
355 }
356 }
357
358 #[tokio::test]
359 #[ignore] async fn test_price_targets_data_validation() {
361 let client = FmpClient::new().unwrap();
362 let result = client.analyst().get_price_targets("AAPL").await;
363 assert!(result.is_ok());
364
365 let targets = result.unwrap();
366 if !targets.is_empty() {
367 let target = &targets[0];
368
369 assert!(!target.symbol.is_empty());
371
372 assert!(target.price_target > 0.0); assert!(!target.analyst_name.is_empty()); assert!(!target.analyst_company.is_empty()); }
381 }
382
383 #[tokio::test]
384 #[ignore] async fn test_grades_data_validation() {
386 let client = FmpClient::new().unwrap();
387 let result = client.analyst().get_grades("AAPL").await;
388 assert!(result.is_ok());
389
390 let grades = result.unwrap();
391 if !grades.is_empty() {
392 let grade = &grades[0];
393
394 assert_eq!(grade.symbol, "AAPL".to_string());
396
397 let new_grade = &grade.new_grade;
399 let _valid_grades = vec![
401 "BUY",
402 "SELL",
403 "HOLD",
404 "STRONG_BUY",
405 "STRONG_SELL",
406 "OUTPERFORM",
407 "UNDERPERFORM",
408 "NEUTRAL",
409 "OVERWEIGHT",
410 "UNDERWEIGHT",
411 ];
412 assert!(!new_grade.is_empty());
414 }
415 }
416
417 #[tokio::test]
418 #[ignore] async fn test_multiple_symbols_consistency() {
420 let client = FmpClient::new().unwrap();
421
422 let symbols = vec!["AAPL", "MSFT", "GOOGL"];
423
424 for symbol in symbols {
425 let result = client.analyst().get_price_targets(symbol).await;
426 assert!(result.is_ok());
427
428 let targets = result.unwrap();
429 if !targets.is_empty() {
430 assert_eq!(targets[0].symbol, symbol.to_string());
432 }
433 }
434 }
435}