Fix annual financial selector and QCOM standardization

This commit is contained in:
2026-03-09 18:50:59 -04:00
parent 1a18ac825d
commit 9f972305e6
9 changed files with 3385 additions and 226 deletions

View File

@@ -13,6 +13,11 @@ type PrimaryPeriodSelection = {
snapshots: FilingTaxonomySnapshotRecord[];
};
type CandidateSelectionInput = {
rows: TaxonomyStatementRow[];
periods: FinancialStatementPeriod[];
};
function parseEpoch(value: string | null) {
if (!value) {
return Number.NaN;
@@ -54,6 +59,147 @@ function preferredDurationDays(filingType: FinancialStatementPeriod['filingType'
return filingType === '10-K' ? 365 : 90;
}
function isPresentedStatementRow(row: TaxonomyStatementRow) {
return row.roleUri !== null && row.order !== Number.MAX_SAFE_INTEGER;
}
function isPlausiblePrimaryPeriod(period: FinancialStatementPeriod, filingDate: string) {
const filingEpoch = parseEpoch(filingDate);
const periodEndEpoch = parseEpoch(period.periodEnd ?? period.filingDate);
if (Number.isFinite(filingEpoch) && Number.isFinite(periodEndEpoch) && periodEndEpoch > filingEpoch) {
return false;
}
const periodStartEpoch = parseEpoch(period.periodStart);
if (Number.isFinite(filingEpoch) && Number.isFinite(periodStartEpoch) && periodStartEpoch > filingEpoch) {
return false;
}
return true;
}
function candidatePeriodsForRows(input: CandidateSelectionInput) {
const usedPeriodIds = new Set<string>();
for (const row of input.rows) {
for (const periodId of Object.keys(row.values)) {
usedPeriodIds.add(periodId);
}
}
return input.periods.filter((period) => usedPeriodIds.has(period.id));
}
function coverageScore(rows: TaxonomyStatementRow[], periodId: string) {
let count = 0;
for (const row of rows) {
if (periodId in row.values) {
count += 1;
}
}
return count;
}
function compareBalancePeriods(
left: FinancialStatementPeriod,
right: FinancialStatementPeriod,
rowCoverage: Map<string, number>
) {
const leftInstant = isInstantPeriod(left) ? 1 : 0;
const rightInstant = isInstantPeriod(right) ? 1 : 0;
if (leftInstant !== rightInstant) {
return rightInstant - leftInstant;
}
const leftCoverage = rowCoverage.get(left.id) ?? 0;
const rightCoverage = rowCoverage.get(right.id) ?? 0;
if (leftCoverage !== rightCoverage) {
return rightCoverage - leftCoverage;
}
const leftDate = parseEpoch(left.periodEnd ?? left.filingDate);
const rightDate = parseEpoch(right.periodEnd ?? right.filingDate);
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
return rightDate - leftDate;
}
return left.id.localeCompare(right.id);
}
function compareFlowPeriods(
left: FinancialStatementPeriod,
right: FinancialStatementPeriod,
rowCoverage: Map<string, number>,
targetDays: number,
preferLaterBeforeDistance: boolean
) {
const leftDuration = isInstantPeriod(left) ? 0 : 1;
const rightDuration = isInstantPeriod(right) ? 0 : 1;
if (leftDuration !== rightDuration) {
return rightDuration - leftDuration;
}
const leftDate = parseEpoch(left.periodEnd ?? left.filingDate);
const rightDate = parseEpoch(right.periodEnd ?? right.filingDate);
if (preferLaterBeforeDistance && Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
return rightDate - leftDate;
}
const leftCoverage = rowCoverage.get(left.id) ?? 0;
const rightCoverage = rowCoverage.get(right.id) ?? 0;
if (leftCoverage !== rightCoverage) {
return rightCoverage - leftCoverage;
}
const leftDistance = Math.abs((periodDurationDays(left) ?? targetDays) - targetDays);
const rightDistance = Math.abs((periodDurationDays(right) ?? targetDays) - targetDays);
if (leftDistance !== rightDistance) {
return leftDistance - rightDistance;
}
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
return rightDate - leftDate;
}
return left.id.localeCompare(right.id);
}
function chooseCandidates(snapshot: FilingTaxonomySnapshotRecord, rows: TaxonomyStatementRow[]) {
const periods = snapshot.periods ?? [];
const presentedRows = rows.filter(isPresentedStatementRow);
const plausiblePresented = candidatePeriodsForRows({
rows: presentedRows,
periods
}).filter((period) => isPlausiblePrimaryPeriod(period, snapshot.filing_date));
if (plausiblePresented.length > 0) {
return {
candidates: plausiblePresented,
coverageRows: presentedRows,
fallbackMode: false
};
}
const allCandidates = candidatePeriodsForRows({
rows,
periods
});
const plausibleAll = allCandidates.filter((period) => isPlausiblePrimaryPeriod(period, snapshot.filing_date));
if (plausibleAll.length > 0) {
return {
candidates: plausibleAll,
coverageRows: rows,
fallbackMode: true
};
}
return {
candidates: allCandidates,
coverageRows: rows,
fallbackMode: true
};
}
function selectPrimaryPeriodFromSnapshot(
snapshot: FilingTaxonomySnapshotRecord,
statement: FinancialStatementKind
@@ -63,45 +209,21 @@ function selectPrimaryPeriodFromSnapshot(
return null;
}
const usedPeriodIds = new Set<string>();
for (const row of rows) {
for (const periodId of Object.keys(row.values)) {
usedPeriodIds.add(periodId);
}
}
const candidates = (snapshot.periods ?? []).filter((period) => usedPeriodIds.has(period.id));
const { candidates, coverageRows, fallbackMode } = chooseCandidates(snapshot, rows);
if (candidates.length === 0) {
return null;
}
if (statement === 'balance') {
const instantCandidates = candidates.filter(isInstantPeriod);
return (instantCandidates.length > 0 ? instantCandidates : candidates)
.sort((left, right) => periodSorter(right, left))[0] ?? null;
}
const rowCoverage = new Map<string, number>(
candidates.map((period) => [period.id, coverageScore(coverageRows, period.id)])
);
const durationCandidates = candidates.filter((period) => !isInstantPeriod(period));
if (durationCandidates.length === 0) {
return candidates.sort((left, right) => periodSorter(right, left))[0] ?? null;
if (statement === 'balance') {
return [...candidates].sort((left, right) => compareBalancePeriods(left, right, rowCoverage))[0] ?? null;
}
const targetDays = preferredDurationDays(snapshot.filing_type);
return durationCandidates.sort((left, right) => {
const leftDate = parseEpoch(left.periodEnd ?? left.filingDate);
const rightDate = parseEpoch(right.periodEnd ?? right.filingDate);
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
return rightDate - leftDate;
}
const leftDistance = Math.abs((periodDurationDays(left) ?? targetDays) - targetDays);
const rightDistance = Math.abs((periodDurationDays(right) ?? targetDays) - targetDays);
if (leftDistance !== rightDistance) {
return leftDistance - rightDistance;
}
return left.id.localeCompare(right.id);
})[0] ?? null;
return [...candidates].sort((left, right) => compareFlowPeriods(left, right, rowCoverage, targetDays, fallbackMode))[0] ?? null;
}
function filingTypeForCadence(cadence: FinancialCadence) {