Consolidate metric definitions with Rust JSON as single source of truth

- Add core.computed.json with 32 ratio definitions (filing + market derived)
- Add Rust types for ComputedDefinition and ComputationSpec
- Create generate-taxonomy.ts to generate TypeScript from Rust JSON
- Generate lib/generated/ (gitignored) with surfaces, computed, kpis
- Update financial-metrics.ts to use generated definitions
- Add build-time generation via 'bun run generate'
- Add taxonomy architecture documentation

Two-phase ratio computation:
- Filing-derived: margins, returns, per-share, growth (Rust computes)
- Market-derived: valuation ratios (TypeScript computes with price data)

All 32 ratios defined in core.computed.json:
- Margins: gross, operating, ebitda, net, fcf
- Returns: roa, roe, roic, roce
- Financial health: debt_to_equity, net_debt_to_ebitda, cash_to_debt, current_ratio
- Per-share: revenue, fcf, book_value
- Growth: yoy metrics + 3y/5y cagr
- Valuation: market_cap, ev, p/e, p/fcf, p/b, ev/sales, ev/ebitda, ev/fcf
This commit is contained in:
2026-03-15 15:22:51 -04:00
parent ed4420b8db
commit 24aa8e33d4
11 changed files with 1453 additions and 123 deletions

View File

@@ -102,6 +102,50 @@ pub struct KpiDefinition {
pub unit: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ComputedPackFile {
pub version: String,
pub pack: String,
pub computed: Vec<ComputedDefinition>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ComputedDefinition {
pub key: String,
pub label: String,
pub category: String,
pub order: i64,
pub unit: String,
pub computation: ComputationSpec,
#[serde(default)]
pub supported_cadences: Vec<String>,
#[serde(default)]
pub requires_external_data: Vec<String>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ComputationSpec {
Ratio {
numerator: String,
denominator: String,
},
YoyGrowth {
source: String,
},
Cagr {
source: String,
years: i64,
},
PerShare {
source: String,
shares_key: String,
},
Simple {
formula: String,
},
}
#[derive(Debug, Deserialize, Clone)]
pub struct UniversalIncomeFile {
pub version: String,
@@ -222,7 +266,9 @@ pub fn load_surface_pack(pack: FiscalPack) -> Result<SurfacePackFile> {
core_file
.surfaces
.into_iter()
.filter(|surface| surface.statement == "balance" || surface.statement == "cash_flow")
.filter(|surface| {
surface.statement == "balance" || surface.statement == "cash_flow"
})
.filter(|surface| {
!pack_inherited_keys
.contains(&(surface.statement.clone(), surface.surface_key.clone()))
@@ -297,6 +343,28 @@ pub fn load_kpi_pack(pack: FiscalPack) -> Result<KpiPackFile> {
Ok(file)
}
pub fn load_computed_pack(pack: FiscalPack) -> Result<ComputedPackFile> {
let taxonomy_dir = resolve_taxonomy_dir()?;
let path = taxonomy_dir
.join("fiscal")
.join("v1")
.join(format!("{}.computed.json", pack.as_str()));
let raw = fs::read_to_string(&path).with_context(|| {
format!(
"taxonomy resolution failed: unable to read {}",
path.display()
)
})?;
let file = serde_json::from_str::<ComputedPackFile>(&raw).with_context(|| {
format!(
"taxonomy resolution failed: unable to parse {}",
path.display()
)
})?;
let _ = (&file.version, &file.pack);
Ok(file)
}
pub fn load_universal_income_definitions() -> Result<UniversalIncomeFile> {
let taxonomy_dir = resolve_taxonomy_dir()?;
let path = taxonomy_dir
@@ -359,6 +427,10 @@ mod tests {
let kpi_pack = load_kpi_pack(FiscalPack::Core).expect("core kpi pack should load");
assert_eq!(kpi_pack.pack, "core");
let computed_pack =
load_computed_pack(FiscalPack::Core).expect("core computed pack should load");
assert_eq!(computed_pack.pack, "core");
let universal_income =
load_universal_income_definitions().expect("universal income config should load");
assert!(!universal_income.rows.is_empty());