- 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
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import type {
|
|
FinancialSurfaceKind,
|
|
RatioRow,
|
|
StandardizedFinancialRow,
|
|
StructuredKpiRow,
|
|
TrendSeries
|
|
} from '@/lib/types';
|
|
import { RATIO_CATEGORIES } from '@/lib/generated';
|
|
import { KPI_CATEGORY_ORDER } from '@/lib/server/financials/kpi-registry';
|
|
|
|
function toTrendSeriesRow(row: {
|
|
key: string;
|
|
label: string;
|
|
category: string;
|
|
unit: TrendSeries['unit'];
|
|
values: Record<string, number | null>;
|
|
}) {
|
|
return {
|
|
key: row.key,
|
|
label: row.label,
|
|
category: row.category,
|
|
unit: row.unit,
|
|
values: row.values
|
|
} satisfies TrendSeries;
|
|
}
|
|
|
|
export function buildFinancialCategories(rows: Array<{ category: string }>, surfaceKind: FinancialSurfaceKind) {
|
|
const counts = new Map<string, number>();
|
|
for (const row of rows) {
|
|
counts.set(row.category, (counts.get(row.category) ?? 0) + 1);
|
|
}
|
|
|
|
const order = surfaceKind === 'ratios'
|
|
? [...RATIO_CATEGORIES]
|
|
: surfaceKind === 'segments_kpis'
|
|
? [...KPI_CATEGORY_ORDER]
|
|
: [...counts.keys()];
|
|
|
|
return order
|
|
.filter((key) => (counts.get(key) ?? 0) > 0)
|
|
.map((key) => ({
|
|
key,
|
|
label: key.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()),
|
|
count: counts.get(key) ?? 0
|
|
}));
|
|
}
|
|
|
|
export function buildTrendSeries(input: {
|
|
surfaceKind: FinancialSurfaceKind;
|
|
statementRows?: StandardizedFinancialRow[];
|
|
ratioRows?: RatioRow[];
|
|
kpiRows?: StructuredKpiRow[];
|
|
}) {
|
|
switch (input.surfaceKind) {
|
|
case 'income_statement':
|
|
return (input.statementRows ?? [])
|
|
.filter((row) => row.key === 'revenue' || row.key === 'net_income')
|
|
.map(toTrendSeriesRow);
|
|
case 'balance_sheet':
|
|
return (input.statementRows ?? [])
|
|
.filter((row) => row.key === 'total_assets' || row.key === 'cash_and_equivalents' || row.key === 'total_debt')
|
|
.map(toTrendSeriesRow);
|
|
case 'cash_flow_statement':
|
|
return (input.statementRows ?? [])
|
|
.filter((row) => row.key === 'operating_cash_flow' || row.key === 'free_cash_flow' || row.key === 'capital_expenditures')
|
|
.map(toTrendSeriesRow);
|
|
case 'ratios':
|
|
return (input.ratioRows ?? [])
|
|
.filter((row) => row.category === 'margins')
|
|
.map(toTrendSeriesRow);
|
|
case 'segments_kpis': {
|
|
const rows = input.kpiRows ?? [];
|
|
const firstCategory = buildFinancialCategories(rows, 'segments_kpis')[0]?.key ?? null;
|
|
return rows
|
|
.filter((row) => row.category === firstCategory)
|
|
.slice(0, 4)
|
|
.map(toTrendSeriesRow);
|
|
}
|
|
default:
|
|
return [];
|
|
}
|
|
}
|