use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::types::symbols_to_owned;
const QUERY_FUNDERMENTAL_DATA_BOX: &str = include_str!("graphql/fundermental_data_box.graphql");
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FundamentalsVariables {
symbols: Vec<String>,
symbol_dialect_type: String,
up_to_historical_period_offset: String,
up_to_query_period_offset: String,
#[serde(rename = "reportedSalesUpToHistoricalPeriod2")]
reported_sales_up_to_historical_period_2: String,
#[serde(rename = "salesEstimatesUpToQueryPeriod2")]
sales_estimates_up_to_query_period_2: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsResponse {
#[serde(default)]
pub market_data: Vec<FundamentalsItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsItem {
pub id: Option<String>,
pub financials: Option<FundamentalsFinancials>,
pub symbology: Option<FundamentalsSymbology>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsFinancials {
pub consensus_financials: Option<FundamentalsConsensus>,
pub estimates: Option<FundamentalsEstimates>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsConsensus {
pub eps: Option<FundamentalsConsensusEps>,
pub sales: Option<FundamentalsConsensusSales>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsConsensusEps {
#[serde(default)]
pub reported_earnings: Vec<FundamentalsReportedPeriod>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsConsensusSales {
#[serde(default)]
pub reported_sales: Vec<FundamentalsReportedPeriod>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsReportedPeriod {
pub value: Option<FundamentalsFormattedValue>,
#[serde(rename = "percentChangeYOY")]
pub percent_change_yoy: Option<FundamentalsFormattedValue>,
pub period_offset: Option<String>,
pub period_end_date: Option<FundamentalsDateValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsEstimates {
#[serde(default)]
pub eps_estimates: Vec<FundamentalsEstimate>,
#[serde(default)]
pub sales_estimates: Vec<FundamentalsEstimate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsEstimate {
pub value: Option<FundamentalsFormattedValue>,
#[serde(rename = "percentChangeYOY")]
pub percent_change_yoy: Option<FundamentalsFormattedValue>,
pub period_offset: Option<String>,
pub period: Option<String>,
pub revision_direction: Option<String>,
}
pub type FundamentalsFormattedValue = crate::types::FormattedFloat;
pub type FundamentalsDateValue = crate::types::DateValue;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsSymbology {
pub company: Option<FundamentalsCompany>,
pub instrument: Option<FundamentalsInstrument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsCompany {
pub company_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsInstrument {
#[serde(default)]
pub symbols: Vec<FundamentalsSymbol>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundamentalsSymbol {
pub value: Option<String>,
#[serde(rename = "type")]
pub node_type: Option<String>,
}
impl Client {
pub async fn fundamentals(
&self,
symbols: &[&str],
symbol_dialect_type: &str,
historical_period_offset: &str,
query_period_offset: &str,
reported_sales_period: &str,
sales_estimates_period: &str,
) -> crate::error::Result<FundamentalsResponse> {
let variables = FundamentalsVariables {
symbols: symbols_to_owned(symbols),
symbol_dialect_type: symbol_dialect_type.to_string(),
up_to_historical_period_offset: historical_period_offset.to_string(),
up_to_query_period_offset: query_period_offset.to_string(),
reported_sales_up_to_historical_period_2: reported_sales_period.to_string(),
sales_estimates_up_to_query_period_2: sales_estimates_period.to_string(),
};
self.graphql_operation(
"FundermentalDataBox",
variables,
QUERY_FUNDERMENTAL_DATA_BOX,
)
.await
}
}
#[cfg(test)]
mod tests {
use crate::test_support::mock_test;
#[tokio::test]
async fn fundamentals_parses_response() {
let (_server, client, mock) = mock_test("FundermentalDataBox").await;
let resp = client
.fundamentals(
&["AAPL"],
"CHARTING",
"P7Y_AGO",
"P2Y_FUTURE",
"P7Y_AGO",
"P2Y_FUTURE",
)
.await
.expect("fundamentals should succeed");
assert_eq!(resp.market_data.len(), 1);
let item = &resp.market_data[0];
assert_eq!(item.id.as_deref(), Some("AAPL"));
let financials = item.financials.as_ref().expect("financials");
let consensus = financials
.consensus_financials
.as_ref()
.expect("consensus_financials");
let eps = consensus.eps.as_ref().expect("eps");
assert_eq!(eps.reported_earnings.len(), 2);
assert_eq!(
eps.reported_earnings[0].value.as_ref().unwrap().value,
Some(1.65)
);
assert_eq!(
eps.reported_earnings[0].period_offset.as_deref(),
Some("CURRENT")
);
assert_eq!(
eps.reported_earnings[0]
.period_end_date
.as_ref()
.unwrap()
.value
.as_deref(),
Some("2026-03-31")
);
let sales = consensus.sales.as_ref().expect("sales");
assert_eq!(sales.reported_sales.len(), 1);
assert_eq!(
sales.reported_sales[0]
.value
.as_ref()
.unwrap()
.formatted_value
.as_deref(),
Some("$95.2B")
);
let estimates = financials.estimates.as_ref().expect("estimates");
assert_eq!(estimates.eps_estimates.len(), 1);
assert_eq!(
estimates.eps_estimates[0].revision_direction.as_deref(),
Some("UP")
);
assert_eq!(
estimates.eps_estimates[0].value.as_ref().unwrap().value,
Some(1.72)
);
assert_eq!(estimates.sales_estimates.len(), 1);
assert_eq!(estimates.sales_estimates[0].period.as_deref(), Some("P1Q"));
let symbology = item.symbology.as_ref().expect("symbology");
let company = symbology.company.as_ref().expect("company");
assert_eq!(company.company_name.as_deref(), Some("Apple Inc."));
let instrument = symbology.instrument.as_ref().expect("instrument");
assert_eq!(instrument.symbols.len(), 1);
assert_eq!(instrument.symbols[0].value.as_deref(), Some("AAPL"));
assert_eq!(instrument.symbols[0].node_type.as_deref(), Some("CHARTING"));
mock.assert();
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_fundamentals() {
let client = crate::test_support::live_client().await;
let resp = client
.fundamentals(
&["AAPL"],
"CHARTING",
"P7Y_AGO",
"P2Y_FUTURE",
"P7Y_AGO",
"P2Y_FUTURE",
)
.await
.expect("live fundamentals should succeed");
assert!(!resp.market_data.is_empty());
}
}