use chrono::{Duration, Utc};
use crate::{
ShareCount,
core::{
YfClient, YfError,
client::{CacheMode, RetryConfig},
},
fundamentals::wire::{TimeseriesData, TimeseriesEnvelope},
};
use super::fetch::fetch_modules;
use super::wire::raw_num;
use super::{
BalanceSheetRow, CashflowRow, Earnings, EarningsQuarter, EarningsQuarterEps, EarningsYear,
IncomeStatementRow,
};
pub(super) async fn income_statement(
client: &YfClient,
symbol: &str,
quarterly: bool,
cache_mode: CacheMode,
retry_override: Option<&RetryConfig>,
) -> Result<Vec<IncomeStatementRow>, YfError> {
let modules = if quarterly {
"incomeStatementHistoryQuarterly"
} else {
"incomeStatementHistory"
};
let root = fetch_modules(client, symbol, modules, cache_mode, retry_override).await?;
let arr = if quarterly {
root.income_statement_history_quarterly
.and_then(|x| x.income_statement_history)
} else {
root.income_statement_history
.and_then(|x| x.income_statement_history)
}
.unwrap_or_default();
Ok(arr
.into_iter()
.map(|n| IncomeStatementRow {
period_end: n.end_date.and_then(|d| d.raw).unwrap_or(0),
total_revenue: n.total_revenue.and_then(raw_num),
gross_profit: n.gross_profit.and_then(raw_num),
operating_income: n.operating_income.and_then(raw_num),
net_income: n.net_income.and_then(raw_num),
})
.collect())
}
pub(super) async fn balance_sheet(
client: &YfClient,
symbol: &str,
quarterly: bool,
cache_mode: CacheMode,
retry_override: Option<&RetryConfig>,
) -> Result<Vec<BalanceSheetRow>, YfError> {
let modules = if quarterly {
"balanceSheetHistoryQuarterly"
} else {
"balanceSheetHistory"
};
let root = fetch_modules(client, symbol, modules, cache_mode, retry_override).await?;
let arr = if quarterly {
root.balance_sheet_history_quarterly
.and_then(|x| x.balance_sheet_statements)
} else {
root.balance_sheet_history
.and_then(|x| x.balance_sheet_statements)
}
.unwrap_or_default();
Ok(arr
.into_iter()
.map(|n| BalanceSheetRow {
period_end: n.end_date.and_then(|d| d.raw).unwrap_or(0),
total_assets: n.total_assets.and_then(raw_num),
total_liabilities: n.total_liab.and_then(raw_num),
total_equity: n.total_stockholder_equity.and_then(raw_num),
cash: n.cash.and_then(raw_num),
long_term_debt: n.long_term_debt.and_then(raw_num),
shares_outstanding: n.shares_outstanding.and_then(|s| s.raw).map(|v| v as u64),
})
.collect())
}
pub(super) async fn cashflow(
client: &YfClient,
symbol: &str,
quarterly: bool,
cache_mode: CacheMode,
retry_override: Option<&RetryConfig>,
) -> Result<Vec<CashflowRow>, YfError> {
let modules = if quarterly {
"cashflowStatementHistoryQuarterly"
} else {
"cashflowStatementHistory"
};
let root = fetch_modules(client, symbol, modules, cache_mode, retry_override).await?;
let arr = if quarterly {
root.cashflow_statement_history_quarterly
.and_then(|x| x.cashflow_statements)
} else {
root.cashflow_statement_history
.and_then(|x| x.cashflow_statements)
}
.unwrap_or_default();
Ok(arr
.into_iter()
.map(|n| {
let ocf = n.total_cash_from_operating_activities.and_then(raw_num);
let capex = n.capital_expenditures.and_then(raw_num);
let fcf = match (n.free_cashflow.and_then(raw_num), ocf, capex) {
(Some(x), _, _) => Some(x),
(None, Some(a), Some(b)) => Some(a - b),
_ => None,
};
CashflowRow {
period_end: n.end_date.and_then(|d| d.raw).unwrap_or(0),
operating_cashflow: ocf,
capital_expenditures: capex,
free_cash_flow: fcf,
net_income: n.net_income.and_then(raw_num),
}
})
.collect())
}
pub(super) async fn earnings(
client: &YfClient,
symbol: &str,
cache_mode: CacheMode,
retry_override: Option<&RetryConfig>,
) -> Result<Earnings, YfError> {
let root = fetch_modules(client, symbol, "earnings", cache_mode, retry_override).await?;
let e = root
.earnings
.ok_or_else(|| YfError::Data("earnings missing".into()))?;
let yearly = e
.financials_chart
.as_ref()
.and_then(|fc| fc.yearly.as_ref())
.map(|v| {
v.iter()
.map(|y| EarningsYear {
year: y.date.unwrap_or(0) as i32,
revenue: y.revenue.as_ref().and_then(|x| x.raw),
earnings: y.earnings.as_ref().and_then(|x| x.raw),
})
.collect()
})
.unwrap_or_default();
let quarterly = e
.financials_chart
.as_ref()
.and_then(|fc| fc.quarterly.as_ref())
.map(|v| {
v.iter()
.map(|q| EarningsQuarter {
period: q.date.clone().unwrap_or_default(),
revenue: q.revenue.as_ref().and_then(|x| x.raw),
earnings: q.earnings.as_ref().and_then(|x| x.raw),
})
.collect()
})
.unwrap_or_default();
let quarterly_eps = e
.earnings_chart
.as_ref()
.and_then(|ec| ec.quarterly.as_ref())
.map(|v| {
v.iter()
.map(|q| EarningsQuarterEps {
period: q.date.clone().unwrap_or_default(),
actual: q.actual.as_ref().and_then(|x| x.raw),
estimate: q.estimate.as_ref().and_then(|x| x.raw),
})
.collect()
})
.unwrap_or_default();
Ok(Earnings {
yearly,
quarterly,
quarterly_eps,
})
}
pub(super) async fn calendar(
client: &YfClient,
symbol: &str,
cache_mode: CacheMode,
retry_override: Option<&RetryConfig>,
) -> Result<super::Calendar, YfError> {
let root = fetch_modules(client, symbol, "calendarEvents", cache_mode, retry_override).await?;
let c = root
.calendar_events
.and_then(|ce| ce.earnings)
.ok_or_else(|| YfError::Data("calendarEvents.earnings missing".into()))?;
let earnings_dates = c
.earnings_date
.unwrap_or_default()
.into_iter()
.filter_map(|d| d.raw)
.collect();
Ok(super::Calendar {
earnings_dates,
ex_dividend_date: c.ex_dividend_date.and_then(|x| x.raw),
dividend_date: c.dividend_date.and_then(|x| x.raw),
})
}
pub(super) async fn shares(
client: &YfClient,
symbol: &str,
start: Option<chrono::DateTime<Utc>>,
end: Option<chrono::DateTime<Utc>>,
quarterly: bool,
cache_mode: CacheMode,
retry_override: Option<&RetryConfig>,
) -> Result<Vec<ShareCount>, YfError> {
let end_ts = end.unwrap_or_else(Utc::now).timestamp();
let start_ts = start
.unwrap_or_else(|| Utc::now() - Duration::days(548))
.timestamp();
let mut url = client.base_timeseries().join(symbol)?;
let type_key = if quarterly {
"quarterlyBasicAverageShares"
} else {
"annualBasicAverageShares"
};
url.query_pairs_mut()
.append_pair("symbol", symbol)
.append_pair("type", type_key)
.append_pair("period1", &start_ts.to_string())
.append_pair("period2", &end_ts.to_string());
let body = if cache_mode == CacheMode::Use {
if let Some(cached) = client.cache_get(&url).await {
cached
} else {
let resp = client
.send_with_retry(client.http().get(url.clone()), retry_override)
.await?;
let endpoint = format!("timeseries_{}", type_key);
let text = crate::core::net::get_text(resp, &endpoint, symbol, "json").await?;
if cache_mode != CacheMode::Bypass {
client.cache_put(&url, &text, None).await;
}
text
}
} else {
let resp = client
.send_with_retry(client.http().get(url.clone()), retry_override)
.await?;
let endpoint = format!("timeseries_{}", type_key);
let text = crate::core::net::get_text(resp, &endpoint, symbol, "json").await?;
if cache_mode != CacheMode::Bypass {
client.cache_put(&url, &text, None).await;
}
text
};
let envelope: TimeseriesEnvelope = serde_json::from_str(&body)
.map_err(|e| YfError::Data(format!("shares timeseries json parse: {e}")))?;
let result_data: Option<TimeseriesData> = envelope
.timeseries
.and_then(|ts| ts.result)
.and_then(|mut v| v.pop());
let (timestamps, mut values_map) = match result_data {
Some(TimeseriesData {
timestamp: Some(ts),
values,
..
}) => (ts, values),
_ => return Ok(vec![]),
};
let values = match values_map.remove(type_key) {
Some(v) => v,
None => return Ok(vec![]),
};
let counts = timestamps
.into_iter()
.zip(values.into_iter())
.filter_map(|(ts, val)| {
val.reported_value
.and_then(|rv| rv.raw)
.map(|shares| ShareCount { date: ts, shares })
})
.collect();
Ok(counts)
}