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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import type { CloudCdsTradePayload } from "../../../api-client";
|
|
2
|
+
import type { DataTableColumn } from "../../../components";
|
|
3
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
4
|
+
import { formatCompact } from "../../../utils/format";
|
|
5
|
+
import { compareSortValues, type SortPreference } from "../../../utils/sort-values";
|
|
6
|
+
|
|
7
|
+
export const CDS_PANE_ID = "cds";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Raw DTCC values are decimals: `Fixed rate-Leg 1 = 0.01` is a 100bp coupon and
|
|
11
|
+
* `Spread-Leg 1 = 0.00256` under notation code "3" is 25.6bp. Only a report that
|
|
12
|
+
* explicitly labels itself basis points or percent is read any other way. Both
|
|
13
|
+
* conversions happen here and nowhere else.
|
|
14
|
+
*/
|
|
15
|
+
const PERCENT_TO_BP = 100;
|
|
16
|
+
const DECIMAL_TO_BP = 10_000;
|
|
17
|
+
|
|
18
|
+
export interface CdsTrade {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Reported spelling, kept verbatim so a display name can be chosen later. */
|
|
21
|
+
issuer: string;
|
|
22
|
+
/** Alias-collapsed grouping key. Internal only; never shown or sent. */
|
|
23
|
+
issuerKey: string;
|
|
24
|
+
/** Execution time when the report carried a usable one, else event time. */
|
|
25
|
+
eventAt: number;
|
|
26
|
+
maturity: string | null;
|
|
27
|
+
notional: number | null;
|
|
28
|
+
notionalCapped: boolean;
|
|
29
|
+
currency: string | null;
|
|
30
|
+
couponBp: number | null;
|
|
31
|
+
/** Only what the report carried. Never derived from upfront or coupon. */
|
|
32
|
+
spreadBp: number | null;
|
|
33
|
+
upfront: number | null;
|
|
34
|
+
upfrontCurrency: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CdsIssuerSummary {
|
|
38
|
+
/** Grouping key, and the row's selection id. */
|
|
39
|
+
key: string;
|
|
40
|
+
/** Best spelling among the aliases in this group. */
|
|
41
|
+
issuer: string;
|
|
42
|
+
trades: number;
|
|
43
|
+
lastTradeAt: number;
|
|
44
|
+
latestSpreadBp: number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Legal-name noise the tape spells inconsistently: "Oracle Corporation",
|
|
49
|
+
* "ORACLE CORPORATION", and "Oracle Cop" are one issuer. Only suffixes and
|
|
50
|
+
* articles belong here. Words that distinguish real entities, such as holdings,
|
|
51
|
+
* group, or a year, must keep their own row.
|
|
52
|
+
*/
|
|
53
|
+
const LEGAL_NAME_NOISE = new Set([
|
|
54
|
+
"the", "co", "company", "corp", "corporation", "cop",
|
|
55
|
+
"inc", "incorporated", "limited", "ltd", "plc",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Obligation vocabulary the tape writes into `UPI Underlier Name` when it has
|
|
60
|
+
* no reference entity: seniority, instrument type, format, rule reference, and
|
|
61
|
+
* currency. A name built only from these describes a bond, not an issuer.
|
|
62
|
+
*/
|
|
63
|
+
const OBLIGATION_NOISE = new Set([
|
|
64
|
+
"sr", "senior", "sub", "subordinated", "jr", "junior",
|
|
65
|
+
"nt", "nts", "note", "notes", "bd", "bds", "bond", "bonds",
|
|
66
|
+
"gtd", "guaranteed", "global", "medium", "term", "mtn", "emtn",
|
|
67
|
+
"144a", "regs", "s", "call",
|
|
68
|
+
"eur", "usd", "gbp", "jpy", "chf", "cad", "aud",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** Whole-string sentinels the feed sends instead of leaving the field empty. */
|
|
72
|
+
const NAME_PLACEHOLDERS = new Set([
|
|
73
|
+
"no name obtainable", "name not available", "not available", "unknown", "n a",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** Case, accents, and punctuation folded away into comparable tokens. */
|
|
77
|
+
function nameTokens(name: string): string[] {
|
|
78
|
+
return name
|
|
79
|
+
.normalize("NFKD")
|
|
80
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[^\p{Letter}\p{Number}]+/gu, " ")
|
|
83
|
+
.trim()
|
|
84
|
+
.split(" ")
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a `UPI Underlier Name` names a company at all. "SR GTD NT 144A" and
|
|
90
|
+
* "MEDIUM TERM NOTES EUR 2.3750 S.10/CALL" are obligation descriptions that
|
|
91
|
+
* would otherwise become fake issuers and outrank real ones, while
|
|
92
|
+
* "Tencent Holdings Limited" and "DT.BANK MTN 17/20" carry a real entity.
|
|
93
|
+
*/
|
|
94
|
+
function identifiesIssuer(name: string): boolean {
|
|
95
|
+
const tokens = nameTokens(name);
|
|
96
|
+
const folded = tokens.join(" ");
|
|
97
|
+
if (
|
|
98
|
+
tokens.length === 0
|
|
99
|
+
|| NAME_PLACEHOLDERS.has(folded)
|
|
100
|
+
|| folded.startsWith("medium term note")
|
|
101
|
+
) return false;
|
|
102
|
+
return tokens.some((token) => (
|
|
103
|
+
!OBLIGATION_NOISE.has(token)
|
|
104
|
+
&& !LEGAL_NAME_NOISE.has(token)
|
|
105
|
+
&& !/^\d+$/.test(token)
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Case, accents, and punctuation folded away, then legal-name noise dropped. */
|
|
110
|
+
export function issuerGroupKey(name: string): string {
|
|
111
|
+
const tokens = nameTokens(name);
|
|
112
|
+
const meaningful = tokens.filter((token) => !LEGAL_NAME_NOISE.has(token));
|
|
113
|
+
// A name made only of noise ("The Company") keeps its tokens rather than
|
|
114
|
+
// collapsing every such issuer into one empty-key row.
|
|
115
|
+
return (meaningful.length > 0 ? meaningful : tokens).join(" ") || name.trim().toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Mixed case reads as a real name; all-caps is the tape shouting. */
|
|
119
|
+
function isMixedCase(name: string): boolean {
|
|
120
|
+
return name !== name.toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Picks the spelling a human would write. Mixed case wins over all-caps first,
|
|
125
|
+
* then the longer name, so a full legal name beats an abbreviated or inverted
|
|
126
|
+
* one ("Oracle Corporation" over "Oracle Cop", "The Boeing Company" over
|
|
127
|
+
* "Boeing Co/The"). Ties break lexically so refreshes do not reshuffle.
|
|
128
|
+
*/
|
|
129
|
+
function preferredIssuerName(current: string, candidate: string): string {
|
|
130
|
+
const currentMixed = isMixedCase(current);
|
|
131
|
+
const candidateMixed = isMixedCase(candidate);
|
|
132
|
+
if (currentMixed !== candidateMixed) return candidateMixed ? candidate : current;
|
|
133
|
+
if (current.length !== candidate.length) return candidate.length > current.length ? candidate : current;
|
|
134
|
+
return candidate.localeCompare(current) < 0 ? candidate : current;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function spreadToBasisPoints(value: number | null, notation: string | null): number | null {
|
|
138
|
+
if (value == null || !Number.isFinite(value)) return null;
|
|
139
|
+
const unit = (notation ?? "").trim().toLowerCase();
|
|
140
|
+
if (unit.startsWith("bp") || unit.includes("basis")) return value;
|
|
141
|
+
if (unit.includes("percent") || unit === "%") return value * PERCENT_TO_BP;
|
|
142
|
+
// Notation code "3", textual "decimal", and an unlabelled raw value are all decimals.
|
|
143
|
+
return value * DECIMAL_TO_BP;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function couponToBasisPoints(fixedRate: number | null): number | null {
|
|
147
|
+
if (fixedRate == null || !Number.isFinite(fixedRate)) return null;
|
|
148
|
+
return fixedRate * DECIMAL_TO_BP;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* When the trade was struck, not when the tape carried it. A lifecycle
|
|
153
|
+
* correction is disseminated long after the fact, so using the event time would
|
|
154
|
+
* date an old trade as new.
|
|
155
|
+
*/
|
|
156
|
+
function tapeTime(trade: CloudCdsTradePayload): number | null {
|
|
157
|
+
const executed = trade.executionTimestamp ? Date.parse(trade.executionTimestamp) : Number.NaN;
|
|
158
|
+
if (Number.isFinite(executed)) return executed;
|
|
159
|
+
const event = Date.parse(trade.eventTimestamp);
|
|
160
|
+
return Number.isFinite(event) ? event : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A reported issuer name is always trusted, even when the UPI underlier beside
|
|
165
|
+
* it is generic. Otherwise the UPI name is used only when it names a company.
|
|
166
|
+
* There is no third fallback: underlier IDs are opaque instrument codes, so
|
|
167
|
+
* showing one would just be a different kind of fake issuer.
|
|
168
|
+
*/
|
|
169
|
+
function issuerOf(trade: CloudCdsTradePayload): string | null {
|
|
170
|
+
const reported = trade.issuerName?.trim();
|
|
171
|
+
if (reported) return reported;
|
|
172
|
+
const underlier = trade.upiUnderlierName?.trim();
|
|
173
|
+
return underlier && identifiesIssuer(underlier) ? underlier : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function normalizeCdsTrades(trades: readonly CloudCdsTradePayload[]): CdsTrade[] {
|
|
177
|
+
const rows: CdsTrade[] = [];
|
|
178
|
+
for (const trade of trades) {
|
|
179
|
+
const eventAt = tapeTime(trade);
|
|
180
|
+
if (eventAt == null) continue;
|
|
181
|
+
const issuer = issuerOf(trade);
|
|
182
|
+
// Nothing names the reference entity, so the row would invent one.
|
|
183
|
+
if (!issuer) continue;
|
|
184
|
+
rows.push({
|
|
185
|
+
id: trade.disseminationId,
|
|
186
|
+
issuer,
|
|
187
|
+
issuerKey: issuerGroupKey(issuer),
|
|
188
|
+
eventAt,
|
|
189
|
+
maturity: trade.maturityDate ?? trade.expirationDate,
|
|
190
|
+
notional: trade.notionalAmount,
|
|
191
|
+
notionalCapped: trade.notionalCapped,
|
|
192
|
+
currency: trade.notionalCurrency,
|
|
193
|
+
couponBp: couponToBasisPoints(trade.fixedRate),
|
|
194
|
+
spreadBp: spreadToBasisPoints(trade.reportedSpread, trade.spreadNotation),
|
|
195
|
+
upfront: trade.upfrontAmount,
|
|
196
|
+
upfrontCurrency: trade.upfrontCurrency,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return rows;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Most-active issuers, with the spread of each issuer's newest report that
|
|
204
|
+
* actually carried one. A quoted issuer whose last print omitted the spread
|
|
205
|
+
* keeps its last known level instead of falling back to "--".
|
|
206
|
+
*/
|
|
207
|
+
export function summarizeIssuers(trades: readonly CdsTrade[]): CdsIssuerSummary[] {
|
|
208
|
+
const byIssuer = new Map<string, CdsIssuerSummary>();
|
|
209
|
+
const spreadAt = new Map<string, number>();
|
|
210
|
+
for (const trade of trades) {
|
|
211
|
+
const current = byIssuer.get(trade.issuerKey);
|
|
212
|
+
if (!current) {
|
|
213
|
+
byIssuer.set(trade.issuerKey, {
|
|
214
|
+
key: trade.issuerKey,
|
|
215
|
+
issuer: trade.issuer,
|
|
216
|
+
trades: 1,
|
|
217
|
+
lastTradeAt: trade.eventAt,
|
|
218
|
+
latestSpreadBp: trade.spreadBp,
|
|
219
|
+
});
|
|
220
|
+
if (trade.spreadBp != null) spreadAt.set(trade.issuerKey, trade.eventAt);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
current.trades += 1;
|
|
224
|
+
current.issuer = preferredIssuerName(current.issuer, trade.issuer);
|
|
225
|
+
if (trade.eventAt > current.lastTradeAt) current.lastTradeAt = trade.eventAt;
|
|
226
|
+
if (trade.spreadBp != null && trade.eventAt >= (spreadAt.get(trade.issuerKey) ?? -Infinity)) {
|
|
227
|
+
current.latestSpreadBp = trade.spreadBp;
|
|
228
|
+
spreadAt.set(trade.issuerKey, trade.eventAt);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return [...byIssuer.values()];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function tradesForIssuer(trades: readonly CdsTrade[], issuerKey: string): CdsTrade[] {
|
|
235
|
+
return trades.filter((trade) => trade.issuerKey === issuerKey);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* What the pane asks for. A tracked ticker already carries its company name; an
|
|
240
|
+
* untracked one yields the bare symbol, which the loader expands through
|
|
241
|
+
* instrument search before it reaches the backend.
|
|
242
|
+
*/
|
|
243
|
+
export function resolveIssuerQuery(
|
|
244
|
+
symbol: string | null,
|
|
245
|
+
ticker: TickerRecord | null,
|
|
246
|
+
): string | null {
|
|
247
|
+
return ticker?.metadata.name?.trim() || symbol?.trim().toUpperCase() || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function formatBp(value: number | null): string {
|
|
251
|
+
if (value == null) return "--";
|
|
252
|
+
return `${Math.abs(value) >= 100 ? Math.round(value) : Math.round(value * 10) / 10}bp`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function formatNotional(trade: Pick<CdsTrade, "notional" | "notionalCapped">): string {
|
|
256
|
+
if (trade.notional == null) return "--";
|
|
257
|
+
return `${formatCompact(trade.notional)}${trade.notionalCapped ? "+" : ""}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function formatUpfront(trade: Pick<CdsTrade, "upfront" | "upfrontCurrency">): string {
|
|
261
|
+
if (trade.upfront == null) return "--";
|
|
262
|
+
const sign = trade.upfront > 0 ? "+" : "";
|
|
263
|
+
return `${sign}${formatCompact(trade.upfront)}${trade.upfrontCurrency ? ` ${trade.upfrontCurrency}` : ""}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function pad(value: number): string {
|
|
267
|
+
return String(value).padStart(2, "0");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** UTC, because DTCC disseminates in UTC and a guessed local zone misdates prints. */
|
|
271
|
+
export function formatEventTime(eventAt: number): string {
|
|
272
|
+
const date = new Date(eventAt);
|
|
273
|
+
return `${pad(date.getUTCMonth() + 1)}/${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function formatAsOf(asOf: string | null): string | null {
|
|
277
|
+
if (!asOf) return null;
|
|
278
|
+
const parsed = Date.parse(asOf);
|
|
279
|
+
return Number.isFinite(parsed) ? `${formatEventTime(parsed)}Z` : asOf;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function formatMaturity(maturity: string | null): string {
|
|
283
|
+
return maturity ?? "--";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export type IssuerColumnId = "issuer" | "trades" | "last" | "spread";
|
|
287
|
+
export type IssuerColumn = DataTableColumn & { id: IssuerColumnId };
|
|
288
|
+
export type IssuerSortPreference = SortPreference<IssuerColumnId>;
|
|
289
|
+
|
|
290
|
+
export const ISSUER_SORT_COLUMN_IDS: readonly IssuerColumnId[] = ["issuer", "trades", "last", "spread"];
|
|
291
|
+
/** "Most active" is the reason the market-wide view exists. */
|
|
292
|
+
export const DEFAULT_ISSUER_SORT: IssuerSortPreference = { columnId: "trades", direction: "desc" };
|
|
293
|
+
|
|
294
|
+
export function buildIssuerColumns(width: number): IssuerColumn[] {
|
|
295
|
+
const issuerWidth = Math.max(16, width - 35);
|
|
296
|
+
return [
|
|
297
|
+
{ id: "issuer", label: "ISSUER", width: issuerWidth, align: "left" },
|
|
298
|
+
{ id: "trades", label: "TRADES", width: 7, align: "right" },
|
|
299
|
+
{ id: "last", label: "LAST UTC", width: 12, align: "left" },
|
|
300
|
+
{ id: "spread", label: "SPREAD", width: 10, align: "right" },
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function issuerSortValue(columnId: IssuerColumnId, row: CdsIssuerSummary): string | number | null {
|
|
305
|
+
switch (columnId) {
|
|
306
|
+
case "issuer":
|
|
307
|
+
return row.issuer;
|
|
308
|
+
case "trades":
|
|
309
|
+
return row.trades;
|
|
310
|
+
case "last":
|
|
311
|
+
return row.lastTradeAt;
|
|
312
|
+
case "spread":
|
|
313
|
+
return row.latestSpreadBp;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function sortIssuers(
|
|
318
|
+
rows: readonly CdsIssuerSummary[],
|
|
319
|
+
sort: IssuerSortPreference,
|
|
320
|
+
): CdsIssuerSummary[] {
|
|
321
|
+
const columnId = sort.columnId;
|
|
322
|
+
if (!columnId) return [...rows];
|
|
323
|
+
return [...rows].sort((left, right) => {
|
|
324
|
+
const compared = compareSortValues(
|
|
325
|
+
issuerSortValue(columnId, left),
|
|
326
|
+
issuerSortValue(columnId, right),
|
|
327
|
+
sort.direction,
|
|
328
|
+
);
|
|
329
|
+
// Stable secondary key so equal counts do not reshuffle between refreshes.
|
|
330
|
+
return compared !== 0 ? compared : left.issuer.localeCompare(right.issuer);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export type TradeColumnId = "time" | "maturity" | "notional" | "currency" | "coupon" | "spread" | "upfront";
|
|
335
|
+
export type TradeColumn = DataTableColumn & { id: TradeColumnId };
|
|
336
|
+
export type TradeSortPreference = SortPreference<TradeColumnId>;
|
|
337
|
+
|
|
338
|
+
export const TRADE_SORT_COLUMN_IDS: readonly TradeColumnId[] = [
|
|
339
|
+
"time",
|
|
340
|
+
"maturity",
|
|
341
|
+
"notional",
|
|
342
|
+
"coupon",
|
|
343
|
+
"spread",
|
|
344
|
+
"upfront",
|
|
345
|
+
];
|
|
346
|
+
export const DEFAULT_TRADE_SORT: TradeSortPreference = { columnId: "time", direction: "desc" };
|
|
347
|
+
|
|
348
|
+
export function buildTradeColumns(width: number): TradeColumn[] {
|
|
349
|
+
const maturityWidth = Math.max(10, Math.min(12, width - 55));
|
|
350
|
+
return [
|
|
351
|
+
{ id: "time", label: "TIME UTC", width: 12, align: "left" },
|
|
352
|
+
{ id: "maturity", label: "MATURITY", width: maturityWidth, align: "left" },
|
|
353
|
+
{ id: "notional", label: "NOTIONAL", width: 11, align: "right" },
|
|
354
|
+
{ id: "currency", label: "CCY", width: 5, align: "left" },
|
|
355
|
+
{ id: "coupon", label: "COUPON", width: 9, align: "right" },
|
|
356
|
+
{ id: "spread", label: "SPREAD", width: 10, align: "right" },
|
|
357
|
+
{ id: "upfront", label: "UPFRONT", width: 13, align: "right" },
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function tradeSortValue(columnId: TradeColumnId, row: CdsTrade): string | number | null {
|
|
362
|
+
switch (columnId) {
|
|
363
|
+
case "time":
|
|
364
|
+
return row.eventAt;
|
|
365
|
+
case "maturity":
|
|
366
|
+
return row.maturity;
|
|
367
|
+
case "notional":
|
|
368
|
+
return row.notional;
|
|
369
|
+
case "currency":
|
|
370
|
+
return row.currency;
|
|
371
|
+
case "coupon":
|
|
372
|
+
return row.couponBp;
|
|
373
|
+
case "spread":
|
|
374
|
+
return row.spreadBp;
|
|
375
|
+
case "upfront":
|
|
376
|
+
return row.upfront;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function sortTrades(rows: readonly CdsTrade[], sort: TradeSortPreference): CdsTrade[] {
|
|
381
|
+
const columnId = sort.columnId;
|
|
382
|
+
if (!columnId) return [...rows];
|
|
383
|
+
return [...rows].sort((left, right) => {
|
|
384
|
+
const compared = compareSortValues(
|
|
385
|
+
tradeSortValue(columnId, left),
|
|
386
|
+
tradeSortValue(columnId, right),
|
|
387
|
+
sort.direction,
|
|
388
|
+
);
|
|
389
|
+
return compared !== 0 ? compared : right.eventAt - left.eventAt;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function nextSort<Id extends string>(
|
|
394
|
+
current: SortPreference<Id>,
|
|
395
|
+
columnId: Id,
|
|
396
|
+
fallback: SortPreference<Id>,
|
|
397
|
+
): SortPreference<Id> {
|
|
398
|
+
if (current.columnId !== columnId) return { columnId, direction: "asc" };
|
|
399
|
+
if (current.direction === "asc") return { columnId, direction: "desc" };
|
|
400
|
+
return fallback;
|
|
401
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { PaneFooterProvider } from "../../../components/layout/pane/footer";
|
|
4
|
+
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
5
|
+
import { AppContext, createInitialState, PaneInstanceProvider } from "../../../state/app/context";
|
|
6
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
7
|
+
import type { CdsActivity } from "./client";
|
|
8
|
+
import { normalizeCdsTrades } from "./model";
|
|
9
|
+
import { CdsPane } from "./pane";
|
|
10
|
+
|
|
11
|
+
let setup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
12
|
+
|
|
13
|
+
const ACTIVITY: CdsActivity = {
|
|
14
|
+
source: "DTCC PPD",
|
|
15
|
+
asOf: "2026-08-25T15:00:00Z",
|
|
16
|
+
// Market-wide: nothing was resolved because nothing was asked for.
|
|
17
|
+
issuer: null,
|
|
18
|
+
trades: normalizeCdsTrades([
|
|
19
|
+
trade("1", "Oracle Corporation", "2026-08-25T10:00:00Z", { reportedSpread: 0.009, spreadNotation: "3" }),
|
|
20
|
+
trade("2", "Oracle Corporation", "2026-08-25T12:00:00Z", { reportedSpread: null }),
|
|
21
|
+
trade("3", "Ford Motor Company", "2026-08-25T11:00:00Z", { reportedSpread: 0.025, spreadNotation: "3" }),
|
|
22
|
+
]),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function trade(
|
|
26
|
+
id: string,
|
|
27
|
+
issuerName: string,
|
|
28
|
+
executionTimestamp: string,
|
|
29
|
+
overrides: { reportedSpread: number | null; spreadNotation?: string | null },
|
|
30
|
+
) {
|
|
31
|
+
return {
|
|
32
|
+
disseminationId: id,
|
|
33
|
+
originalDisseminationId: null,
|
|
34
|
+
actionType: "NEWT",
|
|
35
|
+
eventTimestamp: executionTimestamp,
|
|
36
|
+
executionTimestamp,
|
|
37
|
+
effectiveDate: null,
|
|
38
|
+
expirationDate: null,
|
|
39
|
+
maturityDate: "2031-06-20",
|
|
40
|
+
issuerName,
|
|
41
|
+
underlierId: null,
|
|
42
|
+
underlierIdSource: null,
|
|
43
|
+
upi: null,
|
|
44
|
+
upiFisn: null,
|
|
45
|
+
upiUnderlierName: null,
|
|
46
|
+
notionalAmount: 5_000_000,
|
|
47
|
+
notionalCapped: true,
|
|
48
|
+
notionalCurrency: "USD",
|
|
49
|
+
// Raw DTCC decimal: 0.01 renders as a 100bp coupon.
|
|
50
|
+
fixedRate: 0.01,
|
|
51
|
+
reportedSpread: overrides.reportedSpread,
|
|
52
|
+
spreadNotation: overrides.spreadNotation ?? null,
|
|
53
|
+
upfrontAmount: null,
|
|
54
|
+
upfrontCurrency: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const loadActivity = async () => ACTIVITY;
|
|
59
|
+
|
|
60
|
+
async function settle() {
|
|
61
|
+
for (let index = 0; index < 6; index += 1) {
|
|
62
|
+
await act(async () => {
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
await setup!.renderOnce();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function renderPane() {
|
|
70
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-cds-test"));
|
|
71
|
+
await act(async () => {
|
|
72
|
+
setup = await testRender(
|
|
73
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
74
|
+
<PaneInstanceProvider paneId="cds:market">
|
|
75
|
+
<PaneFooterProvider>
|
|
76
|
+
{() => (
|
|
77
|
+
<CdsPane
|
|
78
|
+
paneId="cds:market"
|
|
79
|
+
paneType="cds"
|
|
80
|
+
focused
|
|
81
|
+
width={92}
|
|
82
|
+
height={16}
|
|
83
|
+
loadActivity={loadActivity}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</PaneFooterProvider>
|
|
87
|
+
</PaneInstanceProvider>
|
|
88
|
+
</AppContext>,
|
|
89
|
+
{ width: 92, height: 16 },
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
await settle();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
afterEach(async () => {
|
|
96
|
+
if (setup) {
|
|
97
|
+
await act(async () => setup?.renderer.destroy());
|
|
98
|
+
setup = undefined;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("CdsPane", () => {
|
|
103
|
+
test("groups market activity by issuer, keeping the newest available spread", async () => {
|
|
104
|
+
await renderPane();
|
|
105
|
+
const frame = setup!.captureCharFrame();
|
|
106
|
+
|
|
107
|
+
// Most active first, and the 250bp report stays 250bp while the percent one becomes 90bp.
|
|
108
|
+
const oracle = frame.indexOf("Oracle Corporation");
|
|
109
|
+
const ford = frame.indexOf("Ford Motor Company");
|
|
110
|
+
expect(oracle).toBeGreaterThanOrEqual(0);
|
|
111
|
+
expect(ford).toBeGreaterThan(oracle);
|
|
112
|
+
expect(frame).toContain("90bp");
|
|
113
|
+
expect(frame).toContain("250bp");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("opens the selected issuer's trades and shows -- for an unreported spread", async () => {
|
|
117
|
+
await renderPane();
|
|
118
|
+
await act(async () => {
|
|
119
|
+
setup!.mockInput.pressEnter();
|
|
120
|
+
await setup!.renderOnce();
|
|
121
|
+
});
|
|
122
|
+
await settle();
|
|
123
|
+
|
|
124
|
+
const frame = setup!.captureCharFrame();
|
|
125
|
+
expect(frame).toContain("NOTIONAL");
|
|
126
|
+
// Capped notional keeps its "+", and the coupon is shown instead of an implied spread.
|
|
127
|
+
expect(frame).toContain("5M+");
|
|
128
|
+
expect(frame).toContain("100bp");
|
|
129
|
+
expect(frame).toContain("--");
|
|
130
|
+
expect(frame).not.toContain("Ford Motor Company");
|
|
131
|
+
});
|
|
132
|
+
});
|