gloomberb 0.17.4 → 0.17.6
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 +3 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/chat.ts +9 -0
- package/src/api-client/commodity-balances.ts +77 -0
- package/src/api-client/data.ts +35 -3
- package/src/api-client/earnings.ts +10 -0
- package/src/api-client/index.ts +5 -0
- package/src/api-client/insider-signals.ts +75 -0
- package/src/api-client/market-discovery.ts +1 -1
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/paths.ts +9 -0
- package/src/api-client/research-activity.ts +10 -3
- package/src/api-client/socket.ts +10 -0
- package/src/api-client/types.ts +25 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/broker-import.ts +23 -21
- package/src/app/runtime/plugin-bindings.ts +13 -7
- package/src/app/runtime/update.ts +64 -4
- package/src/app.tsx +2 -0
- package/src/architecture/core-builtin-allowlist.json +2 -10
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/brokers/broker-portfolio-sync.ts +66 -0
- package/src/brokers/sync-broker-instance.ts +75 -41
- package/src/cli/commands/market.ts +101 -34
- package/src/cli/commands/options-chain.ts +20 -6
- package/src/cli/commands/overview.ts +54 -13
- package/src/cli/commands/search.ts +1 -1
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/commands/ticker.ts +24 -4
- package/src/cli/context.ts +13 -0
- package/src/cli/errors.ts +15 -2
- package/src/cli/fx-pair.ts +77 -0
- package/src/cli/glossary.ts +34 -4
- package/src/cli/history-rows.ts +25 -11
- package/src/cli/index.ts +30 -12
- package/src/cli/listing-arg.ts +61 -138
- package/src/cli/listing-suggestion.ts +3 -21
- package/src/cli/pane-functions/catalog.ts +50 -0
- package/src/cli/pane-functions/freshness.ts +44 -9
- package/src/cli/pane-functions/function-help.ts +9 -9
- package/src/cli/pane-functions/headless.ts +19 -7
- package/src/cli/pane-functions/index.ts +63 -5
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/cli/pane-functions/resolver.ts +29 -8
- package/src/cli/pane-functions/screenshot.ts +2 -0
- package/src/cli/report-tables.ts +11 -4
- package/src/cli/result.ts +2 -0
- package/src/cli/series-search.ts +6 -1
- package/src/cli/ticker-resolution.ts +1 -2
- package/src/cli/unknown-command.ts +18 -0
- package/src/components/chart/composite/composite-chart.tsx +80 -19
- package/src/components/chart/composite/format.ts +17 -0
- package/src/components/chart/composite/legend.tsx +26 -15
- package/src/components/chart/composite/panel-labels.ts +38 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +25 -7
- package/src/components/chart/composite/price-axis-labels.tsx +16 -3
- package/src/components/chart/composite/scene.ts +60 -2
- package/src/components/chart/composite/series-axis-tags.tsx +67 -0
- package/src/components/chart/composite/tools.ts +2 -2
- package/src/components/chart/composite/types.ts +28 -0
- package/src/components/chart/core/view-reset.ts +46 -0
- package/src/components/command-bar/commands/collection.ts +1 -1
- package/src/components/command-bar/commands/direct/index.ts +48 -1
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/list/model.ts +7 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +17 -2
- package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
- package/src/components/command-bar/routes/root/provider-search.ts +6 -0
- package/src/components/command-bar/routes/root/runtime.ts +3 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
- package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
- package/src/components/command-bar/surface/index.tsx +7 -1
- package/src/components/command-bar/workflow/collection-actions.ts +6 -5
- package/src/components/command-bar/workflow/ops.ts +8 -5
- package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
- package/src/components/command-bar/workflow/tickers.ts +1 -1
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/content.tsx +11 -8
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/onboarding/wizard-model.ts +1 -1
- package/src/components/onboarding/wizard-positions.ts +18 -10
- package/src/components/ticker/badge/format.ts +64 -15
- package/src/components/ticker/badge/index.tsx +32 -11
- package/src/components/ticker/badge/list.tsx +4 -3
- package/src/components/ticker/badge/text.tsx +2 -2
- package/src/components/ticker/list-table-view.tsx +8 -1
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/data-table/opentui/index.tsx +61 -20
- package/src/components/ui/data-table/opentui/model.ts +104 -0
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/inline-quick-add.tsx +4 -1
- package/src/components/ui/query-bar-keys.tsx +29 -5
- package/src/components/ui/query-bar.tsx +29 -4
- package/src/components/ui/windowed-list.tsx +27 -0
- package/src/data/config/store/normalize.ts +8 -0
- package/src/docs/snapshot.json +248 -5
- package/src/i18n/es.ts +3 -1
- package/src/i18n/ja.ts +3 -1
- package/src/i18n/ko.ts +3 -1
- package/src/i18n/zh-cn.ts +3 -1
- package/src/i18n/zh-tw.ts +3 -1
- package/src/layout-marketplace/cloud.ts +16 -5
- package/src/layout-marketplace/gallery.tsx +4 -4
- package/src/layout-marketplace/linked-sync.ts +9 -3
- package/src/layout-marketplace/linked.ts +24 -3
- package/src/layout-marketplace/model.ts +39 -4
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +38 -1
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/futures-contract.ts +53 -0
- package/src/market-data/hooks.tsx +225 -30
- package/src/market-data/market/bar-period.ts +71 -0
- package/src/market-data/market/format.ts +14 -2
- package/src/market-data/market/freshness.ts +28 -1
- package/src/market-data/market/status.ts +152 -13
- package/src/market-data/model-dividend-yield.ts +76 -0
- package/src/market-data/quotes/freshness.ts +30 -1
- package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/plugins/builtin/analytics/metrics.ts +8 -4
- package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
- package/src/plugins/builtin/analytics/risk-model.ts +74 -15
- package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
- package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
- package/src/plugins/builtin/chart-composer/formula.ts +233 -0
- package/src/plugins/builtin/chart-composer/headless.ts +27 -2
- package/src/plugins/builtin/chart-composer/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +41 -10
- package/src/plugins/builtin/chart-composer/presets.ts +91 -2
- package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
- package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
- package/src/plugins/builtin/chart-composer/settings.ts +16 -4
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/content/index.tsx +12 -5
- package/src/plugins/builtin/chat/content/mentions.ts +50 -21
- package/src/plugins/builtin/chat/content/scroll.ts +12 -5
- package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
- package/src/plugins/builtin/chat/controller/index.ts +88 -17
- package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
- package/src/plugins/builtin/chat/controller/state.ts +25 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +4 -0
- package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
- package/src/plugins/builtin/chat/conversations.ts +12 -5
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/layout.ts +23 -5
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
- package/src/plugins/builtin/chat/sidebar.tsx +6 -1
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
- package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
- package/src/plugins/builtin/commodity-balances/client.ts +51 -193
- package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
- package/src/plugins/builtin/commodity-balances/model.ts +5 -180
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
- package/src/plugins/builtin/custom-view/index.tsx +1 -1
- package/src/plugins/builtin/custom-view/loader.ts +57 -7
- package/src/plugins/builtin/custom-view/sources.ts +146 -0
- package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
- package/src/plugins/builtin/earnings/board-model.ts +33 -3
- package/src/plugins/builtin/earnings/board-table.ts +59 -14
- package/src/plugins/builtin/earnings/board.tsx +36 -3
- package/src/plugins/builtin/earnings/headless.ts +24 -5
- package/src/plugins/builtin/earnings/history-model.ts +9 -1
- package/src/plugins/builtin/earnings/history-table.ts +16 -5
- package/src/plugins/builtin/earnings/history.tsx +40 -10
- package/src/plugins/builtin/earnings/index.tsx +3 -1
- package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
- package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +4 -0
- package/src/plugins/builtin/futures/model.ts +5 -4
- package/src/plugins/builtin/futures/series.ts +38 -1
- package/src/plugins/builtin/futures-curve/client.ts +4 -4
- package/src/plugins/builtin/futures-curve/headless.ts +33 -8
- package/src/plugins/builtin/futures-curve/index.tsx +4 -4
- package/src/plugins/builtin/futures-curve/model.ts +109 -6
- package/src/plugins/builtin/futures-curve/pane.tsx +24 -13
- package/src/plugins/builtin/insider/headless.ts +74 -4
- package/src/plugins/builtin/insider/index.tsx +99 -5
- package/src/plugins/builtin/insider/signals-client.ts +69 -0
- package/src/plugins/builtin/insider/signals-model.ts +146 -0
- package/src/plugins/builtin/insider/signals.tsx +176 -0
- package/src/plugins/builtin/iv-history/client.ts +41 -2
- package/src/plugins/builtin/iv-history/headless.ts +24 -16
- package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
- package/src/plugins/builtin/jodi/client.ts +42 -205
- package/src/plugins/builtin/jodi/headless.ts +13 -4
- package/src/plugins/builtin/jodi/model.ts +18 -43
- package/src/plugins/builtin/jodi/pane.tsx +5 -0
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/market-movers/client.ts +32 -6
- package/src/plugins/builtin/market-movers/headless.ts +7 -2
- package/src/plugins/builtin/market-movers/model.ts +64 -3
- package/src/plugins/builtin/market-movers/screener.ts +20 -9
- package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
- package/src/plugins/builtin/market-valuation/headless.ts +17 -4
- package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
- package/src/plugins/builtin/market-valuation/view.ts +10 -0
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/options-positioning/client.ts +10 -0
- package/src/plugins/builtin/options-positioning/headless.ts +88 -13
- package/src/plugins/builtin/options-positioning/index.tsx +2 -2
- package/src/plugins/builtin/options-positioning/model.ts +47 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +55 -26
- package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
- package/src/plugins/builtin/options-scenario/client.ts +111 -17
- package/src/plugins/builtin/options-scenario/collar.ts +141 -0
- package/src/plugins/builtin/options-scenario/csp.ts +1 -1
- package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
- package/src/plugins/builtin/options-scenario/headless.ts +28 -12
- package/src/plugins/builtin/options-scenario/index.tsx +1 -1
- package/src/plugins/builtin/options-scenario/model.ts +55 -5
- package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
- package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
- package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
- package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
- package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
- package/src/plugins/builtin/research/event-model.ts +41 -49
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
- package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
- package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/shared/earnings-report.ts +48 -1
- package/src/plugins/builtin/shared/history-span.ts +55 -0
- package/src/plugins/builtin/shared/report-freshness.ts +20 -11
- package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
- package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
- package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
- package/src/plugins/builtin/themes/headless.ts +15 -2
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
- package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
- package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
- package/src/portfolio/valuation-basis.ts +4 -21
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/remote/controller.ts +27 -7
- package/src/remote/schema.ts +25 -9
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/cloudflare/http-proxy.ts +21 -13
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/desktop/query-bar.tsx +1 -1
- package/src/renderers/dom/dom-ui-host.tsx +2 -0
- package/src/renderers/dom/windowed-list.tsx +201 -0
- package/src/renderers/dom/windowed-rows.ts +65 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -7
- package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
- package/src/renderers/electrobun/bun/desktop/update.ts +132 -14
- package/src/renderers/electrobun/shared/protocol.ts +7 -4
- package/src/renderers/electrobun/view/http-fetch.ts +12 -1
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/sources/listing-symbols.ts +8 -4
- package/src/state/app/context/index.tsx +13 -0
- package/src/state/app/tab-cursor.ts +85 -0
- package/src/sync/core-contributors.ts +206 -59
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/test-support/config-store.ts +31 -0
- package/src/test-support/moved-symbols.ts +64 -0
- package/src/theme/colors.ts +30 -0
- package/src/tickers/listing-fate.ts +242 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/tickers/search/index.ts +45 -3
- package/src/tickers/search/ranking.ts +19 -2
- package/src/tickers/search/types.ts +3 -0
- package/src/tickers/search/upsert.ts +22 -7
- package/src/tickers/ticker-changes.ts +26 -9
- package/src/time-series/bar-spacing.ts +19 -0
- package/src/time-series/chart-data.ts +37 -13
- package/src/time-series/field-catalog.ts +5 -2
- package/src/time-series/forward-valuation.ts +10 -7
- package/src/time-series/fundamentals.ts +49 -11
- package/src/time-series/hooks.ts +2 -0
- package/src/time-series/live-bars.ts +3 -1
- package/src/time-series/parsed-history-cache.ts +2 -0
- package/src/time-series/price-units.ts +69 -0
- package/src/time-series/resolve.ts +71 -11
- package/src/time-series/session-bar.ts +138 -0
- package/src/time-series/spec.ts +20 -3
- package/src/time-series/studies.ts +211 -12
- package/src/time-series/transforms.ts +2 -0
- package/src/time-series/types.ts +11 -1
- package/src/time-series/valuation-price.ts +13 -0
- package/src/types/config.ts +24 -1
- package/src/types/financials.ts +2 -0
- package/src/types/headless.ts +27 -0
- package/src/types/plugin.ts +7 -0
- package/src/ui/host.tsx +5 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +128 -17
- package/src/utils/asset-category.ts +20 -0
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/decimal-comma.ts +83 -3
- package/src/utils/fundamentals.ts +7 -2
- package/src/utils/http-proxy-response.ts +66 -9
- package/src/utils/plugin-proxy-hosts.json +1 -5
- package/src/utils/semver.ts +47 -2
- package/src/utils/typo-distance.ts +22 -0
- package/src/plugins/builtin/shared/zip-text.ts +0 -46
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { DataProvider } from "../types/data-provider";
|
|
2
|
+
import type { Quote } from "../types/financials";
|
|
3
|
+
import type { TickerChanges } from "../types/ticker-changes";
|
|
4
|
+
import {
|
|
5
|
+
describeTickerChange,
|
|
6
|
+
movedAwayChange,
|
|
7
|
+
quotedListing,
|
|
8
|
+
startTickerChangeLookup,
|
|
9
|
+
tickerChangeLine,
|
|
10
|
+
type CurrentListing,
|
|
11
|
+
type MovedTickerChange,
|
|
12
|
+
type TickerChange,
|
|
13
|
+
type TickerChangeLookup,
|
|
14
|
+
} from "./ticker-changes";
|
|
15
|
+
import { isQuoteBeforeLatestSession, isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
|
|
16
|
+
import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
|
|
17
|
+
import { canonicalExchange, isUsListingExchange, parsePublicTickerKey, resolveExchangeTimeZone } from "../utils/exchanges";
|
|
18
|
+
import { zonedDateKey } from "../utils/zoned-date-time";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* What became of a symbol, read the way `quote` reads it: the listing's own
|
|
22
|
+
* quote says whether and when it stopped trading, and the symbol's ticker
|
|
23
|
+
* changes say whether its company moved. The CLI commands and the app's panes
|
|
24
|
+
* both decide from here what a symbol has to serve.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// A quote this much older than now, and stale, is a listing that stopped trading, not a quiet day.
|
|
28
|
+
const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The exchange-local date a listing last traded, when its quote is stale and
|
|
32
|
+
* that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
|
|
33
|
+
*/
|
|
34
|
+
function endedTradingDate(quote: Quote, now = Date.now()): string | null {
|
|
35
|
+
const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
|
|
36
|
+
if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
|
|
37
|
+
const zone = resolveExchangeTimeZone(quote.listingExchangeName || quote.exchangeName) ?? "UTC";
|
|
38
|
+
return zonedDateKey(last, zone);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What trades under a symbol now, from its quote: nothing when it has none or stopped long ago. */
|
|
42
|
+
export function tradingListing(quote: Quote | null | undefined, now = Date.now()): CurrentListing | null {
|
|
43
|
+
return quote && !endedTradingDate(quote, now) ? quotedListing(quote) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote that stopped long ago. */
|
|
47
|
+
export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
|
|
48
|
+
const date = endedTradingDate(quote, now);
|
|
49
|
+
if (!date) return null;
|
|
50
|
+
const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
|
|
51
|
+
return `${label}: no longer trading since ${date} (last price ${price})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What became of a symbol, from what `quote` reads it by: the listing's own
|
|
56
|
+
* quote, which says when it stopped trading, and the symbol's ticker changes
|
|
57
|
+
* beside that quote (FB became META).
|
|
58
|
+
*/
|
|
59
|
+
export interface ListingFate {
|
|
60
|
+
/** The exchange-local date the listing last traded, when its quote stopped long ago (TWTR's 2022-10-27). */
|
|
61
|
+
endedOn: string | null;
|
|
62
|
+
/** The symbol's change as `quote` reads it; null when it has none, or was read on a listing abroad. */
|
|
63
|
+
change: TickerChange | null;
|
|
64
|
+
/**
|
|
65
|
+
* The line a command with nothing to serve for the symbol says: `quote`'s
|
|
66
|
+
* change line when its company left it ("FB now lists ...; Meta Platforms
|
|
67
|
+
* trades as META (changed 2022-06-09)"), else, for a listing that stopped,
|
|
68
|
+
* "TWTR stopped trading 2022-10-27; no history or options served". Null
|
|
69
|
+
* for a symbol that trades as it always has.
|
|
70
|
+
*/
|
|
71
|
+
note: string | null;
|
|
72
|
+
/**
|
|
73
|
+
* The change when the symbol's company trades under another symbol now
|
|
74
|
+
* and its own quote shows nothing new listed under it (`movedAwayChange`):
|
|
75
|
+
* the old line, stopped or named for that company. Whatever data comes
|
|
76
|
+
* back under the symbol is then another security's (a listing abroad with
|
|
77
|
+
* the same letters, or the new symbol's bars under the old one), so a
|
|
78
|
+
* command or pane serves none and names the new symbol. Null without a
|
|
79
|
+
* quote to tell by.
|
|
80
|
+
*/
|
|
81
|
+
moved: MovedTickerChange | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Nothing became of the symbol, or nothing could be told. */
|
|
85
|
+
export const NO_FATE: ListingFate = { endedOn: null, change: null, note: null, moved: null };
|
|
86
|
+
/** How long a fate waits for a change lookup it started late, or for a stopped line's. */
|
|
87
|
+
export const LATE_CHANGE_WAIT_MS = 2_000;
|
|
88
|
+
|
|
89
|
+
// The zones of the US exchanges; a venue in another is abroad.
|
|
90
|
+
const US_TIME_ZONES = new Set(["America/New_York", "America/Chicago"]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* "SXR8 -> XETRA" for a bare symbol the data service read on its home listing
|
|
94
|
+
* abroad, as it reads a symbol with no US listing whose listings belong to one
|
|
95
|
+
* company or fund: a row that shows the symbol as typed says which listing it
|
|
96
|
+
* is. Null for a named or saved venue, and for a venue the app does not place
|
|
97
|
+
* outside the US (a US exchange, OTC, a futures exchange).
|
|
98
|
+
*/
|
|
99
|
+
export function bareListingNote(
|
|
100
|
+
listing: { symbol: string; request: { exchange: string } },
|
|
101
|
+
quote: Pick<Quote, "listingExchangeName" | "exchangeName"> | null | undefined,
|
|
102
|
+
): string | null {
|
|
103
|
+
if (listing.request.exchange || !quote || !/^[A-Z0-9]{1,10}$/.test(listing.symbol)) return null;
|
|
104
|
+
const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
105
|
+
const zone = resolveExchangeTimeZone(venue);
|
|
106
|
+
return zone && !US_TIME_ZONES.has(zone) && !isUsListingExchange(venue) ? `${listing.symbol} -> ${venue}` : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `ListingFate` from a listing's quote (null when it has none) and its ticker changes. */
|
|
110
|
+
export function describeListingFate(
|
|
111
|
+
listing: { symbol: string; exchange?: string | null },
|
|
112
|
+
quote: Quote | null,
|
|
113
|
+
changes: TickerChanges | null,
|
|
114
|
+
now = Date.now(),
|
|
115
|
+
): ListingFate {
|
|
116
|
+
const symbol = listing.symbol.trim().toUpperCase();
|
|
117
|
+
const endedOn = quote ? endedTradingDate(quote, now) : null;
|
|
118
|
+
// A bare symbol read on a listing abroad (SXR8 -> XETRA) is not the US symbol the changes are of.
|
|
119
|
+
const abroad = !!quote && !!bareListingNote({ symbol, request: { exchange: listing.exchange ?? "" } }, quote);
|
|
120
|
+
const change = abroad ? null : describeTickerChange(symbol, changes, tradingListing(quote, now));
|
|
121
|
+
const note = tickerChangeLine(change) ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
|
|
122
|
+
return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether a quote's line stopped: a whole session traded without it, or it
|
|
127
|
+
* ended long ago. Only such a line can be a company that moved away, so only
|
|
128
|
+
* its change lookup is waited for.
|
|
129
|
+
*/
|
|
130
|
+
function mayHaveMoved(quote: Quote | null, now = Date.now()): boolean {
|
|
131
|
+
return !!quote && (isQuoteBeforeLatestSession(quote, now) || endedTradingDate(quote, now) != null);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `describeListingFate` for a listing: its quote is `quote` when the command
|
|
136
|
+
* has it, else loaded; its changes come from `changes` when the command started
|
|
137
|
+
* that lookup beside its own request, else from one started here. Fails soft:
|
|
138
|
+
* a source that does not answer says nothing became of it.
|
|
139
|
+
*/
|
|
140
|
+
export async function loadListingFate(
|
|
141
|
+
listing: { symbol: string; exchange?: string | null; request?: { symbol: string; exchange: string } },
|
|
142
|
+
dataProvider: Pick<DataProvider, "getQuote" | "getTickerChanges">,
|
|
143
|
+
options: { quote?: Quote | null | Promise<Quote | null>; changes?: TickerChangeLookup } = {},
|
|
144
|
+
): Promise<ListingFate> {
|
|
145
|
+
const request = listing.request ?? { symbol: listing.symbol, exchange: listing.exchange ?? "" };
|
|
146
|
+
const lookup = options.changes ?? startTickerChangeLookup(dataProvider, listing.symbol, listing.exchange);
|
|
147
|
+
try {
|
|
148
|
+
const quote = await (options.quote !== undefined
|
|
149
|
+
? options.quote
|
|
150
|
+
: dataProvider.getQuote(request.symbol, request.exchange).catch(() => null));
|
|
151
|
+
// A lookup started beside the command's request gets a short grace once that is ready, so a slow
|
|
152
|
+
// one never holds a symbol that trades. A line that stopped is waited for: only its company can
|
|
153
|
+
// have moved, and then the answer decides whether the command serves anything at all.
|
|
154
|
+
const changes = await lookup.settle(!options.changes || mayHaveMoved(quote) ? LATE_CHANGE_WAIT_MS : undefined);
|
|
155
|
+
return describeListingFate(listing, quote, changes);
|
|
156
|
+
} catch {
|
|
157
|
+
return NO_FATE;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The lines a command says about a symbol's fate beside what it served. With
|
|
163
|
+
* nothing served, `fate.note`. With data, `quote`'s change line when the
|
|
164
|
+
* symbol's company left it (FB's bars are the fund listed there now), and, for
|
|
165
|
+
* bars from before the symbol's holder took it (META's from 2021), that they
|
|
166
|
+
* are from its old symbol: "Includes FB-era bars: META was FB until 2022-06-09".
|
|
167
|
+
*/
|
|
168
|
+
export function listingFateNotes(fate: ListingFate, served: { served: boolean; firstDate?: string | null }): string[] {
|
|
169
|
+
if (!served.served) return fate.note ? [fate.note] : [];
|
|
170
|
+
const { change } = fate;
|
|
171
|
+
const first = served.firstDate?.slice(0, 10);
|
|
172
|
+
const earlier = change?.kind === "formerly" && first ? change.changes.filter((hop) => first < hop.date).map((hop) => hop.from) : [];
|
|
173
|
+
return [
|
|
174
|
+
tickerChangeLine(change),
|
|
175
|
+
earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
|
|
176
|
+
].filter((line): line is string => !!line);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* What a report or loader with nothing of its own to serve for a symbol
|
|
181
|
+
* throws when the symbol's company moved (`ListingFate.moved`): its message
|
|
182
|
+
* is the change's line, "HFC became DINO on 2022-03-15".
|
|
183
|
+
*/
|
|
184
|
+
export class MovedSymbolError extends Error {
|
|
185
|
+
constructor(readonly change: MovedTickerChange) {
|
|
186
|
+
super(change.note);
|
|
187
|
+
this.name = "MovedSymbolError";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The listing an instrument's fate is read for: its symbol without the venue
|
|
193
|
+
* a key carries (HFC:NYSE is HFC on NYSE). Null for a broker's contract,
|
|
194
|
+
* which is not a listing the change history is about.
|
|
195
|
+
*/
|
|
196
|
+
export function fateListing(instrument: {
|
|
197
|
+
symbol: string;
|
|
198
|
+
exchange?: string | null;
|
|
199
|
+
brokerId?: string;
|
|
200
|
+
instrument?: unknown;
|
|
201
|
+
}): { symbol: string; exchange: string } | null {
|
|
202
|
+
if (instrument.brokerId || instrument.instrument) return null;
|
|
203
|
+
const key = parsePublicTickerKey(instrument.symbol);
|
|
204
|
+
return { symbol: key.symbol, exchange: key.exchange ?? instrument.exchange ?? "" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* What a pane does with a symbol: serve it, wait to know, or show that its
|
|
209
|
+
* company moved (`ListingFate.moved`) instead of another security's data.
|
|
210
|
+
* A symbol waits only while what is known says nothing trades under it: a
|
|
211
|
+
* quote that shows the line ended waits for the change lookup, and a change
|
|
212
|
+
* that says the company left with nothing known listed there waits for the
|
|
213
|
+
* quote. Any other symbol is served at once, whatever is still coming, a
|
|
214
|
+
* quote cached from an earlier session included. A lookup that failed or
|
|
215
|
+
* answered nothing is no change.
|
|
216
|
+
*/
|
|
217
|
+
export type ListingFateGate =
|
|
218
|
+
| { kind: "serve" }
|
|
219
|
+
| { kind: "wait" }
|
|
220
|
+
| { kind: "moved"; change: MovedTickerChange };
|
|
221
|
+
|
|
222
|
+
const SERVE: ListingFateGate = { kind: "serve" };
|
|
223
|
+
const WAIT: ListingFateGate = { kind: "wait" };
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `ListingFateGate` from what is known so far. `quote` and `changes` are
|
|
227
|
+
* undefined while they load and null when there is none. A move on record
|
|
228
|
+
* is told by the quote, which says whether the old line stopped or something
|
|
229
|
+
* new trades there (FB's fund); until it is in, the change's own word on
|
|
230
|
+
* what lists there now decides whether to wait for it.
|
|
231
|
+
*/
|
|
232
|
+
export function listingFateGate(
|
|
233
|
+
listing: { symbol: string; exchange?: string | null },
|
|
234
|
+
{ quote, changes }: { quote: Quote | null | undefined; changes: TickerChanges | null | undefined },
|
|
235
|
+
now = Date.now(),
|
|
236
|
+
): ListingFateGate {
|
|
237
|
+
if (changes === undefined) return quote && endedTradingDate(quote, now) ? WAIT : SERVE;
|
|
238
|
+
if (!changes?.moved) return SERVE;
|
|
239
|
+
if (quote === undefined) return movedAwayChange(describeTickerChange(listing.symbol, changes)) ? WAIT : SERVE;
|
|
240
|
+
const { moved } = describeListingFate(listing, quote, changes, now);
|
|
241
|
+
return moved ? { kind: "moved", change: moved } : SERVE;
|
|
242
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
2
2
|
import type { SearchRequestContext, DataProvider } from "../types/data-provider";
|
|
3
|
+
import type { Quote } from "../types/financials";
|
|
3
4
|
import type { TickerRecord } from "../types/ticker";
|
|
4
5
|
import {
|
|
5
6
|
AmbiguousTickerError,
|
|
@@ -17,6 +18,15 @@ import { settleTypedListingKey } from "./listing-venues";
|
|
|
17
18
|
|
|
18
19
|
export type { TickerOpenTarget } from "./search/types";
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What an open that found nothing says: neither the key's own quote nor the
|
|
23
|
+
* listing search found the listing. Opens from a board row, a link and the
|
|
24
|
+
* command bar all say it the same way.
|
|
25
|
+
*/
|
|
26
|
+
export function tickerOpenFailureMessage(symbol: string): string {
|
|
27
|
+
return `Could not open ${symbol}: no quote or listing found.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export async function resolveTickerOpenTarget({
|
|
21
31
|
query,
|
|
22
32
|
tickers,
|
|
@@ -85,18 +95,22 @@ export async function resolveTickerOpenTarget({
|
|
|
85
95
|
return { symbol: ticker.metadata.ticker, ticker, created, ...tickerSelectionFromSearchResult(resolved.result) };
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
let quote: Quote | null = null;
|
|
99
|
+
try {
|
|
100
|
+
quote = await dataProvider.getQuote(symbol, "", publicOnly ? { instrument: null } : undefined);
|
|
101
|
+
} catch {
|
|
102
|
+
// No source has a price for the key right now.
|
|
103
|
+
}
|
|
104
|
+
const quoteExchange = quote ? quote.listingExchangeName ?? quote.exchangeName : undefined;
|
|
105
|
+
// A public open (a board row, a link) skips search for its quote. When the
|
|
106
|
+
// quote cannot vouch for the listing, because no source prices it right now
|
|
107
|
+
// or the one that answers prices another venue, search still lists it as
|
|
108
|
+
// the command bar reads it, and the pane shows the quote's own state.
|
|
109
|
+
if (!quote || (preserveListingKey && !quoteNamesListing(quote))) {
|
|
110
|
+
return publicOnly ? openListedPublicTarget() : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
try {
|
|
89
|
-
const quote = await dataProvider.getQuote(symbol, "", publicOnly ? { instrument: null } : undefined);
|
|
90
|
-
const quoteExchange = quote.listingExchangeName ?? quote.exchangeName;
|
|
91
|
-
if (preserveListingKey && quoteExchange) {
|
|
92
|
-
const baseSymbol = requested.exchange ? requested.symbol : symbol.slice(0, symbol.lastIndexOf("."));
|
|
93
|
-
if (!findExactTickerSearchMatch([{ label: baseSymbol, right: quoteExchange }], symbol)) return null;
|
|
94
|
-
}
|
|
95
|
-
if (preserveListingKey) {
|
|
96
|
-
const listingAlias = requested.exchange ? getListingSymbol(requested.symbol, requested.exchange) : symbol;
|
|
97
|
-
const explicitAliasMatches = tickerHasListingSuffix(listingAlias) && normalizeTickerSymbol(quote.symbol) === listingAlias;
|
|
98
|
-
if (!explicitAliasMatches && !findExactTickerSearchMatch([{ label: quote.symbol, right: quoteExchange }], symbol)) return null;
|
|
99
|
-
}
|
|
100
114
|
const quoteSymbol = preserveListingKey ? symbol : normalizeTickerSymbol(quote.symbol || symbol);
|
|
101
115
|
const selection = publicOnly ? tickerSelectionFromSearchResult({ providerId: dataProvider.id, symbol: quoteSymbol,
|
|
102
116
|
name: quote.name || quoteSymbol, exchange: requested.exchange ?? quoteExchange ?? "", currency: quote.currency, type: quote.instrumentType || "" }) : {};
|
|
@@ -122,4 +136,40 @@ export async function resolveTickerOpenTarget({
|
|
|
122
136
|
} catch {
|
|
123
137
|
return null;
|
|
124
138
|
}
|
|
139
|
+
|
|
140
|
+
/** Whether the quote answered for the listing the key names, not the same symbol on another venue. */
|
|
141
|
+
function quoteNamesListing(answer: Quote): boolean {
|
|
142
|
+
try {
|
|
143
|
+
if (quoteExchange) {
|
|
144
|
+
const baseSymbol = requested.exchange ? requested.symbol : symbol.slice(0, symbol.lastIndexOf("."));
|
|
145
|
+
if (!findExactTickerSearchMatch([{ label: baseSymbol, right: quoteExchange }], symbol)) return false;
|
|
146
|
+
}
|
|
147
|
+
const listingAlias = requested.exchange ? getListingSymbol(requested.symbol, requested.exchange) : symbol;
|
|
148
|
+
const explicitAliasMatches = tickerHasListingSuffix(listingAlias) && normalizeTickerSymbol(answer.symbol) === listingAlias;
|
|
149
|
+
return explicitAliasMatches || !!findExactTickerSearchMatch([{ label: answer.symbol, right: quoteExchange }], symbol);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function openListedPublicTarget(): Promise<TickerOpenTarget | null> {
|
|
156
|
+
try {
|
|
157
|
+
// Public listings only: no saved ticker or broker contract stands in for the key.
|
|
158
|
+
const listed = await resolveTickerSearch({
|
|
159
|
+
query: symbol,
|
|
160
|
+
activeTicker: null,
|
|
161
|
+
tickers: new Map(),
|
|
162
|
+
dataProvider,
|
|
163
|
+
searchContext: { ...searchContext, preferBroker: false },
|
|
164
|
+
});
|
|
165
|
+
if (listed?.kind !== "provider" || listed.result.brokerContract) return null;
|
|
166
|
+
const { ticker, created } = await upsertTickerFromSearchResult(tickerRepository, listed.result, {
|
|
167
|
+
tickerSymbol: preserveListingKey ? symbol : undefined,
|
|
168
|
+
});
|
|
169
|
+
return { symbol: ticker.metadata.ticker, ticker, created, ...tickerSelectionFromSearchResult(listed.result) };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof AmbiguousTickerError) throw error;
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
125
175
|
}
|
|
@@ -2,7 +2,7 @@ import type { SearchRequestContext, DataProvider } from "../../types/data-provid
|
|
|
2
2
|
import type { InstrumentSearchResult } from "../../types/instrument";
|
|
3
3
|
import type { TickerRecord } from "../../types/ticker";
|
|
4
4
|
import { canonicalExchange, isSharedExchangeCode, isUsListingExchange, parsePublicTickerKey, publicTickerKey, typedExchangeCode } from "../../utils/exchanges";
|
|
5
|
-
import { getListingSymbol, tickerHasListingSuffix } from "../../sources/listing-symbols";
|
|
5
|
+
import { getListingSymbol, listingSuffixConflictsWithExchange, tickerHasListingSuffix } from "../../sources/listing-symbols";
|
|
6
6
|
import { parseOptionSymbol } from "../../utils/options";
|
|
7
7
|
import { resolveCurrencyUnit } from "../../utils/currency-units";
|
|
8
8
|
import { currencyPairSpelling } from "../../utils/currency-pair";
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./result";
|
|
34
34
|
|
|
35
35
|
import { typedPerpetualListing } from "../perpetual-listing";
|
|
36
|
+
import { settleWithin } from "../../utils/async-deadline";
|
|
36
37
|
export type {
|
|
37
38
|
ResolvedTickerSearch,
|
|
38
39
|
TickerSearchCandidate,
|
|
@@ -319,18 +320,59 @@ export function resolveTickerSearch(options: ResolveTickerSearchOptions): Promis
|
|
|
319
320
|
const symbol = future?.symbol ?? (pair && !pair.ambiguous ? pair.symbol : typed);
|
|
320
321
|
const literal = resolveTickerSymbol(symbol, options, pickListing ? requested : null);
|
|
321
322
|
const alternatives = pickListing ? [] : [...dottedVenueQueries(symbol), ...pair?.ambiguous ? [pair.symbol] : []];
|
|
323
|
+
// A listing suffix search does not carry (CEZ.PR) is read from its own quote, as `quote` reads it.
|
|
324
|
+
const quoted = !pickListing && tickerHasListingSuffix(symbol) && !parsePublicTickerKey(symbol).exchange;
|
|
322
325
|
// Most input has no venue code to retry: hand back the literal lookup
|
|
323
326
|
// itself, so it settles no later than it did before the retry existed.
|
|
324
|
-
if (alternatives.length === 0) return literal;
|
|
327
|
+
if (alternatives.length === 0 && !quoted) return literal;
|
|
325
328
|
return literal.then(async (resolved) => {
|
|
326
329
|
for (const alternative of resolved ? [] : alternatives) {
|
|
327
330
|
const match = await resolveTickerSymbol(alternative, options, null);
|
|
328
331
|
if (match) return match;
|
|
329
332
|
}
|
|
330
|
-
return resolved;
|
|
333
|
+
return resolved ?? (quoted ? quotedSuffixListing(symbol, options) : null);
|
|
331
334
|
});
|
|
332
335
|
}
|
|
333
336
|
|
|
337
|
+
/** As long as a search source gets: past it, the key reads as unmatched. */
|
|
338
|
+
const SUFFIX_QUOTE_TIMEOUT_MS = 5_000;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A suffixed listing (CEZ.PR, PETR4.SA, VOLV-B.ST) that search does not list,
|
|
342
|
+
* vouched for by its own quote: the quote answers for that key, prices it, and
|
|
343
|
+
* names a venue the suffix belongs to. This is how `quote` and opening the
|
|
344
|
+
* key from the command bar read it, so adding it to a watchlist, a portfolio
|
|
345
|
+
* or the first-run positions takes the same listing.
|
|
346
|
+
*/
|
|
347
|
+
async function quotedSuffixListing(
|
|
348
|
+
symbol: string,
|
|
349
|
+
{ dataProvider, searchContext }: ResolveTickerSearchOptions,
|
|
350
|
+
): Promise<ResolvedTickerSearch | null> {
|
|
351
|
+
// No source prices the key, or none answers in time: it stays unmatched.
|
|
352
|
+
const quote = await settleWithin(dataProvider.getQuote(symbol, "", {
|
|
353
|
+
brokerId: searchContext?.brokerId,
|
|
354
|
+
brokerInstanceId: searchContext?.brokerInstanceId,
|
|
355
|
+
}), SUFFIX_QUOTE_TIMEOUT_MS);
|
|
356
|
+
if (!quote) return null;
|
|
357
|
+
const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
358
|
+
if (!exchange || normalizeTickerSymbol(quote.symbol || "") !== symbol
|
|
359
|
+
|| !Number.isFinite(quote.price) || quote.price === 0
|
|
360
|
+
|| listingSuffixConflictsWithExchange(symbol, exchange)) return null;
|
|
361
|
+
return {
|
|
362
|
+
kind: "provider",
|
|
363
|
+
symbol,
|
|
364
|
+
result: {
|
|
365
|
+
providerId: dataProvider.id,
|
|
366
|
+
symbol,
|
|
367
|
+
name: quote.name?.trim() || symbol,
|
|
368
|
+
exchange,
|
|
369
|
+
currency: quote.currency || undefined,
|
|
370
|
+
type: quote.instrumentType || "",
|
|
371
|
+
},
|
|
372
|
+
quote,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
334
376
|
async function resolveTickerSymbol(
|
|
335
377
|
symbol: string,
|
|
336
378
|
{ tickers, dataProvider, searchContext }: ResolveTickerSearchOptions,
|
|
@@ -252,6 +252,7 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
252
252
|
: null,
|
|
253
253
|
explicitIntentScore,
|
|
254
254
|
themeFund: themeWord != null && isThemeFund(item, themeWord) ? 1 : 0,
|
|
255
|
+
themeCompany: themeWord != null && isThemeCompany(item, themeWord) ? 1 : 0,
|
|
255
256
|
normalizedSymbol: normalizeSearchText(item.symbol || item.label),
|
|
256
257
|
symbolMatchRank: scoreSymbolMatchRank(intent, item),
|
|
257
258
|
nameOnly: companyMatchRank > 0 && labelScore + aliasScore <= 0
|
|
@@ -270,6 +271,14 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
270
271
|
if (!namedCompany && leadingFunds.size < LEADING_THEME_FUNDS) leadingFunds.add(symbol);
|
|
271
272
|
if (!leadingFunds.has(symbol)) entry.themeFund = 0;
|
|
272
273
|
}
|
|
274
|
+
// The companies the server sent with the theme (Albemarle, SQM and PLS for "lithium") follow those funds, in its
|
|
275
|
+
// order, ahead of the smaller companies whose names carry the word, so the leaders are on the first screen.
|
|
276
|
+
const leadingCompanies = new Set<string>();
|
|
277
|
+
for (const entry of ranked.filter(({ themeCompany }) => themeCompany).sort(byProviderRank)) {
|
|
278
|
+
const key = getTickerSearchListingKey(entry.item);
|
|
279
|
+
if (!namedCompany && leadingCompanies.size < LEADING_THEME_COMPANIES) leadingCompanies.add(key);
|
|
280
|
+
if (!leadingCompanies.has(key)) entry.themeCompany = 0;
|
|
281
|
+
}
|
|
273
282
|
|
|
274
283
|
// A saved symbol replaces its own source listing, not every exchange using it.
|
|
275
284
|
const matchedLocalListings = new Set(
|
|
@@ -374,7 +383,7 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
374
383
|
|
|
375
384
|
filtered.sort((a, b) => {
|
|
376
385
|
// Precedence is exact symbol, explicit query hints, a sector word's funds
|
|
377
|
-
// in the server's order, whole-word company relevance, issuer relevance, provider order within that issuer, partial
|
|
386
|
+
// and then the companies sent with it, each in the server's order, whole-word company relevance, issuer relevance, provider order within that issuer, partial
|
|
378
387
|
// symbol, saved relevance, then stable source order.
|
|
379
388
|
const aExactSymbolRank = a.symbolMatchRank >= 2 ? a.symbolMatchRank : 0;
|
|
380
389
|
const bExactSymbolRank = b.symbolMatchRank >= 2 ? b.symbolMatchRank : 0;
|
|
@@ -383,7 +392,8 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
383
392
|
return b.explicitIntentScore - a.explicitIntentScore;
|
|
384
393
|
}
|
|
385
394
|
if (b.themeFund !== a.themeFund) return b.themeFund - a.themeFund;
|
|
386
|
-
if (a.
|
|
395
|
+
if (b.themeCompany !== a.themeCompany) return b.themeCompany - a.themeCompany;
|
|
396
|
+
if (a.themeFund || a.themeCompany) {
|
|
387
397
|
const aProviderRank = a.item.providerRank ?? Number.POSITIVE_INFINITY;
|
|
388
398
|
const bProviderRank = b.item.providerRank ?? Number.POSITIVE_INFINITY;
|
|
389
399
|
if (aProviderRank !== bProviderRank) return aProviderRank - bProviderRank;
|
|
@@ -589,6 +599,13 @@ export function themeQueryWord(
|
|
|
589
599
|
|
|
590
600
|
/** How many sector funds lead a sector word's list: the rest of the list is the companies named for it. */
|
|
591
601
|
const LEADING_THEME_FUNDS = 5;
|
|
602
|
+
/** How many companies sent with a sector word follow its funds, before the companies named for it. */
|
|
603
|
+
const LEADING_THEME_COMPANIES = 5;
|
|
604
|
+
|
|
605
|
+
/** A company the server sent with the theme: a sector's leader whose name need not say it (Albemarle for "lithium"). */
|
|
606
|
+
function isThemeCompany(item: Partial<TickerSearchRankableItem>, word: string): boolean {
|
|
607
|
+
return item.instrumentClass === "equity" && !!item.searchKeywords?.some((keyword) => normalizeSearchText(keyword) === word);
|
|
608
|
+
}
|
|
592
609
|
|
|
593
610
|
/**
|
|
594
611
|
* A fund that tracks the theme (sent with it, or named for it) on a US exchange, where the large sector funds
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BrokerContractRef, InstrumentSearchResult, TickerListingRef } from "../../types/instrument";
|
|
2
|
+
import type { Quote } from "../../types/financials";
|
|
2
3
|
import type { TickerRecord } from "../../types/ticker";
|
|
3
4
|
|
|
4
5
|
export type TickerSearchInstrumentClass = "equity" | "fund" | "derivative" | "other";
|
|
@@ -44,6 +45,8 @@ export type ResolvedTickerSearch =
|
|
|
44
45
|
result: InstrumentSearchResult;
|
|
45
46
|
/** A bare symbol's other listings (SYM:EXCH) when its quote chose one of several. */
|
|
46
47
|
otherListings?: string[];
|
|
48
|
+
/** The quote that vouched for a listing search does not carry, so the caller need not ask again. */
|
|
49
|
+
quote?: Quote;
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
export interface TickerOpenTarget {
|
|
@@ -11,6 +11,25 @@ import {
|
|
|
11
11
|
import { canonicalExchange, parsePublicTickerKey, publicTickerKey } from "../../utils/exchanges";
|
|
12
12
|
import { scopedBrokerContractIdentityKey } from "../../utils/instrument-identity";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The key a result is saved under when its symbol is already saved for
|
|
16
|
+
* another listing (`SAP:XETR` beside a saved SAP on NYSE), or null when the
|
|
17
|
+
* symbol's own key serves. Opening a second listing must never re-denominate
|
|
18
|
+
* existing positions.
|
|
19
|
+
*/
|
|
20
|
+
export function otherListingTickerKey(
|
|
21
|
+
result: InstrumentSearchResult,
|
|
22
|
+
symbol: string,
|
|
23
|
+
saved: TickerRecord | null | undefined,
|
|
24
|
+
): string | null {
|
|
25
|
+
const selectedExchange = canonicalExchange(
|
|
26
|
+
result.exchange === "SMART" ? result.primaryExchange : result.exchange || result.primaryExchange,
|
|
27
|
+
);
|
|
28
|
+
const savedExchange = canonicalExchange(saved?.metadata.exchange);
|
|
29
|
+
if (!saved || !selectedExchange || !savedExchange || selectedExchange === savedExchange) return null;
|
|
30
|
+
return publicTickerKey(parsePublicTickerKey(symbol).symbol, selectedExchange);
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
export async function upsertTickerFromSearchResult(
|
|
15
34
|
tickerRepository: AppTickerRepositoryPort,
|
|
16
35
|
result: InstrumentSearchResult,
|
|
@@ -18,13 +37,9 @@ export async function upsertTickerFromSearchResult(
|
|
|
18
37
|
): Promise<{ ticker: TickerRecord; created: boolean }> {
|
|
19
38
|
let symbol = options.tickerSymbol ?? getSearchResultSymbol(result);
|
|
20
39
|
let ticker = await tickerRepository.loadTicker(symbol);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const savedExchange = canonicalExchange(ticker?.metadata.exchange);
|
|
25
|
-
if (ticker && selectedExchange && savedExchange && selectedExchange !== savedExchange) {
|
|
26
|
-
// Opening a second listing must never re-denominate existing positions.
|
|
27
|
-
symbol = publicTickerKey(parsePublicTickerKey(symbol).symbol, selectedExchange);
|
|
40
|
+
const otherListingKey = otherListingTickerKey(result, symbol, ticker);
|
|
41
|
+
if (otherListingKey) {
|
|
42
|
+
symbol = otherListingKey;
|
|
28
43
|
ticker = await tickerRepository.loadTicker(symbol);
|
|
29
44
|
}
|
|
30
45
|
const created = !ticker;
|
|
@@ -39,8 +39,13 @@ type TickerChangeSource = Pick<DataProvider, "getTickerChanges">;
|
|
|
39
39
|
/** A lookup in flight: `settle` waits a little more for it, then gives up and stops the request. */
|
|
40
40
|
export interface TickerChangeLookup {
|
|
41
41
|
settle(graceMs?: number): Promise<TickerChanges | null>;
|
|
42
|
+
/** Its answer once it has one, null when it failed or there is nothing to look up; undefined while it runs. */
|
|
43
|
+
peek(): TickerChanges | null | undefined;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** A change whose company moved to another symbol (`movedAwayChange`). */
|
|
47
|
+
export type MovedTickerChange = TickerChange & { tradesAs: string };
|
|
48
|
+
|
|
44
49
|
/** How long a command waits for a note once its own answer is ready. */
|
|
45
50
|
const TICKER_CHANGE_GRACE_MS = 200;
|
|
46
51
|
/** The longest a lookup runs, for a caller that keeps waiting. */
|
|
@@ -54,12 +59,13 @@ interface Entry {
|
|
|
54
59
|
promise: Promise<TickerChanges | null>;
|
|
55
60
|
controller: AbortController;
|
|
56
61
|
done: boolean;
|
|
62
|
+
value: TickerChanges | null;
|
|
57
63
|
failedAt: number | null;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/** One session's answers per source: a symbol is looked up once. */
|
|
61
67
|
const sessions = new WeakMap<TickerChangeSource, Map<string, Entry>>();
|
|
62
|
-
const NONE: TickerChangeLookup = { settle: async () => null };
|
|
68
|
+
const NONE: TickerChangeLookup = { settle: async () => null, peek: () => null };
|
|
63
69
|
|
|
64
70
|
/** A venue the change history is about: none named, a US exchange, or US over-the-counter. */
|
|
65
71
|
export function isUsSymbolVenue(exchange?: string | null): boolean {
|
|
@@ -99,9 +105,9 @@ function entryFor(symbol: string, source: TickerChangeSource & Required<TickerCh
|
|
|
99
105
|
if (cached && (cached.failedAt === null || Date.now() - cached.failedAt < RETRY_FAILED_AFTER_MS)) return cached;
|
|
100
106
|
const controller = new AbortController();
|
|
101
107
|
const timer = setTimeout(() => controller.abort(), LOOKUP_TIMEOUT_MS);
|
|
102
|
-
const entry: Entry = { promise: Promise.resolve(null), controller, done: false, failedAt: null };
|
|
108
|
+
const entry: Entry = { promise: Promise.resolve(null), controller, done: false, value: null, failedAt: null };
|
|
103
109
|
entry.promise = source.getTickerChanges(symbol, { signal: controller.signal })
|
|
104
|
-
.then(readPayload)
|
|
110
|
+
.then((payload) => entry.value = readPayload(payload))
|
|
105
111
|
.catch(() => {
|
|
106
112
|
entry.failedAt = Date.now();
|
|
107
113
|
return null;
|
|
@@ -140,6 +146,7 @@ export function startTickerChangeLookup(
|
|
|
140
146
|
entry.controller.abort();
|
|
141
147
|
return null;
|
|
142
148
|
},
|
|
149
|
+
peek: () => entry.done ? entry.value : undefined,
|
|
143
150
|
};
|
|
144
151
|
}
|
|
145
152
|
|
|
@@ -153,9 +160,9 @@ function day(date: string): number {
|
|
|
153
160
|
return Date.parse(`${date}T00:00:00Z`);
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
/** "
|
|
157
|
-
function laterHops(hops: readonly TickerChangeHop[]
|
|
158
|
-
return hops.slice(1).map((hop) => `, then ${
|
|
163
|
+
/** ", then MMATQ on 2024-08-21": each later hop of a chain. */
|
|
164
|
+
function laterHops(hops: readonly TickerChangeHop[]): string {
|
|
165
|
+
return hops.slice(1).map((hop) => `, then ${hop.to} on ${hop.date}`).join("");
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
/**
|
|
@@ -167,6 +174,16 @@ export function tickerChangeLine(change: TickerChange | null | undefined): strin
|
|
|
167
174
|
return change?.kind === "moved" ? change.note : null;
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The change when the company that held the symbol trades under another now
|
|
179
|
+
* and nothing new lists under it (HFC became DINO): the symbol has no data of
|
|
180
|
+
* its own. Null for a symbol a new security took (FB lists a fund), one its
|
|
181
|
+
* holder renamed into (META), and one with no change.
|
|
182
|
+
*/
|
|
183
|
+
export function movedAwayChange(change: TickerChange | null | undefined): MovedTickerChange | null {
|
|
184
|
+
return change?.kind === "moved" && !change.listed && change.tradesAs ? { ...change, tradesAs: change.tradesAs } : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
/**
|
|
171
188
|
* The note for a symbol's changes. `current` is what the command found
|
|
172
189
|
* trading under the symbol: null when nothing does, left out when it did not
|
|
@@ -174,7 +191,7 @@ export function tickerChangeLine(change: TickerChange | null | undefined): strin
|
|
|
174
191
|
* left, is that issuer's old line rather than a new listing.
|
|
175
192
|
*
|
|
176
193
|
* "FB now lists ProShares S&P 500 Dynamic Daily Buffer ETF; Meta Platforms trades as META (changed 2022-06-09)"
|
|
177
|
-
* "
|
|
194
|
+
* "HFC became DINO on 2022-03-15"
|
|
178
195
|
* "METV was META until 2022-01-31" (data only: `tickerChangeLine` prints none)
|
|
179
196
|
*/
|
|
180
197
|
export function describeTickerChange(
|
|
@@ -197,10 +214,10 @@ export function describeTickerChange(
|
|
|
197
214
|
const listed = listedHere ? current?.name?.trim() || payload.listedName || "another security" : null;
|
|
198
215
|
const when = moved.hops.length === 1
|
|
199
216
|
? `changed ${first.date}`
|
|
200
|
-
: `changed to ${first.to} on ${first.date}${laterHops(moved.hops
|
|
217
|
+
: `changed to ${first.to} on ${first.date}${laterHops(moved.hops)}`;
|
|
201
218
|
const note = listed
|
|
202
219
|
? `${symbol} now lists ${listed}; ${issuer ?? `the former ${symbol}`} trades as ${moved.symbol} (${when})`
|
|
203
|
-
: `${symbol}
|
|
220
|
+
: `${symbol} became ${first.to} on ${first.date}${laterHops(moved.hops)}`;
|
|
204
221
|
return { symbol, kind: "moved", note, changes: moved.hops, tradesAs: moved.symbol, issuer, listed };
|
|
205
222
|
}
|
|
206
223
|
if (formerly) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The typical spacing of a series of bar times: the median gap between
|
|
5
|
+
* consecutive distinct times, so the long gap a holiday leaves (Golden Week
|
|
6
|
+
* closes Shanghai from 30 Sep to 8 Oct) does not read as the bar size. Null
|
|
7
|
+
* with fewer than two distinct times.
|
|
8
|
+
*/
|
|
9
|
+
export function medianBarGap(times: readonly number[]): number | null {
|
|
10
|
+
const finite = times.filter(Number.isFinite).sort((left, right) => left - right);
|
|
11
|
+
const gaps = finite.slice(1).map((time, index) => time - finite[index]!).filter((gap) => gap > 0).sort((left, right) => left - right);
|
|
12
|
+
return gaps.length > 0 ? gaps[Math.floor((gaps.length - 1) / 2)]! : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A bar size of a day or more read from the typical spacing of its bars (medianBarGap). */
|
|
16
|
+
export function barSizeFromGap(gapMs: number): "1d" | "1wk" | "1mo" {
|
|
17
|
+
const days = gapMs / DAY_MS;
|
|
18
|
+
return days >= 25 ? "1mo" : days >= 6 ? "1wk" : "1d";
|
|
19
|
+
}
|