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

@@ -12,6 +12,8 @@ mod surface_mapper;
mod taxonomy_loader;
mod universal_income;
use taxonomy_loader::{ComputationSpec, ComputedDefinition};
#[cfg(feature = "with-crabrl")]
use crabrl as _;
@@ -112,6 +114,7 @@ pub struct HydrateFilingResponse {
pub surface_rows: SurfaceRowMap,
pub detail_rows: DetailRowStatementMap,
pub kpi_rows: Vec<KpiRowOutput>,
pub computed_definitions: Vec<ComputedDefinitionOutput>,
pub contexts: Vec<ContextOutput>,
pub derived_metrics: FilingMetrics,
pub validation_result: ValidationResultOutput,
@@ -257,6 +260,86 @@ pub struct KpiRowOutput {
pub has_dimensions: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ComputedDefinitionOutput {
pub key: String,
pub label: String,
pub category: String,
pub order: i64,
pub unit: String,
pub computation: ComputationSpecOutput,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_cadences: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_external_data: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ComputationSpecOutput {
Ratio {
numerator: String,
denominator: String,
},
YoyGrowth {
source: String,
},
Cagr {
source: String,
years: i64,
},
PerShare {
source: String,
shares_key: String,
},
Simple {
formula: String,
},
}
impl From<&ComputationSpec> for ComputationSpecOutput {
fn from(spec: &ComputationSpec) -> Self {
match spec {
ComputationSpec::Ratio {
numerator,
denominator,
} => ComputationSpecOutput::Ratio {
numerator: numerator.clone(),
denominator: denominator.clone(),
},
ComputationSpec::YoyGrowth { source } => ComputationSpecOutput::YoyGrowth {
source: source.clone(),
},
ComputationSpec::Cagr { source, years } => ComputationSpecOutput::Cagr {
source: source.clone(),
years: *years,
},
ComputationSpec::PerShare { source, shares_key } => ComputationSpecOutput::PerShare {
source: source.clone(),
shares_key: shares_key.clone(),
},
ComputationSpec::Simple { formula } => ComputationSpecOutput::Simple {
formula: formula.clone(),
},
}
}
}
impl From<&ComputedDefinition> for ComputedDefinitionOutput {
fn from(def: &ComputedDefinition) -> Self {
ComputedDefinitionOutput {
key: def.key.clone(),
label: def.label.clone(),
category: def.category.clone(),
order: def.order,
unit: def.unit.clone(),
computation: ComputationSpecOutput::from(&def.computation),
supported_cadences: def.supported_cadences.clone(),
requires_external_data: def.requires_external_data.clone(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConceptOutput {
pub concept_key: String,
@@ -435,6 +518,7 @@ pub fn hydrate_filing(input: HydrateFilingRequest) -> Result<HydrateFilingRespon
surface_rows: empty_surface_rows,
detail_rows: empty_detail_rows,
kpi_rows: vec![],
computed_definitions: vec![],
contexts: vec![],
derived_metrics: FilingMetrics::default(),
validation_result,
@@ -541,6 +625,18 @@ pub fn hydrate_filing(input: HydrateFilingRequest) -> Result<HydrateFilingRespon
&compact_model.concept_mappings,
);
let computed_pack = taxonomy_loader::load_computed_pack(pack_selection.pack)
.ok()
.or_else(|| taxonomy_loader::load_computed_pack(pack_selector::FiscalPack::Core).ok());
let computed_definitions: Vec<ComputedDefinitionOutput> = computed_pack
.map(|pack| {
pack.computed
.iter()
.map(ComputedDefinitionOutput::from)
.collect()
})
.unwrap_or_default();
let has_rows = materialized
.statement_rows
.values()
@@ -578,6 +674,7 @@ pub fn hydrate_filing(input: HydrateFilingRequest) -> Result<HydrateFilingRespon
surface_rows: compact_model.surface_rows,
detail_rows: compact_model.detail_rows,
kpi_rows: kpi_result.rows,
computed_definitions,
contexts: parsed_instance.contexts,
derived_metrics: metrics::derive_metrics(&facts),
validation_result,

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());

View File

@@ -0,0 +1,389 @@
{
"version": "fiscal-v1",
"pack": "core",
"computed": [
{
"key": "gross_margin",
"label": "Gross Margin",
"category": "margins",
"order": 10,
"unit": "percent",
"computation": {
"type": "ratio",
"numerator": "gross_profit",
"denominator": "revenue"
}
},
{
"key": "operating_margin",
"label": "Operating Margin",
"category": "margins",
"order": 20,
"unit": "percent",
"computation": {
"type": "ratio",
"numerator": "operating_income",
"denominator": "revenue"
}
},
{
"key": "ebitda_margin",
"label": "EBITDA Margin",
"category": "margins",
"order": 30,
"unit": "percent",
"computation": {
"type": "ratio",
"numerator": "ebitda",
"denominator": "revenue"
}
},
{
"key": "net_margin",
"label": "Net Margin",
"category": "margins",
"order": 40,
"unit": "percent",
"computation": {
"type": "ratio",
"numerator": "net_income",
"denominator": "revenue"
}
},
{
"key": "fcf_margin",
"label": "FCF Margin",
"category": "margins",
"order": 50,
"unit": "percent",
"computation": {
"type": "ratio",
"numerator": "free_cash_flow",
"denominator": "revenue"
}
},
{
"key": "roa",
"label": "ROA",
"category": "returns",
"order": 60,
"unit": "percent",
"computation": {
"type": "simple",
"formula": "net_income / average(total_assets)"
}
},
{
"key": "roe",
"label": "ROE",
"category": "returns",
"order": 70,
"unit": "percent",
"computation": {
"type": "simple",
"formula": "net_income / average(total_equity)"
}
},
{
"key": "roic",
"label": "ROIC",
"category": "returns",
"order": 80,
"unit": "percent",
"computation": {
"type": "simple",
"formula": "nopat / average_invested_capital"
}
},
{
"key": "roce",
"label": "ROCE",
"category": "returns",
"order": 90,
"unit": "percent",
"computation": {
"type": "simple",
"formula": "operating_income / average_capital_employed"
}
},
{
"key": "debt_to_equity",
"label": "Debt to Equity",
"category": "financial_health",
"order": 100,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "total_debt",
"denominator": "total_equity"
}
},
{
"key": "net_debt_to_ebitda",
"label": "Net Debt to EBITDA",
"category": "financial_health",
"order": 110,
"unit": "ratio",
"computation": {
"type": "simple",
"formula": "net_debt / ebitda"
}
},
{
"key": "cash_to_debt",
"label": "Cash to Debt",
"category": "financial_health",
"order": 120,
"unit": "ratio",
"computation": {
"type": "simple",
"formula": "(cash_and_equivalents + short_term_investments) / total_debt"
}
},
{
"key": "current_ratio",
"label": "Current Ratio",
"category": "financial_health",
"order": 130,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "current_assets",
"denominator": "current_liabilities"
}
},
{
"key": "revenue_per_share",
"label": "Revenue per Share",
"category": "per_share",
"order": 140,
"unit": "currency",
"computation": {
"type": "per_share",
"source": "revenue",
"shares_key": "diluted_shares"
}
},
{
"key": "fcf_per_share",
"label": "FCF per Share",
"category": "per_share",
"order": 150,
"unit": "currency",
"computation": {
"type": "per_share",
"source": "free_cash_flow",
"shares_key": "diluted_shares"
}
},
{
"key": "book_value_per_share",
"label": "Book Value per Share",
"category": "per_share",
"order": 160,
"unit": "currency",
"computation": {
"type": "per_share",
"source": "total_equity",
"shares_key": "diluted_shares"
}
},
{
"key": "revenue_yoy",
"label": "Revenue YoY",
"category": "growth",
"order": 170,
"unit": "percent",
"computation": {
"type": "yoy_growth",
"source": "revenue"
}
},
{
"key": "net_income_yoy",
"label": "Net Income YoY",
"category": "growth",
"order": 180,
"unit": "percent",
"computation": {
"type": "yoy_growth",
"source": "net_income"
}
},
{
"key": "eps_yoy",
"label": "EPS YoY",
"category": "growth",
"order": 190,
"unit": "percent",
"computation": {
"type": "yoy_growth",
"source": "diluted_eps"
}
},
{
"key": "fcf_yoy",
"label": "FCF YoY",
"category": "growth",
"order": 200,
"unit": "percent",
"computation": {
"type": "yoy_growth",
"source": "free_cash_flow"
}
},
{
"key": "3y_revenue_cagr",
"label": "3Y Revenue CAGR",
"category": "growth",
"order": 210,
"unit": "percent",
"computation": {
"type": "cagr",
"source": "revenue",
"years": 3
},
"supported_cadences": ["annual"]
},
{
"key": "5y_revenue_cagr",
"label": "5Y Revenue CAGR",
"category": "growth",
"order": 220,
"unit": "percent",
"computation": {
"type": "cagr",
"source": "revenue",
"years": 5
},
"supported_cadences": ["annual"]
},
{
"key": "3y_eps_cagr",
"label": "3Y EPS CAGR",
"category": "growth",
"order": 230,
"unit": "percent",
"computation": {
"type": "cagr",
"source": "diluted_eps",
"years": 3
},
"supported_cadences": ["annual"]
},
{
"key": "5y_eps_cagr",
"label": "5Y EPS CAGR",
"category": "growth",
"order": 240,
"unit": "percent",
"computation": {
"type": "cagr",
"source": "diluted_eps",
"years": 5
},
"supported_cadences": ["annual"]
},
{
"key": "market_cap",
"label": "Market Cap",
"category": "valuation",
"order": 250,
"unit": "currency",
"computation": {
"type": "simple",
"formula": "price * diluted_shares"
},
"requires_external_data": ["price"]
},
{
"key": "enterprise_value",
"label": "Enterprise Value",
"category": "valuation",
"order": 260,
"unit": "currency",
"computation": {
"type": "simple",
"formula": "market_cap + total_debt - cash_and_equivalents - short_term_investments"
},
"requires_external_data": ["market_cap"]
},
{
"key": "price_to_earnings",
"label": "Price to Earnings",
"category": "valuation",
"order": 270,
"unit": "ratio",
"computation": {
"type": "simple",
"formula": "price / diluted_eps"
},
"requires_external_data": ["price"]
},
{
"key": "price_to_fcf",
"label": "Price to FCF",
"category": "valuation",
"order": 280,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "market_cap",
"denominator": "free_cash_flow"
},
"requires_external_data": ["market_cap"]
},
{
"key": "price_to_book",
"label": "Price to Book",
"category": "valuation",
"order": 290,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "market_cap",
"denominator": "total_equity"
},
"requires_external_data": ["market_cap"]
},
{
"key": "ev_to_sales",
"label": "EV to Sales",
"category": "valuation",
"order": 300,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "enterprise_value",
"denominator": "revenue"
},
"requires_external_data": ["enterprise_value"]
},
{
"key": "ev_to_ebitda",
"label": "EV to EBITDA",
"category": "valuation",
"order": 310,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "enterprise_value",
"denominator": "ebitda"
},
"requires_external_data": ["enterprise_value"]
},
{
"key": "ev_to_fcf",
"label": "EV to FCF",
"category": "valuation",
"order": 320,
"unit": "ratio",
"computation": {
"type": "ratio",
"numerator": "enterprise_value",
"denominator": "free_cash_flow"
},
"requires_external_data": ["enterprise_value"]
}
]
}