feat(financials-v2): hydrate filing statements and aggregate history

This commit is contained in:
2026-03-02 09:33:58 -05:00
parent bcf4c69c92
commit 3f3182310b
4 changed files with 1532 additions and 3 deletions

View File

@@ -0,0 +1,315 @@
import type {
CompanyFinancialStatementsResponse,
DimensionBreakdownRow,
FilingFaithfulStatementRow,
FinancialHistoryWindow,
FinancialStatementKind,
FinancialStatementMode,
FinancialStatementPeriod,
StandardizedStatementRow
} from '@/lib/types';
import { listFilingsRecords } from '@/lib/server/repos/filings';
import {
countFilingStatementSnapshotStatuses,
type DimensionStatementSnapshotRow,
type FilingFaithfulStatementSnapshotRow,
type FilingStatementSnapshotRecord,
listFilingStatementSnapshotsByTicker,
type StandardizedStatementSnapshotRow
} from '@/lib/server/repos/filing-statements';
type GetCompanyFinancialStatementsInput = {
ticker: string;
mode: FinancialStatementMode;
statement: FinancialStatementKind;
window: FinancialHistoryWindow;
includeDimensions: boolean;
cursor?: string | null;
limit?: number;
v2Enabled: boolean;
queuedSync: boolean;
};
type FinancialStatementRowByMode = StandardizedStatementRow | FilingFaithfulStatementRow;
function safeTicker(input: string) {
return input.trim().toUpperCase();
}
function isFinancialForm(type: string): type is '10-K' | '10-Q' {
return type === '10-K' || type === '10-Q';
}
function rowDimensionMatcher(row: { key: string; concept: string | null }, item: DimensionStatementSnapshotRow) {
const concept = row.concept?.toLowerCase() ?? '';
const itemConcept = item.concept?.toLowerCase() ?? '';
if (item.rowKey === row.key) {
return true;
}
return Boolean(concept && itemConcept && concept === itemConcept);
}
function periodSorter(left: FinancialStatementPeriod, right: FinancialStatementPeriod) {
const byDate = Date.parse(left.filingDate) - Date.parse(right.filingDate);
if (Number.isFinite(byDate) && byDate !== 0) {
return byDate;
}
return left.id.localeCompare(right.id);
}
function resolveDimensionPeriodId(rawPeriodId: string, periods: FinancialStatementPeriod[]) {
const exact = periods.find((period) => period.id === rawPeriodId);
if (exact) {
return exact.id;
}
const byDate = periods.find((period) => period.filingDate === rawPeriodId || period.periodEnd === rawPeriodId);
return byDate?.id ?? null;
}
function getRowsForSnapshot(
snapshot: FilingStatementSnapshotRecord,
mode: FinancialStatementMode,
statement: FinancialStatementKind
) {
if (mode === 'standardized') {
return snapshot.standardized_bundle?.statements?.[statement] ?? [];
}
return snapshot.statement_bundle?.statements?.[statement] ?? [];
}
function buildPeriods(
snapshots: FilingStatementSnapshotRecord[],
mode: FinancialStatementMode,
statement: FinancialStatementKind
) {
const map = new Map<string, FinancialStatementPeriod>();
for (const snapshot of snapshots) {
const rows = getRowsForSnapshot(snapshot, mode, statement);
if (rows.length === 0) {
continue;
}
const sourcePeriods = mode === 'standardized'
? snapshot.standardized_bundle?.periods
: snapshot.statement_bundle?.periods;
for (const period of sourcePeriods ?? []) {
if (!map.has(period.id)) {
map.set(period.id, {
id: period.id,
filingId: period.filingId,
accessionNumber: period.accessionNumber,
filingDate: period.filingDate,
periodEnd: period.periodEnd,
filingType: period.filingType,
periodLabel: period.periodLabel
});
}
}
}
return [...map.values()].sort(periodSorter);
}
function buildRows(
snapshots: FilingStatementSnapshotRecord[],
periods: FinancialStatementPeriod[],
mode: FinancialStatementMode,
statement: FinancialStatementKind,
includeDimensions: boolean
) {
const rowMap = new Map<string, FinancialStatementRowByMode>();
const dimensionMap = includeDimensions
? new Map<string, DimensionBreakdownRow[]>()
: null;
for (const snapshot of snapshots) {
const rows = getRowsForSnapshot(snapshot, mode, statement);
const dimensions = snapshot.dimension_bundle?.statements?.[statement] ?? [];
if (mode === 'standardized') {
for (const sourceRow of rows as StandardizedStatementSnapshotRow[]) {
const existing = rowMap.get(sourceRow.key) as StandardizedStatementRow | undefined;
const hasDimensions = dimensions.some((item) => rowDimensionMatcher(sourceRow, item));
if (!existing) {
rowMap.set(sourceRow.key, {
key: sourceRow.key,
label: sourceRow.label,
concept: sourceRow.concept,
category: sourceRow.category,
sourceConcepts: [...sourceRow.sourceConcepts],
values: { ...sourceRow.values },
hasDimensions
});
continue;
}
existing.hasDimensions = existing.hasDimensions || hasDimensions;
for (const concept of sourceRow.sourceConcepts) {
if (!existing.sourceConcepts.includes(concept)) {
existing.sourceConcepts.push(concept);
}
}
for (const [periodId, value] of Object.entries(sourceRow.values)) {
if (!(periodId in existing.values)) {
existing.values[periodId] = value;
}
}
}
} else {
for (const sourceRow of rows as FilingFaithfulStatementSnapshotRow[]) {
const rowKey = sourceRow.concept ? `concept-${sourceRow.concept.toLowerCase()}` : `label-${sourceRow.key}`;
const existing = rowMap.get(rowKey) as FilingFaithfulStatementRow | undefined;
const hasDimensions = dimensions.some((item) => rowDimensionMatcher(sourceRow, item));
if (!existing) {
rowMap.set(rowKey, {
key: rowKey,
label: sourceRow.label,
concept: sourceRow.concept,
order: sourceRow.order,
depth: sourceRow.depth,
isSubtotal: sourceRow.isSubtotal,
values: { ...sourceRow.values },
hasDimensions
});
continue;
}
existing.hasDimensions = existing.hasDimensions || hasDimensions;
existing.order = Math.min(existing.order, sourceRow.order);
existing.depth = Math.min(existing.depth, sourceRow.depth);
existing.isSubtotal = existing.isSubtotal || sourceRow.isSubtotal;
for (const [periodId, value] of Object.entries(sourceRow.values)) {
if (!(periodId in existing.values)) {
existing.values[periodId] = value;
}
}
}
}
if (dimensionMap) {
for (const item of dimensions) {
const periodId = resolveDimensionPeriodId(item.periodId, periods);
if (!periodId) {
continue;
}
const entry: DimensionBreakdownRow = {
rowKey: item.rowKey,
concept: item.concept,
periodId,
axis: item.axis,
member: item.member,
value: item.value,
unit: item.unit
};
const group = dimensionMap.get(item.rowKey);
if (group) {
group.push(entry);
} else {
dimensionMap.set(item.rowKey, [entry]);
}
}
}
}
const rows = [...rowMap.values()].sort((a, b) => {
const left = mode === 'standardized' ? a.label : `${(a as FilingFaithfulStatementRow).order.toString().padStart(5, '0')}::${a.label}`;
const right = mode === 'standardized' ? b.label : `${(b as FilingFaithfulStatementRow).order.toString().padStart(5, '0')}::${b.label}`;
return left.localeCompare(right);
});
if (mode === 'standardized') {
const standardized = rows as StandardizedStatementRow[];
const core = standardized.filter((row) => row.category === 'core');
const nonCore = standardized.filter((row) => row.category !== 'core');
const orderedRows = [...core, ...nonCore];
return {
rows: orderedRows,
dimensions: dimensionMap ? Object.fromEntries(dimensionMap.entries()) : null
};
}
return {
rows: rows as FilingFaithfulStatementRow[],
dimensions: dimensionMap ? Object.fromEntries(dimensionMap.entries()) : null
};
}
export function defaultFinancialSyncLimit(window: FinancialHistoryWindow) {
return window === 'all' ? 120 : 60;
}
export async function getCompanyFinancialStatements(input: GetCompanyFinancialStatementsInput): Promise<CompanyFinancialStatementsResponse> {
const ticker = safeTicker(input.ticker);
const snapshotResult = await listFilingStatementSnapshotsByTicker({
ticker,
window: input.window,
limit: input.limit,
cursor: input.cursor
});
const statuses = await countFilingStatementSnapshotStatuses(ticker);
const filings = await listFilingsRecords({
ticker,
limit: input.window === 'all' ? 250 : 120
});
const financialFilings = filings.filter((filing) => isFinancialForm(filing.filing_type));
const periods = buildPeriods(snapshotResult.snapshots, input.mode, input.statement);
const rowResult = buildRows(
snapshotResult.snapshots,
periods,
input.mode,
input.statement,
input.includeDimensions
);
const latestFiling = filings[0] ?? null;
return {
company: {
ticker,
companyName: latestFiling?.company_name ?? ticker,
cik: latestFiling?.cik ?? null
},
mode: input.mode,
statement: input.statement,
window: input.window,
periods,
rows: rowResult.rows,
nextCursor: snapshotResult.nextCursor,
coverage: {
filings: periods.length,
rows: rowResult.rows.length,
dimensions: rowResult.dimensions
? Object.values(rowResult.dimensions).reduce((total, rows) => total + rows.length, 0)
: 0
},
dataSourceStatus: {
enabled: input.v2Enabled,
hydratedFilings: statuses.ready,
partialFilings: statuses.partial,
failedFilings: statuses.failed,
pendingFilings: Math.max(0, financialFilings.length - statuses.ready - statuses.partial - statuses.failed),
queuedSync: input.queuedSync
},
dimensionBreakdown: rowResult.dimensions
};
}
export const __financialStatementsInternals = {
buildPeriods,
buildRows,
defaultFinancialSyncLimit
};