use crate::error::{FinanceError, Result};
use crate::models::economic::{EconomicSeries, MacroObservation};
use super::client::{DATE_FIELD, SeriesQuery};
use super::models::{FiscalMeta, FiscalRow};
#[derive(Debug)]
pub(super) struct CuratedSeries {
pub id: &'static str,
pub dataset: &'static str,
pub value_field: &'static str,
pub filter: Option<&'static str>,
pub units: &'static str,
pub frequency: &'static str,
}
pub(super) const CURATED: &[CuratedSeries] = &[
CuratedSeries {
id: "DEBT_TO_PENNY",
dataset: "v2/accounting/od/debt_to_penny",
value_field: "tot_pub_debt_out_amt",
filter: None,
units: "US Dollars",
frequency: "Daily",
},
CuratedSeries {
id: "DEBT_HELD_BY_PUBLIC",
dataset: "v2/accounting/od/debt_to_penny",
value_field: "debt_held_public_amt",
filter: None,
units: "US Dollars",
frequency: "Daily",
},
CuratedSeries {
id: "INTRAGOVERNMENTAL_HOLDINGS",
dataset: "v2/accounting/od/debt_to_penny",
value_field: "intragov_hold_amt",
filter: None,
units: "US Dollars",
frequency: "Daily",
},
CuratedSeries {
id: "AVG_INTEREST_RATE",
dataset: "v2/accounting/od/avg_interest_rates",
value_field: "avg_interest_rate_amt",
filter: Some("security_desc:eq:Total Interest-bearing Debt"),
units: "Percent",
frequency: "Monthly",
},
CuratedSeries {
id: "AVG_INTEREST_RATE_MARKETABLE",
dataset: "v2/accounting/od/avg_interest_rates",
value_field: "avg_interest_rate_amt",
filter: Some("security_desc:eq:Total Marketable"),
units: "Percent",
frequency: "Monthly",
},
CuratedSeries {
id: "OPERATING_CASH_BALANCE",
dataset: "v1/accounting/dts/operating_cash_balance",
value_field: "open_today_bal",
filter: Some("account_type:eq:Treasury General Account (TGA) Opening Balance"),
units: "Millions of US Dollars",
frequency: "Daily",
},
];
#[derive(Debug)]
pub(super) enum Resolved<'a> {
Curated(&'static CuratedSeries),
Passthrough {
dataset: &'a str,
value_field: &'a str,
},
}
impl Resolved<'_> {
pub(super) fn query(&self) -> SeriesQuery<'_> {
match self {
Self::Curated(c) => SeriesQuery {
dataset: c.dataset,
value_field: c.value_field,
filter: c.filter,
},
Self::Passthrough {
dataset,
value_field,
} => SeriesQuery {
dataset,
value_field,
filter: None,
},
}
}
}
pub(super) fn resolve(series_id: &str) -> Result<Resolved<'_>> {
let trimmed = series_id.trim();
if let Some(curated) = CURATED.iter().find(|c| c.id.eq_ignore_ascii_case(trimmed)) {
return Ok(Resolved::Curated(curated));
}
if let Some((dataset, value_field)) = trimmed.rsplit_once(':')
&& !dataset.is_empty()
&& !value_field.is_empty()
{
return Ok(Resolved::Passthrough {
dataset,
value_field,
});
}
Err(FinanceError::InvalidParameter {
param: "series_id".to_string(),
reason: format!(
"'{trimmed}' is not a FiscalData series. Use one of [{}], or the \
passthrough form \"<dataset path>:<value column>\" \
(e.g. \"v2/accounting/od/debt_to_penny:tot_pub_debt_out_amt\")",
CURATED.iter().map(|c| c.id).collect::<Vec<_>>().join(", ")
),
})
}
pub(super) fn parse_value(raw: &str) -> Option<f64> {
crate::adapters::common::numbers::parse_number(raw, &["null"])
}
pub(super) fn to_canonical(
series_id: &str,
resolved: &Resolved<'_>,
rows: Vec<FiscalRow>,
meta: &FiscalMeta,
) -> EconomicSeries {
let value_field = resolved.query().value_field.to_string();
let title = meta.labels.get(&value_field).cloned();
let (units, frequency) = match resolved {
Resolved::Curated(c) => (Some(c.units.to_string()), Some(c.frequency.to_string())),
Resolved::Passthrough { .. } => (
meta.data_types.get(&value_field).map(|t| match t.as_str() {
"CURRENCY" => "US Dollars".to_string(),
"PERCENTAGE" => "Percent".to_string(),
other => other.to_string(),
}),
None,
),
};
let observations: Vec<MacroObservation> = rows
.into_iter()
.filter_map(|mut row| {
let date = row.remove(DATE_FIELD)?;
let value = row.get(&value_field).and_then(|v| parse_value(v));
Some(MacroObservation { date, value })
})
.collect();
EconomicSeries {
series_id: series_id.to_string(),
title,
units,
frequency,
observations,
}
}
pub(crate) async fn fetch_economic_series_response(series_id: &str) -> Result<EconomicSeries> {
let resolved = resolve(series_id)?;
let (rows, meta) = super::client()?.series(&resolved.query()).await?;
Ok(to_canonical(series_id, &resolved, rows, &meta))
}