gloomberb 0.11.0 → 0.11.2
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 +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -8,6 +8,8 @@ import { assetDataProvider, newsProvider, type PluginCapability } from "../../ca
|
|
|
8
8
|
import type {
|
|
9
9
|
AssetDataProvider,
|
|
10
10
|
CachedFinancialsTarget,
|
|
11
|
+
SecFilingDocument,
|
|
12
|
+
SecFilingItem,
|
|
11
13
|
MarketDataRequestContext,
|
|
12
14
|
QuoteBatchResult,
|
|
13
15
|
QuoteSubscriptionTarget,
|
|
@@ -66,6 +68,34 @@ const CLOUD_PROVIDER_MISS_PATTERNS = [
|
|
|
66
68
|
/error in the query/i,
|
|
67
69
|
];
|
|
68
70
|
|
|
71
|
+
function mapCloudSecFiling(item: {
|
|
72
|
+
accessionNumber: string;
|
|
73
|
+
form: string;
|
|
74
|
+
filingDate: string;
|
|
75
|
+
acceptedAt?: string;
|
|
76
|
+
primaryDocument?: string;
|
|
77
|
+
primaryDocDescription?: string;
|
|
78
|
+
items?: string;
|
|
79
|
+
cik: string;
|
|
80
|
+
companyName?: string;
|
|
81
|
+
filingUrl: string;
|
|
82
|
+
primaryDocumentUrl?: string;
|
|
83
|
+
}): SecFilingItem {
|
|
84
|
+
return {
|
|
85
|
+
accessionNumber: item.accessionNumber,
|
|
86
|
+
form: item.form,
|
|
87
|
+
filingDate: new Date(`${item.filingDate}T00:00:00Z`),
|
|
88
|
+
acceptedAt: item.acceptedAt ? new Date(item.acceptedAt) : undefined,
|
|
89
|
+
primaryDocument: item.primaryDocument,
|
|
90
|
+
primaryDocDescription: item.primaryDocDescription,
|
|
91
|
+
items: item.items,
|
|
92
|
+
cik: item.cik,
|
|
93
|
+
companyName: item.companyName,
|
|
94
|
+
filingUrl: item.filingUrl,
|
|
95
|
+
primaryDocumentUrl: item.primaryDocumentUrl,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
function isCloudProviderMiss(error: unknown): boolean {
|
|
70
100
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
101
|
return CLOUD_PROVIDER_MISS_PATTERNS.some((pattern) => pattern.test(message));
|
|
@@ -155,11 +185,10 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
155
185
|
}
|
|
156
186
|
|
|
157
187
|
async canProvide(): Promise<boolean> {
|
|
158
|
-
return
|
|
188
|
+
return true;
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
async getTickerFinancials(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<TickerFinancials> {
|
|
162
|
-
await requireVerifiedSession();
|
|
163
192
|
return withCloudFallback(async () => {
|
|
164
193
|
const response = await apiClient.getCloudFinancials(ticker, exchange);
|
|
165
194
|
if (isStaleCloudResponse(response)) {
|
|
@@ -176,7 +205,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
176
205
|
targets: CachedFinancialsTarget[],
|
|
177
206
|
options: { forceRefresh?: boolean } = {},
|
|
178
207
|
): Promise<TickerFinancialsBatchResult[]> {
|
|
179
|
-
await requireVerifiedSession();
|
|
180
208
|
return withCloudFallback(async () => {
|
|
181
209
|
const response = await apiClient.getCloudFinancialsBatch(
|
|
182
210
|
targets.map((target) => ({
|
|
@@ -185,6 +213,9 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
185
213
|
})),
|
|
186
214
|
options.forceRefresh ? "refresh" : "cache-first",
|
|
187
215
|
);
|
|
216
|
+
if (isStaleCloudResponse(response)) {
|
|
217
|
+
throw createProviderMiss("Cloud financials are stale");
|
|
218
|
+
}
|
|
188
219
|
const payload = unwrapRequiredCloudResponse(response, "Cloud financials are unavailable");
|
|
189
220
|
return payload.items.map((item, index) => {
|
|
190
221
|
const target = targets[index] ?? {
|
|
@@ -207,7 +238,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
207
238
|
}
|
|
208
239
|
|
|
209
240
|
async getQuote(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<Quote> {
|
|
210
|
-
await requireVerifiedSession();
|
|
211
241
|
return withCloudFallback(
|
|
212
242
|
async () => {
|
|
213
243
|
const response = await apiClient.getCloudQuote(ticker, exchange);
|
|
@@ -227,7 +257,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
227
257
|
targets: QuoteSubscriptionTarget[],
|
|
228
258
|
options: { forceRefresh?: boolean } = {},
|
|
229
259
|
): Promise<QuoteBatchResult[]> {
|
|
230
|
-
await requireVerifiedSession();
|
|
231
260
|
return withCloudFallback(async () => {
|
|
232
261
|
const response = await apiClient.getCloudQuotesBatch(
|
|
233
262
|
targets.map((target) => ({
|
|
@@ -236,6 +265,9 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
236
265
|
})),
|
|
237
266
|
options.forceRefresh ? "refresh" : "cache-first",
|
|
238
267
|
);
|
|
268
|
+
if (isStaleCloudResponse(response)) {
|
|
269
|
+
throw createProviderMiss("Cloud quotes are stale");
|
|
270
|
+
}
|
|
239
271
|
const payload = unwrapRequiredCloudResponse(response, "Cloud quotes are unavailable");
|
|
240
272
|
return payload.items.map((item, index) => {
|
|
241
273
|
const target = targets[index] ?? {
|
|
@@ -258,19 +290,54 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
async getExchangeRate(fromCurrency: string): Promise<number> {
|
|
261
|
-
await requireVerifiedSession();
|
|
262
293
|
const response = await apiClient.getCloudExchangeRate(fromCurrency);
|
|
263
294
|
return unwrapRequiredCloudResponse(response, `Cloud exchange rate is unavailable for ${fromCurrency}`).rate;
|
|
264
295
|
}
|
|
265
296
|
|
|
266
297
|
async search(query: string, _context?: SearchRequestContext): Promise<InstrumentSearchResult[]> {
|
|
267
|
-
await requireVerifiedSession();
|
|
268
298
|
return withCloudFallback(
|
|
269
299
|
() => apiClient.searchInstruments(query, 10),
|
|
270
300
|
"Cloud search is unavailable",
|
|
271
301
|
);
|
|
272
302
|
}
|
|
273
303
|
|
|
304
|
+
async getSecFilings(ticker: string, count = 15): Promise<SecFilingItem[]> {
|
|
305
|
+
await requireVerifiedSession();
|
|
306
|
+
return withCloudFallback(async () => {
|
|
307
|
+
const response = await apiClient.getCloudSecFilings({ ticker, limit: count, offset: 0 });
|
|
308
|
+
return response.filings.map(mapCloudSecFiling);
|
|
309
|
+
}, `Cloud SEC filings are unavailable for ${ticker}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async getSecFilingDocuments(filing: SecFilingItem): Promise<SecFilingDocument[]> {
|
|
313
|
+
await requireVerifiedSession();
|
|
314
|
+
return withCloudFallback(async () => {
|
|
315
|
+
const response = await apiClient.getCloudSecFilingDocuments({
|
|
316
|
+
cik: filing.cik,
|
|
317
|
+
accession: filing.accessionNumber,
|
|
318
|
+
form: filing.form,
|
|
319
|
+
primaryDocument: filing.primaryDocument,
|
|
320
|
+
filingUrl: filing.filingUrl,
|
|
321
|
+
});
|
|
322
|
+
return response.documents;
|
|
323
|
+
}, "Cloud SEC filing documents are unavailable");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async getSecFilingContent(filing: SecFilingItem): Promise<string | null> {
|
|
327
|
+
await requireVerifiedSession();
|
|
328
|
+
return withCloudFallback(async () => {
|
|
329
|
+
const response = await apiClient.getCloudSecFilingContent({
|
|
330
|
+
cik: filing.cik,
|
|
331
|
+
accession: filing.accessionNumber,
|
|
332
|
+
form: filing.form,
|
|
333
|
+
primaryDocument: filing.primaryDocument,
|
|
334
|
+
primaryDocumentUrl: filing.primaryDocumentUrl,
|
|
335
|
+
filingUrl: filing.filingUrl,
|
|
336
|
+
});
|
|
337
|
+
return response.content;
|
|
338
|
+
}, "Cloud SEC filing content is unavailable");
|
|
339
|
+
}
|
|
340
|
+
|
|
274
341
|
async getHolders(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<HolderData> {
|
|
275
342
|
await requireVerifiedSession();
|
|
276
343
|
return withCloudFallback(async () => {
|
|
@@ -300,7 +367,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
300
367
|
}
|
|
301
368
|
|
|
302
369
|
async getPriceHistory(ticker: string, exchange: string, range: TimeRange, _context?: MarketDataRequestContext): Promise<PricePoint[]> {
|
|
303
|
-
await requireVerifiedSession();
|
|
304
370
|
const request = toHistoryRequest(range);
|
|
305
371
|
const response = await withCloudFallback(
|
|
306
372
|
() => apiClient.getCloudHistory(ticker, exchange, request),
|
|
@@ -316,7 +382,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
316
382
|
resolution: ManualChartResolution,
|
|
317
383
|
_context?: MarketDataRequestContext,
|
|
318
384
|
): Promise<PricePoint[]> {
|
|
319
|
-
await requireVerifiedSession();
|
|
320
385
|
const interval = toCloudInterval(resolution);
|
|
321
386
|
const endDate = new Date();
|
|
322
387
|
const startDate = getRangeStartDate(bufferRange, endDate);
|
|
@@ -340,7 +405,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
340
405
|
barSize: string,
|
|
341
406
|
_context?: MarketDataRequestContext,
|
|
342
407
|
): Promise<PricePoint[]> {
|
|
343
|
-
await requireVerifiedSession();
|
|
344
408
|
const interval = toCloudInterval(barSize);
|
|
345
409
|
const includeTime = /(min|h)$/i.test(interval);
|
|
346
410
|
const response = await withCloudFallback(
|
|
@@ -355,7 +419,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
355
419
|
}
|
|
356
420
|
|
|
357
421
|
async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
|
|
358
|
-
await requireVerifiedSession();
|
|
359
422
|
return withCloudFallback(async () => {
|
|
360
423
|
const response = await apiClient.getCloudOptionsChain(ticker, exchange, expirationDate);
|
|
361
424
|
const chain = unwrapRequiredCloudResponse(
|
|
@@ -370,9 +433,8 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
370
433
|
targets: QuoteSubscriptionTarget[],
|
|
371
434
|
onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
|
|
372
435
|
): () => void {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
436
|
+
// No-op without a session credential; covers browser cookie sessions too.
|
|
437
|
+
void apiClient.ensureVerifiedSession().catch(() => {});
|
|
376
438
|
const targetMap = new Map<string, QuoteSubscriptionTarget[]>();
|
|
377
439
|
for (const target of targets) {
|
|
378
440
|
const key = quoteTargetKey(target.symbol, target.exchange);
|
|
@@ -421,6 +483,16 @@ export function createGloomberbCloudCapabilities(provider = createGloomberbCloud
|
|
|
421
483
|
const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
|
|
422
484
|
return feed === "ticker" ? !!query.ticker : true;
|
|
423
485
|
},
|
|
486
|
+
async fetchNewsPage(query: NewsQuery) {
|
|
487
|
+
const response = await withCloudFallback(
|
|
488
|
+
() => apiClient.getCloudNews(cloudNewsParams(query)),
|
|
489
|
+
"Cloud news is unavailable",
|
|
490
|
+
);
|
|
491
|
+
return {
|
|
492
|
+
articles: response.items.map((item) => mapCloudNewsArticle(item, query.ticker)),
|
|
493
|
+
nextCursor: response.nextCursor ?? null,
|
|
494
|
+
};
|
|
495
|
+
},
|
|
424
496
|
async fetchNews(query: NewsQuery): Promise<NewsArticle[]> {
|
|
425
497
|
const response = await withCloudFallback(
|
|
426
498
|
() => apiClient.getCloudNews(cloudNewsParams(query)),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mapCloudFinancials } from "./normalizers";
|
|
3
|
+
|
|
4
|
+
describe("mapCloudFinancials", () => {
|
|
5
|
+
test("divides GBp history with the raw quote currency, not the normalized GBP quote", () => {
|
|
6
|
+
const financials = mapCloudFinancials({
|
|
7
|
+
quote: {
|
|
8
|
+
symbol: "VOD",
|
|
9
|
+
price: 23.1,
|
|
10
|
+
currency: "GBp",
|
|
11
|
+
change: 1,
|
|
12
|
+
changePercent: 4.5,
|
|
13
|
+
lastUpdated: Date.parse("2026-05-13T15:00:00Z"),
|
|
14
|
+
listingExchangeName: "LSE",
|
|
15
|
+
exchangeName: "LSE",
|
|
16
|
+
},
|
|
17
|
+
annualStatements: [],
|
|
18
|
+
quarterlyStatements: [],
|
|
19
|
+
priceHistory: [{
|
|
20
|
+
date: "2026-05-13 10:15:00",
|
|
21
|
+
open: 23,
|
|
22
|
+
high: 23.4,
|
|
23
|
+
low: 22.8,
|
|
24
|
+
close: 23.1,
|
|
25
|
+
volume: 1000,
|
|
26
|
+
}],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(financials.quote?.currency).toBe("GBP");
|
|
30
|
+
expect(financials.quote?.price).toBeCloseTo(0.231);
|
|
31
|
+
expect(financials.priceHistory[0]?.close).toBeCloseTo(0.231);
|
|
32
|
+
expect(financials.priceHistory[0]?.date.toISOString()).toBe("2026-05-13T09:15:00.000Z");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -190,10 +190,10 @@ export function mapCloudFinancials(
|
|
|
190
190
|
financials: TickerFinancials,
|
|
191
191
|
providerMeta?: CloudProviderMeta,
|
|
192
192
|
): TickerFinancials {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
193
|
+
const rawQuote = financials.quote as CloudQuotePayload | undefined;
|
|
194
|
+
const quote = rawQuote ? mapQuote(rawQuote, providerMeta) : undefined;
|
|
195
|
+
const divisor = rawQuote ? resolveCurrencyUnit(rawQuote.currency).divisor : 1;
|
|
196
|
+
const exchange = rawQuote?.listingExchangeName ?? rawQuote?.exchangeName ?? "";
|
|
197
197
|
return {
|
|
198
198
|
quote,
|
|
199
199
|
quoteContributions: financials.quoteContributions,
|
|
@@ -204,7 +204,7 @@ export function mapCloudFinancials(
|
|
|
204
204
|
priceHistory: (financials.priceHistory ?? []).map((point) =>
|
|
205
205
|
point.date instanceof Date
|
|
206
206
|
? point
|
|
207
|
-
: mapPricePoint(point as unknown as CloudPricePointPayload, divisor),
|
|
207
|
+
: mapPricePoint(point as unknown as CloudPricePointPayload, divisor, exchange),
|
|
208
208
|
),
|
|
209
209
|
};
|
|
210
210
|
}
|
|
@@ -391,7 +391,7 @@ describe("AssetDataRouter chart history", () => {
|
|
|
391
391
|
expect(history[0]?.close).toBe(102);
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
-
test("accepts previous-session
|
|
394
|
+
test("accepts previous-session history without persisted exchange metadata while closed", async () => {
|
|
395
395
|
const originalDateNow = Date.now;
|
|
396
396
|
Date.now = () => Date.parse("2026-05-17T12:00:00Z");
|
|
397
397
|
|
|
@@ -419,7 +419,7 @@ describe("AssetDataRouter chart history", () => {
|
|
|
419
419
|
};
|
|
420
420
|
|
|
421
421
|
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
422
|
-
const history = await router.getPriceHistoryForResolution("AAPL", "
|
|
422
|
+
const history = await router.getPriceHistoryForResolution("AAPL", "", "1M", "15m");
|
|
423
423
|
|
|
424
424
|
expect(history.map((point) => point.close)).toEqual([101, 102]);
|
|
425
425
|
} finally {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ProviderRouterNewsRoutes } from "./news";
|
|
3
|
+
|
|
4
|
+
function failingSource() {
|
|
5
|
+
return {
|
|
6
|
+
id: "cloud",
|
|
7
|
+
name: "cloud",
|
|
8
|
+
news: {
|
|
9
|
+
fetchNews: async () => {
|
|
10
|
+
throw new Error("cloud down");
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("ProviderRouterNewsRoutes", () => {
|
|
17
|
+
test("throws when every ticker news source fails", async () => {
|
|
18
|
+
const routes = new ProviderRouterNewsRoutes({
|
|
19
|
+
newsSourcesInPriorityOrder: () => [failingSource()],
|
|
20
|
+
logProviderError: () => {},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await expect(routes.getNews({ feed: "ticker", ticker: "AAPL" })).rejects.toThrow("cloud down");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("throws when every global news source fails", async () => {
|
|
27
|
+
const routes = new ProviderRouterNewsRoutes({
|
|
28
|
+
newsSourcesInPriorityOrder: () => [failingSource()],
|
|
29
|
+
logProviderError: () => {},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(routes.getNews({ feed: "latest" })).rejects.toThrow("cloud down");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -16,21 +16,35 @@ export class ProviderRouterNewsRoutes {
|
|
|
16
16
|
const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
|
|
17
17
|
if (feed === "ticker") {
|
|
18
18
|
let firstEmpty: NewsArticle[] | null = null;
|
|
19
|
+
let lastError: unknown = null;
|
|
20
|
+
let sawSuccess = false;
|
|
19
21
|
for (const source of sources) {
|
|
20
22
|
try {
|
|
21
23
|
const articles = await source.news!.fetchNews(query);
|
|
24
|
+
sawSuccess = true;
|
|
22
25
|
if (articles.length > 0) return articles;
|
|
23
26
|
firstEmpty ??= articles;
|
|
24
27
|
} catch (error) {
|
|
28
|
+
lastError = error;
|
|
25
29
|
if (shouldLogProviderError(error)) {
|
|
26
30
|
this.options.logProviderError(`${source.id} failed: ${error}`);
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
if (!sawSuccess && lastError) throw lastError;
|
|
30
35
|
return firstEmpty ?? [];
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
const settled = await Promise.allSettled(sources.map((source) => source.news!.fetchNews(query)));
|
|
34
|
-
|
|
39
|
+
const articles = settled.flatMap((result) => result.status === "fulfilled" ? result.value : []);
|
|
40
|
+
if (
|
|
41
|
+
articles.length === 0
|
|
42
|
+
&& settled.length > 0
|
|
43
|
+
&& settled.every((result) => result.status === "rejected")
|
|
44
|
+
) {
|
|
45
|
+
const rejected = settled.find((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
46
|
+
throw rejected?.reason ?? new Error("News sources are unavailable");
|
|
47
|
+
}
|
|
48
|
+
return articles;
|
|
35
49
|
}
|
|
36
50
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
parseCompanyFactsFinancialStatements,
|
|
6
6
|
parseFilingDocuments,
|
|
7
7
|
parseRecentFilings,
|
|
8
|
+
parseSubmissionArchiveNames,
|
|
8
9
|
parseTickerLookup,
|
|
9
10
|
} from "./sec-edgar";
|
|
10
11
|
|
|
@@ -65,6 +66,17 @@ describe("parseRecentFilings", () => {
|
|
|
65
66
|
expect(filings[0]?.filingUrl).toBe("https://www.sec.gov/Archives/edgar/data/320193/0000320193-24-000123-index.htm");
|
|
66
67
|
expect(filings[0]?.primaryDocumentUrl).toBe("https://www.sec.gov/Archives/edgar/data/320193/000032019324000123/aapl-8k.htm");
|
|
67
68
|
});
|
|
69
|
+
|
|
70
|
+
test("reads older submission archive names", () => {
|
|
71
|
+
expect(parseSubmissionArchiveNames({
|
|
72
|
+
filings: {
|
|
73
|
+
files: [
|
|
74
|
+
{ name: "CIK0000320193-submissions-001.json", filingCount: 1000 },
|
|
75
|
+
{ name: "readme.txt" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
})).toEqual(["CIK0000320193-submissions-001.json"]);
|
|
79
|
+
});
|
|
68
80
|
});
|
|
69
81
|
|
|
70
82
|
describe("parseFilingDocuments", () => {
|
|
@@ -613,6 +625,53 @@ describe("SecEdgarClient", () => {
|
|
|
613
625
|
expect(headersSeen[0]?.From).toBeTruthy();
|
|
614
626
|
});
|
|
615
627
|
|
|
628
|
+
test("loads older EDGAR submission archives when recent filings run out", async () => {
|
|
629
|
+
const urls: string[] = [];
|
|
630
|
+
globalThis.fetch = (async (input: Request | string | URL) => {
|
|
631
|
+
const url = String(input);
|
|
632
|
+
urls.push(url);
|
|
633
|
+
if (url.includes("company_tickers_exchange.json")) {
|
|
634
|
+
return new Response(JSON.stringify({
|
|
635
|
+
fields: ["cik", "name", "ticker", "exchange"],
|
|
636
|
+
data: [[320193, "Apple Inc.", "AAPL", "Nasdaq"]],
|
|
637
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
638
|
+
}
|
|
639
|
+
if (url.endsWith("CIK0000320193.json")) {
|
|
640
|
+
return new Response(JSON.stringify({
|
|
641
|
+
cik: "0000320193",
|
|
642
|
+
name: "Apple Inc.",
|
|
643
|
+
filings: {
|
|
644
|
+
recent: {
|
|
645
|
+
accessionNumber: ["0000320193-24-000123"],
|
|
646
|
+
form: ["8-K"],
|
|
647
|
+
filingDate: ["2024-08-02"],
|
|
648
|
+
acceptanceDateTime: ["20240802120000"],
|
|
649
|
+
primaryDocument: ["aapl-8k.htm"],
|
|
650
|
+
primaryDocDescription: ["Current report"],
|
|
651
|
+
items: [""],
|
|
652
|
+
},
|
|
653
|
+
files: [{ name: "CIK0000320193-submissions-001.json" }],
|
|
654
|
+
},
|
|
655
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
656
|
+
}
|
|
657
|
+
return new Response(JSON.stringify({
|
|
658
|
+
accessionNumber: ["0000320193-18-000001"],
|
|
659
|
+
form: ["4"],
|
|
660
|
+
filingDate: ["2018-01-02"],
|
|
661
|
+
acceptanceDateTime: ["20180102120000"],
|
|
662
|
+
primaryDocument: ["xslF345X03/wk-form4.xml"],
|
|
663
|
+
primaryDocDescription: ["Form 4"],
|
|
664
|
+
items: [""],
|
|
665
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
666
|
+
}) as unknown as typeof fetch;
|
|
667
|
+
|
|
668
|
+
const client = new SecEdgarClient();
|
|
669
|
+
const filings = await client.getRecentFilings("AAPL", 2);
|
|
670
|
+
expect(urls.some((url) => url.endsWith("CIK0000320193-submissions-001.json"))).toBe(true);
|
|
671
|
+
expect(filings.map((filing) => filing.form)).toEqual(["8-K", "4"]);
|
|
672
|
+
expect(filings[1]?.accessionNumber).toBe("0000320193-18-000001");
|
|
673
|
+
});
|
|
674
|
+
|
|
616
675
|
test("surfaces SEC bot blocking errors clearly", async () => {
|
|
617
676
|
globalThis.fetch = (async () => new Response(
|
|
618
677
|
"<html><body>Undeclared Automated Tool</body></html>",
|
package/src/sources/sec-edgar.ts
CHANGED
|
@@ -252,25 +252,35 @@ export function parseTickerLookup(payload: unknown): Map<string, LookupEntry> {
|
|
|
252
252
|
return results;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
export function
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
255
|
+
export function parseSubmissionArchiveNames(payload: unknown): string[] {
|
|
256
|
+
const files = asRecord(asRecord(payload)?.filings)?.files;
|
|
257
|
+
if (!Array.isArray(files)) return [];
|
|
258
|
+
const names: string[] = [];
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
const name = asRecord(file)?.name;
|
|
261
|
+
if (typeof name === "string" && /^CIK\d+-submissions-\d+\.json$/i.test(name)) {
|
|
262
|
+
names.push(name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return names;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseFilingColumns(
|
|
269
|
+
columns: Record<string, unknown> | null | undefined,
|
|
270
|
+
company: { cik: string; displayCik: string; companyName?: string },
|
|
271
|
+
limit: number,
|
|
272
|
+
): SecFilingItem[] {
|
|
273
|
+
if (!columns || !company.displayCik) return [];
|
|
274
|
+
const accessionNumbers = Array.isArray(columns.accessionNumber) ? columns.accessionNumber : [];
|
|
275
|
+
const forms = Array.isArray(columns.form) ? columns.form : [];
|
|
276
|
+
const filingDates = Array.isArray(columns.filingDate) ? columns.filingDate : [];
|
|
277
|
+
const acceptanceTimes = Array.isArray(columns.acceptanceDateTime) ? columns.acceptanceDateTime : [];
|
|
278
|
+
const primaryDocuments = Array.isArray(columns.primaryDocument) ? columns.primaryDocument : [];
|
|
279
|
+
const primaryDescriptions = Array.isArray(columns.primaryDocDescription) ? columns.primaryDocDescription : [];
|
|
280
|
+
const items = Array.isArray(columns.items) ? columns.items : [];
|
|
271
281
|
const total = Math.min(
|
|
272
282
|
Math.max(accessionNumbers.length, forms.length, filingDates.length),
|
|
273
|
-
Math.max(
|
|
283
|
+
Math.max(limit, 0),
|
|
274
284
|
);
|
|
275
285
|
|
|
276
286
|
const results: SecFilingItem[] = [];
|
|
@@ -278,13 +288,13 @@ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[
|
|
|
278
288
|
const accessionNumber = String(accessionNumbers[index] ?? "").trim();
|
|
279
289
|
const form = String(forms[index] ?? "").trim();
|
|
280
290
|
const filingDate = parseDate(filingDates[index]);
|
|
281
|
-
if (!accessionNumber || !form || !filingDate
|
|
291
|
+
if (!accessionNumber || !form || !filingDate) continue;
|
|
282
292
|
|
|
283
293
|
const accessionNumberNoDashes = stripAccessionDashes(accessionNumber);
|
|
284
294
|
const primaryDocument = String(primaryDocuments[index] ?? "").trim() || undefined;
|
|
285
|
-
const filingUrl = `https://www.sec.gov/Archives/edgar/data/${displayCik}/${accessionNumber}-index.htm`;
|
|
295
|
+
const filingUrl = `https://www.sec.gov/Archives/edgar/data/${company.displayCik}/${accessionNumber}-index.htm`;
|
|
286
296
|
const primaryDocumentUrl = primaryDocument
|
|
287
|
-
? `https://www.sec.gov/Archives/edgar/data/${displayCik}/${accessionNumberNoDashes}/${primaryDocument}`
|
|
297
|
+
? `https://www.sec.gov/Archives/edgar/data/${company.displayCik}/${accessionNumberNoDashes}/${primaryDocument}`
|
|
288
298
|
: undefined;
|
|
289
299
|
|
|
290
300
|
results.push({
|
|
@@ -295,8 +305,8 @@ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[
|
|
|
295
305
|
primaryDocument,
|
|
296
306
|
primaryDocDescription: String(primaryDescriptions[index] ?? "").trim() || undefined,
|
|
297
307
|
items: String(items[index] ?? "").trim() || undefined,
|
|
298
|
-
cik,
|
|
299
|
-
companyName,
|
|
308
|
+
cik: company.cik,
|
|
309
|
+
companyName: company.companyName,
|
|
300
310
|
filingUrl,
|
|
301
311
|
primaryDocumentUrl,
|
|
302
312
|
});
|
|
@@ -305,6 +315,28 @@ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[
|
|
|
305
315
|
return results;
|
|
306
316
|
}
|
|
307
317
|
|
|
318
|
+
function submissionCompany(payload: unknown, fallbackCik = ""): {
|
|
319
|
+
cik: string;
|
|
320
|
+
displayCik: string;
|
|
321
|
+
companyName?: string;
|
|
322
|
+
} {
|
|
323
|
+
const record = asRecord(payload);
|
|
324
|
+
const cik = zeroPadCik(record?.cik) ?? fallbackCik;
|
|
325
|
+
return {
|
|
326
|
+
cik,
|
|
327
|
+
displayCik: String(Number(cik || "0")),
|
|
328
|
+
companyName: typeof record?.name === "string" ? record.name : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[] {
|
|
333
|
+
const record = asRecord(payload);
|
|
334
|
+
if (!record) return [];
|
|
335
|
+
const recent = asRecord(asRecord(record.filings)?.recent)
|
|
336
|
+
?? (Array.isArray(record.accessionNumber) ? record : null);
|
|
337
|
+
return parseFilingColumns(recent, submissionCompany(record), count);
|
|
338
|
+
}
|
|
339
|
+
|
|
308
340
|
export function parseFilingDocuments(indexHtml: string, filing: SecFilingItem): SecFilingDocument[] {
|
|
309
341
|
const documents: SecFilingDocument[] = [];
|
|
310
342
|
const primaryUrl = normalizeDocumentName(filing.primaryDocumentUrl);
|
|
@@ -666,7 +698,15 @@ export class SecEdgarClient {
|
|
|
666
698
|
if (!entry) return [];
|
|
667
699
|
|
|
668
700
|
const payload = await this.fetchJson<unknown>(`${SUBMISSIONS_URL}/CIK${entry.cik}.json`);
|
|
669
|
-
|
|
701
|
+
const filings = parseRecentFilings(payload, count);
|
|
702
|
+
if (filings.length >= count) return filings;
|
|
703
|
+
const company = submissionCompany(payload, entry.cik);
|
|
704
|
+
for (const name of parseSubmissionArchiveNames(payload)) {
|
|
705
|
+
if (filings.length >= count) break;
|
|
706
|
+
const older = await this.fetchJson<unknown>(`${SUBMISSIONS_URL}/${name}`);
|
|
707
|
+
filings.push(...parseFilingColumns(asRecord(older), company, count - filings.length));
|
|
708
|
+
}
|
|
709
|
+
return filings;
|
|
670
710
|
}
|
|
671
711
|
|
|
672
712
|
async getFinancialStatements(ticker: string): Promise<SecCompanyFactsStatements | null> {
|
|
@@ -81,6 +81,9 @@ export function yahooRawDate(value: unknown): string | undefined {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function inferEarningsTiming(date: Date): EarningsEvent["timing"] {
|
|
84
|
+
if (date.getUTCHours() === 0 && date.getUTCMinutes() === 0 && date.getUTCSeconds() === 0) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
84
87
|
const hour = date.getUTCHours();
|
|
85
88
|
if (hour >= 20) return "AMC";
|
|
86
89
|
if (hour <= 13) return "BMO";
|
|
@@ -154,7 +154,7 @@ export async function loadYahooTickerFinancials(
|
|
|
154
154
|
low52w: meta.fiftyTwoWeekLow,
|
|
155
155
|
marketCap: latest("trailingMarketCap"),
|
|
156
156
|
name: meta.shortName || meta.longName,
|
|
157
|
-
lastUpdated:
|
|
157
|
+
lastUpdated: yahooMarketTimestamp(meta),
|
|
158
158
|
exchangeName: meta.exchangeName,
|
|
159
159
|
fullExchangeName: meta.fullExchangeName,
|
|
160
160
|
listingExchangeName: meta.exchangeName,
|
|
@@ -233,7 +233,7 @@ export async function loadYahooQuote(
|
|
|
233
233
|
high52w: meta.fiftyTwoWeekHigh,
|
|
234
234
|
low52w: meta.fiftyTwoWeekLow,
|
|
235
235
|
name: meta.shortName || meta.longName,
|
|
236
|
-
lastUpdated:
|
|
236
|
+
lastUpdated: yahooMarketTimestamp(meta),
|
|
237
237
|
exchangeName: meta.exchangeName,
|
|
238
238
|
fullExchangeName: meta.fullExchangeName,
|
|
239
239
|
listingExchangeName: meta.exchangeName,
|
|
@@ -245,3 +245,11 @@ export async function loadYahooQuote(
|
|
|
245
245
|
...extHours,
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
+
|
|
249
|
+
function yahooMarketTimestamp(meta: NonNullable<ChartResult["meta"]>): number {
|
|
250
|
+
const marketTime = meta.regularMarketTime;
|
|
251
|
+
if (typeof marketTime === "number" && Number.isFinite(marketTime) && marketTime > 0) {
|
|
252
|
+
return marketTime < 1e12 ? marketTime * 1000 : marketTime;
|
|
253
|
+
}
|
|
254
|
+
return Date.now();
|
|
255
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getYahooSymbol, getYahooSymbolsToTry } from "./symbols";
|
|
3
|
+
|
|
4
|
+
describe("Yahoo symbol routing", () => {
|
|
5
|
+
test("canonicalizes MIC aliases before applying exchange suffixes", () => {
|
|
6
|
+
expect(getYahooSymbol("VOD", "XLON")).toBe("VOD.L");
|
|
7
|
+
expect(getYahooSymbolsToTry("0700", "XHKG")).toEqual(["0700.HK"]);
|
|
8
|
+
expect(getYahooSymbolsToTry("SHOP", "XTSE")).toEqual(["SHOP.TO"]);
|
|
9
|
+
});
|
|
10
|
+
});
|