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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataProvider } from "../../../types/data-provider";
|
|
2
2
|
import type { Quote } from "../../../types/financials";
|
|
3
|
+
import { getActiveQuoteDisplay } from "../../../market-data/market/status";
|
|
3
4
|
import type { AlertRule } from "./types";
|
|
4
5
|
|
|
5
6
|
export function normalizeAlertSymbol(value: string | null | undefined): string {
|
|
@@ -17,12 +18,19 @@ export function createQuoteErrorMessage(
|
|
|
17
18
|
export async function resolveAlertQuote(
|
|
18
19
|
marketData: Pick<DataProvider, "getQuote">,
|
|
19
20
|
symbol: string,
|
|
21
|
+
exchange = "",
|
|
20
22
|
): Promise<Quote> {
|
|
21
|
-
const quote = await marketData.getQuote(symbol,
|
|
22
|
-
|
|
23
|
+
const quote = await marketData.getQuote(symbol, exchange);
|
|
24
|
+
const display = getActiveQuoteDisplay(quote);
|
|
25
|
+
if (!display || !Number.isFinite(display.price)) {
|
|
23
26
|
throw new Error(`No quote found for "${symbol}".`);
|
|
24
27
|
}
|
|
25
|
-
return
|
|
28
|
+
return {
|
|
29
|
+
...quote,
|
|
30
|
+
price: display.price,
|
|
31
|
+
change: display.change,
|
|
32
|
+
changePercent: display.changePercent,
|
|
33
|
+
};
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export function quoteAlertFields(
|
|
@@ -5,6 +5,7 @@ import { EmptyState, Tabs } from "../../../components";
|
|
|
5
5
|
import type { PaneProps } from "../../../types/plugin";
|
|
6
6
|
import type { PluginModule } from "../plugin-module";
|
|
7
7
|
import { colors } from "../../../theme/colors";
|
|
8
|
+
import { convertCurrency } from "../../../utils/format";
|
|
8
9
|
import {
|
|
9
10
|
getFocusedCollectionId,
|
|
10
11
|
useAppSelector,
|
|
@@ -212,8 +213,14 @@ function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
|
|
|
212
213
|
activePortfolio,
|
|
213
214
|
brokerPerformance: brokerPerformance.performance,
|
|
214
215
|
portfolioStats,
|
|
216
|
+
convertAccountValue: (value) => convertCurrency(
|
|
217
|
+
value,
|
|
218
|
+
accountState?.account.currency || baseCurrency,
|
|
219
|
+
baseCurrency,
|
|
220
|
+
effectiveExchangeRates,
|
|
221
|
+
),
|
|
215
222
|
}),
|
|
216
|
-
[accountState, activePortfolio, brokerPerformance.performance, portfolioStats],
|
|
223
|
+
[accountState, activePortfolio, baseCurrency, brokerPerformance.performance, effectiveExchangeRates, portfolioStats],
|
|
217
224
|
);
|
|
218
225
|
|
|
219
226
|
const riskRows = useMemo(
|
|
@@ -340,6 +347,9 @@ export const portfolioAnalyticsModule: PluginModule = {
|
|
|
340
347
|
defaultPosition: "right",
|
|
341
348
|
defaultMode: "floating",
|
|
342
349
|
defaultFloatingSize: { width: 80, height: 30 },
|
|
350
|
+
portableShare: {
|
|
351
|
+
private: { params: true, settings: true, state: true },
|
|
352
|
+
},
|
|
343
353
|
},
|
|
344
354
|
],
|
|
345
355
|
|
|
@@ -142,23 +142,25 @@ export function buildAnalyticsSummaryRows({
|
|
|
142
142
|
accountState,
|
|
143
143
|
brokerPerformance,
|
|
144
144
|
portfolioStats,
|
|
145
|
+
convertAccountValue = (value) => value,
|
|
145
146
|
}: {
|
|
146
147
|
accountState: ResolvedPortfolioAccountState | null;
|
|
147
148
|
activePortfolio: Portfolio | null;
|
|
148
149
|
brokerPerformance: BrokerPortfolioPerformance | null;
|
|
149
150
|
portfolioStats: PortfolioSummaryTotals;
|
|
151
|
+
convertAccountValue?: (value: number) => number;
|
|
150
152
|
}): AnalyticsMetricRow[] {
|
|
151
153
|
const rows: AnalyticsMetricRow[] = [];
|
|
152
154
|
const account = accountState?.account;
|
|
153
|
-
const accountMetrics = resolvePortfolioAccountMetrics(portfolioStats, account);
|
|
155
|
+
const accountMetrics = resolvePortfolioAccountMetrics(portfolioStats, account, convertAccountValue);
|
|
154
156
|
const accountFreshness = formatAccountFreshness(account);
|
|
155
|
-
const totalMarketValue = resolvePortfolioMarketValue(portfolioStats, account);
|
|
157
|
+
const totalMarketValue = resolvePortfolioMarketValue(portfolioStats, account, convertAccountValue);
|
|
156
158
|
|
|
157
159
|
if (account?.netLiquidation != null) {
|
|
158
160
|
rows.push({
|
|
159
161
|
id: "net-liquidation",
|
|
160
162
|
label: "Net Liq",
|
|
161
|
-
value: formatCompact(account.netLiquidation),
|
|
163
|
+
value: formatCompact(convertAccountValue(account.netLiquidation)),
|
|
162
164
|
color: colors.text,
|
|
163
165
|
});
|
|
164
166
|
}
|
|
@@ -184,7 +186,7 @@ export function buildAnalyticsSummaryRows({
|
|
|
184
186
|
rows.push({
|
|
185
187
|
id: "cash",
|
|
186
188
|
label: "Cash",
|
|
187
|
-
value: formatCompact(account.totalCashValue),
|
|
189
|
+
value: formatCompact(convertAccountValue(account.totalCashValue)),
|
|
188
190
|
color: colors.text,
|
|
189
191
|
});
|
|
190
192
|
}
|
|
@@ -307,6 +307,9 @@ export const brokerManagerModule: PluginModule = {
|
|
|
307
307
|
defaultPosition: "right",
|
|
308
308
|
defaultMode: "floating",
|
|
309
309
|
defaultFloatingSize: { width: 92, height: 24 },
|
|
310
|
+
portableShare: {
|
|
311
|
+
private: { title: true, params: true, settings: true, state: true },
|
|
312
|
+
},
|
|
310
313
|
},
|
|
311
314
|
],
|
|
312
315
|
paneTemplates: [
|
|
@@ -122,7 +122,7 @@ export async function loadBuildoutData(token: string | null) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
export async function getBuildoutProToken() {
|
|
125
|
-
if (!apiClient.
|
|
125
|
+
if (!apiClient.isSignedIn()) return null;
|
|
126
126
|
|
|
127
127
|
const session = await apiClient.getSession().catch(() => null);
|
|
128
128
|
if (!session) return null;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
DataTableStackView,
|
|
5
5
|
EmptyState,
|
|
6
6
|
Spinner,
|
|
7
|
+
useTableLoadMore,
|
|
7
8
|
usePaneFooter,
|
|
8
9
|
type DataTableKeyEvent,
|
|
9
10
|
type PaneHint,
|
|
@@ -245,24 +246,22 @@ export function BuildoutPane({ focused, width, height }: PaneProps) {
|
|
|
245
246
|
return true;
|
|
246
247
|
}, [canFavorite, toggleFavorite]);
|
|
247
248
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}, [activeTab, loadCompanies, loadIntel, loadSites, selectedList, state]);
|
|
249
|
+
const activePage = state.status === "ready" ? activeBuildoutPage(state, activeTab, selectedList) : null;
|
|
250
|
+
const loadMoreActiveRows = useTableLoadMore(
|
|
251
|
+
tableScrollRef,
|
|
252
|
+
!!activePage && !activePage.loadingMore && !!activePage.hasMore && !activePage.error,
|
|
253
|
+
() => {
|
|
254
|
+
if (!activePage) return;
|
|
255
|
+
if (activeTab === "companies" && selectedList) {
|
|
256
|
+
void loadCompanies(selectedList, activePage.offset, true);
|
|
257
|
+
} else if (activeTab === "sites") {
|
|
258
|
+
void loadSites(activePage.offset, true);
|
|
259
|
+
} else if (activeTab === "intel") {
|
|
260
|
+
void loadIntel(activePage.offset, true);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
LOAD_MORE_THRESHOLD,
|
|
264
|
+
);
|
|
266
265
|
|
|
267
266
|
const handleRootKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
268
267
|
if (event.name === "u" && state.status === "ready" && state.access !== "pro") {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { CloudCdsResponse } from "../../../api-client";
|
|
3
|
+
import type { InstrumentSearchResult } from "../../../types/instrument";
|
|
4
|
+
import { loadCdsActivity } from "./client";
|
|
5
|
+
|
|
6
|
+
function result(symbol: string, name: string): InstrumentSearchResult {
|
|
7
|
+
return { providerId: "gloomberb-cloud", symbol, name, exchange: "NYSE", type: "STK" };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const EMPTY_CDS: CloudCdsResponse = { source: "DTCC PPD", asOf: null, trades: [] };
|
|
11
|
+
|
|
12
|
+
/** Records what the loader searched for and what it then asked the backend. */
|
|
13
|
+
function spy(searchResults: InstrumentSearchResult[] | Error = []) {
|
|
14
|
+
const searched: string[] = [];
|
|
15
|
+
const requested: Array<string | undefined> = [];
|
|
16
|
+
return {
|
|
17
|
+
searched,
|
|
18
|
+
requested,
|
|
19
|
+
fetchCds: async (params: { issuer?: string }) => {
|
|
20
|
+
requested.push(params.issuer);
|
|
21
|
+
return EMPTY_CDS;
|
|
22
|
+
},
|
|
23
|
+
searchInstruments: async (query: string) => {
|
|
24
|
+
searched.push(query);
|
|
25
|
+
if (searchResults instanceof Error) throw searchResults;
|
|
26
|
+
return searchResults;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("loadCdsActivity", () => {
|
|
32
|
+
test("expands a bare ticker through search before requesting CDS", async () => {
|
|
33
|
+
// A near-miss is returned first, so first-result-wins would send the wrong name.
|
|
34
|
+
const calls = spy([result("ORCL.MX", "Oracle de Mexico"), result("ORCL", "Oracle Corporation")]);
|
|
35
|
+
|
|
36
|
+
const activity = await loadCdsActivity("ORCL", calls.fetchCds, calls.searchInstruments);
|
|
37
|
+
|
|
38
|
+
expect(calls.searched).toEqual(["ORCL"]);
|
|
39
|
+
expect(calls.requested).toEqual(["Oracle Corporation"]);
|
|
40
|
+
// The pane body reads this back, so ORCL must not survive resolution.
|
|
41
|
+
expect(activity.issuer).toBe("Oracle Corporation");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("falls back to the first result when no symbol matches exactly", async () => {
|
|
45
|
+
const calls = spy([result("AVGO", "Broadcom Inc."), result("AVGOP", "Broadcom Preferred")]);
|
|
46
|
+
|
|
47
|
+
await loadCdsActivity("BRCM", calls.fetchCds, calls.searchInstruments);
|
|
48
|
+
|
|
49
|
+
expect(calls.requested).toEqual(["Broadcom Inc."]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("sends a company name straight through without searching", async () => {
|
|
53
|
+
const calls = spy([result("ORCL", "Oracle Corporation")]);
|
|
54
|
+
|
|
55
|
+
const activity = await loadCdsActivity("Tencent Holdings Limited", calls.fetchCds, calls.searchInstruments);
|
|
56
|
+
|
|
57
|
+
expect(calls.searched).toEqual([]);
|
|
58
|
+
expect(calls.requested).toEqual(["Tencent Holdings Limited"]);
|
|
59
|
+
expect(activity.issuer).toBe("Tencent Holdings Limited");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("keeps the raw ticker when search is empty or fails", async () => {
|
|
63
|
+
const empty = spy([]);
|
|
64
|
+
await loadCdsActivity("ORCL", empty.fetchCds, empty.searchInstruments);
|
|
65
|
+
expect(empty.requested).toEqual(["ORCL"]);
|
|
66
|
+
|
|
67
|
+
// A search outage must not also take out the CDS request.
|
|
68
|
+
const broken = spy(new Error("search unavailable"));
|
|
69
|
+
const activity = await loadCdsActivity("ORCL", broken.fetchCds, broken.searchInstruments);
|
|
70
|
+
expect(broken.requested).toEqual(["ORCL"]);
|
|
71
|
+
expect(activity.issuer).toBe("ORCL");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("market-wide load searches nothing and sends no issuer", async () => {
|
|
75
|
+
const calls = spy([result("ORCL", "Oracle Corporation")]);
|
|
76
|
+
|
|
77
|
+
const activity = await loadCdsActivity(null, calls.fetchCds, calls.searchInstruments);
|
|
78
|
+
|
|
79
|
+
expect(calls.searched).toEqual([]);
|
|
80
|
+
expect(calls.requested).toEqual([undefined]);
|
|
81
|
+
expect(activity.issuer).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { apiClient, type CloudCdsResponse } from "../../../api-client";
|
|
2
|
+
import type { InstrumentSearchResult } from "../../../types/instrument";
|
|
3
|
+
import { normalizeCdsTrades, type CdsTrade } from "./model";
|
|
4
|
+
|
|
5
|
+
/** One trading week of public dissemination is what "recent activity" means here. */
|
|
6
|
+
const CDS_HISTORY_DAYS = 5;
|
|
7
|
+
const CDS_TRADE_LIMIT = 500;
|
|
8
|
+
|
|
9
|
+
export interface CdsActivity {
|
|
10
|
+
source: string;
|
|
11
|
+
asOf: string | null;
|
|
12
|
+
/** The issuer name actually sent, after any symbol lookup. */
|
|
13
|
+
issuer: string | null;
|
|
14
|
+
trades: CdsTrade[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CdsActivityLoader = (issuer: string | null) => Promise<CdsActivity>;
|
|
18
|
+
|
|
19
|
+
type CdsFetch = (params: { issuer?: string; days?: number; limit?: number }) => Promise<CloudCdsResponse>;
|
|
20
|
+
type InstrumentSearch = (query: string, limit?: number) => Promise<InstrumentSearchResult[]>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A bare ticker with normal symbol punctuation and no spaces. Anything longer or
|
|
24
|
+
* containing a space is already a company name and is sent to the backend as-is.
|
|
25
|
+
*/
|
|
26
|
+
const TICKER_LIKE = /^[A-Za-z0-9][A-Za-z0-9.^:-]{0,11}$/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The backend matches DTCC issuer names, so a ticker has to become a company
|
|
30
|
+
* name first. An untracked symbol has no local metadata to expand it, and its
|
|
31
|
+
* quote name never arrives for a fixed binding, so the shared instrument search
|
|
32
|
+
* is the only reliable expansion. A search outage falls back to the raw input
|
|
33
|
+
* rather than taking the CDS request down with it.
|
|
34
|
+
*/
|
|
35
|
+
export async function resolveIssuerName(
|
|
36
|
+
issuer: string,
|
|
37
|
+
searchInstruments: InstrumentSearch,
|
|
38
|
+
): Promise<string> {
|
|
39
|
+
if (!TICKER_LIKE.test(issuer)) return issuer;
|
|
40
|
+
try {
|
|
41
|
+
const results = await searchInstruments(issuer);
|
|
42
|
+
const symbol = issuer.toUpperCase();
|
|
43
|
+
const match = results.find((result) => result.symbol.toUpperCase() === symbol) ?? results[0];
|
|
44
|
+
return match?.name?.trim() || issuer;
|
|
45
|
+
} catch {
|
|
46
|
+
return issuer;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function loadCdsActivity(
|
|
51
|
+
issuer: string | null,
|
|
52
|
+
fetchCds: CdsFetch = (params) => apiClient.getCloudCds(params),
|
|
53
|
+
searchInstruments: InstrumentSearch = (query, limit) => apiClient.searchInstruments(query, limit),
|
|
54
|
+
): Promise<CdsActivity> {
|
|
55
|
+
const resolved = issuer ? await resolveIssuerName(issuer, searchInstruments) : null;
|
|
56
|
+
const response = await fetchCds({
|
|
57
|
+
...(resolved ? { issuer: resolved } : {}),
|
|
58
|
+
days: CDS_HISTORY_DAYS,
|
|
59
|
+
limit: CDS_TRADE_LIMIT,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
source: response.source,
|
|
63
|
+
asOf: response.asOf,
|
|
64
|
+
issuer: resolved,
|
|
65
|
+
trades: normalizeCdsTrades(response.trades ?? []),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PaneTemplateCreateOptions } from "../../../types/plugin";
|
|
2
|
+
import type { PluginModule } from "../plugin-module";
|
|
3
|
+
import { CDS_PANE_ID } from "./model";
|
|
4
|
+
import { CdsPane } from "./pane";
|
|
5
|
+
|
|
6
|
+
/** Only an explicit argument binds a ticker; bare `CDS` stays market-wide. */
|
|
7
|
+
function explicitSymbol(options?: PaneTemplateCreateOptions): string | null {
|
|
8
|
+
const raw = options?.symbol ?? options?.ticker?.metadata.ticker ?? options?.arg;
|
|
9
|
+
return raw?.trim().toUpperCase() || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const cdsModule: PluginModule = {
|
|
13
|
+
panes: [{
|
|
14
|
+
id: CDS_PANE_ID,
|
|
15
|
+
name: "Single-Name CDS",
|
|
16
|
+
icon: "D",
|
|
17
|
+
component: CdsPane,
|
|
18
|
+
defaultPosition: "right",
|
|
19
|
+
defaultMode: "floating",
|
|
20
|
+
defaultFloatingSize: { width: 92, height: 22 },
|
|
21
|
+
}],
|
|
22
|
+
paneTemplates: [{
|
|
23
|
+
id: "cds-pane",
|
|
24
|
+
paneId: CDS_PANE_ID,
|
|
25
|
+
label: "Single-Name CDS",
|
|
26
|
+
description: "Single-name corporate CDS trade activity from DTCC public dissemination.",
|
|
27
|
+
keywords: ["cds", "credit", "default", "swap", "single name", "issuer", "dtcc", "protection"],
|
|
28
|
+
shortcut: { prefix: "CDS", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
|
|
29
|
+
createInstance: (_context, options) => {
|
|
30
|
+
const symbol = explicitSymbol(options);
|
|
31
|
+
return symbol
|
|
32
|
+
? {
|
|
33
|
+
instanceId: `${CDS_PANE_ID}:${symbol}`,
|
|
34
|
+
title: `CDS ${symbol}`,
|
|
35
|
+
binding: { kind: "fixed", symbol },
|
|
36
|
+
placement: "floating",
|
|
37
|
+
}
|
|
38
|
+
: { instanceId: `${CDS_PANE_ID}:market`, title: "CDS", placement: "floating" };
|
|
39
|
+
},
|
|
40
|
+
}],
|
|
41
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { CloudCdsTradePayload } from "../../../api-client";
|
|
3
|
+
import {
|
|
4
|
+
formatNotional,
|
|
5
|
+
issuerGroupKey,
|
|
6
|
+
normalizeCdsTrades,
|
|
7
|
+
resolveIssuerQuery,
|
|
8
|
+
spreadToBasisPoints,
|
|
9
|
+
summarizeIssuers,
|
|
10
|
+
tradesForIssuer,
|
|
11
|
+
} from "./model";
|
|
12
|
+
|
|
13
|
+
function payload(overrides: Partial<CloudCdsTradePayload> = {}): CloudCdsTradePayload {
|
|
14
|
+
return {
|
|
15
|
+
disseminationId: "1",
|
|
16
|
+
originalDisseminationId: null,
|
|
17
|
+
actionType: "NEWT",
|
|
18
|
+
eventTimestamp: "2026-08-25T14:30:00Z",
|
|
19
|
+
executionTimestamp: "2026-08-25T14:30:00Z",
|
|
20
|
+
effectiveDate: null,
|
|
21
|
+
expirationDate: null,
|
|
22
|
+
maturityDate: "2031-06-20",
|
|
23
|
+
issuerName: "Oracle Corporation",
|
|
24
|
+
underlierId: null,
|
|
25
|
+
underlierIdSource: null,
|
|
26
|
+
upi: null,
|
|
27
|
+
upiFisn: null,
|
|
28
|
+
upiUnderlierName: null,
|
|
29
|
+
notionalAmount: 5_000_000,
|
|
30
|
+
notionalCapped: false,
|
|
31
|
+
notionalCurrency: "USD",
|
|
32
|
+
// Raw DTCC decimals: 0.01 is a 100bp coupon.
|
|
33
|
+
fixedRate: 0.01,
|
|
34
|
+
reportedSpread: null,
|
|
35
|
+
spreadNotation: null,
|
|
36
|
+
upfrontAmount: null,
|
|
37
|
+
upfrontCurrency: null,
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("CDS spread units", () => {
|
|
43
|
+
test("converts reported spreads to basis points by notation", () => {
|
|
44
|
+
// Notation code "3" is what the raw DTCC feed carries for a decimal spread.
|
|
45
|
+
expect(spreadToBasisPoints(0.00256, "3")).toBeCloseTo(25.6, 6);
|
|
46
|
+
expect(spreadToBasisPoints(0.00256, "Decimal")).toBeCloseTo(25.6, 6);
|
|
47
|
+
// Unlabelled raw values are decimals too, not percent.
|
|
48
|
+
expect(spreadToBasisPoints(0.00256, null)).toBeCloseTo(25.6, 6);
|
|
49
|
+
expect(spreadToBasisPoints(25.6, "BPS")).toBe(25.6);
|
|
50
|
+
expect(spreadToBasisPoints(25.6, "Basis points")).toBe(25.6);
|
|
51
|
+
expect(spreadToBasisPoints(0.256, "Percentage")).toBeCloseTo(25.6, 6);
|
|
52
|
+
expect(spreadToBasisPoints(null, "3")).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("normalizeCdsTrades", () => {
|
|
57
|
+
test("keeps the reported spread only, and falls back through issuer names", () => {
|
|
58
|
+
const [withSpread, withoutName] = normalizeCdsTrades([
|
|
59
|
+
payload({ reportedSpread: 0.00256, spreadNotation: "3", fixedRate: 0.05 }),
|
|
60
|
+
payload({ disseminationId: "2", issuerName: null, upiUnderlierName: "ACME INC" }),
|
|
61
|
+
]);
|
|
62
|
+
expect(withSpread!.spreadBp).toBeCloseTo(25.6, 6);
|
|
63
|
+
expect(withSpread!.couponBp).toBeCloseTo(500, 6);
|
|
64
|
+
// A missing spread is never back-solved from coupon or upfront.
|
|
65
|
+
expect(withoutName!.spreadBp).toBeNull();
|
|
66
|
+
expect(withoutName!.couponBp).toBeCloseTo(100, 6);
|
|
67
|
+
expect(withoutName!.issuer).toBe("ACME INC");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("dates a trade by execution time so a late correction is not a new print", () => {
|
|
71
|
+
const [corrected, noExecution, unusableExecution] = normalizeCdsTrades([
|
|
72
|
+
payload({
|
|
73
|
+
actionType: "CORR",
|
|
74
|
+
originalDisseminationId: "9",
|
|
75
|
+
executionTimestamp: "2026-08-18T09:15:00Z",
|
|
76
|
+
eventTimestamp: "2026-08-25T14:30:00Z",
|
|
77
|
+
}),
|
|
78
|
+
payload({ disseminationId: "2", executionTimestamp: null }),
|
|
79
|
+
payload({ disseminationId: "3", executionTimestamp: "not-a-date" }),
|
|
80
|
+
]);
|
|
81
|
+
expect(corrected!.eventAt).toBe(Date.parse("2026-08-18T09:15:00Z"));
|
|
82
|
+
expect(noExecution!.eventAt).toBe(Date.parse("2026-08-25T14:30:00Z"));
|
|
83
|
+
expect(unusableExecution!.eventAt).toBe(Date.parse("2026-08-25T14:30:00Z"));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("drops reports with an unusable timestamp instead of dating them to 1970", () => {
|
|
87
|
+
expect(normalizeCdsTrades([
|
|
88
|
+
payload({ executionTimestamp: null, eventTimestamp: "not-a-date" }),
|
|
89
|
+
])).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("marks capped notionals so a floor is not read as the trade size", () => {
|
|
93
|
+
expect(formatNotional({ notional: 5_000_000, notionalCapped: true })).toBe("5M+");
|
|
94
|
+
expect(formatNotional({ notional: 5_000_000, notionalCapped: false })).toBe("5M");
|
|
95
|
+
expect(formatNotional({ notional: null, notionalCapped: false })).toBe("--");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("summarizeIssuers", () => {
|
|
100
|
+
const trades = normalizeCdsTrades([
|
|
101
|
+
payload({ disseminationId: "a", executionTimestamp: "2026-08-25T10:00:00Z", reportedSpread: 0.009, spreadNotation: "3" }),
|
|
102
|
+
payload({ disseminationId: "b", executionTimestamp: "2026-08-25T14:00:00Z", reportedSpread: null }),
|
|
103
|
+
payload({ disseminationId: "c", executionTimestamp: "2026-08-25T12:00:00Z", reportedSpread: 0.011, spreadNotation: "3" }),
|
|
104
|
+
payload({ disseminationId: "d", executionTimestamp: "2026-08-25T09:00:00Z", issuerName: "Ford Motor Company" }),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
test("groups by issuer with count, last trade, and the newest available spread", () => {
|
|
108
|
+
const summaries = summarizeIssuers(trades);
|
|
109
|
+
const oracle = summaries.find((row) => row.issuer === "Oracle Corporation")!;
|
|
110
|
+
expect(oracle.trades).toBe(3);
|
|
111
|
+
expect(oracle.lastTradeAt).toBe(Date.parse("2026-08-25T14:00:00Z"));
|
|
112
|
+
// The newest print carried no spread, so the last quoted level survives.
|
|
113
|
+
expect(oracle.latestSpreadBp).toBeCloseTo(110, 6);
|
|
114
|
+
expect(summaries.find((row) => row.issuer === "Ford Motor Company")!.latestSpreadBp).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("generic obligation names", () => {
|
|
119
|
+
// Verbatim `UPI Underlier Name` values seen dominating the live market view.
|
|
120
|
+
const GENERIC = [
|
|
121
|
+
"SR NT",
|
|
122
|
+
"SR NT 144A",
|
|
123
|
+
"NT",
|
|
124
|
+
"SR GTD NT 144A",
|
|
125
|
+
"No name obtainable",
|
|
126
|
+
"MEDIUM TERM NOTES EUR 2.3750 S.10/CALL",
|
|
127
|
+
"MEDIUM TERM NOTES EUR 0.6250 S.001STLA/CALL",
|
|
128
|
+
"GLOBAL BD",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
function unnamed(id: string, upiUnderlierName: string | null) {
|
|
132
|
+
return payload({ disseminationId: id, issuerName: null, upiUnderlierName });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test("drops records whose only name is a generic obligation label", () => {
|
|
136
|
+
const rows = normalizeCdsTrades([
|
|
137
|
+
...GENERIC.map((name, index) => unnamed(`g${index}`, name)),
|
|
138
|
+
unnamed("empty", ""),
|
|
139
|
+
unnamed("missing", null),
|
|
140
|
+
]);
|
|
141
|
+
expect(rows).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("accepts UPI underlier names that identify an issuer", () => {
|
|
145
|
+
const rows = normalizeCdsTrades([
|
|
146
|
+
unnamed("t", "Tencent Holdings Limited"),
|
|
147
|
+
unnamed("d", "DT.BANK MTN 17/20"),
|
|
148
|
+
]);
|
|
149
|
+
expect(rows.map((row) => row.issuer)).toEqual(["Tencent Holdings Limited", "DT.BANK MTN 17/20"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("keeps a reported issuer name and never falls back to an underlier id", () => {
|
|
153
|
+
const rows = normalizeCdsTrades([
|
|
154
|
+
payload({ disseminationId: "b", issuerName: "Broadcom Inc", upiUnderlierName: "SR NT 144A" }),
|
|
155
|
+
payload({ disseminationId: "x", issuerName: null, upiUnderlierName: null, underlierId: "US11135FAX50" }),
|
|
156
|
+
]);
|
|
157
|
+
expect(rows.map((row) => row.issuer)).toEqual(["Broadcom Inc"]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("leaves the summary to real issuers when fake ones outnumber them", () => {
|
|
161
|
+
const summaries = summarizeIssuers(normalizeCdsTrades([
|
|
162
|
+
...GENERIC.map((name, index) => unnamed(`g${index}`, name)),
|
|
163
|
+
payload({ disseminationId: "b", issuerName: "Broadcom Inc", upiUnderlierName: "SR GTD NT 144A" }),
|
|
164
|
+
payload({ disseminationId: "o", issuerName: "Oracle Corporation", upiUnderlierName: "SR NT" }),
|
|
165
|
+
]));
|
|
166
|
+
expect(summaries.map((row) => row.issuer).sort()).toEqual(["Broadcom Inc", "Oracle Corporation"]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("issuer aliases", () => {
|
|
171
|
+
// Spellings the deployed backend actually returns for one reference entity,
|
|
172
|
+
// deliberately in an order where the best one is not the first seen.
|
|
173
|
+
const ORACLE = ["ORACLE CORPORATION", "Oracle Cop", "Oracle Corporation"];
|
|
174
|
+
const BOEING = ["THE BOEING COMPANY", "Boeing Co/The", "The Boeing Company"];
|
|
175
|
+
|
|
176
|
+
const aliasTrades = normalizeCdsTrades([
|
|
177
|
+
...ORACLE.map((issuerName, index) => payload({
|
|
178
|
+
disseminationId: `o${index}`,
|
|
179
|
+
issuerName,
|
|
180
|
+
executionTimestamp: `2026-08-25T1${index}:00:00Z`,
|
|
181
|
+
reportedSpread: 0.009,
|
|
182
|
+
spreadNotation: "3",
|
|
183
|
+
})),
|
|
184
|
+
...BOEING.map((issuerName, index) => payload({
|
|
185
|
+
disseminationId: `b${index}`,
|
|
186
|
+
issuerName,
|
|
187
|
+
executionTimestamp: `2026-08-25T0${index}:00:00Z`,
|
|
188
|
+
})),
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
test("collapses case, punctuation, and legal-suffix aliases onto one key", () => {
|
|
192
|
+
expect(new Set(ORACLE.map(issuerGroupKey))).toEqual(new Set(["oracle"]));
|
|
193
|
+
expect(new Set(BOEING.map(issuerGroupKey))).toEqual(new Set(["boeing"]));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("keeps meaningful words that distinguish real entities", () => {
|
|
197
|
+
expect(issuerGroupKey("Oracle Holdings Corp")).not.toBe(issuerGroupKey("Oracle Corporation"));
|
|
198
|
+
expect(issuerGroupKey("Boeing Capital Group")).not.toBe(issuerGroupKey("The Boeing Company"));
|
|
199
|
+
expect(issuerGroupKey("Acme 2031 Ltd")).not.toBe(issuerGroupKey("Acme 2026 Ltd"));
|
|
200
|
+
// A name that is only noise keeps its own row instead of an empty key.
|
|
201
|
+
expect(issuerGroupKey("The Company")).toBe("the company");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("counts every alias in one row and displays the best spelling", () => {
|
|
205
|
+
const summaries = summarizeIssuers(aliasTrades);
|
|
206
|
+
expect(summaries).toHaveLength(2);
|
|
207
|
+
|
|
208
|
+
const oracle = summaries.find((row) => row.key === "oracle")!;
|
|
209
|
+
expect(oracle.trades).toBe(3);
|
|
210
|
+
expect(oracle.issuer).toBe("Oracle Corporation");
|
|
211
|
+
|
|
212
|
+
const boeing = summaries.find((row) => row.key === "boeing")!;
|
|
213
|
+
expect(boeing.trades).toBe(3);
|
|
214
|
+
expect(boeing.issuer).toBe("The Boeing Company");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("drills down on the key so aliases open together", () => {
|
|
218
|
+
expect(tradesForIssuer(aliasTrades, "oracle").map((row) => row.issuer)).toEqual(ORACLE);
|
|
219
|
+
expect(tradesForIssuer(aliasTrades, "boeing").map((row) => row.issuer)).toEqual(BOEING);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("resolveIssuerQuery", () => {
|
|
224
|
+
const ticker = { metadata: { ticker: "ORCL", name: "Oracle Corporation" } } as never;
|
|
225
|
+
|
|
226
|
+
test("prefers tracked metadata and otherwise hands the loader a bare symbol", () => {
|
|
227
|
+
expect(resolveIssuerQuery("ORCL", ticker)).toBe("Oracle Corporation");
|
|
228
|
+
// Untracked: the loader expands this through instrument search.
|
|
229
|
+
expect(resolveIssuerQuery("orcl", null)).toBe("ORCL");
|
|
230
|
+
expect(resolveIssuerQuery(null, null)).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
});
|