use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::graphql::GraphQLRequest;
use crate::types::symbols_to_owned;
const QUERY_FUNDERMENTAL_DATA_BOX: &str = r#"query FundermentalDataBox(
$symbols: [String!]!
$symbolDialectType: MDSymbolDialectType!
$upToHistoricalPeriodOffset: MDUpToQueryPeriodOffsetHistorical!
$upToQueryPeriodOffset: MDUpToQueryPeriodOffsetFuture!
$reportedSalesUpToHistoricalPeriod2: MDUpToQueryPeriodOffsetHistorical!
$salesEstimatesUpToQueryPeriod2: MDUpToQueryPeriodOffsetFuture!
) {
marketData(symbols: $symbols, symbolDialectType: $symbolDialectType) {
financials {
consensusFinancials {
eps {
reportedEarnings(upToHistoricalPeriodOffset: $upToHistoricalPeriodOffset) {
value {
formattedValue
value
}
percentChangeYOY {
formattedValue
value
}
periodOffset
periodEndDate {
value
}
}
}
sales {
reportedSales(upToHistoricalPeriodOffset: $reportedSalesUpToHistoricalPeriod2) {
value {
formattedValue
value
}
percentChangeYOY {
formattedValue
value
}
periodEndDate {
value
}
periodOffset
}
}
}
estimates {
epsEstimates(upToQueryPeriodOffset: $upToQueryPeriodOffset) {
value {
value
formattedValue
}
percentChangeYOY {
value
formattedValue
}
periodOffset
period
revisionDirection
}
salesEstimates(upToQueryPeriodOffset: $salesEstimatesUpToQueryPeriod2) {
value {
value
formattedValue
}
percentChangeYOY {
value
formattedValue
}
periodOffset
period
}
}
}
id
symbology {
company {
companyName
}
instrument {
symbols {
value
type
}
}
}
}
}"#;
#[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(),
};
let request = GraphQLRequest {
operation_name: "FundermentalDataBox".to_string(),
variables,
query: QUERY_FUNDERMENTAL_DATA_BOX.to_string(),
};
self.graphql_post(&request).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());
}
}