use serde::{Deserialize, Serialize};
use crate::adapters::common::encode_path_segment;
use crate::error::Result;
use super::build_client;
use super::models::Period;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AnalystEstimate {
pub symbol: Option<String>,
pub date: Option<String>,
#[serde(rename = "estimatedRevenueLow")]
pub estimated_revenue_low: Option<f64>,
#[serde(rename = "estimatedRevenueHigh")]
pub estimated_revenue_high: Option<f64>,
#[serde(rename = "estimatedRevenueAvg")]
pub estimated_revenue_avg: Option<f64>,
#[serde(rename = "estimatedEbitdaLow")]
pub estimated_ebitda_low: Option<f64>,
#[serde(rename = "estimatedEbitdaHigh")]
pub estimated_ebitda_high: Option<f64>,
#[serde(rename = "estimatedEbitdaAvg")]
pub estimated_ebitda_avg: Option<f64>,
#[serde(rename = "estimatedEpsAvg")]
pub estimated_eps_avg: Option<f64>,
#[serde(rename = "estimatedEpsHigh")]
pub estimated_eps_high: Option<f64>,
#[serde(rename = "estimatedEpsLow")]
pub estimated_eps_low: Option<f64>,
#[serde(rename = "numberAnalystEstimatedRevenue")]
pub number_analyst_estimated_revenue: Option<i32>,
#[serde(rename = "numberAnalystsEstimatedEps")]
pub number_analysts_estimated_eps: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AnalystRecommendation {
pub symbol: Option<String>,
pub date: Option<String>,
#[serde(rename = "analystRatingsBuy")]
pub analyst_ratings_buy: Option<i32>,
#[serde(rename = "analystRatingsHold")]
pub analyst_ratings_hold: Option<i32>,
#[serde(rename = "analystRatingsSell")]
pub analyst_ratings_sell: Option<i32>,
#[serde(rename = "analystRatingsStrongBuy")]
pub analyst_ratings_strong_buy: Option<i32>,
#[serde(rename = "analystRatingsStrongSell")]
pub analyst_ratings_strong_sell: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EarningsSurprise {
pub date: Option<String>,
pub symbol: Option<String>,
#[serde(rename = "actualEarningResult")]
pub actual_earning_result: Option<f64>,
#[serde(rename = "estimatedEarning")]
pub estimated_earning: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockGrade {
pub symbol: Option<String>,
pub date: Option<String>,
#[serde(rename = "gradingCompany")]
pub grading_company: Option<String>,
#[serde(rename = "previousGrade")]
pub previous_grade: Option<String>,
#[serde(rename = "newGrade")]
pub new_grade: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EarningsTranscript {
pub symbol: Option<String>,
pub quarter: Option<i32>,
pub year: Option<i32>,
pub date: Option<String>,
pub content: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EarningsTranscriptRef {
pub symbol: Option<String>,
pub quarter: Option<i32>,
pub year: Option<i32>,
pub date: Option<String>,
}
pub async fn analyst_estimates(
symbol: &str,
period: Period,
limit: u32,
) -> Result<Vec<AnalystEstimate>> {
let client = build_client()?;
let path = format!("/api/v3/analyst-estimates/{}", encode_path_segment(symbol));
let limit_str = limit.to_string();
client
.get(&path, &[("period", period.as_str()), ("limit", &limit_str)])
.await
}
pub async fn analyst_recommendations(symbol: &str) -> Result<Vec<AnalystRecommendation>> {
let client = build_client()?;
let path = format!(
"/api/v3/analyst-stock-recommendations/{}",
encode_path_segment(symbol)
);
client.get(&path, &[]).await
}
pub async fn earnings_surprises(symbol: &str) -> Result<Vec<EarningsSurprise>> {
let client = build_client()?;
let path = format!("/api/v3/earnings-surprises/{}", encode_path_segment(symbol));
client.get(&path, &[]).await
}
pub async fn stock_grade(symbol: &str, limit: u32) -> Result<Vec<StockGrade>> {
let client = build_client()?;
let path = format!("/api/v3/grade/{}", encode_path_segment(symbol));
let limit_str = limit.to_string();
client.get(&path, &[("limit", &*limit_str)]).await
}
pub async fn earnings_transcript(
symbol: &str,
quarter: u32,
year: u32,
) -> Result<Vec<EarningsTranscript>> {
let client = build_client()?;
let path = format!(
"/api/v3/earning_call_transcript/{}",
encode_path_segment(symbol)
);
let q = quarter.to_string();
let y = year.to_string();
client.get(&path, &[("quarter", &*q), ("year", &*y)]).await
}
pub async fn earnings_transcript_list(symbol: &str) -> Result<Vec<EarningsTranscriptRef>> {
let client = build_client()?;
client
.get("/api/v4/earning_call_transcript", &[("symbol", symbol)])
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_analyst_estimates_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/analyst-estimates/AAPL")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
mockito::Matcher::UrlEncoded("period".into(), "quarter".into()),
mockito::Matcher::UrlEncoded("limit".into(), "4".into()),
]))
.with_status(200)
.with_body(
serde_json::json!([
{
"symbol": "AAPL",
"date": "2024-03-31",
"estimatedRevenueAvg": 90000000000.0,
"estimatedEpsAvg": 1.50,
"numberAnalystEstimatedRevenue": 30,
"numberAnalystsEstimatedEps": 28
}
])
.to_string(),
)
.create_async()
.await;
let client = super::super::build_test_client(&server.url()).unwrap();
let resp: Vec<AnalystEstimate> = client
.get(
"/api/v3/analyst-estimates/AAPL",
&[("period", "quarter"), ("limit", "4")],
)
.await
.unwrap();
assert_eq!(resp.len(), 1);
assert_eq!(resp[0].symbol.as_deref(), Some("AAPL"));
assert!((resp[0].estimated_eps_avg.unwrap() - 1.50).abs() < 0.01);
}
#[tokio::test]
async fn test_earnings_surprises_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/earnings-surprises/AAPL")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apikey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!([
{
"date": "2024-01-25",
"symbol": "AAPL",
"actualEarningResult": 2.18,
"estimatedEarning": 2.10
}
])
.to_string(),
)
.create_async()
.await;
let client = super::super::build_test_client(&server.url()).unwrap();
let resp: Vec<EarningsSurprise> = client
.get("/api/v3/earnings-surprises/AAPL", &[])
.await
.unwrap();
assert_eq!(resp.len(), 1);
assert!((resp[0].actual_earning_result.unwrap() - 2.18).abs() < 0.01);
}
}