gloomberb 0.2.0 → 0.3.1
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 +20 -19
- package/bin/gloomberb +3 -0
- package/package.json +5 -4
- package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
- package/src/app.tsx +1330 -0
- package/src/brokers/sync-broker-instance.test.ts +100 -0
- package/src/brokers/sync-broker-instance.ts +341 -0
- package/src/cli/commands/plugins.ts +173 -0
- package/src/cli/commands/portfolio.ts +210 -0
- package/src/cli/commands/search.ts +120 -0
- package/src/cli/commands/ticker.ts +375 -0
- package/src/cli/commands/watchlist.ts +210 -0
- package/src/cli/context.ts +62 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/helpers.ts +85 -0
- package/src/cli/index.ts +143 -0
- package/src/cli/ticker-resolution.ts +39 -0
- package/src/cli/types.ts +15 -0
- package/src/cli.test.ts +361 -0
- package/src/components/chart/chart-data.ts +239 -0
- package/src/components/chart/chart-renderer.ts +1199 -0
- package/src/components/chart/chart-types.ts +82 -0
- package/src/components/chart/chart-viewport.ts +21 -0
- package/src/components/chart/chart.test.ts +439 -0
- package/src/components/chart/comparison-chart-data.test.ts +109 -0
- package/src/components/chart/comparison-chart-data.ts +316 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
- package/src/components/chart/comparison-chart-renderer.ts +354 -0
- package/src/components/chart/comparison-stock-chart.tsx +1486 -0
- package/src/components/chart/cursor-motion.ts +48 -0
- package/src/components/chart/native/chart-rasterizer.ts +732 -0
- package/src/components/chart/native/kitty-adapter.ts +12 -0
- package/src/components/chart/native/kitty-manager.ts +142 -0
- package/src/components/chart/native/kitty-protocol.ts +110 -0
- package/src/components/chart/native/kitty-support.ts +71 -0
- package/src/components/chart/native/kitty.test.ts +620 -0
- package/src/components/chart/native/renderer-selection.ts +24 -0
- package/src/components/chart/native/surface-manager.ts +225 -0
- package/src/components/chart/native/surface-sync.ts +34 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
- package/src/components/chart/stock-chart.test.ts +107 -0
- package/src/components/chart/stock-chart.tsx +1556 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
- package/src/components/command-bar/command-bar.test.tsx +1524 -0
- package/src/components/command-bar/command-bar.tsx +4823 -0
- package/src/components/command-bar/command-registry.ts +261 -0
- package/src/components/command-bar/helpers.test.ts +224 -0
- package/src/components/command-bar/helpers.ts +371 -0
- package/src/components/command-bar/root-shortcuts.ts +158 -0
- package/src/components/command-bar/view-model.test.ts +152 -0
- package/src/components/command-bar/view-model.ts +185 -0
- package/src/components/command-bar/workflow-ops.test.ts +70 -0
- package/src/components/command-bar/workflow-ops.ts +433 -0
- package/src/components/command-bar/workflow-types.ts +127 -0
- package/src/components/detail-feed-view.test.tsx +105 -0
- package/src/components/detail-feed-view.tsx +295 -0
- package/src/components/index.ts +32 -0
- package/src/components/layout/floating-pane.tsx +49 -0
- package/src/components/layout/header.tsx +129 -0
- package/src/components/layout/pane-header.tsx +44 -0
- package/src/components/layout/pane.tsx +49 -0
- package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
- package/src/components/layout/pixel-mouse.test.ts +40 -0
- package/src/components/layout/shell.test.tsx +625 -0
- package/src/components/layout/shell.tsx +1200 -0
- package/src/components/layout/status-bar.test.tsx +173 -0
- package/src/components/layout/status-bar.tsx +138 -0
- package/src/components/markdown-editor.tsx +134 -0
- package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
- package/src/components/pane-settings-dialog.tsx +468 -0
- package/src/components/pane-template-wizard.test.tsx +52 -0
- package/src/components/pane-template-wizard.tsx +199 -0
- package/src/components/price-selector-dialog.tsx +195 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/tab-bar.tsx +17 -0
- package/src/components/ticker-badge-text.test.tsx +160 -0
- package/src/components/ticker-badge-text.tsx +58 -0
- package/src/components/ticker-badge.tsx +88 -0
- package/src/components/ticker-list-table.tsx +188 -0
- package/src/components/toggle-list.tsx +74 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/data-table.tsx +210 -0
- package/src/components/ui/external-link.tsx +22 -0
- package/src/components/ui/fields.tsx +187 -0
- package/src/components/ui/frame.tsx +73 -0
- package/src/components/ui/index.ts +53 -0
- package/src/components/ui/list-view.tsx +166 -0
- package/src/components/ui/loading.tsx +70 -0
- package/src/components/ui/status.tsx +83 -0
- package/src/components/ui/tabs.tsx +47 -0
- package/src/components/ui/toggle.tsx +164 -0
- package/src/components/ui/ui.test.tsx +134 -0
- package/src/data/app-persistence.ts +25 -0
- package/src/data/config-store.test.ts +310 -0
- package/src/data/config-store.ts +469 -0
- package/src/data/plugin-state-store.ts +102 -0
- package/src/data/resource-store.ts +323 -0
- package/src/data/session-store.ts +52 -0
- package/src/data/sqlite-cache.test.ts +103 -0
- package/src/data/sqlite-database.ts +62 -0
- package/src/data/sqlite-json.ts +19 -0
- package/src/data/ticker-repository.ts +45 -0
- package/src/data/ticker-store.ts +46 -0
- package/src/index.tsx +67 -0
- package/src/market-data/coordinator.test.ts +369 -0
- package/src/market-data/coordinator.ts +580 -0
- package/src/market-data/hooks.tsx +234 -0
- package/src/market-data/query-store.ts +28 -0
- package/src/market-data/request-types.test.ts +64 -0
- package/src/market-data/request-types.ts +70 -0
- package/src/market-data/result-types.ts +56 -0
- package/src/market-data/selectors.ts +166 -0
- package/src/market-data/trace.ts +7 -0
- package/src/pane-settings.test.ts +71 -0
- package/src/pane-settings.ts +73 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
- package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
- package/src/plugins/builtin/ai/index.test.tsx +139 -0
- package/src/plugins/builtin/ai/index.tsx +86 -0
- package/src/plugins/builtin/ai/providers.ts +99 -0
- package/src/plugins/builtin/ai/runner.ts +82 -0
- package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
- package/src/plugins/builtin/ai/screener-contract.ts +174 -0
- package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
- package/src/plugins/builtin/ai/settings.ts +79 -0
- package/src/plugins/builtin/ai/ticker-context.ts +64 -0
- package/src/plugins/builtin/ai/utils.ts +71 -0
- package/src/plugins/builtin/ask-ai.test.tsx +258 -0
- package/src/plugins/builtin/ask-ai.tsx +9 -0
- package/src/plugins/builtin/chat-controller.test.ts +217 -0
- package/src/plugins/builtin/chat-controller.ts +340 -0
- package/src/plugins/builtin/chat.test.tsx +451 -0
- package/src/plugins/builtin/chat.tsx +620 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
- package/src/plugins/builtin/comparison-chart.tsx +169 -0
- package/src/plugins/builtin/data-providers.ts +16 -0
- package/src/plugins/builtin/debug.tsx +351 -0
- package/src/plugins/builtin/help.test.tsx +110 -0
- package/src/plugins/builtin/help.tsx +281 -0
- package/src/plugins/builtin/layout-manager.tsx +264 -0
- package/src/plugins/builtin/manual-entry.tsx +25 -0
- package/src/plugins/builtin/news.tsx +143 -0
- package/src/plugins/builtin/notes-files.ts +58 -0
- package/src/plugins/builtin/notes.tsx +396 -0
- package/src/plugins/builtin/options-availability.test.tsx +179 -0
- package/src/plugins/builtin/options-availability.ts +158 -0
- package/src/plugins/builtin/options.tsx +364 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
- package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
- package/src/plugins/builtin/portfolio-list.test.ts +74 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
- package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
- package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
- package/src/plugins/builtin/sec.tsx +250 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
- package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
- package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
- package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
- package/src/plugins/builtin/yahoo.ts +18 -0
- package/src/plugins/event-bus.ts +37 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
- package/src/plugins/ibkr/account-cache.test.ts +101 -0
- package/src/plugins/ibkr/account-cache.ts +137 -0
- package/src/plugins/ibkr/broker-adapter.ts +168 -0
- package/src/plugins/ibkr/config.test.ts +68 -0
- package/src/plugins/ibkr/config.ts +262 -0
- package/src/plugins/ibkr/dialogs.tsx +83 -0
- package/src/plugins/ibkr/flex.test.ts +107 -0
- package/src/plugins/ibkr/flex.ts +267 -0
- package/src/plugins/ibkr/fundamental-parser.ts +149 -0
- package/src/plugins/ibkr/gateway-helpers.ts +28 -0
- package/src/plugins/ibkr/gateway-selection.ts +51 -0
- package/src/plugins/ibkr/gateway-service.test.ts +450 -0
- package/src/plugins/ibkr/gateway-service.ts +2228 -0
- package/src/plugins/ibkr/index.test.tsx +183 -0
- package/src/plugins/ibkr/index.tsx +153 -0
- package/src/plugins/ibkr/instance-selection.test.ts +121 -0
- package/src/plugins/ibkr/instance-selection.ts +36 -0
- package/src/plugins/ibkr/price-normalization.test.ts +38 -0
- package/src/plugins/ibkr/price-normalization.ts +59 -0
- package/src/plugins/ibkr/trade-badge.tsx +27 -0
- package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
- package/src/plugins/ibkr/trade-tab.tsx +1050 -0
- package/src/plugins/ibkr/trade-utils.test.ts +175 -0
- package/src/plugins/ibkr/trade-utils.ts +149 -0
- package/src/plugins/ibkr/trading-pane.tsx +438 -0
- package/src/plugins/ibkr/trading-state.test.ts +109 -0
- package/src/plugins/ibkr/trading-state.ts +339 -0
- package/src/plugins/loader.ts +71 -0
- package/src/plugins/pane-manager.test.ts +103 -0
- package/src/plugins/pane-manager.ts +974 -0
- package/src/plugins/plugin-persistence.ts +85 -0
- package/src/plugins/plugin-runtime.test.tsx +185 -0
- package/src/plugins/plugin-runtime.tsx +175 -0
- package/src/plugins/prediction-markets/cache.ts +114 -0
- package/src/plugins/prediction-markets/categories.ts +223 -0
- package/src/plugins/prediction-markets/chart.tsx +512 -0
- package/src/plugins/prediction-markets/columns.ts +133 -0
- package/src/plugins/prediction-markets/controller-data.ts +614 -0
- package/src/plugins/prediction-markets/controller.ts +725 -0
- package/src/plugins/prediction-markets/detail/book.tsx +118 -0
- package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
- package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
- package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
- package/src/plugins/prediction-markets/detail.test.tsx +47 -0
- package/src/plugins/prediction-markets/index.tsx +102 -0
- package/src/plugins/prediction-markets/keyboard.ts +64 -0
- package/src/plugins/prediction-markets/launch.test.ts +126 -0
- package/src/plugins/prediction-markets/launch.ts +240 -0
- package/src/plugins/prediction-markets/metrics.ts +311 -0
- package/src/plugins/prediction-markets/navigation.ts +90 -0
- package/src/plugins/prediction-markets/outcome-order.ts +19 -0
- package/src/plugins/prediction-markets/pane.test.tsx +425 -0
- package/src/plugins/prediction-markets/pane.tsx +294 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
- package/src/plugins/prediction-markets/rows.ts +229 -0
- package/src/plugins/prediction-markets/services/fetch.ts +97 -0
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
- package/src/plugins/prediction-markets/settings.ts +164 -0
- package/src/plugins/prediction-markets/status.ts +24 -0
- package/src/plugins/prediction-markets/table.tsx +84 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
- package/src/plugins/prediction-markets/types.ts +186 -0
- package/src/plugins/registry.ts +633 -0
- package/src/sources/exchange-rates.ts +18 -0
- package/src/sources/gloomberb-cloud.test.ts +199 -0
- package/src/sources/gloomberb-cloud.ts +326 -0
- package/src/sources/provider-errors.ts +19 -0
- package/src/sources/provider-router.test.ts +1476 -0
- package/src/sources/provider-router.ts +1405 -0
- package/src/sources/sec-edgar.test.ts +286 -0
- package/src/sources/sec-edgar.ts +526 -0
- package/src/sources/yahoo-finance.test.ts +14 -0
- package/src/sources/yahoo-finance.ts +1017 -0
- package/src/state/app-activity.test.ts +41 -0
- package/src/state/app-activity.ts +106 -0
- package/src/state/app-bootstrap.test.ts +471 -0
- package/src/state/app-bootstrap.ts +308 -0
- package/src/state/app-context.test.ts +406 -0
- package/src/state/app-context.test.tsx +132 -0
- package/src/state/app-context.tsx +969 -0
- package/src/state/selectors.ts +67 -0
- package/src/state/session-persistence.test.ts +128 -0
- package/src/state/session-persistence.ts +161 -0
- package/src/state/ticker-refresh-queue.test.ts +46 -0
- package/src/state/ticker-refresh-queue.ts +40 -0
- package/src/state/use-inline-tickers.ts +174 -0
- package/src/state/use-quote-streaming.test.ts +37 -0
- package/src/state/use-quote-streaming.test.tsx +70 -0
- package/src/state/use-quote-streaming.ts +80 -0
- package/src/state/use-session-snapshot.ts +84 -0
- package/src/test-support/data-provider.ts +22 -0
- package/src/theme/color-utils.ts +67 -0
- package/src/theme/colors.ts +128 -0
- package/src/theme/themes.test.ts +45 -0
- package/src/theme/themes.ts +491 -0
- package/src/types/broker.ts +105 -0
- package/src/types/config.ts +434 -0
- package/src/types/data-provider.ts +81 -0
- package/src/types/financials.ts +142 -0
- package/src/types/instrument.ts +29 -0
- package/src/types/persistence.ts +21 -0
- package/src/types/plugin.ts +354 -0
- package/src/types/ticker.ts +58 -0
- package/src/types/trading.ts +85 -0
- package/src/updater.test.ts +162 -0
- package/src/updater.ts +247 -0
- package/src/utils/api-client.ts +666 -0
- package/src/utils/ascii-chart.ts +172 -0
- package/src/utils/broker-instances.ts +47 -0
- package/src/utils/cli-output.ts +83 -0
- package/src/utils/currency-units.test.ts +49 -0
- package/src/utils/currency-units.ts +82 -0
- package/src/utils/debug-log.ts +133 -0
- package/src/utils/exchange-rate-map.ts +8 -0
- package/src/utils/format.ts +158 -0
- package/src/utils/fuzzy-search.ts +34 -0
- package/src/utils/market-status.ts +95 -0
- package/src/utils/options.ts +86 -0
- package/src/utils/price-history.ts +39 -0
- package/src/utils/quote-time.test.ts +22 -0
- package/src/utils/quote-time.ts +50 -0
- package/src/utils/sec.test.ts +55 -0
- package/src/utils/sec.ts +45 -0
- package/src/utils/ticker-list.test.ts +22 -0
- package/src/utils/ticker-list.ts +35 -0
- package/src/utils/ticker-metadata.ts +51 -0
- package/src/utils/ticker-search.test.ts +227 -0
- package/src/utils/ticker-search.ts +764 -0
- package/src/utils/ticker-tokenizer.test.ts +48 -0
- package/src/utils/ticker-tokenizer.ts +72 -0
- package/src/version.ts +1 -0
- package/install.mjs +0 -68
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { hostname } from "os";
|
|
2
|
+
import type { SecFilingItem } from "../types/data-provider";
|
|
3
|
+
|
|
4
|
+
const LOOKUP_URL = "https://www.sec.gov/files/company_tickers_exchange.json";
|
|
5
|
+
const SUBMISSIONS_URL = "https://data.sec.gov/submissions";
|
|
6
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
7
|
+
const MAX_CONTENT_CHARS = 16_000;
|
|
8
|
+
const OWNERSHIP_FORMS = new Set(["3", "3/A", "4", "4/A", "5", "5/A"]);
|
|
9
|
+
const PDF_FALLBACK_MESSAGE = "This filing's primary document is a PDF. Inline PDF extraction is not supported here; open the filing URL below to view the full document.";
|
|
10
|
+
|
|
11
|
+
function sanitizeIdentityPart(value: string, fallback: string): string {
|
|
12
|
+
const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
13
|
+
return sanitized || fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractEmail(value: string | undefined): string | null {
|
|
17
|
+
if (!value) return null;
|
|
18
|
+
const match = value.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
|
|
19
|
+
return match?.[0] ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SEC_FROM =
|
|
23
|
+
process.env.SEC_FROM_EMAIL?.trim()
|
|
24
|
+
|| extractEmail(process.env.SEC_USER_AGENT)
|
|
25
|
+
|| `${sanitizeIdentityPart(process.env.USER ?? "gloomberb", "gloomberb")}@${sanitizeIdentityPart(`${hostname()}.local`, "localhost.localdomain")}`;
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_SEC_USER_AGENT =
|
|
28
|
+
process.env.SEC_USER_AGENT?.trim()
|
|
29
|
+
|| `Gloomberb/0.1 (${sanitizeIdentityPart(hostname(), "localhost")}; contact=${DEFAULT_SEC_FROM})`;
|
|
30
|
+
|
|
31
|
+
type LookupEntry = {
|
|
32
|
+
cik: string;
|
|
33
|
+
exchange?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function normalize(value?: string): string {
|
|
38
|
+
return (value ?? "").trim().toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function zeroPadCik(value: unknown): string | null {
|
|
42
|
+
const digits = String(value ?? "").replace(/\D/g, "");
|
|
43
|
+
if (!digits) return null;
|
|
44
|
+
return digits.padStart(10, "0");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseTimestamp(value: unknown): Date | undefined {
|
|
48
|
+
const digits = String(value ?? "").trim();
|
|
49
|
+
if (!/^\d{14}$/.test(digits)) return undefined;
|
|
50
|
+
const year = Number(digits.slice(0, 4));
|
|
51
|
+
const month = Number(digits.slice(4, 6));
|
|
52
|
+
const day = Number(digits.slice(6, 8));
|
|
53
|
+
const hour = Number(digits.slice(8, 10));
|
|
54
|
+
const minute = Number(digits.slice(10, 12));
|
|
55
|
+
const second = Number(digits.slice(12, 14));
|
|
56
|
+
return new Date(Date.UTC(year, month - 1, day, hour, minute, second));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseDate(value: unknown): Date | undefined {
|
|
60
|
+
const text = String(value ?? "").trim();
|
|
61
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return undefined;
|
|
62
|
+
return new Date(`${text}T00:00:00Z`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function truncateWithEllipsis(text: string, width: number): string {
|
|
66
|
+
if (text.length <= width) return text;
|
|
67
|
+
if (width <= 3) return text.slice(0, width);
|
|
68
|
+
return `${text.slice(0, width - 3)}...`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stripAccessionDashes(accessionNumber: string): string {
|
|
72
|
+
return accessionNumber.replace(/-/g, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isHtmlResponse(body: string): boolean {
|
|
76
|
+
return /^\s*<!DOCTYPE html/i.test(body) || /^\s*<html/i.test(body);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPdfDocument(body: string, contentType = "", url = ""): boolean {
|
|
80
|
+
return /pdf/i.test(contentType)
|
|
81
|
+
|| /\.pdf(?:$|[?#])/i.test(url)
|
|
82
|
+
|| /^\s*%PDF-/i.test(body)
|
|
83
|
+
|| /^\s*begin 644 [^\n]+\.pdf/im.test(body)
|
|
84
|
+
|| /<PDF>/i.test(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isSecBlockMessage(body: string): boolean {
|
|
88
|
+
return /Undeclared Automated Tool|Request Rate Threshold Exceeded/i.test(body);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
92
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
93
|
+
? value as Record<string, unknown>
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function decodeHtmlEntities(text: string): string {
|
|
98
|
+
return text
|
|
99
|
+
.replace(/ /gi, " ")
|
|
100
|
+
.replace(/&/gi, "&")
|
|
101
|
+
.replace(/</gi, "<")
|
|
102
|
+
.replace(/>/gi, ">")
|
|
103
|
+
.replace(/"/gi, "\"")
|
|
104
|
+
.replace(/'|'/gi, "'")
|
|
105
|
+
.replace(/&#(\d+);/g, (_match, digits: string) => String.fromCodePoint(Number(digits)))
|
|
106
|
+
.replace(/&#x([0-9a-f]+);/gi, (_match, hex: string) => String.fromCodePoint(parseInt(hex, 16)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function collapseDocumentLines(text: string): string[] {
|
|
110
|
+
const rawLines = text
|
|
111
|
+
.replace(/\r/g, "")
|
|
112
|
+
.split("\n")
|
|
113
|
+
.map((line) => line.replace(/[ \t]+/g, " ").trim())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const line of rawLines) {
|
|
118
|
+
if (lines[lines.length - 1] === line) continue;
|
|
119
|
+
if (/^(xmlns|schemaRef|contextRef|unitRef|xsi:|ix:header|link:schemaRef|xbrli:|xbrldi:|xlink:|ix:|ixt:|ixt-sec:|dei:|ecd:|us-gaap:|iso4217:|srt:|country:|utr:|xl:)/i.test(line)) continue;
|
|
120
|
+
if (/^(?:[a-z]{2,10}:[A-Za-z0-9][\w.-]*)(?:\s+[a-z]{2,10}:[A-Za-z0-9][\w.-]*){2,}$/i.test(line)) continue;
|
|
121
|
+
if (/^LOGO$/i.test(line)) continue;
|
|
122
|
+
lines.push(line);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function trimLeadingNoise(lines: string[]): string[] {
|
|
129
|
+
const startIndex = lines.findIndex((line) => /^(UNITED STATES|FORM [0-9A-Z-]+|SCHEDULE 14A|PROXY STATEMENT|NOTICE OF|ANNUAL REPORT|QUARTERLY REPORT|CURRENT REPORT)/i.test(line));
|
|
130
|
+
if (startIndex <= 0) return lines;
|
|
131
|
+
|
|
132
|
+
const prefix = lines.slice(0, startIndex);
|
|
133
|
+
const noiseCount = prefix.filter((line) => /^Table of Contents$/i.test(line) || line.length < 5).length;
|
|
134
|
+
return noiseCount >= Math.max(1, prefix.length - 1)
|
|
135
|
+
? lines.slice(startIndex)
|
|
136
|
+
: lines;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getTagInner(xml: string, tag: string): string | null {
|
|
140
|
+
const match = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
|
|
141
|
+
return match?.[1] ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getTagInners(xml: string, tag: string): string[] {
|
|
145
|
+
return [...xml.matchAll(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "gi"))]
|
|
146
|
+
.map((match) => match[1] ?? "")
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cleanXmlText(text: string): string {
|
|
151
|
+
return decodeHtmlEntities(
|
|
152
|
+
text
|
|
153
|
+
.replace(/<footnoteId[^>]*\/>/gi, " ")
|
|
154
|
+
.replace(/<[^>]+>/g, " ")
|
|
155
|
+
.replace(/[ \t]+/g, " ")
|
|
156
|
+
.trim(),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractXmlValue(xml: string, path: string[]): string | null {
|
|
161
|
+
let current: string | null = xml;
|
|
162
|
+
for (const tag of path) {
|
|
163
|
+
current = current ? getTagInner(current, tag) : null;
|
|
164
|
+
if (!current) return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const nestedValue = getTagInner(current, "value");
|
|
168
|
+
const text = cleanXmlText(nestedValue ?? current);
|
|
169
|
+
return text || null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatOwnershipCode(value: string | null): string | null {
|
|
173
|
+
if (!value) return null;
|
|
174
|
+
if (value === "D") return "Direct";
|
|
175
|
+
if (value === "I") return "Indirect";
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatOwnershipTransaction(section: string): string | null {
|
|
180
|
+
const security = extractXmlValue(section, ["securityTitle"]);
|
|
181
|
+
const date = extractXmlValue(section, ["transactionDate"]);
|
|
182
|
+
const code = extractXmlValue(section, ["transactionCoding", "transactionCode"]);
|
|
183
|
+
const shares = extractXmlValue(section, ["transactionAmounts", "transactionShares"]);
|
|
184
|
+
const action = extractXmlValue(section, ["transactionAmounts", "transactionAcquiredDisposedCode"]);
|
|
185
|
+
const price = extractXmlValue(section, ["transactionAmounts", "transactionPricePerShare"]);
|
|
186
|
+
const owned = extractXmlValue(section, ["postTransactionAmounts", "sharesOwnedFollowingTransaction"]);
|
|
187
|
+
const ownership = formatOwnershipCode(extractXmlValue(section, ["ownershipNature", "directOrIndirectOwnership"]));
|
|
188
|
+
const nature = extractXmlValue(section, ["ownershipNature", "natureOfOwnership"]);
|
|
189
|
+
|
|
190
|
+
const segments = [
|
|
191
|
+
security,
|
|
192
|
+
date,
|
|
193
|
+
code ? `Code ${code}` : null,
|
|
194
|
+
shares ? `${shares} shares${action ? ` ${action}` : ""}` : action ? `Action ${action}` : null,
|
|
195
|
+
price ? `@ ${price}` : null,
|
|
196
|
+
owned ? `Owned ${owned}` : null,
|
|
197
|
+
ownership ? `Ownership ${ownership}` : null,
|
|
198
|
+
nature ? `Nature ${nature}` : null,
|
|
199
|
+
].filter((value): value is string => !!value);
|
|
200
|
+
|
|
201
|
+
return segments.length > 0 ? `- ${segments.join(" | ")}` : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatOwnershipHolding(section: string): string | null {
|
|
205
|
+
const security = extractXmlValue(section, ["securityTitle"]);
|
|
206
|
+
const owned = extractXmlValue(section, ["postTransactionAmounts", "sharesOwnedFollowingTransaction"]);
|
|
207
|
+
const ownership = formatOwnershipCode(extractXmlValue(section, ["ownershipNature", "directOrIndirectOwnership"]));
|
|
208
|
+
const nature = extractXmlValue(section, ["ownershipNature", "natureOfOwnership"]);
|
|
209
|
+
|
|
210
|
+
const segments = [
|
|
211
|
+
security,
|
|
212
|
+
owned ? `Owned ${owned}` : null,
|
|
213
|
+
ownership ? `Ownership ${ownership}` : null,
|
|
214
|
+
nature ? `Nature ${nature}` : null,
|
|
215
|
+
].filter((value): value is string => !!value);
|
|
216
|
+
|
|
217
|
+
return segments.length > 0 ? `- ${segments.join(" | ")}` : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractOwnershipDocumentSummary(document: string, form: string): string | null {
|
|
221
|
+
if (!/<ownershipDocument[\s>]/i.test(document) || !OWNERSHIP_FORMS.has(form.toUpperCase())) return null;
|
|
222
|
+
|
|
223
|
+
const lines: string[] = [];
|
|
224
|
+
const issuerName = extractXmlValue(document, ["issuer", "issuerName"]) ?? extractXmlValue(document, ["issuerName"]);
|
|
225
|
+
const symbol = extractXmlValue(document, ["issuer", "issuerTradingSymbol"]) ?? extractXmlValue(document, ["issuerTradingSymbol"]);
|
|
226
|
+
const reportDate = extractXmlValue(document, ["periodOfReport"]);
|
|
227
|
+
const ownerName = extractXmlValue(document, ["reportingOwner", "reportingOwnerId", "rptOwnerName"])
|
|
228
|
+
?? extractXmlValue(document, ["rptOwnerName"]);
|
|
229
|
+
|
|
230
|
+
lines.push([`Form ${form}`, issuerName, symbol].filter(Boolean).join(" | "));
|
|
231
|
+
if (reportDate) lines.push(`Report date ${reportDate}`);
|
|
232
|
+
if (ownerName) lines.push(`Owner ${ownerName}`);
|
|
233
|
+
|
|
234
|
+
const transactionLines = getTagInners(document, "nonDerivativeTransaction")
|
|
235
|
+
.map((section) => formatOwnershipTransaction(section))
|
|
236
|
+
.filter((value): value is string => !!value);
|
|
237
|
+
|
|
238
|
+
const holdingLines = getTagInners(document, "nonDerivativeHolding")
|
|
239
|
+
.map((section) => formatOwnershipHolding(section))
|
|
240
|
+
.filter((value): value is string => !!value);
|
|
241
|
+
|
|
242
|
+
if (transactionLines.length > 0) {
|
|
243
|
+
lines.push("", "Transactions", ...transactionLines.slice(0, 12));
|
|
244
|
+
} else if (holdingLines.length > 0) {
|
|
245
|
+
lines.push("", "Holdings", ...holdingLines.slice(0, 12));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const result = lines.filter(Boolean).join("\n");
|
|
249
|
+
return result || null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function extractFilingContent(
|
|
253
|
+
document: string,
|
|
254
|
+
contentType = "",
|
|
255
|
+
options: { form?: string; sourceUrl?: string } = {},
|
|
256
|
+
): string | null {
|
|
257
|
+
if (isPdfDocument(document, contentType, options.sourceUrl)) {
|
|
258
|
+
return PDF_FALLBACK_MESSAGE;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const ownershipSummary = options.form ? extractOwnershipDocumentSummary(document, options.form) : null;
|
|
262
|
+
if (ownershipSummary) return ownershipSummary;
|
|
263
|
+
|
|
264
|
+
const looksLikeMarkup = /html|xml/i.test(contentType) || /^\s*</.test(document);
|
|
265
|
+
let text = document;
|
|
266
|
+
|
|
267
|
+
if (looksLikeMarkup) {
|
|
268
|
+
text = text
|
|
269
|
+
.replace(/<head[\s\S]*?<\/head>/gi, " ")
|
|
270
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
271
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
272
|
+
.replace(/<svg[\s\S]*?<\/svg>/gi, " ")
|
|
273
|
+
.replace(/<div[^>]*display\s*:\s*none[^>]*>[\s\S]*?<\/div>/gi, " ")
|
|
274
|
+
.replace(/<ix:header[\s\S]*?<\/ix:header>/gi, " ")
|
|
275
|
+
.replace(/<ix:hidden[\s\S]*?<\/ix:hidden>/gi, " ")
|
|
276
|
+
.replace(/<ix:references[\s\S]*?<\/ix:references>/gi, " ")
|
|
277
|
+
.replace(/<ix:resources[\s\S]*?<\/ix:resources>/gi, " ")
|
|
278
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
279
|
+
.replace(/<\/(p|div|section|article|header|footer|li|tr|table|h\d|title)>/gi, "\n")
|
|
280
|
+
.replace(/<(br|hr)\s*\/?>/gi, "\n")
|
|
281
|
+
.replace(/<(p|div|section|article|header|footer|li|tr|table|h\d|title)[^>]*>/gi, "\n")
|
|
282
|
+
.replace(/<td[^>]*>/gi, " ")
|
|
283
|
+
.replace(/<[^>]+>/g, " ");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const lines = trimLeadingNoise(collapseDocumentLines(decodeHtmlEntities(text)));
|
|
287
|
+
if (lines.length === 0) return null;
|
|
288
|
+
|
|
289
|
+
let result = lines.join("\n");
|
|
290
|
+
if (result.length > MAX_CONTENT_CHARS) {
|
|
291
|
+
result = `${result.slice(0, MAX_CONTENT_CHARS).trimEnd()}\n\n[truncated]`;
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function parseTickerLookup(payload: unknown): Map<string, LookupEntry> {
|
|
297
|
+
const results = new Map<string, LookupEntry>();
|
|
298
|
+
const record = asRecord(payload);
|
|
299
|
+
if (!record) return results;
|
|
300
|
+
|
|
301
|
+
const fields = Array.isArray(record.fields)
|
|
302
|
+
? record.fields.map((field) => String(field))
|
|
303
|
+
: null;
|
|
304
|
+
const data = Array.isArray(record.data) ? record.data : null;
|
|
305
|
+
|
|
306
|
+
if (fields && data) {
|
|
307
|
+
const cikIndex = fields.findIndex((field) => normalize(field) === "CIK");
|
|
308
|
+
const tickerIndex = fields.findIndex((field) => normalize(field) === "TICKER");
|
|
309
|
+
const exchangeIndex = fields.findIndex((field) => normalize(field) === "EXCHANGE");
|
|
310
|
+
const nameIndex = fields.findIndex((field) => normalize(field) === "NAME");
|
|
311
|
+
|
|
312
|
+
for (const row of data) {
|
|
313
|
+
if (!Array.isArray(row)) continue;
|
|
314
|
+
const ticker = normalize(String(row[tickerIndex] ?? ""));
|
|
315
|
+
const cik = zeroPadCik(row[cikIndex]);
|
|
316
|
+
if (!ticker || !cik) continue;
|
|
317
|
+
results.set(ticker, {
|
|
318
|
+
cik,
|
|
319
|
+
exchange: typeof row[exchangeIndex] === "string" ? row[exchangeIndex] as string : undefined,
|
|
320
|
+
name: typeof row[nameIndex] === "string" ? row[nameIndex] as string : undefined,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (results.size > 0) return results;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const value of Object.values(record)) {
|
|
327
|
+
const entry = asRecord(value);
|
|
328
|
+
if (!entry) continue;
|
|
329
|
+
const ticker = normalize(String(entry.ticker ?? entry.symbol ?? ""));
|
|
330
|
+
const cik = zeroPadCik(entry.cik ?? entry.cik_str);
|
|
331
|
+
if (!ticker || !cik) continue;
|
|
332
|
+
results.set(ticker, {
|
|
333
|
+
cik,
|
|
334
|
+
exchange: typeof entry.exchange === "string" ? entry.exchange : undefined,
|
|
335
|
+
name: typeof entry.title === "string"
|
|
336
|
+
? entry.title
|
|
337
|
+
: typeof entry.name === "string"
|
|
338
|
+
? entry.name
|
|
339
|
+
: undefined,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[] {
|
|
347
|
+
const record = asRecord(payload);
|
|
348
|
+
const filings = asRecord(record?.filings);
|
|
349
|
+
const recent = asRecord(filings?.recent);
|
|
350
|
+
if (!record || !recent) return [];
|
|
351
|
+
|
|
352
|
+
const accessionNumbers = Array.isArray(recent.accessionNumber) ? recent.accessionNumber : [];
|
|
353
|
+
const forms = Array.isArray(recent.form) ? recent.form : [];
|
|
354
|
+
const filingDates = Array.isArray(recent.filingDate) ? recent.filingDate : [];
|
|
355
|
+
const acceptanceTimes = Array.isArray(recent.acceptanceDateTime) ? recent.acceptanceDateTime : [];
|
|
356
|
+
const primaryDocuments = Array.isArray(recent.primaryDocument) ? recent.primaryDocument : [];
|
|
357
|
+
const primaryDescriptions = Array.isArray(recent.primaryDocDescription) ? recent.primaryDocDescription : [];
|
|
358
|
+
const items = Array.isArray(recent.items) ? recent.items : [];
|
|
359
|
+
const cik = zeroPadCik(record.cik) ?? "";
|
|
360
|
+
const displayCik = String(Number(cik || "0"));
|
|
361
|
+
const companyName = typeof record.name === "string" ? record.name : undefined;
|
|
362
|
+
const total = Math.min(
|
|
363
|
+
Math.max(accessionNumbers.length, forms.length, filingDates.length),
|
|
364
|
+
Math.max(count, 0),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const results: SecFilingItem[] = [];
|
|
368
|
+
for (let index = 0; index < total; index += 1) {
|
|
369
|
+
const accessionNumber = String(accessionNumbers[index] ?? "").trim();
|
|
370
|
+
const form = String(forms[index] ?? "").trim();
|
|
371
|
+
const filingDate = parseDate(filingDates[index]);
|
|
372
|
+
if (!accessionNumber || !form || !filingDate || !displayCik) continue;
|
|
373
|
+
|
|
374
|
+
const accessionNumberNoDashes = stripAccessionDashes(accessionNumber);
|
|
375
|
+
const primaryDocument = String(primaryDocuments[index] ?? "").trim() || undefined;
|
|
376
|
+
const filingUrl = `https://www.sec.gov/Archives/edgar/data/${displayCik}/${accessionNumber}-index.htm`;
|
|
377
|
+
const primaryDocumentUrl = primaryDocument
|
|
378
|
+
? `https://www.sec.gov/Archives/edgar/data/${displayCik}/${accessionNumberNoDashes}/${primaryDocument}`
|
|
379
|
+
: undefined;
|
|
380
|
+
|
|
381
|
+
results.push({
|
|
382
|
+
accessionNumber,
|
|
383
|
+
form,
|
|
384
|
+
filingDate,
|
|
385
|
+
acceptedAt: parseTimestamp(acceptanceTimes[index]),
|
|
386
|
+
primaryDocument,
|
|
387
|
+
primaryDocDescription: String(primaryDescriptions[index] ?? "").trim() || undefined,
|
|
388
|
+
items: String(items[index] ?? "").trim() || undefined,
|
|
389
|
+
cik,
|
|
390
|
+
companyName,
|
|
391
|
+
filingUrl,
|
|
392
|
+
primaryDocumentUrl,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return results;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export class SecEdgarClient {
|
|
400
|
+
private lookupPromise: Promise<Map<string, LookupEntry>> | null = null;
|
|
401
|
+
|
|
402
|
+
private defaultHeaders() {
|
|
403
|
+
return {
|
|
404
|
+
"User-Agent": DEFAULT_SEC_USER_AGENT,
|
|
405
|
+
From: DEFAULT_SEC_FROM,
|
|
406
|
+
Accept: "application/json,text/plain,*/*",
|
|
407
|
+
"Accept-Encoding": "gzip, deflate",
|
|
408
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
409
|
+
Referer: "https://www.sec.gov/",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async fetchJson<T>(url: string): Promise<T> {
|
|
414
|
+
const response = await fetch(url, {
|
|
415
|
+
headers: this.defaultHeaders(),
|
|
416
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
417
|
+
});
|
|
418
|
+
const body = await response.text();
|
|
419
|
+
|
|
420
|
+
if (!response.ok) {
|
|
421
|
+
if (isSecBlockMessage(body)) {
|
|
422
|
+
throw new Error("SEC blocked the request. Try setting SEC_USER_AGENT to 'MyApp AdminContact@example.com'.");
|
|
423
|
+
}
|
|
424
|
+
throw new Error(`SEC request failed (${response.status}): ${truncateWithEllipsis(body.trim(), 160)}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (isHtmlResponse(body)) {
|
|
428
|
+
if (isSecBlockMessage(body)) {
|
|
429
|
+
throw new Error("SEC blocked the request. Try setting SEC_USER_AGENT to 'MyApp AdminContact@example.com'.");
|
|
430
|
+
}
|
|
431
|
+
throw new Error("SEC returned HTML instead of JSON.");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return JSON.parse(body) as T;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private async fetchText(url: string): Promise<{ body: string; contentType: string }> {
|
|
438
|
+
const response = await fetch(url, {
|
|
439
|
+
headers: this.defaultHeaders(),
|
|
440
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
441
|
+
});
|
|
442
|
+
const body = await response.text();
|
|
443
|
+
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
if (isSecBlockMessage(body)) {
|
|
446
|
+
throw new Error("SEC blocked the request. Try setting SEC_USER_AGENT to 'MyApp AdminContact@example.com'.");
|
|
447
|
+
}
|
|
448
|
+
throw new Error(`SEC request failed (${response.status}): ${truncateWithEllipsis(body.trim(), 160)}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isSecBlockMessage(body)) {
|
|
452
|
+
throw new Error("SEC blocked the request. Try setting SEC_USER_AGENT to 'MyApp AdminContact@example.com'.");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
body,
|
|
457
|
+
contentType: response.headers.get("content-type") ?? "",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private extractArchiveLinks(indexHtml: string): string[] {
|
|
462
|
+
const matches = [...indexHtml.matchAll(/<a[^>]+href="([^"]+)"[^>]*>/gi)];
|
|
463
|
+
const urls = matches
|
|
464
|
+
.map((match) => match[1] ?? "")
|
|
465
|
+
.map((href) => {
|
|
466
|
+
if (/^https?:\/\//i.test(href)) return href;
|
|
467
|
+
if (href.startsWith("/Archives/")) return `https://www.sec.gov${href}`;
|
|
468
|
+
return null;
|
|
469
|
+
})
|
|
470
|
+
.filter((value): value is string => !!value && value.includes("/Archives/edgar/data/"));
|
|
471
|
+
|
|
472
|
+
return [...new Set(urls)];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async findAlternativeHtmlDocumentUrl(filingUrl: string, currentUrl?: string): Promise<string | null> {
|
|
476
|
+
const { body } = await this.fetchText(filingUrl);
|
|
477
|
+
const links = this.extractArchiveLinks(body);
|
|
478
|
+
return links.find((url) => (
|
|
479
|
+
url !== currentUrl
|
|
480
|
+
&& !/-index\.htm(?:$|[?#])/i.test(url)
|
|
481
|
+
&& /\.(?:html?|xhtml)(?:$|[?#])/i.test(url)
|
|
482
|
+
)) ?? null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private async loadLookup(): Promise<Map<string, LookupEntry>> {
|
|
486
|
+
if (!this.lookupPromise) {
|
|
487
|
+
this.lookupPromise = this.fetchJson<unknown>(LOOKUP_URL)
|
|
488
|
+
.then((payload) => parseTickerLookup(payload))
|
|
489
|
+
.catch((error) => {
|
|
490
|
+
this.lookupPromise = null;
|
|
491
|
+
throw error;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return this.lookupPromise;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async getRecentFilings(ticker: string, count = 15): Promise<SecFilingItem[]> {
|
|
498
|
+
const normalizedTicker = normalize(ticker);
|
|
499
|
+
if (!normalizedTicker) return [];
|
|
500
|
+
|
|
501
|
+
const lookup = await this.loadLookup();
|
|
502
|
+
const entry = lookup.get(normalizedTicker);
|
|
503
|
+
if (!entry) return [];
|
|
504
|
+
|
|
505
|
+
const payload = await this.fetchJson<unknown>(`${SUBMISSIONS_URL}/CIK${entry.cik}.json`);
|
|
506
|
+
return parseRecentFilings(payload, count);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async getFilingContent(filing: Pick<SecFilingItem, "primaryDocumentUrl" | "filingUrl" | "form">): Promise<string | null> {
|
|
510
|
+
const targetUrl = filing.primaryDocumentUrl || filing.filingUrl;
|
|
511
|
+
if (!targetUrl) return null;
|
|
512
|
+
|
|
513
|
+
if (filing.primaryDocumentUrl && isPdfDocument("", "", filing.primaryDocumentUrl)) {
|
|
514
|
+
const alternativeUrl = filing.filingUrl
|
|
515
|
+
? await this.findAlternativeHtmlDocumentUrl(filing.filingUrl, filing.primaryDocumentUrl)
|
|
516
|
+
: null;
|
|
517
|
+
if (!alternativeUrl) return PDF_FALLBACK_MESSAGE;
|
|
518
|
+
|
|
519
|
+
const { body, contentType } = await this.fetchText(alternativeUrl);
|
|
520
|
+
return extractFilingContent(body, contentType, { form: filing.form, sourceUrl: alternativeUrl });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const { body, contentType } = await this.fetchText(targetUrl);
|
|
524
|
+
return extractFilingContent(body, contentType, { form: filing.form, sourceUrl: targetUrl });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { YahooFinanceClient } from "./yahoo-finance";
|
|
3
|
+
|
|
4
|
+
describe("YahooFinanceClient exchange aliases", () => {
|
|
5
|
+
test("tries the Taipei Exchange suffix for TPEX tickers", () => {
|
|
6
|
+
const provider = new YahooFinanceClient() as any;
|
|
7
|
+
expect(provider.getSymbolsToTry("3105", "TPEX")).toEqual(["3105.TWO", "3105.TW"]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("prefers Frankfurt-style symbols for FWB2 listings", () => {
|
|
11
|
+
const provider = new YahooFinanceClient() as any;
|
|
12
|
+
expect(provider.getSymbolsToTry("HY9H", "FWB2")).toEqual(["HY9H.F", "HY9H.DE"]);
|
|
13
|
+
});
|
|
14
|
+
});
|