Files
Neon-Desk/lib/server/financials/standardize.ts

153 lines
4.9 KiB
TypeScript

import type {
DimensionBreakdownRow,
FinancialStatementKind,
FinancialStatementPeriod,
StandardizedFinancialRow,
TaxonomyFactRow,
TaxonomyStatementRow
} from '@/lib/types';
function valueOrNull(values: Record<string, number | null>, periodId: string) {
return periodId in values ? values[periodId] : null;
}
function sumValues(values: Array<number | null>, treatNullAsZero = false) {
if (!treatNullAsZero && values.some((value) => value === null)) {
return null;
}
return values.reduce<number>((sum, value) => sum + (value ?? 0), 0);
}
export function factMatchesPeriod(fact: TaxonomyFactRow, period: FinancialStatementPeriod) {
if (period.periodStart) {
return fact.periodStart === period.periodStart && fact.periodEnd === period.periodEnd;
}
return (fact.periodInstant ?? fact.periodEnd) === period.periodEnd;
}
export function buildDimensionBreakdown(
facts: TaxonomyFactRow[],
periods: FinancialStatementPeriod[],
faithfulRows: TaxonomyStatementRow[],
standardizedRows: StandardizedFinancialRow[]
) {
const periodByFilingId = new Map<number, FinancialStatementPeriod>();
for (const period of periods) {
periodByFilingId.set(period.filingId, period);
}
const faithfulRowByKey = new Map(faithfulRows.map((row) => [row.key, row]));
const standardizedRowsBySource = new Map<string, StandardizedFinancialRow[]>();
for (const row of standardizedRows) {
for (const sourceRowKey of row.sourceRowKeys) {
const existing = standardizedRowsBySource.get(sourceRowKey);
if (existing) {
existing.push(row);
} else {
standardizedRowsBySource.set(sourceRowKey, [row]);
}
}
}
const map = new Map<string, DimensionBreakdownRow[]>();
const pushRow = (key: string, row: DimensionBreakdownRow) => {
const existing = map.get(key);
if (existing) {
existing.push(row);
} else {
map.set(key, [row]);
}
};
for (const fact of facts) {
if (fact.dimensions.length === 0) {
continue;
}
const period = periodByFilingId.get(fact.filingId) ?? null;
if (!period || !factMatchesPeriod(fact, period)) {
continue;
}
const faithfulRow = faithfulRowByKey.get(fact.conceptKey) ?? null;
const standardizedMatches = standardizedRowsBySource.get(fact.conceptKey) ?? [];
for (const dimension of fact.dimensions) {
const faithfulDimensionRow: DimensionBreakdownRow = {
rowKey: fact.conceptKey,
concept: fact.qname,
sourceRowKey: fact.conceptKey,
sourceLabel: faithfulRow?.label ?? null,
periodId: period.id,
axis: dimension.axis,
member: dimension.member,
value: fact.value,
unit: fact.unit,
provenanceType: 'taxonomy'
};
pushRow(fact.conceptKey, faithfulDimensionRow);
for (const standardizedRow of standardizedMatches) {
pushRow(standardizedRow.key, {
...faithfulDimensionRow,
rowKey: standardizedRow.key
});
}
}
}
return map.size > 0 ? Object.fromEntries(map.entries()) : null;
}
function cloneStandardizedRows(rows: StandardizedFinancialRow[]) {
return rows.map((row) => ({
...row,
values: { ...row.values },
sourceConcepts: [...row.sourceConcepts],
sourceRowKeys: [...row.sourceRowKeys],
sourceFactIds: [...row.sourceFactIds],
resolvedSourceRowKeys: { ...row.resolvedSourceRowKeys }
}));
}
export function buildLtmStandardizedRows(
quarterlyRows: StandardizedFinancialRow[],
quarterlyPeriods: FinancialStatementPeriod[],
ltmPeriods: FinancialStatementPeriod[],
statement: Extract<FinancialStatementKind, 'income' | 'balance' | 'cash_flow' | 'equity' | 'disclosure'>
) {
const sortedQuarterlyPeriods = [...quarterlyPeriods].sort((left, right) => {
return Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate);
});
const result = cloneStandardizedRows(quarterlyRows).map((row) => ({
...row,
values: {} as Record<string, number | null>,
resolvedSourceRowKeys: {} as Record<string, string | null>
}));
for (const row of result) {
const source = quarterlyRows.find((entry) => entry.key === row.key);
if (!source) {
continue;
}
for (const ltmPeriod of ltmPeriods) {
const anchorIndex = sortedQuarterlyPeriods.findIndex((period) => `ltm:${period.id}` === ltmPeriod.id);
if (anchorIndex < 3) {
continue;
}
const slice = sortedQuarterlyPeriods.slice(anchorIndex - 3, anchorIndex + 1);
const sourceValues = slice.map((period) => source.values[period.id] ?? null);
row.values[ltmPeriod.id] = statement === 'balance' || statement === 'equity'
? sourceValues[sourceValues.length - 1] ?? null
: sumValues(sourceValues);
row.resolvedSourceRowKeys[ltmPeriod.id] = source.formulaKey ? null : source.resolvedSourceRowKeys[slice[slice.length - 1]?.id ?? ''] ?? null;
}
}
return result;
}