Automate issuer overlay creation from ticker searches

This commit is contained in:
2026-03-19 20:44:58 -04:00
parent 17de3dd72d
commit 391d6d34ce
79 changed files with 4746 additions and 695 deletions

View File

@@ -10,7 +10,11 @@ const SURFACE_CHILDREN: Partial<
Record<
Extract<
FinancialSurfaceKind,
"income_statement" | "balance_sheet" | "cash_flow_statement"
| "income_statement"
| "balance_sheet"
| "cash_flow_statement"
| "equity_statement"
| "disclosures"
>,
Record<string, string[]>
>
@@ -110,6 +114,8 @@ const SURFACE_CHILDREN: Partial<
"other_financing_activities",
],
},
equity_statement: {},
disclosures: {},
};
export type StatementInspectorSelection = {
@@ -151,7 +157,7 @@ export type StatementTreeSection = {
nodes: StatementTreeNode[];
};
export type StatementTreeModel = {
type StatementTreeModel = {
sections: StatementTreeSection[];
autoExpandedKeys: Set<string>;
visibleNodeCount: number;
@@ -184,7 +190,11 @@ const UNMAPPED_SECTION_LABEL = "Unmapped / Residual";
function surfaceConfigForKind(
surfaceKind: Extract<
FinancialSurfaceKind,
"income_statement" | "balance_sheet" | "cash_flow_statement"
| "income_statement"
| "balance_sheet"
| "cash_flow_statement"
| "equity_statement"
| "disclosures"
>,
) {
return SURFACE_CHILDREN[surfaceKind] ?? {};
@@ -198,6 +208,101 @@ function normalize(value: string) {
return value.trim().toLowerCase();
}
function normalizeConceptIdentity(value: string | null | undefined) {
if (!value) {
return null;
}
const trimmed = value.trim().toLowerCase();
if (trimmed.length === 0) {
return null;
}
const withoutNamespace = trimmed.includes(":")
? (trimmed.split(":").pop() ?? trimmed)
: trimmed;
const normalized = withoutNamespace.replace(/[\s_-]+/g, "");
return normalized.length > 0 ? normalized : null;
}
function surfaceConceptIdentities(row: SurfaceFinancialRow) {
const identities = new Set<string>();
const addIdentity = (value: string | null | undefined) => {
const normalized = normalizeConceptIdentity(value);
if (normalized) {
identities.add(normalized);
}
};
addIdentity(row.key);
for (const sourceConcept of row.sourceConcepts ?? []) {
addIdentity(sourceConcept);
}
for (const sourceRowKey of row.sourceRowKeys ?? []) {
addIdentity(sourceRowKey);
}
for (const resolvedSourceRowKey of Object.values(
row.resolvedSourceRowKeys ?? {},
)) {
addIdentity(resolvedSourceRowKey);
}
return identities;
}
function detailConceptIdentities(row: DetailFinancialRow) {
const identities = new Set<string>();
const addIdentity = (value: string | null | undefined) => {
const normalized = normalizeConceptIdentity(value);
if (normalized) {
identities.add(normalized);
}
};
addIdentity(row.key);
addIdentity(row.conceptKey);
addIdentity(row.qname);
addIdentity(row.localName);
return identities;
}
function dedupeDetailRowsAgainstChildSurfaces(
detailRows: DetailFinancialRow[],
childSurfaceRows: SurfaceFinancialRow[],
) {
if (detailRows.length === 0 || childSurfaceRows.length === 0) {
return detailRows;
}
const childConceptIdentities = new Set<string>();
for (const childSurfaceRow of childSurfaceRows) {
for (const identity of surfaceConceptIdentities(childSurfaceRow)) {
childConceptIdentities.add(identity);
}
}
if (childConceptIdentities.size === 0) {
return detailRows;
}
return detailRows.filter((detailRow) => {
for (const identity of detailConceptIdentities(detailRow)) {
if (childConceptIdentities.has(identity)) {
return false;
}
}
return true;
});
}
function searchTextForSurface(row: SurfaceFinancialRow) {
return [
row.label,
@@ -283,7 +388,11 @@ function countNodes(nodes: StatementTreeNode[]) {
export function buildStatementTree(input: {
surfaceKind: Extract<
FinancialSurfaceKind,
"income_statement" | "balance_sheet" | "cash_flow_statement"
| "income_statement"
| "balance_sheet"
| "cash_flow_statement"
| "equity_statement"
| "disclosures"
>;
rows: SurfaceFinancialRow[];
statementDetails: SurfaceDetailMap | null;
@@ -316,8 +425,9 @@ export function buildStatementTree(input: {
Boolean(candidate),
)
.sort(sortSurfaceRows);
const detailRows = [...(input.statementDetails?.[row.key] ?? [])].sort(
sortDetailRows,
const detailRows = dedupeDetailRowsAgainstChildSurfaces(
[...(input.statementDetails?.[row.key] ?? [])].sort(sortDetailRows),
childSurfaceRows,
);
const childSurfaceNodes = childSurfaceRows
.map((childRow) => buildSurfaceNode(childRow, level + 1))
@@ -389,6 +499,22 @@ export function buildStatementTree(input: {
.map((row) => buildSurfaceNode(row, 0))
.filter((node): node is StatementTreeSurfaceNode => Boolean(node));
const totalVisibleDetailCount = input.rows.reduce((sum, row) => {
const childSurfaceRows = (config[row.key] ?? [])
.map((key) => rowByKey.get(key))
.filter((candidate): candidate is SurfaceFinancialRow =>
Boolean(candidate),
);
const detailRows = dedupeDetailRowsAgainstChildSurfaces(
[...(input.statementDetails?.[row.key] ?? [])].sort(sortDetailRows),
childSurfaceRows,
);
return sum + detailRows.length;
}, 0);
const unmappedDetailCount =
input.statementDetails?.[UNMAPPED_DETAIL_GROUP_KEY]?.length ?? 0;
if (input.categories.length === 0) {
const sections: StatementTreeSection[] =
rootNodes.length > 0
@@ -415,11 +541,7 @@ export function buildStatementTree(input: {
0,
),
totalNodeCount:
input.rows.length +
Object.values(input.statementDetails ?? {}).reduce(
(sum, rows) => sum + rows.length,
0,
),
input.rows.length + totalVisibleDetailCount + unmappedDetailCount,
};
}
@@ -472,18 +594,18 @@ export function buildStatementTree(input: {
0,
),
totalNodeCount:
input.rows.length +
Object.values(input.statementDetails ?? {}).reduce(
(sum, rows) => sum + rows.length,
0,
),
input.rows.length + totalVisibleDetailCount + unmappedDetailCount,
};
}
export function resolveStatementSelection(input: {
surfaceKind: Extract<
FinancialSurfaceKind,
"income_statement" | "balance_sheet" | "cash_flow_statement"
| "income_statement"
| "balance_sheet"
| "cash_flow_statement"
| "equity_statement"
| "disclosures"
>;
rows: SurfaceFinancialRow[];
statementDetails: SurfaceDetailMap | null;
@@ -510,14 +632,16 @@ export function resolveStatementSelection(input: {
Boolean(candidate),
)
.sort(sortSurfaceRows);
const detailRows = dedupeDetailRowsAgainstChildSurfaces(
[...(input.statementDetails?.[row.key] ?? [])].sort(sortDetailRows),
childSurfaceRows,
);
return {
kind: "surface",
row,
childSurfaceRows,
detailRows: [...(input.statementDetails?.[row.key] ?? [])].sort(
sortDetailRows,
),
detailRows,
};
}