Automate issuer overlay creation from ticker searches
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user