Add research workspace and graphing flows

This commit is contained in:
2026-03-07 16:52:35 -05:00
parent db01f207a5
commit 62bacdf104
37 changed files with 5494 additions and 434 deletions

147
lib/financial-metrics.ts Normal file
View File

@@ -0,0 +1,147 @@
import type {
FinancialCadence,
FinancialSurfaceKind,
FinancialUnit,
RatioRow
} from '@/lib/types';
export type GraphableFinancialSurfaceKind = Extract<
FinancialSurfaceKind,
'income_statement' | 'balance_sheet' | 'cash_flow_statement' | 'ratios'
>;
export type StatementMetricDefinition = {
key: string;
label: string;
category: string;
order: number;
unit: FinancialUnit;
localNames?: readonly string[];
labelIncludes?: readonly string[];
};
export type RatioMetricDefinition = {
key: string;
label: string;
category: string;
order: number;
unit: RatioRow['unit'];
denominatorKey: string | null;
supportedCadences?: readonly FinancialCadence[];
};
export const GRAPHABLE_FINANCIAL_SURFACES: readonly GraphableFinancialSurfaceKind[] = [
'income_statement',
'balance_sheet',
'cash_flow_statement',
'ratios'
] as const;
export const INCOME_STATEMENT_METRIC_DEFINITIONS: StatementMetricDefinition[] = [
{ key: 'revenue', label: 'Revenue', category: 'revenue', order: 10, unit: 'currency', localNames: ['RevenueFromContractWithCustomerExcludingAssessedTax', 'Revenues', 'SalesRevenueNet', 'TotalRevenuesAndOtherIncome'], labelIncludes: ['revenue', 'sales'] },
{ key: 'cost_of_revenue', label: 'Cost of Revenue', category: 'expense', order: 20, unit: 'currency', localNames: ['CostOfRevenue', 'CostOfGoodsSold', 'CostOfSales', 'CostOfProductsSold', 'CostOfServices'], labelIncludes: ['cost of revenue', 'cost of sales', 'cost of goods sold'] },
{ key: 'gross_profit', label: 'Gross Profit', category: 'profit', order: 30, unit: 'currency', localNames: ['GrossProfit'] },
{ key: 'gross_margin', label: 'Gross Margin', category: 'margin', order: 40, unit: 'percent', labelIncludes: ['gross margin'] },
{ key: 'research_and_development', label: 'Research & Development', category: 'opex', order: 50, unit: 'currency', localNames: ['ResearchAndDevelopmentExpense'], labelIncludes: ['research and development'] },
{ key: 'sales_and_marketing', label: 'Sales & Marketing', category: 'opex', order: 60, unit: 'currency', localNames: ['SellingAndMarketingExpense'], labelIncludes: ['sales and marketing', 'selling and marketing'] },
{ key: 'general_and_administrative', label: 'General & Administrative', category: 'opex', order: 70, unit: 'currency', localNames: ['GeneralAndAdministrativeExpense'], labelIncludes: ['general and administrative'] },
{ key: 'selling_general_and_administrative', label: 'Selling, General & Administrative', category: 'opex', order: 80, unit: 'currency', localNames: ['SellingGeneralAndAdministrativeExpense'], labelIncludes: ['selling, general', 'selling general'] },
{ key: 'operating_income', label: 'Operating Income', category: 'profit', order: 90, unit: 'currency', localNames: ['OperatingIncomeLoss', 'IncomeLossFromOperations'], labelIncludes: ['operating income'] },
{ key: 'operating_margin', label: 'Operating Margin', category: 'margin', order: 100, unit: 'percent', labelIncludes: ['operating margin'] },
{ key: 'interest_income', label: 'Interest Income', category: 'non_operating', order: 110, unit: 'currency', localNames: ['InterestIncomeExpenseNonoperatingNet', 'InterestIncomeOther', 'InvestmentIncomeInterest'], labelIncludes: ['interest income'] },
{ key: 'interest_expense', label: 'Interest Expense', category: 'non_operating', order: 120, unit: 'currency', localNames: ['InterestExpense', 'InterestAndDebtExpense'], labelIncludes: ['interest expense'] },
{ key: 'other_non_operating_income', label: 'Other Non-Operating Income', category: 'non_operating', order: 130, unit: 'currency', localNames: ['OtherNonoperatingIncomeExpense', 'NonoperatingIncomeExpense'], labelIncludes: ['other non-operating', 'non-operating income'] },
{ key: 'pretax_income', label: 'Pretax Income', category: 'profit', order: 140, unit: 'currency', localNames: ['IncomeBeforeTaxExpenseBenefit', 'PretaxIncome'], labelIncludes: ['income before taxes', 'pretax income'] },
{ key: 'income_tax_expense', label: 'Income Tax Expense', category: 'tax', order: 150, unit: 'currency', localNames: ['IncomeTaxExpenseBenefit'], labelIncludes: ['income tax'] },
{ key: 'effective_tax_rate', label: 'Effective Tax Rate', category: 'tax', order: 160, unit: 'percent', labelIncludes: ['effective tax rate'] },
{ key: 'net_income', label: 'Net Income', category: 'profit', order: 170, unit: 'currency', localNames: ['NetIncomeLoss', 'ProfitLoss'], labelIncludes: ['net income'] },
{ key: 'diluted_eps', label: 'Diluted EPS', category: 'per_share', order: 180, unit: 'currency', localNames: ['EarningsPerShareDiluted', 'DilutedEarningsPerShare'], labelIncludes: ['diluted eps', 'diluted earnings per share'] },
{ key: 'basic_eps', label: 'Basic EPS', category: 'per_share', order: 190, unit: 'currency', localNames: ['EarningsPerShareBasic', 'BasicEarningsPerShare'], labelIncludes: ['basic eps', 'basic earnings per share'] },
{ key: 'diluted_shares', label: 'Diluted Shares', category: 'shares', order: 200, unit: 'shares', localNames: ['WeightedAverageNumberOfDilutedSharesOutstanding', 'WeightedAverageNumberOfShareOutstandingDiluted'], labelIncludes: ['diluted shares', 'weighted average diluted'] },
{ key: 'basic_shares', label: 'Basic Shares', category: 'shares', order: 210, unit: 'shares', localNames: ['WeightedAverageNumberOfSharesOutstandingBasic', 'WeightedAverageNumberOfShareOutstandingBasicAndDiluted'], labelIncludes: ['basic shares', 'weighted average basic'] },
{ key: 'depreciation_and_amortization', label: 'Depreciation & Amortization', category: 'non_cash', order: 220, unit: 'currency', localNames: ['DepreciationDepletionAndAmortization', 'DepreciationAmortizationAndAccretionNet', 'DepreciationAndAmortization'], labelIncludes: ['depreciation', 'amortization'] },
{ key: 'ebitda', label: 'EBITDA', category: 'profit', order: 230, unit: 'currency', localNames: ['OperatingIncomeLoss'], labelIncludes: ['ebitda'] },
{ key: 'stock_based_compensation', label: 'Stock-Based Compensation', category: 'non_cash', order: 240, unit: 'currency', localNames: ['ShareBasedCompensation', 'AllocatedShareBasedCompensationExpense'], labelIncludes: ['stock-based compensation', 'share-based compensation'] }
] as const satisfies StatementMetricDefinition[];
export const BALANCE_SHEET_METRIC_DEFINITIONS: StatementMetricDefinition[] = [
{ key: 'cash_and_equivalents', label: 'Cash & Equivalents', category: 'asset', order: 10, unit: 'currency', localNames: ['CashAndCashEquivalentsAtCarryingValue', 'CashCashEquivalentsAndShortTermInvestments', 'CashAndShortTermInvestments'], labelIncludes: ['cash and cash equivalents'] },
{ key: 'short_term_investments', label: 'Short-Term Investments', category: 'asset', order: 20, unit: 'currency', localNames: ['AvailableForSaleSecuritiesCurrent', 'ShortTermInvestments'], labelIncludes: ['short-term investments', 'marketable securities'] },
{ key: 'accounts_receivable', label: 'Accounts Receivable', category: 'asset', order: 30, unit: 'currency', localNames: ['AccountsReceivableNetCurrent', 'ReceivablesNetCurrent'], labelIncludes: ['accounts receivable'] },
{ key: 'inventory', label: 'Inventory', category: 'asset', order: 40, unit: 'currency', localNames: ['InventoryNet'], labelIncludes: ['inventory'] },
{ key: 'other_current_assets', label: 'Other Current Assets', category: 'asset', order: 50, unit: 'currency', localNames: ['OtherAssetsCurrent'], labelIncludes: ['other current assets'] },
{ key: 'current_assets', label: 'Current Assets', category: 'asset', order: 60, unit: 'currency', localNames: ['AssetsCurrent'], labelIncludes: ['current assets'] },
{ key: 'property_plant_equipment', label: 'Property, Plant & Equipment', category: 'asset', order: 70, unit: 'currency', localNames: ['PropertyPlantAndEquipmentNet'], labelIncludes: ['property, plant and equipment', 'property and equipment'] },
{ key: 'goodwill', label: 'Goodwill', category: 'asset', order: 80, unit: 'currency', localNames: ['Goodwill'], labelIncludes: ['goodwill'] },
{ key: 'intangible_assets', label: 'Intangible Assets', category: 'asset', order: 90, unit: 'currency', localNames: ['FiniteLivedIntangibleAssetsNet', 'IndefiniteLivedIntangibleAssetsExcludingGoodwill', 'IntangibleAssetsNetExcludingGoodwill'], labelIncludes: ['intangible assets'] },
{ key: 'total_assets', label: 'Total Assets', category: 'asset', order: 100, unit: 'currency', localNames: ['Assets'], labelIncludes: ['total assets'] },
{ key: 'accounts_payable', label: 'Accounts Payable', category: 'liability', order: 110, unit: 'currency', localNames: ['AccountsPayableCurrent'], labelIncludes: ['accounts payable'] },
{ key: 'accrued_liabilities', label: 'Accrued Liabilities', category: 'liability', order: 120, unit: 'currency', localNames: ['AccruedLiabilitiesCurrent'], labelIncludes: ['accrued liabilities'] },
{ key: 'deferred_revenue_current', label: 'Deferred Revenue, Current', category: 'liability', order: 130, unit: 'currency', localNames: ['ContractWithCustomerLiabilityCurrent', 'DeferredRevenueCurrent'], labelIncludes: ['deferred revenue current', 'current deferred revenue'] },
{ key: 'current_liabilities', label: 'Current Liabilities', category: 'liability', order: 140, unit: 'currency', localNames: ['LiabilitiesCurrent'], labelIncludes: ['current liabilities'] },
{ key: 'long_term_debt', label: 'Long-Term Debt', category: 'liability', order: 150, unit: 'currency', localNames: ['LongTermDebtNoncurrent', 'LongTermDebt', 'DebtNoncurrent', 'LongTermDebtAndCapitalLeaseObligations'], labelIncludes: ['long-term debt'] },
{ key: 'current_debt', label: 'Current Debt', category: 'liability', order: 160, unit: 'currency', localNames: ['DebtCurrent', 'ShortTermBorrowings', 'LongTermDebtCurrent'], labelIncludes: ['current debt', 'short-term debt'] },
{ key: 'lease_liabilities', label: 'Lease Liabilities', category: 'liability', order: 170, unit: 'currency', localNames: ['OperatingLeaseLiabilityNoncurrent', 'FinanceLeaseLiabilityNoncurrent', 'LesseeOperatingLeaseLiability'], labelIncludes: ['lease liabilities'] },
{ key: 'total_debt', label: 'Total Debt', category: 'liability', order: 180, unit: 'currency', localNames: ['DebtAndFinanceLeaseLiabilities', 'Debt'], labelIncludes: ['total debt'] },
{ key: 'deferred_revenue_noncurrent', label: 'Deferred Revenue, Noncurrent', category: 'liability', order: 190, unit: 'currency', localNames: ['ContractWithCustomerLiabilityNoncurrent', 'DeferredRevenueNoncurrent'], labelIncludes: ['deferred revenue noncurrent'] },
{ key: 'total_liabilities', label: 'Total Liabilities', category: 'liability', order: 200, unit: 'currency', localNames: ['Liabilities'], labelIncludes: ['total liabilities'] },
{ key: 'retained_earnings', label: 'Retained Earnings', category: 'equity', order: 210, unit: 'currency', localNames: ['RetainedEarningsAccumulatedDeficit'], labelIncludes: ['retained earnings', 'accumulated deficit'] },
{ key: 'total_equity', label: 'Total Equity', category: 'equity', order: 220, unit: 'currency', localNames: ['StockholdersEquity', 'StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest', 'PartnersCapital'], labelIncludes: ['total equity', 'stockholders equity', 'stockholders equity'] },
{ key: 'net_cash_position', label: 'Net Cash Position', category: 'liquidity', order: 230, unit: 'currency', labelIncludes: ['net cash position'] }
] as const satisfies StatementMetricDefinition[];
export const CASH_FLOW_STATEMENT_METRIC_DEFINITIONS: StatementMetricDefinition[] = [
{ key: 'operating_cash_flow', label: 'Operating Cash Flow', category: 'cash_flow', order: 10, unit: 'currency', localNames: ['NetCashProvidedByUsedInOperatingActivities', 'NetCashProvidedByUsedInOperatingActivitiesContinuingOperations'], labelIncludes: ['operating cash flow'] },
{ key: 'capital_expenditures', label: 'Capital Expenditures', category: 'cash_flow', order: 20, unit: 'currency', localNames: ['PaymentsToAcquirePropertyPlantAndEquipment', 'CapitalExpendituresIncurredButNotYetPaid'], labelIncludes: ['capital expenditures', 'capital expenditure'] },
{ key: 'free_cash_flow', label: 'Free Cash Flow', category: 'cash_flow', order: 30, unit: 'currency', labelIncludes: ['free cash flow'] },
{ key: 'stock_based_compensation', label: 'Stock-Based Compensation', category: 'non_cash', order: 40, unit: 'currency', localNames: ['ShareBasedCompensation', 'AllocatedShareBasedCompensationExpense'], labelIncludes: ['stock-based compensation', 'share-based compensation'] },
{ key: 'acquisitions', label: 'Acquisitions', category: 'investing', order: 50, unit: 'currency', localNames: ['PaymentsToAcquireBusinessesNetOfCashAcquired'], labelIncludes: ['acquisitions'] },
{ key: 'share_repurchases', label: 'Share Repurchases', category: 'financing', order: 60, unit: 'currency', localNames: ['PaymentsForRepurchaseOfCommonStock', 'PaymentsForRepurchaseOfEquity'], labelIncludes: ['share repurchases', 'repurchase of common stock'] },
{ key: 'dividends_paid', label: 'Dividends Paid', category: 'financing', order: 70, unit: 'currency', localNames: ['PaymentsOfDividends', 'PaymentsOfDividendsCommonStock'], labelIncludes: ['dividends paid'] },
{ key: 'debt_issued', label: 'Debt Issued', category: 'financing', order: 80, unit: 'currency', localNames: ['ProceedsFromIssuanceOfLongTermDebt'], labelIncludes: ['debt issued'] },
{ key: 'debt_repaid', label: 'Debt Repaid', category: 'financing', order: 90, unit: 'currency', localNames: ['RepaymentsOfLongTermDebt', 'RepaymentsOfDebt'], labelIncludes: ['debt repaid', 'repayment of debt'] }
] as const satisfies StatementMetricDefinition[];
export const RATIO_DEFINITIONS: RatioMetricDefinition[] = [
{ key: 'gross_margin', label: 'Gross Margin', category: 'margins', order: 10, unit: 'percent', denominatorKey: 'revenue' },
{ key: 'operating_margin', label: 'Operating Margin', category: 'margins', order: 20, unit: 'percent', denominatorKey: 'revenue' },
{ key: 'ebitda_margin', label: 'EBITDA Margin', category: 'margins', order: 30, unit: 'percent', denominatorKey: 'revenue' },
{ key: 'net_margin', label: 'Net Margin', category: 'margins', order: 40, unit: 'percent', denominatorKey: 'revenue' },
{ key: 'fcf_margin', label: 'FCF Margin', category: 'margins', order: 50, unit: 'percent', denominatorKey: 'revenue' },
{ key: 'roa', label: 'ROA', category: 'returns', order: 60, unit: 'percent', denominatorKey: 'total_assets' },
{ key: 'roe', label: 'ROE', category: 'returns', order: 70, unit: 'percent', denominatorKey: 'total_equity' },
{ key: 'roic', label: 'ROIC', category: 'returns', order: 80, unit: 'percent', denominatorKey: 'average_invested_capital' },
{ key: 'roce', label: 'ROCE', category: 'returns', order: 90, unit: 'percent', denominatorKey: 'average_capital_employed' },
{ key: 'debt_to_equity', label: 'Debt to Equity', category: 'financial_health', order: 100, unit: 'ratio', denominatorKey: 'total_equity' },
{ key: 'net_debt_to_ebitda', label: 'Net Debt to EBITDA', category: 'financial_health', order: 110, unit: 'ratio', denominatorKey: 'ebitda' },
{ key: 'cash_to_debt', label: 'Cash to Debt', category: 'financial_health', order: 120, unit: 'ratio', denominatorKey: 'total_debt' },
{ key: 'current_ratio', label: 'Current Ratio', category: 'financial_health', order: 130, unit: 'ratio', denominatorKey: 'current_liabilities' },
{ key: 'revenue_per_share', label: 'Revenue per Share', category: 'per_share', order: 140, unit: 'currency', denominatorKey: 'diluted_shares' },
{ key: 'fcf_per_share', label: 'FCF per Share', category: 'per_share', order: 150, unit: 'currency', denominatorKey: 'diluted_shares' },
{ key: 'book_value_per_share', label: 'Book Value per Share', category: 'per_share', order: 160, unit: 'currency', denominatorKey: 'diluted_shares' },
{ key: 'revenue_yoy', label: 'Revenue YoY', category: 'growth', order: 170, unit: 'percent', denominatorKey: 'revenue' },
{ key: 'net_income_yoy', label: 'Net Income YoY', category: 'growth', order: 180, unit: 'percent', denominatorKey: 'net_income' },
{ key: 'eps_yoy', label: 'EPS YoY', category: 'growth', order: 190, unit: 'percent', denominatorKey: 'diluted_eps' },
{ key: 'fcf_yoy', label: 'FCF YoY', category: 'growth', order: 200, unit: 'percent', denominatorKey: 'free_cash_flow' },
{ key: '3y_revenue_cagr', label: '3Y Revenue CAGR', category: 'growth', order: 210, unit: 'percent', denominatorKey: 'revenue', supportedCadences: ['annual'] },
{ key: '5y_revenue_cagr', label: '5Y Revenue CAGR', category: 'growth', order: 220, unit: 'percent', denominatorKey: 'revenue', supportedCadences: ['annual'] },
{ key: '3y_eps_cagr', label: '3Y EPS CAGR', category: 'growth', order: 230, unit: 'percent', denominatorKey: 'diluted_eps', supportedCadences: ['annual'] },
{ key: '5y_eps_cagr', label: '5Y EPS CAGR', category: 'growth', order: 240, unit: 'percent', denominatorKey: 'diluted_eps', supportedCadences: ['annual'] },
{ key: 'market_cap', label: 'Market Cap', category: 'valuation', order: 250, unit: 'currency', denominatorKey: null },
{ key: 'enterprise_value', label: 'Enterprise Value', category: 'valuation', order: 260, unit: 'currency', denominatorKey: null },
{ key: 'price_to_earnings', label: 'Price to Earnings', category: 'valuation', order: 270, unit: 'ratio', denominatorKey: 'diluted_eps' },
{ key: 'price_to_fcf', label: 'Price to FCF', category: 'valuation', order: 280, unit: 'ratio', denominatorKey: 'free_cash_flow' },
{ key: 'price_to_book', label: 'Price to Book', category: 'valuation', order: 290, unit: 'ratio', denominatorKey: 'total_equity' },
{ key: 'ev_to_sales', label: 'EV to Sales', category: 'valuation', order: 300, unit: 'ratio', denominatorKey: 'revenue' },
{ key: 'ev_to_ebitda', label: 'EV to EBITDA', category: 'valuation', order: 310, unit: 'ratio', denominatorKey: 'ebitda' },
{ key: 'ev_to_fcf', label: 'EV to FCF', category: 'valuation', order: 320, unit: 'ratio', denominatorKey: 'free_cash_flow' }
] as const satisfies RatioMetricDefinition[];
export const RATIO_CATEGORY_ORDER = [
'margins',
'returns',
'financial_health',
'per_share',
'growth',
'valuation'
] as const;