gloomberb 0.16.3 → 0.16.4
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/package.json +4 -1
- package/src/api-client/askg.ts +43 -0
- package/src/api-client/auth.ts +15 -0
- package/src/api-client/beneficial-owners.ts +4 -0
- package/src/api-client/chat.ts +19 -0
- package/src/api-client/company-kpis.ts +14 -2
- package/src/api-client/data.ts +34 -16
- package/src/api-client/index.ts +12 -0
- package/src/api-client/normalizers.ts +27 -0
- package/src/api-client/paths.ts +88 -23
- package/src/api-client/request.ts +2 -1
- package/src/api-client/research-activity.ts +31 -0
- package/src/api-client/socket.ts +39 -8
- package/src/api-client/types.ts +57 -1
- package/src/api-client/upgrade-experiment.ts +90 -0
- package/src/app/global-shortcuts.ts +11 -1
- package/src/app/keybindings/actions.ts +10 -0
- package/src/app/pane-runtime/index.tsx +8 -0
- package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +65 -25
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
- package/src/app/runtime/desktop-deeplink.ts +5 -0
- package/src/app/star-prompt/model.ts +98 -0
- package/src/app/star-prompt/runtime.ts +41 -0
- package/src/app/star-prompt/status-line.tsx +190 -0
- package/src/app.tsx +13 -7
- package/src/architecture/core-builtin-allowlist.json +70 -0
- package/src/brokers/signed-in/adapter.ts +33 -2
- package/src/brokers/signed-in/client.ts +10 -0
- package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
- package/src/brokers/signed-in/sign-in.ts +62 -5
- package/src/capabilities/registry.ts +8 -1
- package/src/capabilities/types.ts +2 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +359 -99
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +164 -26
- package/src/cli/commands/search.ts +29 -8
- package/src/cli/commands/system.ts +99 -19
- package/src/cli/commands/themes.ts +64 -0
- package/src/cli/commands/ticker.ts +62 -30
- package/src/cli/desktop-pane-shot.ts +97 -16
- package/src/cli/errors.ts +25 -2
- package/src/cli/freshness.ts +7 -4
- package/src/cli/fx-pair.ts +57 -0
- package/src/cli/help.ts +14 -1
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +9 -6
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +50 -6
- package/src/cli/pane-functions/capabilities.ts +8 -2
- package/src/cli/pane-functions/catalog.ts +13 -2
- package/src/cli/pane-functions/data.ts +36 -5
- package/src/cli/pane-functions/dom.ts +21 -13
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +67 -22
- package/src/cli/pane-functions/headless.ts +5 -4
- package/src/cli/pane-functions/index.ts +3 -0
- package/src/cli/pane-functions/market-session.ts +133 -0
- package/src/cli/pane-functions/options.ts +15 -4
- package/src/cli/pane-functions/report-notices.ts +63 -0
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +4 -6
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +105 -23
- package/src/cli/registry.ts +2 -1
- package/src/cli/result.ts +50 -13
- package/src/cli/row-window.ts +76 -0
- package/src/cli/scroll-truncation.ts +34 -0
- package/src/components/chart/composite/composite-chart.tsx +43 -4
- package/src/components/chart/composite/legend.tsx +39 -6
- package/src/components/chart/composite/rasterizer.ts +64 -4
- package/src/components/chart/composite/series-cues.ts +71 -0
- package/src/components/chart/composite/text-renderer.ts +16 -8
- package/src/components/chart/composite/types.ts +3 -0
- package/src/components/command-bar/commands/direct/index.ts +9 -1
- package/src/components/command-bar/commands/plugin/items.ts +3 -1
- package/src/components/command-bar/commands/registry.ts +7 -0
- package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/routes/root/command-items.ts +2 -0
- package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
- package/src/components/command-bar/routes/root/results.ts +40 -10
- package/src/components/command-bar/routes/root/runtime.ts +8 -0
- package/src/components/command-bar/routes/root/search-providers.ts +97 -14
- package/src/components/command-bar/surface/index.tsx +18 -4
- package/src/components/command-bar/theme-picker.tsx +48 -13
- package/src/components/command-bar/workflow/ops.ts +5 -3
- package/src/components/data-table/view.tsx +28 -9
- package/src/components/layout/floating-pane.tsx +6 -2
- package/src/components/layout/market-summary.ts +23 -9
- package/src/components/layout/pane/content.tsx +46 -29
- package/src/components/layout/pane/cycle-order.ts +2 -11
- package/src/components/layout/pane/header.tsx +2 -1
- package/src/components/layout/pane/title.ts +1 -1
- package/src/components/layout/presentation.ts +22 -0
- package/src/components/layout/shell/active-drag.ts +1 -1
- package/src/components/layout/shell/drag/index.ts +4 -0
- package/src/components/layout/shell/drag/live.ts +76 -5
- package/src/components/layout/shell/drag/runtime.ts +8 -8
- package/src/components/layout/shell/index.tsx +47 -6
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +51 -10
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/pane/new-layout.ts +116 -0
- package/src/components/layout/shell/shortcuts.ts +5 -2
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-text.tsx +9 -5
- package/src/components/metric-treemap/heat-surface.tsx +27 -8
- package/src/components/metric-treemap/heat-tiles.ts +48 -17
- package/src/components/onboarding/onboarding-frame.tsx +33 -10
- package/src/components/onboarding/onboarding-wizard.tsx +49 -40
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
- package/src/components/onboarding/pro-step-layout.ts +86 -0
- package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
- package/src/components/ui/button.tsx +10 -0
- package/src/components/ui/data-table/index.tsx +38 -0
- package/src/components/ui/data-table/types.ts +8 -1
- package/src/components/ui/data-table/visible-range.ts +2 -0
- package/src/components/ui/icon.tsx +11 -2
- package/src/components/ui/index.ts +1 -1
- package/src/core/connection-health.ts +1 -1
- package/src/core/state/app/layout-reducer.ts +89 -3
- package/src/core/state/app/state.ts +8 -5
- package/src/core/state/app/types.ts +13 -2
- package/src/data/config/layout.ts +55 -21
- package/src/data/config/store/fold-panes.ts +149 -0
- package/src/data/config/store/migrations.ts +7 -0
- package/src/data/config/store/node.ts +27 -6
- package/src/data/config/store/normalize.ts +37 -3
- package/src/i18n/es.ts +14 -1
- package/src/i18n/ja.ts +14 -1
- package/src/i18n/ko.ts +14 -1
- package/src/i18n/zh-cn.ts +14 -1
- package/src/i18n/zh-tw.ts +14 -1
- package/src/layout/desks.ts +1 -2
- package/src/layout/pane-layout-move.ts +264 -0
- package/src/layout/pane-manager/hidden-panes.ts +160 -0
- package/src/layout/pane-manager/index.ts +1 -0
- package/src/market-data/coordinator/entries.ts +7 -1
- package/src/market-data/coordinator/quotes.ts +2 -2
- package/src/market-data/market/freshness.ts +118 -18
- package/src/market-data/market/status.ts +40 -0
- package/src/market-data/options-alternatives.ts +69 -0
- package/src/market-data/quotes/contributions.ts +19 -2
- package/src/market-data/quotes/metadata.ts +16 -0
- package/src/market-data/quotes/screener-live-quotes.ts +56 -5
- package/src/plugins/builtin/account-management/keyboard.ts +3 -0
- package/src/plugins/builtin/account-management/model.ts +1 -0
- package/src/plugins/builtin/account-management/navigation.ts +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +31 -4
- package/src/plugins/builtin/alerts/quotes.ts +3 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
- package/src/plugins/builtin/analytics/risk-model.ts +37 -8
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/broker-manager/model.ts +8 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
- package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
- package/src/plugins/builtin/broker-manager/table.ts +5 -0
- package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +34 -36
- package/src/plugins/builtin/chat/command-bar.ts +138 -0
- package/src/plugins/builtin/chat/composer-commands.ts +21 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
- package/src/plugins/builtin/chat/content/footer.tsx +5 -3
- package/src/plugins/builtin/chat/content/index.tsx +21 -1
- package/src/plugins/builtin/chat/content/mentions.ts +2 -1
- package/src/plugins/builtin/chat/content/user-map.ts +3 -1
- package/src/plugins/builtin/chat/controller/index.ts +48 -0
- package/src/plugins/builtin/chat/conversations.ts +256 -0
- package/src/plugins/builtin/chat/discord-command.ts +91 -0
- package/src/plugins/builtin/chat/edit-window.ts +12 -6
- package/src/plugins/builtin/chat/ghost-user.ts +14 -0
- package/src/plugins/builtin/chat/index.tsx +40 -2
- package/src/plugins/builtin/chat/layout.ts +2 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/message/header.tsx +24 -15
- package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/pane-requests.ts +60 -0
- package/src/plugins/builtin/chat/pane-state.ts +32 -2
- package/src/plugins/builtin/chat/pane.tsx +8 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
- package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
- package/src/plugins/builtin/cloud/askg/model.ts +129 -70
- package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
- package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
- package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
- package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
- package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
- package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
- package/src/plugins/builtin/cloud/plugin.tsx +36 -1
- package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
- package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
- package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
- package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
- package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
- package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
- package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
- package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
- package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
- package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
- package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
- package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
- package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
- package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
- package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
- package/src/plugins/builtin/comment-letters/client.ts +1 -3
- package/src/plugins/builtin/company-kpis/client.ts +23 -8
- package/src/plugins/builtin/company-kpis/headless.ts +3 -1
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
- package/src/plugins/builtin/composite-plugins.ts +107 -64
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
- package/src/plugins/builtin/credit-boards/index.tsx +21 -21
- package/src/plugins/builtin/credit-boards/model.ts +0 -1
- package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
- package/src/plugins/builtin/debt-maturities/client.ts +37 -9
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/data.ts +28 -7
- package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
- package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +42 -17
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/index.tsx +68 -22
- package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +17 -0
- package/src/plugins/builtin/executives/data.ts +17 -6
- package/src/plugins/builtin/executives/pane.tsx +16 -7
- package/src/plugins/builtin/filing-events/pane.tsx +22 -5
- package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
- package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
- package/src/plugins/builtin/gpu/headless.ts +9 -3
- package/src/plugins/builtin/gpu/model.ts +14 -5
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/headless.ts +6 -1
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/insider/index.tsx +4 -4
- package/src/plugins/builtin/iv-history/charts.tsx +21 -7
- package/src/plugins/builtin/iv-history/client.ts +12 -2
- package/src/plugins/builtin/iv-history/format.ts +33 -2
- package/src/plugins/builtin/iv-history/headless.ts +22 -10
- package/src/plugins/builtin/iv-history/index.tsx +3 -3
- package/src/plugins/builtin/iv-history/model.ts +61 -13
- package/src/plugins/builtin/iv-history/pane.tsx +11 -3
- package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
- package/src/plugins/builtin/jobs/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
- package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
- package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
- package/src/plugins/builtin/layout-manager/index.tsx +9 -1
- package/src/plugins/builtin/macro-day/client.ts +24 -0
- package/src/plugins/builtin/macro-day/headless.ts +11 -8
- package/src/plugins/builtin/macro-day/index.tsx +3 -0
- package/src/plugins/builtin/macro-day/model.ts +11 -6
- package/src/plugins/builtin/macro-day/pane.tsx +12 -8
- package/src/plugins/builtin/macro-day/release-table.ts +40 -0
- package/src/plugins/builtin/macro-day/releases.ts +62 -44
- package/src/plugins/builtin/market-heatmap/data.ts +25 -5
- package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
- package/src/plugins/builtin/market-heatmap/model.ts +94 -3
- package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
- package/src/plugins/builtin/market-movers/headless.ts +31 -4
- package/src/plugins/builtin/market-movers/index.tsx +8 -1
- package/src/plugins/builtin/market-movers/model.ts +71 -17
- package/src/plugins/builtin/market-movers/table.ts +50 -23
- package/src/plugins/builtin/news/headless.ts +9 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
- package/src/plugins/builtin/openfda/client.ts +1 -1
- package/src/plugins/builtin/options/carry.ts +95 -0
- package/src/plugins/builtin/options/index.tsx +58 -5
- package/src/plugins/builtin/options/strike-window.ts +183 -0
- package/src/plugins/builtin/options/table.ts +55 -3
- package/src/plugins/builtin/options/types.ts +8 -1
- package/src/plugins/builtin/options/view.tsx +161 -12
- package/src/plugins/builtin/options-positioning/headless.ts +4 -2
- package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
- package/src/plugins/builtin/options-scenario/client.ts +73 -17
- package/src/plugins/builtin/options-scenario/headless.ts +71 -24
- package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
- package/src/plugins/builtin/options-scenario/index.tsx +9 -2
- package/src/plugins/builtin/options-scenario/model.ts +33 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
- package/src/plugins/builtin/pe-band/model.ts +2 -2
- package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
- package/src/plugins/builtin/perps/equity-row.tsx +8 -5
- package/src/plugins/builtin/perps/model.ts +16 -5
- package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
- package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
- package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
- package/src/plugins/builtin/plugin-module.ts +87 -4
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
- package/src/plugins/builtin/realized-vol/headless.ts +3 -0
- package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
- package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
- package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
- package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
- package/src/plugins/builtin/risk-factors/data.ts +23 -9
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
- package/src/plugins/builtin/seasonality/pane.tsx +1 -1
- package/src/plugins/builtin/sec/index.tsx +5 -5
- package/src/plugins/builtin/sectors/client.ts +10 -2
- package/src/plugins/builtin/sectors/headless.ts +35 -6
- package/src/plugins/builtin/sectors/index.tsx +18 -1
- package/src/plugins/builtin/sectors/sector-model.ts +150 -41
- package/src/plugins/builtin/shared/command-tab.ts +51 -0
- package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
- package/src/plugins/builtin/shared/members-menu.tsx +4 -4
- package/src/plugins/builtin/shared/report-freshness.ts +50 -2
- package/src/plugins/builtin/shared/ticker-request.ts +25 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
- package/src/plugins/builtin/shared/volatility/options.ts +1 -46
- package/src/plugins/builtin/short-interest/index.tsx +19 -4
- package/src/plugins/builtin/short-interest/surface.tsx +4 -4
- package/src/plugins/builtin/short-volume/index.tsx +1 -10
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
- package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
- package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
- package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
- package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
- package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
- package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
- package/src/plugins/builtin/vol-surface/client.ts +6 -2
- package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
- package/src/plugins/builtin/vol-surface/headless.ts +124 -20
- package/src/plugins/builtin/vol-surface/model.ts +23 -11
- package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
- package/src/plugins/builtin/vol-surface/tables.ts +124 -0
- package/src/plugins/builtin/world-indices/client.ts +22 -4
- package/src/plugins/builtin/world-indices/headless.ts +54 -27
- package/src/plugins/builtin/world-indices/model.ts +4 -2
- package/src/plugins/builtin/world-indices/table.tsx +13 -2
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/builtin-editorial.ts +126 -0
- package/src/plugins/catalog-backend.ts +16 -9
- package/src/plugins/catalog-browser.ts +127 -63
- package/src/plugins/catalog-manifest.ts +73 -40
- package/src/plugins/catalog-ui.ts +26 -6
- package/src/plugins/extracted-plugins.ts +7 -1
- package/src/plugins/ownership.ts +150 -7
- package/src/plugins/plugin-functions.ts +54 -0
- package/src/plugins/plugin-off.ts +36 -0
- package/src/plugins/registry/host-actions.ts +16 -0
- package/src/plugins/registry/index.ts +84 -24
- package/src/plugins/registry/plugin-state.ts +25 -1
- package/src/plugins/runtime/context.tsx +29 -0
- package/src/react/input.ts +48 -1
- package/src/remote/app-host.tsx +5 -0
- package/src/remote/controller.ts +3 -53
- package/src/remote/matching.ts +69 -0
- package/src/remote/schema.ts +1 -1
- package/src/remote/semantic-tree.tsx +14 -0
- package/src/remote/types.ts +2 -0
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/query-bar.tsx +59 -32
- package/src/renderers/dom/host/box.tsx +1 -1
- package/src/renderers/dom/host/drag-shield.ts +17 -6
- package/src/renderers/dom/host/live-frame.ts +10 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +13 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +11 -5
- package/src/sources/provider-router/financials.ts +42 -15
- package/src/sources/provider-router/history.ts +27 -11
- package/src/sources/provider-router/index.ts +1 -1
- package/src/sources/provider-router/primary.ts +23 -5
- package/src/sources/sec-edgar.ts +68 -8
- package/src/sync/core-contributors.ts +4 -3
- package/src/test-support/cli-context.ts +2 -1
- package/src/test-support/data-provider.ts +34 -0
- package/src/test-support/shell-pane-keys.tsx +1 -0
- package/src/theme/series-colors.ts +46 -0
- package/src/theme/themes.ts +149 -0
- package/src/tickers/search/index.ts +22 -2
- package/src/tickers/search/ranking.ts +45 -1
- package/src/tickers/search/types.ts +3 -1
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/headless.ts +11 -0
- package/src/types/instrument.ts +8 -0
- package/src/types/plugin.ts +49 -3
- package/src/ui/host.tsx +8 -4
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history.ts +39 -6
- package/src/utils/rpc-timeout-error.ts +6 -2
- package/src/utils/sec-eps-basis.ts +159 -30
- package/src/utils/sec.ts +38 -34
- package/src/utils/throttled-fetch.ts +51 -22
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { DockLayoutNode, DockSplitNode, LayoutConfig, PaneInstanceConfig } from "../../types/config";
|
|
2
|
+
import { collectDockLeafRefs, getDockedInstanceIds, getNodeAtPath, replaceNodeAtPath } from "./dock-tree";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A hidden part of the saved dock tree: the largest subtree with no pane on
|
|
6
|
+
* screen, and where it sat beside the panes that are.
|
|
7
|
+
*/
|
|
8
|
+
interface HiddenDockBranch {
|
|
9
|
+
node: DockLayoutNode;
|
|
10
|
+
axis: DockSplitNode["axis"];
|
|
11
|
+
ratio: number;
|
|
12
|
+
/** Which child of its split it was. */
|
|
13
|
+
side: 0 | 1;
|
|
14
|
+
/** The shown panes on the other side of that split. */
|
|
15
|
+
besideIds: ReadonlySet<string>;
|
|
16
|
+
depth: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isAllHidden(node: DockLayoutNode, hidden: ReadonlySet<string>): boolean {
|
|
20
|
+
return getDockedInstanceIds(node).every((instanceId) => hidden.has(instanceId));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function collectHiddenBranches(
|
|
24
|
+
node: DockLayoutNode,
|
|
25
|
+
hidden: ReadonlySet<string>,
|
|
26
|
+
depth: number,
|
|
27
|
+
out: HiddenDockBranch[],
|
|
28
|
+
): void {
|
|
29
|
+
if (node.kind !== "split") return;
|
|
30
|
+
const children = [node.first, node.second] as const;
|
|
31
|
+
children.forEach((child, index) => {
|
|
32
|
+
const side = index as 0 | 1;
|
|
33
|
+
if (!isAllHidden(child, hidden)) {
|
|
34
|
+
collectHiddenBranches(child, hidden, depth + 1, out);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const sibling = children[1 - side]!;
|
|
38
|
+
out.push({
|
|
39
|
+
node: child,
|
|
40
|
+
axis: node.axis,
|
|
41
|
+
ratio: node.ratio,
|
|
42
|
+
side,
|
|
43
|
+
besideIds: new Set(getDockedInstanceIds(sibling).filter((instanceId) => !hidden.has(instanceId))),
|
|
44
|
+
depth,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function commonPathPrefix(paths: ReadonlyArray<ReadonlyArray<0 | 1>>): Array<0 | 1> {
|
|
50
|
+
const [first, ...rest] = paths;
|
|
51
|
+
if (!first) return [];
|
|
52
|
+
let length = first.length;
|
|
53
|
+
for (const path of rest) {
|
|
54
|
+
let index = 0;
|
|
55
|
+
while (index < length && index < path.length && path[index] === first[index]) index += 1;
|
|
56
|
+
length = index;
|
|
57
|
+
}
|
|
58
|
+
return first.slice(0, length);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Puts a hidden branch back beside the smallest subtree that holds the panes
|
|
63
|
+
* it sat beside, or beside everything when those are gone. Branches put back
|
|
64
|
+
* first sat deeper, so the target grows over them to keep the old nesting.
|
|
65
|
+
*/
|
|
66
|
+
function reattachBranch(
|
|
67
|
+
root: DockLayoutNode | null,
|
|
68
|
+
branch: HiddenDockBranch,
|
|
69
|
+
hidden: ReadonlySet<string>,
|
|
70
|
+
): DockLayoutNode {
|
|
71
|
+
if (!root) return branch.node;
|
|
72
|
+
const beside = collectDockLeafRefs(root).filter((leaf) => branch.besideIds.has(leaf.instanceId));
|
|
73
|
+
let path = beside.length > 0 ? commonPathPrefix(beside.map((leaf) => leaf.path)) : [];
|
|
74
|
+
while (path.length > 0) {
|
|
75
|
+
const parent = getNodeAtPath(root, path.slice(0, -1));
|
|
76
|
+
if (parent?.kind !== "split") break;
|
|
77
|
+
const sibling = path[path.length - 1] === 0 ? parent.second : parent.first;
|
|
78
|
+
if (!isAllHidden(sibling, hidden)) break;
|
|
79
|
+
path = path.slice(0, -1);
|
|
80
|
+
}
|
|
81
|
+
const target = getNodeAtPath(root, path) ?? root;
|
|
82
|
+
const split: DockSplitNode = {
|
|
83
|
+
kind: "split",
|
|
84
|
+
axis: branch.axis,
|
|
85
|
+
ratio: branch.ratio,
|
|
86
|
+
first: branch.side === 0 ? branch.node : target,
|
|
87
|
+
second: branch.side === 0 ? target : branch.node,
|
|
88
|
+
};
|
|
89
|
+
return replaceNodeAtPath(root, path, split);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function restoreHiddenDockTree(
|
|
93
|
+
saved: DockLayoutNode | null,
|
|
94
|
+
edited: DockLayoutNode | null,
|
|
95
|
+
hidden: ReadonlySet<string>,
|
|
96
|
+
): DockLayoutNode | null {
|
|
97
|
+
if (!saved || !getDockedInstanceIds(saved).some((instanceId) => hidden.has(instanceId))) return edited;
|
|
98
|
+
if (isAllHidden(saved, hidden)) {
|
|
99
|
+
return edited ? { kind: "split", axis: "horizontal", ratio: 0.5, first: saved, second: edited } : saved;
|
|
100
|
+
}
|
|
101
|
+
const branches: HiddenDockBranch[] = [];
|
|
102
|
+
collectHiddenBranches(saved, hidden, 0, branches);
|
|
103
|
+
branches.sort((a, b) => b.depth - a.depth);
|
|
104
|
+
let root = edited;
|
|
105
|
+
for (const branch of branches) root = reattachBranch(root, branch, hidden);
|
|
106
|
+
return root;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The edited list with each hidden entry back after the entry it followed in
|
|
111
|
+
* the saved one, so the order of what is on screen stays the edit's.
|
|
112
|
+
*/
|
|
113
|
+
function restoreHiddenEntries<T extends { instanceId: string }>(
|
|
114
|
+
saved: readonly T[],
|
|
115
|
+
edited: readonly T[],
|
|
116
|
+
hidden: ReadonlySet<string>,
|
|
117
|
+
): T[] {
|
|
118
|
+
if (!saved.some((entry) => hidden.has(entry.instanceId))) return [...edited];
|
|
119
|
+
const result = [...edited];
|
|
120
|
+
let previousId: string | null = null;
|
|
121
|
+
for (const entry of saved) {
|
|
122
|
+
if (hidden.has(entry.instanceId)) {
|
|
123
|
+
const index = previousId === null ? -1 : result.findIndex((candidate) => candidate.instanceId === previousId);
|
|
124
|
+
result.splice(index + 1, 0, entry);
|
|
125
|
+
previousId = entry.instanceId;
|
|
126
|
+
} else if (result.some((candidate) => candidate.instanceId === entry.instanceId)) {
|
|
127
|
+
previousId = entry.instanceId;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Panes whose plugin is off, or whose pane type nothing registered, stay in
|
|
135
|
+
* the saved layout while the shell leaves them off screen. The shell edits
|
|
136
|
+
* the layout it shows, so before an edit is saved this puts every hidden pane
|
|
137
|
+
* back from `saved`: its instance and state, its floating or popped-out frame,
|
|
138
|
+
* and its docked branch beside the panes it sat next to. A hidden pane the
|
|
139
|
+
* edit itself kept is left as the edit has it.
|
|
140
|
+
*
|
|
141
|
+
* With nothing edited, `restoreHiddenPanes(saved, shown)` is `saved` again.
|
|
142
|
+
*/
|
|
143
|
+
export function restoreHiddenPanes(
|
|
144
|
+
saved: LayoutConfig,
|
|
145
|
+
edited: LayoutConfig,
|
|
146
|
+
isHidden: (instance: PaneInstanceConfig) => boolean,
|
|
147
|
+
): LayoutConfig {
|
|
148
|
+
const editedIds = new Set(edited.instances.map((instance) => instance.instanceId));
|
|
149
|
+
const hidden = new Set(saved.instances
|
|
150
|
+
.filter((instance) => !editedIds.has(instance.instanceId) && isHidden(instance))
|
|
151
|
+
.map((instance) => instance.instanceId));
|
|
152
|
+
if (hidden.size === 0) return edited;
|
|
153
|
+
return {
|
|
154
|
+
...edited,
|
|
155
|
+
dockRoot: restoreHiddenDockTree(saved.dockRoot, edited.dockRoot, hidden),
|
|
156
|
+
instances: restoreHiddenEntries(saved.instances, edited.instances, hidden),
|
|
157
|
+
floating: restoreHiddenEntries(saved.floating, edited.floating, hidden),
|
|
158
|
+
detached: restoreHiddenEntries(saved.detached ?? [], edited.detached ?? [], hidden),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -140,7 +140,7 @@ export function readyQuoteEntry(
|
|
|
140
140
|
quote: Quote,
|
|
141
141
|
source: string,
|
|
142
142
|
attempts: ProviderAttempt[],
|
|
143
|
-
options: { keepNewerHeldQuote?: boolean } = {},
|
|
143
|
+
options: { keepNewerHeldQuote?: boolean; acceptLastKnown?: boolean } = {},
|
|
144
144
|
): QueryEntry<Quote> {
|
|
145
145
|
const held = options.keepNewerHeldQuote ? heldQuoteSupersedes(current, quote) : null;
|
|
146
146
|
if (held) {
|
|
@@ -148,6 +148,12 @@ export function readyQuoteEntry(
|
|
|
148
148
|
}
|
|
149
149
|
if (isQuoteStaleForCurrentSession(quote)) {
|
|
150
150
|
const keepFreshQuote = hasFreshQuoteForCurrentSession([current.data, current.lastGoodData]);
|
|
151
|
+
// A request answers with a stale quote only when no source has a current
|
|
152
|
+
// one (`acceptLastKnown`). With nothing current held, that last known
|
|
153
|
+
// price, flagged stale, beats no price. Stream ticks never bring one in.
|
|
154
|
+
if (options.acceptLastKnown && !keepFreshQuote) {
|
|
155
|
+
return readyEntry(current, { ...quote, stale: true }, source, attempts, { keepLastGoodOnEmpty: true });
|
|
156
|
+
}
|
|
151
157
|
return readyEntry(current, null, current.source ?? source, attempts, { keepLastGoodOnEmpty: keepFreshQuote });
|
|
152
158
|
}
|
|
153
159
|
return readyEntry(current, quote, source, attempts, { keepLastGoodOnEmpty: true });
|
|
@@ -111,7 +111,7 @@ export async function loadQuoteEntry({
|
|
|
111
111
|
const resolvedQuote = resolveQuote?.(instrument, quote) ?? quote;
|
|
112
112
|
const source = resolvedQuote.providerId ?? dataProvider.id;
|
|
113
113
|
const attempts = [createAttempt(source, startedAt, "success")];
|
|
114
|
-
return quoteStore.update(key, (current) => readyQuoteEntry(current, resolvedQuote, source, attempts, { keepNewerHeldQuote: true }));
|
|
114
|
+
return quoteStore.update(key, (current) => readyQuoteEntry(current, resolvedQuote, source, attempts, { keepNewerHeldQuote: true, acceptLastKnown: true }));
|
|
115
115
|
} catch (error) {
|
|
116
116
|
return storeQuoteFailure(quoteStore, key, dataProvider.id, startedAt, error);
|
|
117
117
|
}
|
|
@@ -182,7 +182,7 @@ export async function loadQuoteBatchEntries({
|
|
|
182
182
|
const quote = resolveQuote?.(instrument, item.quote) ?? item.quote;
|
|
183
183
|
const source = quote.providerId ?? dataProvider.id;
|
|
184
184
|
const attempts = [createAttempt(source, Date.now(), "success")];
|
|
185
|
-
results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts, { keepNewerHeldQuote: true })));
|
|
185
|
+
results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts, { keepNewerHeldQuote: true, acceptLastKnown: true })));
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -9,6 +9,11 @@ import { quoteFutureToleranceMs } from "../quotes/clock";
|
|
|
9
9
|
import { zonedDateKey, zonedDateTimeParts, zonedWallClockToUtcMs } from "../../utils/zoned-date-time";
|
|
10
10
|
|
|
11
11
|
const ALWAYS_OPEN_EXCHANGES = new Set(["CCC"]);
|
|
12
|
+
|
|
13
|
+
/** Spot crypto venues have no equity open or close. */
|
|
14
|
+
export function isAlwaysOpenExchange(exchange: string | undefined): boolean {
|
|
15
|
+
return ALWAYS_OPEN_EXCHANGES.has(canonicalExchange(exchange));
|
|
16
|
+
}
|
|
12
17
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
13
18
|
const OVERNIGHT_CLOSE_MAX_AGE_MS = 20 * 60 * 60 * 1000;
|
|
14
19
|
const ALWAYS_OPEN_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
@@ -61,6 +66,11 @@ const REGULAR_OPEN_MINUTES: Record<string, number> = {
|
|
|
61
66
|
B3: 10 * 60,
|
|
62
67
|
BYMA: 11 * 60,
|
|
63
68
|
JSE: 9 * 60,
|
|
69
|
+
// The opening auction ends at 10:00 and the feed stamps its first bar 09:59.
|
|
70
|
+
TASE: 9 * 60 + 59,
|
|
71
|
+
TADAWUL: 10 * 60,
|
|
72
|
+
QE: 9 * 60 + 30,
|
|
73
|
+
DFM: 10 * 60,
|
|
64
74
|
};
|
|
65
75
|
// Local regular close with the closing auction, rounded up. A close taken too
|
|
66
76
|
// early would let a copy fetched during the auction pass as final.
|
|
@@ -74,7 +84,37 @@ const REGULAR_CLOSE_MINUTES: Record<string, number> = {
|
|
|
74
84
|
JPX: 15 * 60 + 30, HKEX: 16 * 60 + 10, TWSE: 14 * 60 + 30, TPEX: 14 * 60 + 30,
|
|
75
85
|
NSE: 16 * 60, BSE: 16 * 60, ASX: 16 * 60 + 15, SGX: 17 * 60 + 20, KRX: 16 * 60, KOSDAQ: 16 * 60,
|
|
76
86
|
NZX: 17 * 60, SSE: 15 * 60 + 30, SZSE: 15 * 60 + 30,
|
|
77
|
-
BMV: 15 * 60 + 10, B3: 18 * 60 + 30, BYMA: 17 * 60 + 10, JSE: 17 * 60 + 15,
|
|
87
|
+
BMV: 15 * 60 + 10, B3: 18 * 60 + 30, BYMA: 17 * 60 + 10, JSE: 17 * 60 + 15,
|
|
88
|
+
// Monday to Thursday: trading at last ends 17:30, after the closing auction.
|
|
89
|
+
// Not later: a complete 5-minute copy ends with its 17:09 bar, which must
|
|
90
|
+
// reach the close less the half hour a history copy may lag.
|
|
91
|
+
TASE: 17 * 60 + 30,
|
|
92
|
+
// Continuous trading to 15:00, the closing auction to 15:10 and trading at
|
|
93
|
+
// last to 15:20. A complete 5-minute copy ends with its 14:55 bar.
|
|
94
|
+
TADAWUL: 15 * 60 + 20,
|
|
95
|
+
// Continuous trading to 13:00, the closing auction to 13:10 and trading at
|
|
96
|
+
// last to 13:15. A complete 5-minute copy ends with its 13:05 or 13:10 bar.
|
|
97
|
+
QE: 13 * 60 + 15,
|
|
98
|
+
// Continuous trading to 14:45, the closing auction to 14:55 and trading at
|
|
99
|
+
// the close to 15:00. A complete 5-minute copy ends with its 14:55 bar.
|
|
100
|
+
DFM: 15 * 60,
|
|
101
|
+
};
|
|
102
|
+
// Venues whose week is not Monday to Friday, as their days off (0 is Sunday).
|
|
103
|
+
// Tadawul and Qatar trade Sunday to Thursday. Dubai's DFM has traded Monday to
|
|
104
|
+
// Friday since 2022, like the venues not listed here.
|
|
105
|
+
const WEEKEND_DAYS: Record<string, readonly number[]> = {
|
|
106
|
+
TADAWUL: [5, 6],
|
|
107
|
+
QE: [5, 6],
|
|
108
|
+
};
|
|
109
|
+
const DEFAULT_WEEKEND_DAYS: readonly number[] = [0, 6];
|
|
110
|
+
// Venues whose regular session ends earlier on one weekday every week, as
|
|
111
|
+
// local minutes by weekday (0 is Sunday); the other days close as above. TASE
|
|
112
|
+
// has traded Monday to Friday since January 2026, and on Fridays trading at
|
|
113
|
+
// last ends 13:50 (closing auction 13:44 to 13:45). A complete 5-minute copy
|
|
114
|
+
// ends with its 13:29 bar. Holidays and the shortened days around Sukkot and
|
|
115
|
+
// Pesach are not weekly and are not listed.
|
|
116
|
+
const WEEKDAY_CLOSE_MINUTES: Record<string, Partial<Record<number, number>>> = {
|
|
117
|
+
TASE: { 5: 13 * 60 + 50 },
|
|
78
118
|
};
|
|
79
119
|
// Venues that pause at midday, as local minutes [start, end), with a zone for
|
|
80
120
|
// those the session tables above do not cover. Jakarta pauses longer on Fridays.
|
|
@@ -189,12 +229,17 @@ function isPublishedClosure(exchange: string, date: string): boolean {
|
|
|
189
229
|
return getPublishedUsEquityCalendarDay(exchange, date) === "closed";
|
|
190
230
|
}
|
|
191
231
|
|
|
232
|
+
/** Whether `weekday` (0 is Sunday) is in the venue's trading week. */
|
|
233
|
+
function isTradingWeekday(exchange: string, weekday: number): boolean {
|
|
234
|
+
return !(WEEKEND_DAYS[exchange] ?? DEFAULT_WEEKEND_DAYS).includes(weekday);
|
|
235
|
+
}
|
|
236
|
+
|
|
192
237
|
function isLocalTradingDay(exchange: string, date: string): boolean {
|
|
193
238
|
const weekday = localWeekday(date);
|
|
194
|
-
return weekday != null &&
|
|
239
|
+
return weekday != null && isTradingWeekday(exchange, weekday) && !isPublishedClosure(exchange, date);
|
|
195
240
|
}
|
|
196
241
|
|
|
197
|
-
/**
|
|
242
|
+
/** Days of the venue's trading week in (earlier, later], less published closures for venues with a calendar. */
|
|
198
243
|
function localTradingDaysBetween(exchange: string, earlierDate: string, laterDate: string): number {
|
|
199
244
|
const earlierDay = isoLocalDateToUtcDay(earlierDate);
|
|
200
245
|
const laterDay = isoLocalDateToUtcDay(laterDate);
|
|
@@ -212,6 +257,12 @@ function localWeekday(date: string): number | null {
|
|
|
212
257
|
return day == null ? null : new Date(day * MS_PER_DAY).getUTCDay();
|
|
213
258
|
}
|
|
214
259
|
|
|
260
|
+
/** The local close of the venue's regular session on `date`, in minutes of the day. */
|
|
261
|
+
function regularCloseMinutes(exchange: string, date: string): number | undefined {
|
|
262
|
+
const weekday = localWeekday(date);
|
|
263
|
+
return (weekday == null ? undefined : WEEKDAY_CLOSE_MINUTES[exchange]?.[weekday]) ?? REGULAR_CLOSE_MINUTES[exchange];
|
|
264
|
+
}
|
|
265
|
+
|
|
215
266
|
/** The zone a venue's session dates are read in, or null when unknown. */
|
|
216
267
|
export function sessionCalendarTimeZone(exchange: string | undefined): string | null {
|
|
217
268
|
const canonical = canonicalExchange(exchange);
|
|
@@ -221,9 +272,9 @@ export function sessionCalendarTimeZone(exchange: string | undefined): string |
|
|
|
221
272
|
|
|
222
273
|
/**
|
|
223
274
|
* The latest regular session that closed at or before `time`: the published
|
|
224
|
-
* calendar for US venues, otherwise
|
|
225
|
-
* venue without a known close hour is taken to close at
|
|
226
|
-
* for round-the-clock and unknown venues.
|
|
275
|
+
* calendar for US venues, otherwise the days of the venue's trading week less
|
|
276
|
+
* published closures. A venue without a known close hour is taken to close at
|
|
277
|
+
* local midnight. Null for round-the-clock and unknown venues.
|
|
227
278
|
*/
|
|
228
279
|
export function latestRegularSessionClose(
|
|
229
280
|
exchange: string | undefined,
|
|
@@ -241,7 +292,7 @@ export function latestRegularSessionClose(
|
|
|
241
292
|
if (published) {
|
|
242
293
|
if (published.kind === "session") close = published.close;
|
|
243
294
|
} else if (isLocalTradingDay(canonical, date)) {
|
|
244
|
-
const minutes =
|
|
295
|
+
const minutes = regularCloseMinutes(canonical, date) ?? 24 * 60;
|
|
245
296
|
close = zonedWallClockToUtcMs(timeZone, Number(date.slice(0, 4)), Number(date.slice(5, 7)),
|
|
246
297
|
Number(date.slice(8, 10)), Math.floor(minutes / 60), minutes % 60, 0);
|
|
247
298
|
}
|
|
@@ -267,8 +318,8 @@ export function regularSessionCloseUtcMinute(exchange: string | undefined, time:
|
|
|
267
318
|
/**
|
|
268
319
|
* The open of the regular session `time` falls in, or of the latest one
|
|
269
320
|
* before it: the published calendar for US venues, otherwise the venue's
|
|
270
|
-
* local open on a
|
|
271
|
-
* round-the-clock venues and venues without a known open hour.
|
|
321
|
+
* local open on a day of its trading week that is not a published closure.
|
|
322
|
+
* Null for round-the-clock venues and venues without a known open hour.
|
|
272
323
|
*/
|
|
273
324
|
export function latestRegularSessionOpen(exchange: string | undefined, time: number): number | null {
|
|
274
325
|
const canonical = canonicalExchange(exchange);
|
|
@@ -294,6 +345,40 @@ export function latestRegularSessionOpen(exchange: string | undefined, time: num
|
|
|
294
345
|
return null;
|
|
295
346
|
}
|
|
296
347
|
|
|
348
|
+
/**
|
|
349
|
+
* The open of the first regular session after `time`, with its local date and
|
|
350
|
+
* the venue's zone: the published calendar for US venues, otherwise the
|
|
351
|
+
* venue's local open on the next day of its trading week that is not a
|
|
352
|
+
* published closure. Null for round-the-clock venues and venues without a
|
|
353
|
+
* known open hour.
|
|
354
|
+
*/
|
|
355
|
+
export function nextRegularSessionOpen(
|
|
356
|
+
exchange: string | undefined,
|
|
357
|
+
time: number,
|
|
358
|
+
): { date: string; open: number; timeZone: string } | null {
|
|
359
|
+
const canonical = canonicalExchange(exchange);
|
|
360
|
+
const timeZone = sessionCalendarTimeZone(canonical);
|
|
361
|
+
if (!timeZone || ALWAYS_OPEN_EXCHANGES.has(canonical) || !Number.isFinite(time)) return null;
|
|
362
|
+
const minutes = REGULAR_OPEN_MINUTES[canonical];
|
|
363
|
+
const { year, month, day } = zonedDateTimeParts(time, timeZone);
|
|
364
|
+
const today = Date.UTC(year, month - 1, day) / MS_PER_DAY;
|
|
365
|
+
for (let offset = 0; offset <= SESSION_LOOKBACK_DAYS; offset++) {
|
|
366
|
+
const date = new Date((today + offset) * MS_PER_DAY).toISOString().slice(0, 10);
|
|
367
|
+
const published = getPublishedUsEquitySession(canonical, date);
|
|
368
|
+
let open: number | null = null;
|
|
369
|
+
if (published) {
|
|
370
|
+
if (published.kind === "session") open = published.open;
|
|
371
|
+
} else if (minutes === undefined) {
|
|
372
|
+
return null;
|
|
373
|
+
} else if (isLocalTradingDay(canonical, date)) {
|
|
374
|
+
open = zonedWallClockToUtcMs(timeZone, Number(date.slice(0, 4)), Number(date.slice(5, 7)),
|
|
375
|
+
Number(date.slice(8, 10)), Math.floor(minutes / 60), minutes % 60, 0);
|
|
376
|
+
}
|
|
377
|
+
if (open != null && open > time) return { date, open, timeZone };
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
297
382
|
/**
|
|
298
383
|
* Whether `time` is inside a regular session: at or after its open and
|
|
299
384
|
* before its close. Null for venues without known hours.
|
|
@@ -309,7 +394,7 @@ export function isRegularSessionTime(exchange: string | undefined, time: number)
|
|
|
309
394
|
/**
|
|
310
395
|
* True when the venue's full-day closures for the year of `date` are
|
|
311
396
|
* published: US venues, JPX, NSE, BSE, SSE, SZSE, KRX, KOSDAQ, TWSE, TPEX,
|
|
312
|
-
* HKEX, SGX and ASX. Elsewhere a local holiday reads as a
|
|
397
|
+
* HKEX, SGX and ASX. Elsewhere a local holiday reads as a trading day.
|
|
313
398
|
*/
|
|
314
399
|
export function hasPublishedSessionCalendar(exchange: string | undefined, date: string): boolean {
|
|
315
400
|
const canonical = canonicalExchange(exchange);
|
|
@@ -345,7 +430,7 @@ export function isMiddayBreakPrint(
|
|
|
345
430
|
if (!pause || !timeZone || !Number.isFinite(timestampMs) || !Number.isFinite(now) || timestampMs > now) return false;
|
|
346
431
|
const { year, month, day } = zonedDateTimeParts(now, timeZone);
|
|
347
432
|
const weekday = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
|
348
|
-
if (
|
|
433
|
+
if (!isTradingWeekday(canonical, weekday)) return false;
|
|
349
434
|
const [start, end] = weekday === 5 && pause.friday ? pause.friday : pause.weekdays;
|
|
350
435
|
const at = (minutes: number) => zonedWallClockToUtcMs(timeZone, year, month, day, Math.floor(minutes / 60), minutes % 60, 0);
|
|
351
436
|
const breakStart = at(start);
|
|
@@ -443,20 +528,35 @@ export function isUsRegularCloseQuoteInPostSession(
|
|
|
443
528
|
}
|
|
444
529
|
|
|
445
530
|
/**
|
|
446
|
-
* Whether a US print belongs to the
|
|
447
|
-
*
|
|
531
|
+
* Whether a US print belongs to the extended-hours session the source labels,
|
|
532
|
+
* or to the regular session it extends. POST, after today's regular close (a
|
|
533
|
+
* published early close included) and before 20:00: a print from today's
|
|
534
|
+
* regular session or its after-hours. PRE, before today's open: a print from
|
|
535
|
+
* the previous regular session or since, so its after-hours or this morning's
|
|
536
|
+
* pre-market. An earlier print is not, and neither is any print on a day
|
|
537
|
+
* without a session.
|
|
448
538
|
*/
|
|
449
|
-
export function
|
|
539
|
+
export function isUsExtendedHoursSessionPrint(
|
|
450
540
|
timestampMs: number,
|
|
451
541
|
exchange: string | undefined,
|
|
542
|
+
marketState: MarketState | undefined,
|
|
452
543
|
now = Date.now(),
|
|
453
544
|
): boolean {
|
|
454
545
|
const canonical = canonicalExchange(exchange);
|
|
455
|
-
if (!isUsListingExchange(canonical)) return false;
|
|
546
|
+
if ((marketState !== "PRE" && marketState !== "POST") || !isUsListingExchange(canonical)) return false;
|
|
456
547
|
if (!Number.isFinite(timestampMs) || !Number.isFinite(now) || timestampMs > now + quoteFutureToleranceMs()) return false;
|
|
457
|
-
if (!Number.isFinite(new Date(now).getTime())
|
|
458
|
-
const
|
|
459
|
-
|
|
548
|
+
if (!Number.isFinite(new Date(now).getTime())) return false;
|
|
549
|
+
const today = exchangeLocalDate(canonical, now);
|
|
550
|
+
const close = latestRegularSessionClose(canonical, now);
|
|
551
|
+
if (!today || !close || !isLocalTradingDay(canonical, today)) return false;
|
|
552
|
+
const session = usSessionState(now);
|
|
553
|
+
const closedToday = close.date === today;
|
|
554
|
+
if (marketState === "POST" ? !closedToday || (session !== "REGULAR" && session !== "POST") : closedToday || session !== "PRE") {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
// The open of the session that closed last: today's after the close, the previous one before the open.
|
|
558
|
+
const open = latestRegularSessionOpen(canonical, close.close);
|
|
559
|
+
return open != null && timestampMs >= open;
|
|
460
560
|
}
|
|
461
561
|
|
|
462
562
|
export function isTimestampStaleForExchangeSession(
|
|
@@ -24,6 +24,9 @@ export interface ActiveQuoteDisplay {
|
|
|
24
24
|
|
|
25
25
|
export type ExtendedSession = "PRE" | "POST";
|
|
26
26
|
|
|
27
|
+
/** What reports call each extended session's line or column. */
|
|
28
|
+
export const EXTENDED_SESSION_LABELS: Record<ExtendedSession, string> = { PRE: "Pre-Market", POST: "After Hours" };
|
|
29
|
+
|
|
27
30
|
export interface ExtendedSessionDisplay extends ActiveQuoteDisplay {
|
|
28
31
|
session: ExtendedSession;
|
|
29
32
|
}
|
|
@@ -278,11 +281,48 @@ export function getExtendedSessionDisplay(quote: Quote | null | undefined): Exte
|
|
|
278
281
|
return { session: "POST", price, change, changePercent: (change / close) * 100 };
|
|
279
282
|
}
|
|
280
283
|
|
|
284
|
+
/**
|
|
285
|
+
* A quote's headline and extended print as flat figures, for a report row or
|
|
286
|
+
* an export: price, change and changePercent are the regular session's
|
|
287
|
+
* (getRegularSessionDisplay), the extended fields the pre-market or
|
|
288
|
+
* after-hours print measured from its close (getExtendedSessionDisplay), all
|
|
289
|
+
* null when there is none.
|
|
290
|
+
*/
|
|
291
|
+
export function getQuoteSessionFields(quote: Quote | null | undefined) {
|
|
292
|
+
const headline = getRegularSessionDisplay(quote);
|
|
293
|
+
const extended = getExtendedSessionDisplay(quote);
|
|
294
|
+
return {
|
|
295
|
+
price: headline?.price ?? null,
|
|
296
|
+
change: headline?.change ?? null,
|
|
297
|
+
changePercent: headline?.changePercent ?? null,
|
|
298
|
+
extendedSession: extended?.session ?? null,
|
|
299
|
+
extendedPrice: extended?.price ?? null,
|
|
300
|
+
extendedChange: extended?.change ?? null,
|
|
301
|
+
extendedChangePercent: extended?.changePercent ?? null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
/** The extended display only while its session is open: the pre-market or the after-hours session. */
|
|
282
306
|
function getOpenExtendedSessionDisplay(quote: Quote | null | undefined): ExtendedSessionDisplay | null {
|
|
283
307
|
return quote?.marketState === "PRE" || quote?.marketState === "POST" ? getExtendedSessionDisplay(quote) : null;
|
|
284
308
|
}
|
|
285
309
|
|
|
310
|
+
/**
|
|
311
|
+
* The regular session alone, for a board that never shows extended trading:
|
|
312
|
+
* the live quote while the session trades, then the completed session's close
|
|
313
|
+
* and its move (getCompletedRegularSessionDisplay). Null outside the session
|
|
314
|
+
* when the quote cannot say which close that is, rather than an extended
|
|
315
|
+
* print or a flat move standing in for one. A quote that does not say its
|
|
316
|
+
* session is read as the regular one, as getRegularSessionDisplay does.
|
|
317
|
+
*/
|
|
318
|
+
export function getRegularSessionMoveDisplay(quote: Quote | null | undefined): ActiveQuoteDisplay | null {
|
|
319
|
+
if (!quote) return null;
|
|
320
|
+
if (quote.marketState == null || quote.marketState === "REGULAR") {
|
|
321
|
+
return { price: quote.price, change: quote.change, changePercent: quote.changePercent };
|
|
322
|
+
}
|
|
323
|
+
return getCompletedRegularSessionDisplay(quote);
|
|
324
|
+
}
|
|
325
|
+
|
|
286
326
|
/**
|
|
287
327
|
* The move a board of tiles colors by: the open pre-market or after-hours
|
|
288
328
|
* session's, from the regular close, otherwise the day's regular session.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseFuturesGeneric } from "../utils/futures-generic";
|
|
2
|
+
|
|
3
|
+
/** A US-listed fund whose options track an instrument that has none of its own. */
|
|
4
|
+
export interface ListedOptionsAlternative {
|
|
5
|
+
symbol: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Futures roots with no option chain here, and the listed fund whose options
|
|
11
|
+
* are the nearest stand-in. One table for every surface that says so.
|
|
12
|
+
*/
|
|
13
|
+
const FUTURES_ALTERNATIVES: Readonly<Record<string, ListedOptionsAlternative>> = {
|
|
14
|
+
GC: { symbol: "GLD", name: "SPDR Gold Shares" },
|
|
15
|
+
SI: { symbol: "SLV", name: "iShares Silver Trust" },
|
|
16
|
+
HG: { symbol: "CPER", name: "United States Copper Index Fund" },
|
|
17
|
+
PL: { symbol: "PPLT", name: "abrdn Physical Platinum Shares" },
|
|
18
|
+
PA: { symbol: "PALL", name: "abrdn Physical Palladium Shares" },
|
|
19
|
+
CL: { symbol: "USO", name: "United States Oil Fund" },
|
|
20
|
+
BZ: { symbol: "BNO", name: "United States Brent Oil Fund" },
|
|
21
|
+
NG: { symbol: "UNG", name: "United States Natural Gas Fund" },
|
|
22
|
+
LTH: { symbol: "LIT", name: "Global X Lithium & Battery Tech ETF" },
|
|
23
|
+
UX: { symbol: "URA", name: "Global X Uranium ETF" },
|
|
24
|
+
ZC: { symbol: "CORN", name: "Teucrium Corn Fund" },
|
|
25
|
+
ZW: { symbol: "WEAT", name: "Teucrium Wheat Fund" },
|
|
26
|
+
ZS: { symbol: "SOYB", name: "Teucrium Soybean Fund" },
|
|
27
|
+
ES: { symbol: "SPY", name: "SPDR S&P 500 ETF" },
|
|
28
|
+
NQ: { symbol: "QQQ", name: "Invesco QQQ Trust" },
|
|
29
|
+
YM: { symbol: "DIA", name: "SPDR Dow Jones Industrial Average ETF" },
|
|
30
|
+
RTY: { symbol: "IWM", name: "iShares Russell 2000 ETF" },
|
|
31
|
+
ZB: { symbol: "TLT", name: "iShares 20+ Year Treasury Bond ETF" },
|
|
32
|
+
ZN: { symbol: "IEF", name: "iShares 7-10 Year Treasury Bond ETF" },
|
|
33
|
+
"6E": { symbol: "FXE", name: "Invesco CurrencyShares Euro Trust" },
|
|
34
|
+
"6J": { symbol: "FXY", name: "Invesco CurrencyShares Japanese Yen Trust" },
|
|
35
|
+
BTC: { symbol: "IBIT", name: "iShares Bitcoin Trust" },
|
|
36
|
+
ETH: { symbol: "ETHA", name: "iShares Ethereum Trust" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const MONTH_CODE_CONTRACT = /^([A-Z0-9]{1,3}?)[FGHJKMNQUVXZ]\d{1,4}$/;
|
|
40
|
+
|
|
41
|
+
/** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. */
|
|
42
|
+
function futuresRoot(symbol: string): string | null {
|
|
43
|
+
const ticker = symbol.trim().toUpperCase().replace(/[.:][A-Z]+$/, "");
|
|
44
|
+
if (ticker.endsWith("=F")) {
|
|
45
|
+
const body = ticker.slice(0, -2);
|
|
46
|
+
return FUTURES_ALTERNATIVES[body] ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
|
|
47
|
+
}
|
|
48
|
+
const generic = parseFuturesGeneric(ticker);
|
|
49
|
+
if (generic) return generic.root;
|
|
50
|
+
const contract = MONTH_CODE_CONTRACT.exec(ticker)?.[1];
|
|
51
|
+
return contract && FUTURES_ALTERNATIVES[contract] ? contract : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The listed fund whose options stand in for an instrument with no chain, if there is one. */
|
|
55
|
+
export function listedOptionsAlternative(symbol: string): ListedOptionsAlternative | null {
|
|
56
|
+
const root = futuresRoot(symbol);
|
|
57
|
+
return root ? FUTURES_ALTERNATIVES[root] ?? null : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether an options request failed because nothing lists options on the instrument. */
|
|
61
|
+
export function isOptionsUnavailableError(error: unknown): boolean {
|
|
62
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
63
|
+
return /\boptions provider available for\b/i.test(message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** "Options are not available for GC=F.": the dead end itself, before any follow-up. */
|
|
67
|
+
export function optionsUnavailableTitle(symbol: string): string {
|
|
68
|
+
return `Options are not available for ${symbol.trim().toUpperCase()}.`;
|
|
69
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { resolvePriceBasis } from "../market/price-basis";
|
|
2
|
+
import { isAlwaysOpenExchange } from "../market/freshness";
|
|
2
3
|
import type {
|
|
4
|
+
MarketState,
|
|
3
5
|
Quote,
|
|
4
6
|
QuoteContribution,
|
|
5
7
|
QuoteContributionMap,
|
|
6
8
|
SessionConfidence,
|
|
7
9
|
TickerFinancials,
|
|
8
10
|
} from "../../types/financials";
|
|
11
|
+
import { classifyInstrumentType } from "../../tickers/instrument-kind";
|
|
9
12
|
import { quoteTradingDay, reconcileQuoteDayRange } from "./day-range";
|
|
10
13
|
|
|
11
14
|
const RETAINED_DESCRIPTIVE_FIELDS = ["high52w", "low52w", "marketCap", "name", "instrumentType"] as const;
|
|
@@ -48,6 +51,17 @@ function inferSessionConfidence(
|
|
|
48
51
|
return "unknown";
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* A CCC coin trades through the equity close. The live tape often leaves the
|
|
56
|
+
* session off, and a copied equity close would paint the coin shut.
|
|
57
|
+
*/
|
|
58
|
+
function alwaysOpenSession(quote: Pick<Quote, "listingExchangeName" | "exchangeName" | "instrumentType" | "marketState">): { marketState: MarketState; sessionConfidence: SessionConfidence } | null {
|
|
59
|
+
const open = isAlwaysOpenExchange(quote.listingExchangeName ?? quote.exchangeName)
|
|
60
|
+
|| classifyInstrumentType(quote.instrumentType) === "crypto";
|
|
61
|
+
if (!open || quote.marketState === "REGULAR") return null;
|
|
62
|
+
return { marketState: "REGULAR", sessionConfidence: "derived" };
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
function normalizeListingExchange(
|
|
52
66
|
quote: Quote | QuoteContribution,
|
|
53
67
|
providerId: string,
|
|
@@ -128,6 +142,8 @@ export function normalizeQuoteContribution(
|
|
|
128
142
|
const { listingExchangeName, listingExchangeFullName } = normalizeListingExchange(quote, providerId);
|
|
129
143
|
const routingExchangeName = quote.routingExchangeName;
|
|
130
144
|
const routingExchangeFullName = quote.routingExchangeFullName ?? routingExchangeName;
|
|
145
|
+
const exchangeName = listingExchangeName ?? quote.exchangeName;
|
|
146
|
+
const session = alwaysOpenSession({ ...quote, listingExchangeName, exchangeName });
|
|
131
147
|
|
|
132
148
|
return finalizeSessionFields(reconcileQuoteDayRange({
|
|
133
149
|
...quote,
|
|
@@ -136,9 +152,10 @@ export function normalizeQuoteContribution(
|
|
|
136
152
|
listingExchangeFullName,
|
|
137
153
|
routingExchangeName,
|
|
138
154
|
routingExchangeFullName,
|
|
139
|
-
exchangeName
|
|
155
|
+
exchangeName,
|
|
140
156
|
fullExchangeName: listingExchangeFullName ?? quote.fullExchangeName,
|
|
141
|
-
|
|
157
|
+
...session,
|
|
158
|
+
sessionConfidence: session?.sessionConfidence ?? inferSessionConfidence(quote, providerId),
|
|
142
159
|
}));
|
|
143
160
|
}
|
|
144
161
|
|
|
@@ -38,6 +38,18 @@ function metadataExchange(value?: string): string {
|
|
|
38
38
|
return exchange === "NY MERCANTILE" ? "NYMEX" : exchange;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The pair an FX symbol names, base then quote: EURUSD for EUR/USD and
|
|
43
|
+
* EURUSD=X, USDJPY for USD/JPY, USDJPY=X and JPY=X, the dollar pair's short
|
|
44
|
+
* spelling.
|
|
45
|
+
*/
|
|
46
|
+
function currencyPairCode(symbol: string): string | null {
|
|
47
|
+
const pair = /^([A-Z]{3})\/([A-Z]{3})$/.exec(symbol) ?? /^([A-Z]{3})([A-Z]{3})=X$/.exec(symbol);
|
|
48
|
+
if (pair) return `${pair[1]}${pair[2]}`;
|
|
49
|
+
const dollarPair = /^([A-Z]{3})=X$/.exec(symbol);
|
|
50
|
+
return dollarPair ? `USD${dollarPair[1]}` : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/** Qualified listings must agree; provider suffix spellings use the existing exact-listing normalizer. */
|
|
42
54
|
export function quoteMetadataMatchesTarget(metadata: QuoteMetadata, symbol: string, exchange?: string): boolean {
|
|
43
55
|
const target = parsePublicTickerKey(symbol);
|
|
@@ -48,6 +60,10 @@ export function quoteMetadataMatchesTarget(metadata: QuoteMetadata, symbol: stri
|
|
|
48
60
|
if (symbolExchange && actualExchange && symbolExchange !== actualExchange) return false;
|
|
49
61
|
if (requestedExchange && actualExchange !== requestedExchange) return false;
|
|
50
62
|
if (target.symbol === actual.symbol) return true;
|
|
63
|
+
// Gloom Cloud answers a currency pair in one spelling however it was asked
|
|
64
|
+
// for: EUR/USD comes back as EURUSD=X on its currency venue.
|
|
65
|
+
const pair = actualExchange === "CCY" ? currencyPairCode(actual.symbol) : null;
|
|
66
|
+
if (pair && pair === currencyPairCode(target.symbol)) return true;
|
|
51
67
|
const listing = requestedExchange || actualExchange;
|
|
52
68
|
if (!listing) return false;
|
|
53
69
|
const candidates = getListingSymbolsToTry(target.symbol, listing, { exactExchange: true });
|