use std::collections::BTreeMap;
use pomelo_data::fundamentals::{
write_fundamentals, FundamentalRow, FUNDAMENTALS_DIR, FUNDAMENTAL_FIELDS,
};
use pomelo_data::ObjectSink;
use serde_json::Value;
use super::http::Fetcher;
use super::util::{iso_to_i32, num};
use super::HttpClient;
use super::FMP_BASE;
const FUNDAMENTAL_KEYS: &[&[&str]] = &[
&["priceToEarningsRatio", "peRatio", "priceEarningsRatio"], &["priceToSalesRatio", "priceSalesRatio"], &["priceToBookRatio", "priceBookValueRatio", "pbRatio"], &["returnOnEquity"], &["netProfitMargin", "netIncomeMargin"], &["debtToEquityRatio", "debtEquityRatio", "debtToEquity"], &["marketCap", "marketCapitalization"], &["grossProfitMargin"], &["receivablesTurnover"], &[
"debtToAssetsRatio",
"debtToAssets",
"totalDebtToTotalAssets",
], &["revenue"], &["revenueGrowth", "growthRevenue"], &["epsgrowth", "epsGrowth", "growthEPS"], &["operatingIncomeGrowth", "growthOperatingIncome"], &["netIncomeGrowth", "growthNetIncome"], &["grossProfitGrowth", "growthGrossProfit"], ];
pub(crate) const FILING_DATE_KEYS: &[&str] = &["filingDate", "fillingDate", "acceptedDate"];
pub(crate) struct Snapshot {
pub visible: i32,
pub values: Vec<f64>,
pub fell_back: bool,
}
pub(crate) fn annual_url(endpoint: &str, sym: &str, key: &str) -> String {
format!("{FMP_BASE}/stable/{endpoint}?symbol={sym}&period=annual&limit=40&apikey={key}")
}
pub(crate) fn merge_fundamentals(bodies: &[Vec<Value>]) -> Vec<Snapshot> {
let mut by_period: BTreeMap<i32, serde_json::Map<String, Value>> = BTreeMap::new();
for rows in bodies {
for row in rows {
let Some(obj) = row.as_object() else { continue };
let Some(d) = obj.get("date").and_then(Value::as_str).and_then(iso_to_i32) else {
continue;
};
let entry = by_period.entry(d).or_default();
for (k, v) in obj {
entry.entry(k.clone()).or_insert_with(|| v.clone());
}
}
}
let mut snapshots: Vec<Snapshot> = by_period
.into_iter()
.map(|(period_end, obj)| {
let values = FUNDAMENTAL_KEYS
.iter()
.map(|keys| num(&obj, keys).unwrap_or(f64::NAN))
.collect();
let filed = FILING_DATE_KEYS
.iter()
.find_map(|k| obj.get(*k).and_then(Value::as_str).and_then(iso_to_i32));
let (visible, fell_back) = match filed {
Some(f) => (f, false),
None => (period_end, true),
};
Snapshot {
visible,
values,
fell_back,
}
})
.collect();
snapshots.sort_by_key(|s| s.visible);
snapshots
}
pub(crate) fn densify_fundamentals(
snapshots: &[Snapshot],
price_days: &[i32],
) -> Vec<FundamentalRow> {
let nfields = FUNDAMENTAL_FIELDS.len();
let mut rows = Vec::with_capacity(price_days.len());
let mut si = 0usize;
let mut current = vec![f64::NAN; nfields];
for &day in price_days {
let mut event = 0.0;
while si < snapshots.len() && snapshots[si].visible <= day {
current = snapshots[si].values.clone();
event = 1.0;
si += 1;
}
rows.push(FundamentalRow {
day,
values: current.clone(),
report_event: event,
});
}
rows
}
pub(crate) fn sync_fundamentals<H: HttpClient>(
fetcher: &Fetcher<H>,
sink: &impl ObjectSink,
sym: &str,
api_key: &str,
price_days: &[i32],
) -> Result<bool, String> {
eprintln!("{sym}: fetching fundamentals…");
let mut bodies: Vec<Vec<Value>> = ["ratios", "key-metrics", "financial-growth"]
.iter()
.map(|ep| fetcher.get_rows(&annual_url(ep, sym, api_key)))
.collect::<Result<_, _>>()?;
match fetcher.get_rows(&annual_url("income-statement", sym, api_key)) {
Ok(rows) => bodies.push(rows),
Err(e) => eprintln!(
"{sym}: income-statement unavailable ({e}); \
fundamentals fall back to fiscal period-end visibility"
),
}
let snapshots = merge_fundamentals(&bodies);
if snapshots.is_empty() {
return Ok(false);
}
let fell_back = snapshots.iter().filter(|s| s.fell_back).count();
let rows = densify_fundamentals(&snapshots, price_days);
let bytes = write_fundamentals(&rows).map_err(|e| e.to_string())?;
sink.put(&format!("{FUNDAMENTALS_DIR}/{sym}.csv.gz"), &bytes)
.map_err(|e| e.to_string())?;
if fell_back > 0 {
eprintln!(
"{sym}: wrote {} fundamental rows ({} annual snapshots; {fell_back} had no filing \
date → visible on fiscal period-end, may be optimistic)",
rows.len(),
snapshots.len(),
);
} else {
eprintln!(
"{sym}: wrote {} fundamental rows ({} annual snapshots, filing-date visibility)",
rows.len(),
snapshots.len(),
);
}
Ok(true)
}