Expand financials surfaces with ratios, KPIs, and cadence support

- Add bundled financial modeling pipeline (ratios, KPI dimensions/notes, trend series, standardization)
- Introduce company financial bundles storage (Drizzle migration + repo wiring)
- Refactor financials page/API/query flow to use surfaceKind + cadence and new response shapes
This commit is contained in:
2026-03-07 15:16:35 -05:00
parent a42622ba6e
commit db01f207a5
33 changed files with 3589 additions and 1643 deletions

View File

@@ -4,8 +4,9 @@ import type {
CoveragePriority,
CoverageStatus,
Filing,
FinancialHistoryWindow,
FinancialCadence,
FinancialStatementKind,
FinancialSurfaceKind,
ResearchJournalEntryType,
TaskStatus
} from '@/lib/types';
@@ -15,7 +16,7 @@ import { asErrorMessage, jsonError } from '@/lib/server/http';
import { buildPortfolioSummary } from '@/lib/server/portfolio';
import {
defaultFinancialSyncLimit,
getCompanyFinancialTaxonomy
getCompanyFinancials
} from '@/lib/server/financial-taxonomy';
import { redactInternalFilingAnalysisFields } from '@/lib/server/api/filing-redaction';
import {
@@ -68,7 +69,16 @@ const FINANCIAL_STATEMENT_KINDS: FinancialStatementKind[] = [
'equity',
'comprehensive_income'
];
const FINANCIAL_HISTORY_WINDOWS: FinancialHistoryWindow[] = ['10y', 'all'];
const FINANCIAL_CADENCES: FinancialCadence[] = ['annual', 'quarterly', 'ltm'];
const FINANCIAL_SURFACES: FinancialSurfaceKind[] = [
'income_statement',
'balance_sheet',
'cash_flow_statement',
'ratios',
'segments_kpis',
'adjusted',
'custom_metrics'
];
const COVERAGE_STATUSES: CoverageStatus[] = ['backlog', 'active', 'watch', 'archive'];
const COVERAGE_PRIORITIES: CoveragePriority[] = ['low', 'medium', 'high'];
const JOURNAL_ENTRY_TYPES: ResearchJournalEntryType[] = ['note', 'filing_note', 'status_change'];
@@ -152,10 +162,29 @@ function asStatementKind(value: unknown): FinancialStatementKind {
: 'income';
}
function asHistoryWindow(value: unknown): FinancialHistoryWindow {
return FINANCIAL_HISTORY_WINDOWS.includes(value as FinancialHistoryWindow)
? value as FinancialHistoryWindow
: '10y';
function asCadence(value: unknown): FinancialCadence {
return FINANCIAL_CADENCES.includes(value as FinancialCadence)
? value as FinancialCadence
: 'annual';
}
function surfaceFromLegacyStatement(statement: FinancialStatementKind): FinancialSurfaceKind {
switch (statement) {
case 'balance':
return 'balance_sheet';
case 'cash_flow':
return 'cash_flow_statement';
default:
return 'income_statement';
}
}
function asSurfaceKind(surface: unknown, statement: unknown): FinancialSurfaceKind {
if (FINANCIAL_SURFACES.includes(surface as FinancialSurfaceKind)) {
return surface as FinancialSurfaceKind;
}
return surfaceFromLegacyStatement(asStatementKind(statement));
}
function asCoverageStatus(value: unknown) {
@@ -926,8 +955,8 @@ export const app = new Elysia({ prefix: '/api' })
return jsonError('ticker is required');
}
const statement = asStatementKind(query.statement);
const window = asHistoryWindow(query.window);
const surfaceKind = asSurfaceKind(query.surface, query.statement);
const cadence = asCadence(query.cadence);
const includeDimensions = asBoolean(query.includeDimensions, false);
const includeFacts = asBoolean(query.includeFacts, false);
const cursor = typeof query.cursor === 'string' && query.cursor.trim().length > 0
@@ -943,10 +972,10 @@ export const app = new Elysia({ prefix: '/api' })
? Number(query.factsLimit)
: undefined;
let payload = await getCompanyFinancialTaxonomy({
let payload = await getCompanyFinancials({
ticker,
statement,
window,
surfaceKind,
cadence,
includeDimensions,
includeFacts,
factsCursor,
@@ -961,7 +990,7 @@ export const app = new Elysia({ prefix: '/api' })
const shouldQueueSync = cursor === null && (
payload.dataSourceStatus.pendingFilings > 0
|| payload.coverage.filings === 0
|| (window === 'all' && payload.nextCursor !== null)
|| payload.nextCursor !== null
);
if (shouldQueueSync) {
@@ -972,7 +1001,7 @@ export const app = new Elysia({ prefix: '/api' })
taskType: 'sync_filings',
payload: buildSyncFilingsPayload({
ticker,
limit: defaultFinancialSyncLimit(window),
limit: defaultFinancialSyncLimit(),
category: watchlistItem?.category,
tags: watchlistItem?.tags
}),
@@ -999,6 +1028,20 @@ export const app = new Elysia({ prefix: '/api' })
}, {
query: t.Object({
ticker: t.String({ minLength: 1 }),
surface: t.Optional(t.Union([
t.Literal('income_statement'),
t.Literal('balance_sheet'),
t.Literal('cash_flow_statement'),
t.Literal('ratios'),
t.Literal('segments_kpis'),
t.Literal('adjusted'),
t.Literal('custom_metrics')
])),
cadence: t.Optional(t.Union([
t.Literal('annual'),
t.Literal('quarterly'),
t.Literal('ltm')
])),
statement: t.Optional(t.Union([
t.Literal('income'),
t.Literal('balance'),
@@ -1006,7 +1049,6 @@ export const app = new Elysia({ prefix: '/api' })
t.Literal('equity'),
t.Literal('comprehensive_income')
])),
window: t.Optional(t.Union([t.Literal('10y'), t.Literal('all')])),
includeDimensions: t.Optional(t.Union([t.String(), t.Boolean()])),
includeFacts: t.Optional(t.Union([t.String(), t.Boolean()])),
cursor: t.Optional(t.String()),