gloomberb 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +3 -2
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +10 -1
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +8 -1
- package/src/cli/native-smoke.ts +5 -0
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/core/pointer.ts +32 -6
- package/src/components/chart/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/bitmap.ts +16 -9
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/list/view.tsx +1 -1
- package/src/components/command-bar/theme-picker.tsx +1 -1
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +6 -5
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +45 -4
- package/src/components/layout/pane/index.tsx +6 -4
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +204 -0
- package/src/components/layout/shell/drag/runtime.ts +9 -2
- package/src/components/layout/shell/index.test.tsx +92 -2
- package/src/components/layout/shell/index.tsx +45 -3
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/pane/layers.tsx +10 -4
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/status-bar.test.tsx +6 -1
- package/src/components/layout/status-bar.tsx +10 -6
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +12 -14
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/ui/data-table/opentui/index.tsx +29 -22
- package/src/components/ui/data-table/types.ts +10 -4
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/list-view.tsx +1 -1
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +27 -12
- package/src/components/ui/ui.test.tsx +87 -9
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/options/table.ts +5 -5
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +43 -0
- package/src/plugins/prediction-markets/pane.tsx +50 -22
- package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
- package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/box.tsx +6 -1
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +18 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/native-smoke.ts +1 -0
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CachedResourceRecord } from "../../data/resource-store";
|
|
2
2
|
import type { AnalystResearchData, CorporateActionsData, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
|
|
3
3
|
import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
|
|
4
|
+
import { mergeFinancialStatementRows } from "../../utils/financial-statements";
|
|
4
5
|
import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
|
|
5
6
|
import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
6
7
|
import {
|
|
@@ -122,33 +123,20 @@ export function hasDetailedStatementRows(data: TickerFinancials | null | undefin
|
|
|
122
123
|
return rows.some((row) => DETAILED_STATEMENT_KEYS.some((key) => typeof row[key] === "number"));
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (primaryRows.length === 0) return fallbackRows;
|
|
130
|
-
if (fallbackRows.length === 0) return primaryRows;
|
|
131
|
-
|
|
132
|
-
const fallbackByDate = new Map(fallbackRows.map((row) => [row.date, row]));
|
|
133
|
-
const primaryDates = new Set(primaryRows.map((row) => row.date));
|
|
134
|
-
const mergedRows = primaryRows.map((row) => ({
|
|
135
|
-
...fallbackByDate.get(row.date),
|
|
136
|
-
...row,
|
|
137
|
-
date: row.date,
|
|
138
|
-
}));
|
|
139
|
-
|
|
140
|
-
for (const row of fallbackRows) {
|
|
141
|
-
if (!primaryDates.has(row.date)) mergedRows.push(row);
|
|
142
|
-
}
|
|
126
|
+
export function hasDeepStatementHistory(data: TickerFinancials | null | undefined): boolean {
|
|
127
|
+
if (!data) return false;
|
|
128
|
+
return data.annualStatements.length >= 5 || data.quarterlyStatements.length >= 8;
|
|
129
|
+
}
|
|
143
130
|
|
|
144
|
-
|
|
131
|
+
export function hasShallowStatementHistory(data: TickerFinancials | null | undefined): boolean {
|
|
132
|
+
return hasStatementRows(data) && !hasDeepStatementHistory(data);
|
|
145
133
|
}
|
|
146
134
|
|
|
147
135
|
export function mergeMissingStatementArrays(primary: TickerFinancials, fallback: TickerFinancials): TickerFinancials {
|
|
148
136
|
return {
|
|
149
137
|
...primary,
|
|
150
|
-
annualStatements:
|
|
151
|
-
quarterlyStatements:
|
|
138
|
+
annualStatements: mergeFinancialStatementRows(primary.annualStatements, fallback.annualStatements),
|
|
139
|
+
quarterlyStatements: mergeFinancialStatementRows(primary.quarterlyStatements, fallback.quarterlyStatements),
|
|
152
140
|
};
|
|
153
141
|
}
|
|
154
142
|
|
|
@@ -216,8 +204,8 @@ export function mergeFinancials(primary: TickerFinancials | null, fallback: Tick
|
|
|
216
204
|
profile: mergeDefinedObject(primary.profile, fallback.profile),
|
|
217
205
|
fundamentals: mergeDefinedObject(primary.fundamentals, fallback.fundamentals),
|
|
218
206
|
priceHistory: normalizePriceHistory(dominant.priceHistory.length > 0 ? dominant.priceHistory : secondary.priceHistory),
|
|
219
|
-
annualStatements:
|
|
220
|
-
quarterlyStatements:
|
|
207
|
+
annualStatements: mergeFinancialStatementRows(primary.annualStatements, fallback.annualStatements),
|
|
208
|
+
quarterlyStatements: mergeFinancialStatementRows(primary.quarterlyStatements, fallback.quarterlyStatements),
|
|
221
209
|
});
|
|
222
210
|
}
|
|
223
211
|
|
|
@@ -1069,6 +1069,195 @@ describe("AssetDataRouter", () => {
|
|
|
1069
1069
|
expect(yahooCalls).toBe(1);
|
|
1070
1070
|
});
|
|
1071
1071
|
|
|
1072
|
+
test("supplements shallow preferred provider statement history", async () => {
|
|
1073
|
+
const cloudProvider: DataProvider = {
|
|
1074
|
+
...fallbackProvider,
|
|
1075
|
+
id: "cloud",
|
|
1076
|
+
name: "Cloud",
|
|
1077
|
+
priority: 100,
|
|
1078
|
+
async getTickerFinancials() {
|
|
1079
|
+
return makeFinancials({
|
|
1080
|
+
profile: { sector: "Technology" },
|
|
1081
|
+
quote: makeQuote({ symbol: "LINK", price: 25 }),
|
|
1082
|
+
quarterlyStatements: [
|
|
1083
|
+
{ date: "2025-03-31", operatingCashFlow: -271_000 },
|
|
1084
|
+
{ date: "2025-06-30", operatingCashFlow: -138_000 },
|
|
1085
|
+
{ date: "2025-09-30", operatingCashFlow: 653_000 },
|
|
1086
|
+
{ date: "2025-12-31", operatingCashFlow: -356_000 },
|
|
1087
|
+
{ date: "2026-03-31", operatingCashFlow: -543_000 },
|
|
1088
|
+
],
|
|
1089
|
+
});
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
let yahooCalls = 0;
|
|
1093
|
+
const yahooProvider: DataProvider = {
|
|
1094
|
+
...fallbackProvider,
|
|
1095
|
+
id: "yahoo",
|
|
1096
|
+
name: "Yahoo",
|
|
1097
|
+
priority: 1000,
|
|
1098
|
+
async getTickerFinancials() {
|
|
1099
|
+
yahooCalls += 1;
|
|
1100
|
+
return makeFinancials({
|
|
1101
|
+
quarterlyStatements: [
|
|
1102
|
+
{ date: "2024-03-31", operatingCashFlow: -601_000 },
|
|
1103
|
+
{ date: "2024-06-30", operatingCashFlow: -488_000 },
|
|
1104
|
+
{ date: "2024-09-30", operatingCashFlow: -204_000 },
|
|
1105
|
+
{ date: "2024-12-31", operatingCashFlow: 122_000 },
|
|
1106
|
+
],
|
|
1107
|
+
});
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
1112
|
+
const merged = await router.getTickerFinancials("LINK", "NASDAQ");
|
|
1113
|
+
|
|
1114
|
+
expect(yahooCalls).toBe(1);
|
|
1115
|
+
expect(merged.profile?.sector).toBe("Technology");
|
|
1116
|
+
expect(merged.quote?.price).toBe(25);
|
|
1117
|
+
expect(merged.quarterlyStatements.map((row) => row.date)).toEqual([
|
|
1118
|
+
"2024-03-31",
|
|
1119
|
+
"2024-06-30",
|
|
1120
|
+
"2024-09-30",
|
|
1121
|
+
"2024-12-31",
|
|
1122
|
+
"2025-03-31",
|
|
1123
|
+
"2025-06-30",
|
|
1124
|
+
"2025-09-30",
|
|
1125
|
+
"2025-12-31",
|
|
1126
|
+
"2026-03-31",
|
|
1127
|
+
]);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test("enriches shallow batch provider statement history through the single financial route", async () => {
|
|
1131
|
+
const shallowCloudRows = [
|
|
1132
|
+
{ date: "2025-03-31", eps: 0.44 },
|
|
1133
|
+
{ date: "2025-06-30", eps: 0.54 },
|
|
1134
|
+
{ date: "2025-09-30", eps: 0.75 },
|
|
1135
|
+
{ date: "2025-12-31", eps: 0.92 },
|
|
1136
|
+
{ date: "2026-03-31", eps: 0.84 },
|
|
1137
|
+
];
|
|
1138
|
+
let cloudBatchCalls = 0;
|
|
1139
|
+
let cloudSingleCalls = 0;
|
|
1140
|
+
const cloudProvider: DataProvider = {
|
|
1141
|
+
...fallbackProvider,
|
|
1142
|
+
id: "cloud",
|
|
1143
|
+
name: "Cloud",
|
|
1144
|
+
priority: 100,
|
|
1145
|
+
async getTickerFinancials() {
|
|
1146
|
+
cloudSingleCalls += 1;
|
|
1147
|
+
return makeFinancials({
|
|
1148
|
+
profile: { sector: "Technology" },
|
|
1149
|
+
quote: makeQuote({ symbol: "AMD", price: 125 }),
|
|
1150
|
+
quarterlyStatements: shallowCloudRows,
|
|
1151
|
+
});
|
|
1152
|
+
},
|
|
1153
|
+
async getTickerFinancialsBatch(targets) {
|
|
1154
|
+
cloudBatchCalls += 1;
|
|
1155
|
+
return targets.map((target) => ({
|
|
1156
|
+
target,
|
|
1157
|
+
financials: makeFinancials({
|
|
1158
|
+
profile: { sector: "Technology" },
|
|
1159
|
+
quote: makeQuote({ symbol: target.symbol, price: 125 }),
|
|
1160
|
+
quarterlyStatements: shallowCloudRows,
|
|
1161
|
+
}),
|
|
1162
|
+
}));
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
let yahooCalls = 0;
|
|
1166
|
+
const yahooProvider: DataProvider = {
|
|
1167
|
+
...fallbackProvider,
|
|
1168
|
+
id: "yahoo",
|
|
1169
|
+
name: "Yahoo",
|
|
1170
|
+
priority: 1000,
|
|
1171
|
+
async getTickerFinancials() {
|
|
1172
|
+
yahooCalls += 1;
|
|
1173
|
+
return makeFinancials({
|
|
1174
|
+
quarterlyStatements: [
|
|
1175
|
+
{ date: "2024-03-31", eps: 0.07 },
|
|
1176
|
+
{ date: "2024-06-30", eps: 0.16 },
|
|
1177
|
+
{ date: "2024-09-30", eps: 0.47 },
|
|
1178
|
+
{ date: "2024-12-31", eps: 0.29 },
|
|
1179
|
+
],
|
|
1180
|
+
});
|
|
1181
|
+
},
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
1185
|
+
const results = await router.getTickerFinancialsBatch([{ symbol: "AMD", exchange: "NASDAQ" }], { forceRefresh: true });
|
|
1186
|
+
const financials = results[0]?.financials;
|
|
1187
|
+
|
|
1188
|
+
expect(cloudBatchCalls).toBe(1);
|
|
1189
|
+
expect(cloudSingleCalls).toBe(1);
|
|
1190
|
+
expect(yahooCalls).toBe(1);
|
|
1191
|
+
expect(financials?.profile?.sector).toBe("Technology");
|
|
1192
|
+
expect(financials?.quote?.symbol).toBe("AMD");
|
|
1193
|
+
expect(financials?.quarterlyStatements.map((row) => row.date)).toEqual([
|
|
1194
|
+
"2024-03-31",
|
|
1195
|
+
"2024-06-30",
|
|
1196
|
+
"2024-09-30",
|
|
1197
|
+
"2024-12-31",
|
|
1198
|
+
"2025-03-31",
|
|
1199
|
+
"2025-06-30",
|
|
1200
|
+
"2025-09-30",
|
|
1201
|
+
"2025-12-31",
|
|
1202
|
+
"2026-03-31",
|
|
1203
|
+
]);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("refreshes shallow cached provider statement history", async () => {
|
|
1207
|
+
const dbPath = createTempDbPath("cached-shallow-statement-history");
|
|
1208
|
+
const persistence = new AppPersistence(dbPath);
|
|
1209
|
+
const cloudProvider: DataProvider = {
|
|
1210
|
+
...fallbackProvider,
|
|
1211
|
+
id: "cloud",
|
|
1212
|
+
name: "Cloud",
|
|
1213
|
+
priority: 100,
|
|
1214
|
+
async getTickerFinancials() {
|
|
1215
|
+
return makeFinancials({
|
|
1216
|
+
profile: { sector: "Technology" },
|
|
1217
|
+
quote: makeQuote({ symbol: "LINK", price: 25 }),
|
|
1218
|
+
quarterlyStatements: [
|
|
1219
|
+
{ date: "2025-03-31", operatingCashFlow: -271_000 },
|
|
1220
|
+
{ date: "2025-06-30", operatingCashFlow: -138_000 },
|
|
1221
|
+
{ date: "2025-09-30", operatingCashFlow: 653_000 },
|
|
1222
|
+
{ date: "2025-12-31", operatingCashFlow: -356_000 },
|
|
1223
|
+
{ date: "2026-03-31", operatingCashFlow: -543_000 },
|
|
1224
|
+
],
|
|
1225
|
+
});
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
1228
|
+
let yahooCalls = 0;
|
|
1229
|
+
const yahooProvider: DataProvider = {
|
|
1230
|
+
...fallbackProvider,
|
|
1231
|
+
id: "yahoo",
|
|
1232
|
+
name: "Yahoo",
|
|
1233
|
+
priority: 1000,
|
|
1234
|
+
async getTickerFinancials() {
|
|
1235
|
+
yahooCalls += 1;
|
|
1236
|
+
return makeFinancials({
|
|
1237
|
+
quarterlyStatements: [
|
|
1238
|
+
{ date: "2024-03-31", operatingCashFlow: -601_000 },
|
|
1239
|
+
{ date: "2024-06-30", operatingCashFlow: -488_000 },
|
|
1240
|
+
{ date: "2024-09-30", operatingCashFlow: -204_000 },
|
|
1241
|
+
{ date: "2024-12-31", operatingCashFlow: 122_000 },
|
|
1242
|
+
],
|
|
1243
|
+
});
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
const seedRouter = new AssetDataRouter(null, [cloudProvider], persistence.resources);
|
|
1249
|
+
await seedRouter.getTickerFinancials("LINK", "NASDAQ");
|
|
1250
|
+
|
|
1251
|
+
const cachedRouter = new AssetDataRouter(yahooProvider, [cloudProvider], persistence.resources);
|
|
1252
|
+
const merged = await cachedRouter.getTickerFinancials("LINK", "NASDAQ");
|
|
1253
|
+
|
|
1254
|
+
expect(yahooCalls).toBe(1);
|
|
1255
|
+
expect(merged.quarterlyStatements).toHaveLength(9);
|
|
1256
|
+
} finally {
|
|
1257
|
+
persistence.close();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1072
1261
|
test("fills missing preferred statement fields from richer fallback rows", async () => {
|
|
1073
1262
|
const cloudProvider: DataProvider = {
|
|
1074
1263
|
...fallbackProvider,
|
|
@@ -6,6 +6,7 @@ import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/reso
|
|
|
6
6
|
import { shouldLogProviderError } from "../provider-errors";
|
|
7
7
|
import {
|
|
8
8
|
hasDetailedStatementRows,
|
|
9
|
+
hasDeepStatementHistory,
|
|
9
10
|
hasStatementRows,
|
|
10
11
|
mergeMissingStatementArrays,
|
|
11
12
|
} from "./financials";
|
|
@@ -80,7 +81,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
80
81
|
);
|
|
81
82
|
if (!primaryResult) {
|
|
82
83
|
primaryResult = { sourceKey, value };
|
|
83
|
-
if (hasDetailedStatementRows(value)) return primaryResult;
|
|
84
|
+
if (hasDetailedStatementRows(value) && hasDeepStatementHistory(value)) return primaryResult;
|
|
84
85
|
continue;
|
|
85
86
|
}
|
|
86
87
|
if (hasStatementRows(value)) {
|
|
@@ -88,7 +89,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
88
89
|
sourceKey: primaryResult.sourceKey,
|
|
89
90
|
value: mergeMissingStatementArrays(primaryResult.value, value),
|
|
90
91
|
};
|
|
91
|
-
if (hasDetailedStatementRows(primaryResult.value)) return primaryResult;
|
|
92
|
+
if (hasDetailedStatementRows(primaryResult.value) && hasDeepStatementHistory(primaryResult.value)) return primaryResult;
|
|
92
93
|
}
|
|
93
94
|
} catch (error) {
|
|
94
95
|
if (shouldLogProviderError(error)) {
|
|
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
SecEdgarClient,
|
|
4
4
|
extractFilingContent,
|
|
5
|
+
parseCompanyFactsFinancialStatements,
|
|
5
6
|
parseFilingDocuments,
|
|
6
7
|
parseRecentFilings,
|
|
7
8
|
parseTickerLookup,
|
|
@@ -119,6 +120,88 @@ describe("parseFilingDocuments", () => {
|
|
|
119
120
|
});
|
|
120
121
|
});
|
|
121
122
|
|
|
123
|
+
describe("parseCompanyFactsFinancialStatements", () => {
|
|
124
|
+
test("maps SEC company facts into deeper annual and quarterly statement rows", () => {
|
|
125
|
+
const fact = (tag: string, unit: string, rows: unknown[]) => ({
|
|
126
|
+
[tag]: {
|
|
127
|
+
units: {
|
|
128
|
+
[unit]: rows,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const duration = (end: string, val: number, overrides: Record<string, unknown> = {}) => ({
|
|
133
|
+
start: `${end.slice(0, 4)}-01-01`,
|
|
134
|
+
end,
|
|
135
|
+
val,
|
|
136
|
+
filed: end,
|
|
137
|
+
form: "10-K",
|
|
138
|
+
fp: "FY",
|
|
139
|
+
...overrides,
|
|
140
|
+
});
|
|
141
|
+
const instant = (end: string, val: number, overrides: Record<string, unknown> = {}) => ({
|
|
142
|
+
end,
|
|
143
|
+
val,
|
|
144
|
+
filed: end,
|
|
145
|
+
form: "10-K",
|
|
146
|
+
fp: "FY",
|
|
147
|
+
...overrides,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const statements = parseCompanyFactsFinancialStatements({
|
|
151
|
+
facts: {
|
|
152
|
+
"us-gaap": {
|
|
153
|
+
...fact("RevenueFromContractWithCustomerExcludingAssessedTax", "USD", [
|
|
154
|
+
duration("2020-12-31", 100),
|
|
155
|
+
duration("2021-03-31", 30, { form: "10-Q", fp: "Q1", frame: "CY2021Q1" }),
|
|
156
|
+
duration("2021-06-30", 40, { form: "10-Q", fp: "Q2", frame: "CY2021Q2" }),
|
|
157
|
+
]),
|
|
158
|
+
...fact("NetCashProvidedByUsedInOperatingActivities", "USD", [
|
|
159
|
+
duration("2020-12-31", 25),
|
|
160
|
+
duration("2021-03-31", 8, { form: "10-Q", fp: "Q1", frame: "CY2021Q1" }),
|
|
161
|
+
]),
|
|
162
|
+
...fact("PaymentsToAcquirePropertyPlantAndEquipment", "USD", [
|
|
163
|
+
duration("2020-12-31", 5),
|
|
164
|
+
duration("2021-03-31", 3, { form: "10-Q", fp: "Q1", frame: "CY2021Q1" }),
|
|
165
|
+
]),
|
|
166
|
+
...fact("Assets", "USD", [
|
|
167
|
+
instant("2020-12-31", 500),
|
|
168
|
+
instant("2021-03-31", 520, { form: "10-Q", fp: "Q1", frame: "CY2021Q1I" }),
|
|
169
|
+
]),
|
|
170
|
+
...fact("EarningsPerShareDiluted", "USD/shares", [
|
|
171
|
+
duration("2020-12-31", 1.25),
|
|
172
|
+
duration("2021-03-31", 0.35, { form: "10-Q", fp: "Q1", frame: "CY2021Q1" }),
|
|
173
|
+
]),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(statements.annualStatements).toEqual([{
|
|
179
|
+
date: "2020-12-31",
|
|
180
|
+
totalRevenue: 100,
|
|
181
|
+
operatingCashFlow: 25,
|
|
182
|
+
capitalExpenditure: -5,
|
|
183
|
+
freeCashFlow: 20,
|
|
184
|
+
totalAssets: 500,
|
|
185
|
+
eps: 1.25,
|
|
186
|
+
}]);
|
|
187
|
+
expect(statements.quarterlyStatements).toEqual([
|
|
188
|
+
{
|
|
189
|
+
date: "2021-03-31",
|
|
190
|
+
totalRevenue: 30,
|
|
191
|
+
operatingCashFlow: 8,
|
|
192
|
+
capitalExpenditure: -3,
|
|
193
|
+
freeCashFlow: 5,
|
|
194
|
+
totalAssets: 520,
|
|
195
|
+
eps: 0.35,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
date: "2021-06-30",
|
|
199
|
+
totalRevenue: 40,
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
122
205
|
describe("SecEdgarClient", () => {
|
|
123
206
|
test("extracts readable text from filing html", () => {
|
|
124
207
|
const content = extractFilingContent(`
|
package/src/sources/sec-edgar.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SecFilingDocument, SecFilingItem } from "../types/data-provider";
|
|
2
|
+
import type { FinancialStatement } from "../types/financials";
|
|
2
3
|
import { truncateWithEllipsis } from "../utils/text-wrap";
|
|
3
4
|
import {
|
|
4
5
|
PDF_FALLBACK_MESSAGE,
|
|
@@ -10,6 +11,7 @@ export { extractFilingContent } from "./sec-edgar/content";
|
|
|
10
11
|
|
|
11
12
|
const LOOKUP_URL = "https://www.sec.gov/files/company_tickers_exchange.json";
|
|
12
13
|
const SUBMISSIONS_URL = "https://data.sec.gov/submissions";
|
|
14
|
+
const COMPANY_FACTS_URL = "https://data.sec.gov/api/xbrl/companyfacts";
|
|
13
15
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
14
16
|
|
|
15
17
|
function sanitizeIdentityPart(value: string, fallback: string): string {
|
|
@@ -52,6 +54,69 @@ type LookupEntry = {
|
|
|
52
54
|
name?: string;
|
|
53
55
|
};
|
|
54
56
|
|
|
57
|
+
type CompanyFactsEntry = {
|
|
58
|
+
start?: string;
|
|
59
|
+
end?: string;
|
|
60
|
+
val?: number;
|
|
61
|
+
fy?: number | null;
|
|
62
|
+
fp?: string | null;
|
|
63
|
+
form?: string;
|
|
64
|
+
filed?: string;
|
|
65
|
+
frame?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type CompanyFactsStatementField = {
|
|
69
|
+
field: keyof FinancialStatement;
|
|
70
|
+
tags: string[];
|
|
71
|
+
units: string[];
|
|
72
|
+
periodType: "duration" | "instant";
|
|
73
|
+
transform?: (value: number) => number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type SecCompanyFactsStatements = {
|
|
77
|
+
annualStatements: FinancialStatement[];
|
|
78
|
+
quarterlyStatements: FinancialStatement[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const COMPANY_FACTS_STATEMENT_FIELDS: CompanyFactsStatementField[] = [
|
|
82
|
+
{
|
|
83
|
+
field: "totalRevenue",
|
|
84
|
+
tags: ["RevenueFromContractWithCustomerExcludingAssessedTax", "Revenues", "SalesRevenueNet"],
|
|
85
|
+
units: ["USD"],
|
|
86
|
+
periodType: "duration",
|
|
87
|
+
},
|
|
88
|
+
{ field: "grossProfit", tags: ["GrossProfit"], units: ["USD"], periodType: "duration" },
|
|
89
|
+
{ field: "operatingIncome", tags: ["OperatingIncomeLoss"], units: ["USD"], periodType: "duration" },
|
|
90
|
+
{ field: "netIncome", tags: ["NetIncomeLoss", "ProfitLoss"], units: ["USD"], periodType: "duration" },
|
|
91
|
+
{
|
|
92
|
+
field: "operatingCashFlow",
|
|
93
|
+
tags: [
|
|
94
|
+
"NetCashProvidedByUsedInOperatingActivities",
|
|
95
|
+
"NetCashProvidedByUsedInOperatingActivitiesContinuingOperations",
|
|
96
|
+
],
|
|
97
|
+
units: ["USD"],
|
|
98
|
+
periodType: "duration",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
field: "capitalExpenditure",
|
|
102
|
+
tags: ["PaymentsToAcquirePropertyPlantAndEquipment"],
|
|
103
|
+
units: ["USD"],
|
|
104
|
+
periodType: "duration",
|
|
105
|
+
transform: (value) => -Math.abs(value),
|
|
106
|
+
},
|
|
107
|
+
{ field: "totalAssets", tags: ["Assets"], units: ["USD"], periodType: "instant" },
|
|
108
|
+
{
|
|
109
|
+
field: "totalEquity",
|
|
110
|
+
tags: [
|
|
111
|
+
"StockholdersEquity",
|
|
112
|
+
"StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest",
|
|
113
|
+
],
|
|
114
|
+
units: ["USD"],
|
|
115
|
+
periodType: "instant",
|
|
116
|
+
},
|
|
117
|
+
{ field: "eps", tags: ["EarningsPerShareDiluted"], units: ["USD/shares"], periodType: "duration" },
|
|
118
|
+
];
|
|
119
|
+
|
|
55
120
|
function normalize(value?: string): string {
|
|
56
121
|
return (value ?? "").trim().toUpperCase();
|
|
57
122
|
}
|
|
@@ -297,6 +362,160 @@ export function parseFilingDocuments(indexHtml: string, filing: SecFilingItem):
|
|
|
297
362
|
return documents;
|
|
298
363
|
}
|
|
299
364
|
|
|
365
|
+
function companyFactsRecord(value: unknown): Record<string, unknown> | null {
|
|
366
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
367
|
+
? value as Record<string, unknown>
|
|
368
|
+
: null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function companyFactsEntries(payload: unknown, tag: string, units: string[]): CompanyFactsEntry[] {
|
|
372
|
+
const facts = companyFactsRecord(companyFactsRecord(companyFactsRecord(payload)?.facts)?.["us-gaap"]);
|
|
373
|
+
const fact = companyFactsRecord(facts?.[tag]);
|
|
374
|
+
const unitRecord = companyFactsRecord(fact?.units);
|
|
375
|
+
if (!unitRecord) return [];
|
|
376
|
+
|
|
377
|
+
for (const unit of units) {
|
|
378
|
+
const entries = unitRecord[unit];
|
|
379
|
+
if (!Array.isArray(entries)) continue;
|
|
380
|
+
return entries
|
|
381
|
+
.map((entry) => companyFactsRecord(entry))
|
|
382
|
+
.filter((entry): entry is Record<string, unknown> => !!entry)
|
|
383
|
+
.map((entry) => ({
|
|
384
|
+
start: typeof entry.start === "string" ? entry.start : undefined,
|
|
385
|
+
end: typeof entry.end === "string" ? entry.end : undefined,
|
|
386
|
+
val: typeof entry.val === "number" ? entry.val : undefined,
|
|
387
|
+
fy: typeof entry.fy === "number" ? entry.fy : null,
|
|
388
|
+
fp: typeof entry.fp === "string" ? entry.fp : null,
|
|
389
|
+
form: typeof entry.form === "string" ? entry.form : undefined,
|
|
390
|
+
filed: typeof entry.filed === "string" ? entry.filed : undefined,
|
|
391
|
+
frame: typeof entry.frame === "string" ? entry.frame : undefined,
|
|
392
|
+
}))
|
|
393
|
+
.filter((entry) => (
|
|
394
|
+
typeof entry.val === "number"
|
|
395
|
+
&& Number.isFinite(entry.val)
|
|
396
|
+
&& typeof entry.end === "string"
|
|
397
|
+
&& /^\d{4}-\d{2}-\d{2}$/.test(entry.end)
|
|
398
|
+
));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function secFormRank(form: string | undefined): number {
|
|
405
|
+
const normalized = normalize(form);
|
|
406
|
+
if (normalized === "10-K/A") return 4;
|
|
407
|
+
if (normalized === "10-K") return 3;
|
|
408
|
+
if (normalized === "10-Q/A") return 2;
|
|
409
|
+
if (normalized === "10-Q") return 1;
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function companyFactsFiledRank(value: string | undefined): number {
|
|
414
|
+
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return 0;
|
|
415
|
+
return Date.parse(`${value}T00:00:00Z`) || 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function compareCompanyFactsEntries(left: CompanyFactsEntry, right: CompanyFactsEntry | undefined): number {
|
|
419
|
+
if (!right) return 1;
|
|
420
|
+
const filedDiff = companyFactsFiledRank(left.filed) - companyFactsFiledRank(right.filed);
|
|
421
|
+
if (filedDiff !== 0) return filedDiff;
|
|
422
|
+
const formDiff = secFormRank(left.form) - secFormRank(right.form);
|
|
423
|
+
if (formDiff !== 0) return formDiff;
|
|
424
|
+
return String(left.frame ?? "").localeCompare(String(right.frame ?? ""));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isAnnualCompanyFact(entry: CompanyFactsEntry): boolean {
|
|
428
|
+
const form = normalize(entry.form);
|
|
429
|
+
return (form === "10-K" || form === "10-K/A") && normalize(entry.fp ?? undefined) === "FY";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isQuarterlyCompanyFact(entry: CompanyFactsEntry, periodType: "duration" | "instant"): boolean {
|
|
433
|
+
if (periodType === "instant") {
|
|
434
|
+
return /^CY\d{4}Q[1-4]I$/.test(entry.frame ?? "")
|
|
435
|
+
|| (
|
|
436
|
+
(normalize(entry.form) === "10-Q" || normalize(entry.form) === "10-Q/A")
|
|
437
|
+
&& /^Q[1-3]$/.test(normalize(entry.fp ?? undefined))
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return /^CY\d{4}Q[1-4]$/.test(entry.frame ?? "");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function setCompanyFactValue(
|
|
444
|
+
rows: Map<string, FinancialStatement>,
|
|
445
|
+
selectedFacts: Map<string, CompanyFactsEntry>,
|
|
446
|
+
date: string,
|
|
447
|
+
field: keyof FinancialStatement,
|
|
448
|
+
value: number,
|
|
449
|
+
entry: CompanyFactsEntry,
|
|
450
|
+
): void {
|
|
451
|
+
const selectionKey = `${date}:${String(field)}`;
|
|
452
|
+
if (compareCompanyFactsEntries(entry, selectedFacts.get(selectionKey)) < 0) return;
|
|
453
|
+
const row = rows.get(date) ?? { date };
|
|
454
|
+
(row as Record<string, unknown>)[field] = value;
|
|
455
|
+
rows.set(date, row);
|
|
456
|
+
selectedFacts.set(selectionKey, entry);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function fillCompanyFactsStatementRows(
|
|
460
|
+
rows: Map<string, FinancialStatement>,
|
|
461
|
+
selectedFacts: Map<string, CompanyFactsEntry>,
|
|
462
|
+
entries: CompanyFactsEntry[],
|
|
463
|
+
field: CompanyFactsStatementField,
|
|
464
|
+
period: "annual" | "quarterly",
|
|
465
|
+
): void {
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
if (!entry.end || typeof entry.val !== "number") continue;
|
|
468
|
+
const matchesPeriod = period === "annual"
|
|
469
|
+
? isAnnualCompanyFact(entry)
|
|
470
|
+
: isQuarterlyCompanyFact(entry, field.periodType);
|
|
471
|
+
if (!matchesPeriod) continue;
|
|
472
|
+
setCompanyFactValue(
|
|
473
|
+
rows,
|
|
474
|
+
selectedFacts,
|
|
475
|
+
entry.end,
|
|
476
|
+
field.field,
|
|
477
|
+
field.transform ? field.transform(entry.val) : entry.val,
|
|
478
|
+
entry,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function finalizeCompanyFactsStatements(rows: Map<string, FinancialStatement>): FinancialStatement[] {
|
|
484
|
+
const statements = Array.from(rows.values()).sort((left, right) => left.date.localeCompare(right.date));
|
|
485
|
+
for (const statement of statements) {
|
|
486
|
+
if (
|
|
487
|
+
typeof statement.freeCashFlow !== "number"
|
|
488
|
+
&& typeof statement.operatingCashFlow === "number"
|
|
489
|
+
&& typeof statement.capitalExpenditure === "number"
|
|
490
|
+
) {
|
|
491
|
+
statement.freeCashFlow = statement.operatingCashFlow + statement.capitalExpenditure;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return statements;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function parseCompanyFactsFinancialStatements(payload: unknown): SecCompanyFactsStatements {
|
|
498
|
+
const annualRows = new Map<string, FinancialStatement>();
|
|
499
|
+
const quarterlyRows = new Map<string, FinancialStatement>();
|
|
500
|
+
const annualSelectedFacts = new Map<string, CompanyFactsEntry>();
|
|
501
|
+
const quarterlySelectedFacts = new Map<string, CompanyFactsEntry>();
|
|
502
|
+
|
|
503
|
+
for (const field of COMPANY_FACTS_STATEMENT_FIELDS) {
|
|
504
|
+
for (const tag of field.tags) {
|
|
505
|
+
const entries = companyFactsEntries(payload, tag, field.units);
|
|
506
|
+
if (entries.length === 0) continue;
|
|
507
|
+
fillCompanyFactsStatementRows(annualRows, annualSelectedFacts, entries, field, "annual");
|
|
508
|
+
fillCompanyFactsStatementRows(quarterlyRows, quarterlySelectedFacts, entries, field, "quarterly");
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
annualStatements: finalizeCompanyFactsStatements(annualRows),
|
|
515
|
+
quarterlyStatements: finalizeCompanyFactsStatements(quarterlyRows),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
300
519
|
export class SecEdgarClient {
|
|
301
520
|
private lookupPromise: Promise<Map<string, LookupEntry>> | null = null;
|
|
302
521
|
|
|
@@ -407,6 +626,18 @@ export class SecEdgarClient {
|
|
|
407
626
|
return parseRecentFilings(payload, count);
|
|
408
627
|
}
|
|
409
628
|
|
|
629
|
+
async getFinancialStatements(ticker: string): Promise<SecCompanyFactsStatements | null> {
|
|
630
|
+
const normalizedTicker = normalize(ticker);
|
|
631
|
+
if (!normalizedTicker) return null;
|
|
632
|
+
|
|
633
|
+
const lookup = await this.loadLookup();
|
|
634
|
+
const entry = lookup.get(normalizedTicker);
|
|
635
|
+
if (!entry) return null;
|
|
636
|
+
|
|
637
|
+
const payload = await this.fetchJson<unknown>(`${COMPANY_FACTS_URL}/CIK${entry.cik}.json`);
|
|
638
|
+
return parseCompanyFactsFinancialStatements(payload);
|
|
639
|
+
}
|
|
640
|
+
|
|
410
641
|
async getFilingDocuments(filing: SecFilingItem): Promise<SecFilingDocument[]> {
|
|
411
642
|
const { body } = await this.fetchText(filing.filingUrl);
|
|
412
643
|
return parseFilingDocuments(body, filing);
|