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:
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
389
rust/taxonomy/fiscal/v1/core.computed.json
Normal file
389
rust/taxonomy/fiscal/v1/core.computed.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user