gloomberb 0.18.1 → 0.18.3
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 +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +21 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/api-client/types.ts +5 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/market.ts +354 -187
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +167 -60
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/help.ts +1 -1
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +12 -1
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +51 -3
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +19 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +11 -5
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +27 -3
- package/src/cli/pane-functions/headless.ts +31 -11
- package/src/cli/pane-functions/index.ts +67 -7
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +21 -6
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +47 -11
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +25 -7
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +69 -8
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +3 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +20 -4
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/app-services.ts +4 -0
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +5 -5
- package/src/i18n/es.ts +31 -3
- package/src/i18n/ja.ts +31 -3
- package/src/i18n/ko.ts +31 -3
- package/src/i18n/zh-cn.ts +32 -4
- package/src/i18n/zh-tw.ts +32 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +35 -0
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +30 -5
- package/src/plugins/builtin/equity-screener/model.ts +68 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +43 -21
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +76 -0
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +48 -10
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +118 -35
- package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +64 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +5 -28
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +40 -8
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +22 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +51 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/index.ts +57 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +18 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +62 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +57 -8
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +11 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +19 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +28 -1
- package/src/utils/currency-units.ts +32 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
- package/src/utils/sec.ts +68 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { UpdateFailureReport } from "../api-client";
|
|
2
|
+
import type { UpdateChannel } from "../types/config";
|
|
3
|
+
import type { UpdateFailure } from "../updater/failure";
|
|
4
|
+
import { logFailure } from "../utils/log-failure";
|
|
5
|
+
import { compareSemver } from "../utils/semver";
|
|
6
|
+
import { VERSION } from "../version";
|
|
7
|
+
import { reportCrash, type CrashReportContext } from "./crash-reports";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Failed update steps, sent to error tracking through the crash reporter (and
|
|
11
|
+
* so behind the same off switch). A report is the step, a fixed code, the
|
|
12
|
+
* versions, the channel, the platform and how the app was installed: never the
|
|
13
|
+
* error's text, which can hold a path, a user name or a signed URL.
|
|
14
|
+
*
|
|
15
|
+
* The process that runs the update reports it: the terminal app, and the
|
|
16
|
+
* desktop app's Bun process. The desktop view and the web install no host, so
|
|
17
|
+
* the same failure is never sent twice. One report per step, code and app
|
|
18
|
+
* version a day is enough to see a stuck updater; the hourly check that keeps
|
|
19
|
+
* failing offline adds nothing after the first.
|
|
20
|
+
*
|
|
21
|
+
* The desktop host also keeps what an install was about to do, so the next
|
|
22
|
+
* launch can tell an install that never took, or an update downloaded and
|
|
23
|
+
* then quit without, from one that worked (`reviewLastUpdateAttempt`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Where the running app sits. macOS installs replace the app bundle in place,
|
|
28
|
+
* which only works from a folder the user can write: not from a disk image,
|
|
29
|
+
* and not from the read-only copy App Translocation runs a quarantined app from.
|
|
30
|
+
*/
|
|
31
|
+
export type UpdateInstallKind =
|
|
32
|
+
| "desktop-applications"
|
|
33
|
+
| "desktop-translocated"
|
|
34
|
+
| "desktop-volume"
|
|
35
|
+
| "desktop-elsewhere"
|
|
36
|
+
/** Windows and Linux: the app folder the installer created. */
|
|
37
|
+
| "desktop-app-data"
|
|
38
|
+
| "terminal-binary"
|
|
39
|
+
| "terminal-managed"
|
|
40
|
+
| "terminal-package-manager"
|
|
41
|
+
| "terminal-source"
|
|
42
|
+
| "unknown";
|
|
43
|
+
|
|
44
|
+
/** One update the app downloaded or started installing, and when. */
|
|
45
|
+
export interface UpdateAttempt {
|
|
46
|
+
from: string;
|
|
47
|
+
to: string;
|
|
48
|
+
channel: UpdateChannel;
|
|
49
|
+
at: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What the host keeps on disk between launches. */
|
|
53
|
+
export interface UpdateReportState {
|
|
54
|
+
/** When each step, code and app version was last reported, in ms. */
|
|
55
|
+
sent: Record<string, number>;
|
|
56
|
+
/** A downloaded update waiting for the user's Restart. */
|
|
57
|
+
staged?: UpdateAttempt;
|
|
58
|
+
/** An install the user started; the next launch says whether it took. */
|
|
59
|
+
applying?: UpdateAttempt;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UpdateFailureReporterHost {
|
|
63
|
+
installKind: UpdateInstallKind;
|
|
64
|
+
/** The running version; defaults to this build's. */
|
|
65
|
+
appVersion?: string;
|
|
66
|
+
/** `process.platform` and `process.arch`. */
|
|
67
|
+
platform: string;
|
|
68
|
+
arch: string;
|
|
69
|
+
load(): UpdateReportState;
|
|
70
|
+
save(state: UpdateReportState): void;
|
|
71
|
+
now?(): number;
|
|
72
|
+
/** Defaults to the crash reporter. */
|
|
73
|
+
send?(report: UpdateFailureReport): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface UpdateFailureContext {
|
|
77
|
+
/** The version the update was going to, when one was known. */
|
|
78
|
+
targetVersion?: string | null;
|
|
79
|
+
channel?: UpdateChannel | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
83
|
+
/** Older attempts say nothing about this launch: the user installed by hand, or reinstalled. */
|
|
84
|
+
const ATTEMPT_MAX_AGE_MS = 30 * DAY_MS;
|
|
85
|
+
/**
|
|
86
|
+
* The install quits the app and opens the new one as soon as it is gone. A new
|
|
87
|
+
* version first seen this long after is one the user opened again by hand.
|
|
88
|
+
*/
|
|
89
|
+
const RELAUNCH_GRACE_MS = 10 * 60 * 1000;
|
|
90
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,60})?$/;
|
|
91
|
+
|
|
92
|
+
let host: UpdateFailureReporterHost | null = null;
|
|
93
|
+
|
|
94
|
+
function cleanVersion(value: string | null | undefined): string | undefined {
|
|
95
|
+
const version = value?.trim().replace(/^v/, "");
|
|
96
|
+
return version && VERSION_PATTERN.test(version) ? version : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function reportPlatform(platform: string): UpdateFailureReport["platform"] {
|
|
100
|
+
if (platform === "darwin") return "macos";
|
|
101
|
+
if (platform === "win32") return "windows";
|
|
102
|
+
if (platform === "linux") return "linux";
|
|
103
|
+
return "other";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function reportArch(arch: string): UpdateFailureReport["arch"] {
|
|
107
|
+
return arch === "arm64" || arch === "x64" ? arch : "other";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The report, from fixed values only: anything that is not a version, a
|
|
112
|
+
* known channel, platform or install kind is left out or named `other`.
|
|
113
|
+
*/
|
|
114
|
+
function buildUpdateFailureReport(
|
|
115
|
+
failure: UpdateFailure,
|
|
116
|
+
fields: UpdateFailureContext & {
|
|
117
|
+
appVersion: string;
|
|
118
|
+
platform: string;
|
|
119
|
+
arch: string;
|
|
120
|
+
installKind: UpdateInstallKind;
|
|
121
|
+
},
|
|
122
|
+
): UpdateFailureReport {
|
|
123
|
+
const targetVersion = cleanVersion(fields.targetVersion);
|
|
124
|
+
return {
|
|
125
|
+
phase: failure.phase,
|
|
126
|
+
code: failure.code,
|
|
127
|
+
appVersion: cleanVersion(fields.appVersion) ?? "unknown",
|
|
128
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
129
|
+
channel: fields.channel === "nightly" ? "nightly" : "stable",
|
|
130
|
+
platform: reportPlatform(fields.platform),
|
|
131
|
+
arch: reportArch(fields.arch),
|
|
132
|
+
installKind: fields.installKind,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The report as the crash reporter sends it. Today's server keeps the error's
|
|
138
|
+
* type, message, plugin and pane: the message is the step and code, so error
|
|
139
|
+
* tracking groups one issue per failure, and the pane holds the rest as
|
|
140
|
+
* `key=value` pairs. The `update` field carries the same values for a server
|
|
141
|
+
* that reads it.
|
|
142
|
+
*/
|
|
143
|
+
function updateFailureCrashReport(report: UpdateFailureReport): {
|
|
144
|
+
error: { name: string; message: string };
|
|
145
|
+
context: CrashReportContext;
|
|
146
|
+
} {
|
|
147
|
+
const details = [
|
|
148
|
+
`to=${report.targetVersion ?? "none"}`,
|
|
149
|
+
`channel=${report.channel}`,
|
|
150
|
+
`platform=${report.platform}-${report.arch}`,
|
|
151
|
+
`install=${report.installKind}`,
|
|
152
|
+
].join(" ");
|
|
153
|
+
return {
|
|
154
|
+
// A plain object, not an Error: it has no stack, so no file path goes along.
|
|
155
|
+
error: { name: "UpdateFailed", message: `${report.phase}: ${report.code}` },
|
|
156
|
+
context: { kind: "plugin", plugin: "updater", pane: details, update: report },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sendThroughCrashReporter(report: UpdateFailureReport): void {
|
|
161
|
+
const { error, context } = updateFailureCrashReport(report);
|
|
162
|
+
reportCrash(error, context);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Takes the day's slot for `key`, or says it is used: one report per step,
|
|
167
|
+
* code and version a day. Drops what is older than a day while at it.
|
|
168
|
+
*/
|
|
169
|
+
function takeReportSlot(state: UpdateReportState, key: string, now: number): boolean {
|
|
170
|
+
for (const [entry, at] of Object.entries(state.sent)) {
|
|
171
|
+
if (!(now - at < DAY_MS) || at > now) delete state.sent[entry];
|
|
172
|
+
}
|
|
173
|
+
if (state.sent[key] !== undefined) return false;
|
|
174
|
+
state.sent[key] = now;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function loadState(current: UpdateFailureReporterHost): UpdateReportState {
|
|
179
|
+
try {
|
|
180
|
+
const state = current.load();
|
|
181
|
+
return { ...state, sent: { ...state.sent } };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logFailure("telemetry-update-failures", "Could not read update report state")(error);
|
|
184
|
+
return { sent: {} };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function saveState(current: UpdateFailureReporterHost, state: UpdateReportState): void {
|
|
189
|
+
try {
|
|
190
|
+
current.save(state);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logFailure("telemetry-update-failures", "Could not save update report state")(error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sendIfDue(
|
|
197
|
+
current: UpdateFailureReporterHost,
|
|
198
|
+
state: UpdateReportState,
|
|
199
|
+
failure: UpdateFailure,
|
|
200
|
+
context: UpdateFailureContext,
|
|
201
|
+
now: number,
|
|
202
|
+
): void {
|
|
203
|
+
const report = buildUpdateFailureReport(failure, {
|
|
204
|
+
...context,
|
|
205
|
+
appVersion: current.appVersion ?? VERSION,
|
|
206
|
+
platform: current.platform,
|
|
207
|
+
arch: current.arch,
|
|
208
|
+
installKind: current.installKind,
|
|
209
|
+
});
|
|
210
|
+
if (!takeReportSlot(state, `${report.phase}|${report.code}|${report.appVersion}`, now)) return;
|
|
211
|
+
(current.send ?? sendThroughCrashReporter)(report);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function installUpdateFailureReporter(next: UpdateFailureReporterHost): () => void {
|
|
215
|
+
host = next;
|
|
216
|
+
return () => {
|
|
217
|
+
if (host === next) host = null;
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Reports one failed step. Never throws; does nothing where no host is installed. */
|
|
222
|
+
export function reportUpdateFailure(failure: UpdateFailure, context: UpdateFailureContext = {}): void {
|
|
223
|
+
const current = host;
|
|
224
|
+
if (!current) return;
|
|
225
|
+
try {
|
|
226
|
+
const now = current.now?.() ?? Date.now();
|
|
227
|
+
const state = loadState(current);
|
|
228
|
+
// An install that failed in this run is reported here, not again at the next launch.
|
|
229
|
+
if (failure.phase === "apply" || failure.phase === "relaunch") delete state.applying;
|
|
230
|
+
sendIfDue(current, state, failure, context, now);
|
|
231
|
+
saveState(current, state);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logFailure("telemetry-update-failures", "Could not report update failure")(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function recordAttempt(field: "staged" | "applying", to: string, channel: UpdateChannel): void {
|
|
238
|
+
const current = host;
|
|
239
|
+
if (!current) return;
|
|
240
|
+
const state = loadState(current);
|
|
241
|
+
const attempt: UpdateAttempt = { from: current.appVersion ?? VERSION, to, channel, at: current.now?.() ?? Date.now() };
|
|
242
|
+
if (field === "applying") delete state.staged;
|
|
243
|
+
state[field] = attempt;
|
|
244
|
+
saveState(current, state);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** A downloaded update is waiting for the user's Restart. */
|
|
248
|
+
export function noteUpdateStaged(to: string, channel: UpdateChannel): void {
|
|
249
|
+
recordAttempt("staged", to, channel);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The user pressed Restart: the next launch should be `to`. */
|
|
253
|
+
export function noteUpdateApplying(to: string, channel: UpdateChannel): void {
|
|
254
|
+
recordAttempt("applying", to, channel);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The waiting update was taken back (a channel switch); quitting without it is no longer news. */
|
|
258
|
+
export function forgetStagedUpdate(): void {
|
|
259
|
+
const current = host;
|
|
260
|
+
if (!current) return;
|
|
261
|
+
const state = loadState(current);
|
|
262
|
+
if (!state.staged) return;
|
|
263
|
+
delete state.staged;
|
|
264
|
+
saveState(current, state);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run once at launch: what came of the last update this app started.
|
|
269
|
+
*
|
|
270
|
+
* - Restarted into the new version: nothing to report, unless the new version
|
|
271
|
+
* only came up long after, which is the app failing to reopen itself.
|
|
272
|
+
* - Still the old version after an install: it never took.
|
|
273
|
+
* - Still the old version with a download waiting: the app was quit without
|
|
274
|
+
* restarting into it, which the stuck installs may come down to.
|
|
275
|
+
*/
|
|
276
|
+
export function reviewLastUpdateAttempt(): void {
|
|
277
|
+
const current = host;
|
|
278
|
+
if (!current) return;
|
|
279
|
+
const currentVersion = current.appVersion ?? VERSION;
|
|
280
|
+
try {
|
|
281
|
+
const state = loadState(current);
|
|
282
|
+
const { applying, staged } = state;
|
|
283
|
+
if (!applying && !staged) return;
|
|
284
|
+
delete state.applying;
|
|
285
|
+
delete state.staged;
|
|
286
|
+
const now = current.now?.() ?? Date.now();
|
|
287
|
+
const fresh = (attempt: UpdateAttempt | undefined) => !!attempt && now - attempt.at < ATTEMPT_MAX_AGE_MS && attempt.at <= now;
|
|
288
|
+
if (applying && fresh(applying)) {
|
|
289
|
+
const context = { targetVersion: applying.to, channel: applying.channel };
|
|
290
|
+
if (applying.from === currentVersion) {
|
|
291
|
+
sendIfDue(current, state, { phase: "apply", code: "not_applied" }, context, now);
|
|
292
|
+
} else if ((compareSemver(currentVersion, applying.to) ?? -1) >= 0 && now - applying.at > RELAUNCH_GRACE_MS) {
|
|
293
|
+
sendIfDue(current, state, { phase: "relaunch", code: "relaunch_missed" }, context, now);
|
|
294
|
+
}
|
|
295
|
+
} else if (staged && fresh(staged) && staged.from === currentVersion) {
|
|
296
|
+
sendIfDue(current, state, { phase: "apply", code: "not_restarted" }, { targetVersion: staged.to, channel: staged.channel }, now);
|
|
297
|
+
}
|
|
298
|
+
saveState(current, state);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logFailure("telemetry-update-failures", "Could not review the last update")(error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Test seam. */
|
|
305
|
+
export function resetUpdateFailureReporterForTests(): void {
|
|
306
|
+
host = null;
|
|
307
|
+
}
|
|
@@ -2,7 +2,7 @@ import { logFailure } from "../utils/log-failure";
|
|
|
2
2
|
import type { DataProvider } from "../types/data-provider";
|
|
3
3
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
4
4
|
import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../utils/exchanges";
|
|
5
|
-
import {
|
|
5
|
+
import { companyNameMatch, shortCompanyName } from "./company-name";
|
|
6
6
|
import { LISTING_SEARCH_LIMIT } from "./search";
|
|
7
7
|
import { classifyInstrumentKind } from "./search/ranking";
|
|
8
8
|
import { isProviderBusy, type ProviderBusyError } from "../sources/provider-errors";
|
|
@@ -28,10 +28,12 @@ export function isCompanyNameInput(input: string): boolean {
|
|
|
28
28
|
/**
|
|
29
29
|
* The listing a company's name stands for when it is typed where a symbol
|
|
30
30
|
* goes, as an entry of a comma list ("Whirlpool Corporation" is WHR on
|
|
31
|
-
* NYSE). Of search's listings whose names have every word of it
|
|
32
|
-
*
|
|
31
|
+
* NYSE). Of search's listings whose names have every word of it, or a word
|
|
32
|
+
* it shortens ("philip morris intl" is Philip Morris International; the name
|
|
33
|
+
* is also searched without its legal form, which can find more), the company
|
|
33
34
|
* whose name says no more than the one typed goes first, so a subsidiary that
|
|
34
|
-
* shares the word (Whirlpool of India) does not
|
|
35
|
+
* shares the word (Whirlpool of India) does not, and of those a name matched
|
|
36
|
+
* word for word before one a short form matched; then its line on a US
|
|
35
37
|
* exchange, then a plain line before a depositary receipt, the shortest
|
|
36
38
|
* symbol (WHR before WHR-A) and search's order. Contracts, preferred shares
|
|
37
39
|
* and notes are never the listing. Null when none matches; when none does
|
|
@@ -55,10 +57,10 @@ export async function findListingOfCompanyName(
|
|
|
55
57
|
const ranked = results.flatMap((result, order) => {
|
|
56
58
|
const exchange = canonicalExchange(result.exchange === "SMART" ? result.primaryExchange : result.exchange || result.primaryExchange);
|
|
57
59
|
const type = result.brokerContract?.secType || result.type || "";
|
|
58
|
-
const
|
|
59
|
-
if (!exchange ||
|
|
60
|
+
const match = companyNameMatch(name, result.name);
|
|
61
|
+
if (!exchange || match == null || classifyInstrumentKind(type) === "derivative" || NOT_COMMON.test(type)
|
|
60
62
|
|| parsePublicTickerKey(result.symbol).exchange) return [];
|
|
61
|
-
return [{ result, exchange, rank: [extra, isUsListingExchange(exchange) ? 0 : 1, RECEIPT.test(type) ? 1 : 0, result.symbol.length, order] }];
|
|
63
|
+
return [{ result, exchange, rank: [match.extra, match.abbreviated, isUsListingExchange(exchange) ? 0 : 1, RECEIPT.test(type) ? 1 : 0, result.symbol.length, order] }];
|
|
62
64
|
}).sort((left, right) => {
|
|
63
65
|
const at = left.rank.findIndex((value, index) => value !== right.rank[index]);
|
|
64
66
|
return at < 0 ? 0 : left.rank[at]! - right.rank[at]!;
|
|
@@ -56,6 +56,49 @@ export function companyNameExtraWords(typed: string, name: string | null | undef
|
|
|
56
56
|
return words.size - wanted.size;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Whether a typed word is a short form of a name's word, as a list writes
|
|
61
|
+
* one: its first letter and the rest in order, at least three letters
|
|
62
|
+
* (`intl` for international, `hldgs` for holdings, `grp` for group).
|
|
63
|
+
*/
|
|
64
|
+
function abbreviates(short: string, word: string): boolean {
|
|
65
|
+
if (short.length < 3 || short.length >= word.length || short[0] !== word[0] || !/^\p{L}+$/u.test(short)) return false;
|
|
66
|
+
let at = 0;
|
|
67
|
+
for (const char of word) {
|
|
68
|
+
if (char === short[at]) at += 1;
|
|
69
|
+
if (at === short.length) return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `companyNameExtraWords` for a name typed with short forms, as a list or a
|
|
76
|
+
* spreadsheet writes it ("philip morris intl" for Philip Morris
|
|
77
|
+
* International Inc.): each typed word is one of the name's, or a short form
|
|
78
|
+
* of another of its words (`abbreviates`). `extra` counts the name's words
|
|
79
|
+
* left unmatched, `abbreviated` the typed words read as short forms, so an
|
|
80
|
+
* exact name ranks before one only a short form reaches. Null when a typed
|
|
81
|
+
* word is in the name neither way, or nothing was typed.
|
|
82
|
+
*/
|
|
83
|
+
export function companyNameMatch(typed: string, name: string | null | undefined): { extra: number; abbreviated: number } | null {
|
|
84
|
+
const wanted = companyWords(typed);
|
|
85
|
+
const words = companyWords(name);
|
|
86
|
+
if (wanted.size === 0) return null;
|
|
87
|
+
const matched = new Set<string>();
|
|
88
|
+
let abbreviated = 0;
|
|
89
|
+
for (const word of wanted) {
|
|
90
|
+
if (words.has(word)) {
|
|
91
|
+
matched.add(word);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const long = [...words].find((candidate) => !matched.has(candidate) && !wanted.has(candidate) && abbreviates(word, candidate));
|
|
95
|
+
if (!long) return null;
|
|
96
|
+
matched.add(long);
|
|
97
|
+
abbreviated += 1;
|
|
98
|
+
}
|
|
99
|
+
return { extra: words.size - matched.size, abbreviated };
|
|
100
|
+
}
|
|
101
|
+
|
|
59
102
|
/** Items grouped by the company their names are, in the order each company first appears. Unnamed items are left out. */
|
|
60
103
|
export function groupByCompany<T>(items: readonly T[], nameOf: (item: T) => string | null | undefined): T[][] {
|
|
61
104
|
const groups: T[][] = [];
|
|
@@ -19,6 +19,7 @@ import { isQuoteBeforeLatestSession, isQuoteStaleForCurrentSession } from "../ma
|
|
|
19
19
|
import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
|
|
20
20
|
import { canonicalExchange, isUsListingExchange, parsePublicTickerKey, resolveExchangeTimeZone } from "../utils/exchanges";
|
|
21
21
|
import { zonedDateKey } from "../utils/zoned-date-time";
|
|
22
|
+
import { delistingLabel } from "../market-data/quotes/delisting";
|
|
22
23
|
|
|
23
24
|
/*
|
|
24
25
|
* What became of a symbol, read the way `quote` reads it: the listing's own
|
|
@@ -33,8 +34,10 @@ const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
|
|
|
33
34
|
/**
|
|
34
35
|
* The exchange-local date a listing last traded, when its quote is stale and
|
|
35
36
|
* that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
|
|
37
|
+
* A quote that says its listing no longer trades gives its last session.
|
|
36
38
|
*/
|
|
37
39
|
function endedTradingDate(quote: Quote, now = Date.now()): string | null {
|
|
40
|
+
if (quote.delisting) return quote.delisting.lastTradeDate;
|
|
38
41
|
const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
|
|
39
42
|
if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
|
|
40
43
|
return quoteDateKey(quote, last);
|
|
@@ -60,12 +63,18 @@ export function tradingListing(quote: Quote | null | undefined, now = Date.now()
|
|
|
60
63
|
return quote && !endedTradingDate(quote, now) ? quotedListing(quote) : null;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote
|
|
68
|
+
* that stopped long ago; "TWTR: Delisted Oct 27, 2022 (last price $53.86)"
|
|
69
|
+
* when the quote says the listing no longer trades.
|
|
70
|
+
*/
|
|
64
71
|
export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
|
|
65
72
|
const date = endedTradingDate(quote, now);
|
|
66
73
|
if (!date) return null;
|
|
67
74
|
const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
|
|
68
|
-
return
|
|
75
|
+
return quote.delisting
|
|
76
|
+
? `${label}: ${delistingLabel(quote.delisting)} (last price ${price})`
|
|
77
|
+
: `${label}: no longer trading since ${date} (last price ${price})`;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -81,11 +90,17 @@ export interface ListingFate {
|
|
|
81
90
|
/**
|
|
82
91
|
* The line a command with nothing to serve for the symbol says: `quote`'s
|
|
83
92
|
* change line when its company left it ("FB now lists ...; Meta Platforms
|
|
84
|
-
* trades as META (changed 2022-06-09)"), else, for a listing
|
|
85
|
-
* "TWTR
|
|
86
|
-
*
|
|
93
|
+
* trades as META (changed 2022-06-09)"), else, for a listing the service
|
|
94
|
+
* says no longer trades, "TWTR: Delisted Oct 27, 2022", and for another
|
|
95
|
+
* that stopped, "XYZ stopped trading 2022-10-27; no history or options
|
|
96
|
+
* served". Null for a symbol that trades as it always has.
|
|
87
97
|
*/
|
|
88
98
|
note: string | null;
|
|
99
|
+
/**
|
|
100
|
+
* "TWTR: Delisted Oct 27, 2022" when the listing's quote says it no longer
|
|
101
|
+
* trades: its history is served, ending on that session, and says so.
|
|
102
|
+
*/
|
|
103
|
+
delisted?: string;
|
|
89
104
|
/**
|
|
90
105
|
* The change when the symbol's company trades under another symbol now
|
|
91
106
|
* and its own quote shows nothing new listed under it (`movedAwayChange`):
|
|
@@ -156,9 +171,10 @@ export function describeListingFate(
|
|
|
156
171
|
const abroad = !!quote && !!bareListingNote({ symbol, request: { exchange: listing.exchange ?? "" } }, quote);
|
|
157
172
|
const current = tradingListing(quote, now);
|
|
158
173
|
const change = abroad ? null : describeTickerChange(symbol, changes, current);
|
|
159
|
-
const
|
|
174
|
+
const delisted = quote?.delisting && !abroad ? `${symbol}: ${delistingLabel(quote.delisting)}` : undefined;
|
|
175
|
+
const note = tickerChangeLine(change) ?? delisted ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
|
|
160
176
|
const formerIssuer = abroad ? null : describeFormerIssuer(symbol, changes, current);
|
|
161
|
-
return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer };
|
|
177
|
+
return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer, ...(delisted ? { delisted } : {}) };
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
/**
|
|
@@ -200,7 +216,8 @@ export async function loadListingFate(
|
|
|
200
216
|
|
|
201
217
|
/**
|
|
202
218
|
* The lines a command says about a symbol's fate beside what it served. With
|
|
203
|
-
* nothing served, `fate.note`. With data, `
|
|
219
|
+
* nothing served, `fate.note`. With data, `fate.delisted` for a listing that
|
|
220
|
+
* no longer trades ("TWTR: Delisted Oct 27, 2022"), `quote`'s change line when the
|
|
204
221
|
* symbol's company left it (FB's bars are the fund listed there now), and, for
|
|
205
222
|
* bars from before the symbol's holder took it (META's from 2021), that they
|
|
206
223
|
* are from its old symbol: "Includes FB-era bars: META was FB until 2022-06-09".
|
|
@@ -211,11 +228,43 @@ export function listingFateNotes(fate: ListingFate, served: { served: boolean; f
|
|
|
211
228
|
const first = served.firstDate?.slice(0, 10);
|
|
212
229
|
const earlier = change?.kind === "formerly" && first ? change.changes.filter((hop) => first < hop.date).map((hop) => hop.from) : [];
|
|
213
230
|
return [
|
|
231
|
+
// What it served ends on the listing's last session, and says so.
|
|
232
|
+
fate.delisted ?? null,
|
|
214
233
|
tickerChangeLine(change),
|
|
215
234
|
earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
|
|
216
235
|
].filter((line): line is string => !!line);
|
|
217
236
|
}
|
|
218
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A report's notes and errors beside what became of its symbol, worded as
|
|
240
|
+
* `quote`, `history` and `options` word it: with nothing served, the fate is
|
|
241
|
+
* why ("TWTR: Delisted Oct 27, 2022", "FB now lists ...; Meta Platforms
|
|
242
|
+
* trades as META") and stands in for the errors the empty load raised ("Not
|
|
243
|
+
* a ticker", "No options provider"); with data, `listingFateNotes` beside
|
|
244
|
+
* the errors. A symbol whose company moved, with nothing listed under it now,
|
|
245
|
+
* has nothing of its own to serve and throws `MovedSymbolError`.
|
|
246
|
+
*/
|
|
247
|
+
export function listingFateReport(
|
|
248
|
+
fate: ListingFate,
|
|
249
|
+
served: { served: boolean; firstDate?: string | null },
|
|
250
|
+
errors: readonly string[],
|
|
251
|
+
): { notes: string[]; errors: string[] } {
|
|
252
|
+
if (fate.moved) throw new MovedSymbolError(fate.moved);
|
|
253
|
+
const notes = listingFateNotes(fate, served);
|
|
254
|
+
return { notes, errors: !served.served && notes.length > 0 ? [] : [...errors] };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A report that served nothing for its symbol (`listingFateReport`): what
|
|
259
|
+
* became of the symbol is the one reason it is empty, said once in place of
|
|
260
|
+
* every table's "No data." and of the load's errors; the errors alone when
|
|
261
|
+
* nothing became of it.
|
|
262
|
+
*/
|
|
263
|
+
export function listingFateEmpty(fate: ListingFate, errors: readonly string[]): { empty?: string; errors: string[] } {
|
|
264
|
+
const report = listingFateReport(fate, { served: false }, errors);
|
|
265
|
+
return report.notes.length ? { empty: report.notes.join(" "), errors: report.errors } : { errors: report.errors };
|
|
266
|
+
}
|
|
267
|
+
|
|
219
268
|
/**
|
|
220
269
|
* What a report or loader with nothing of its own to serve for a symbol
|
|
221
270
|
* throws when the symbol's company moved (`ListingFate.moved`): its message
|
|
@@ -38,6 +38,7 @@ import { readTerminalListingCode } from "../terminal-codes";
|
|
|
38
38
|
import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
|
|
39
39
|
import { settleWithin } from "../../utils/async-deadline";
|
|
40
40
|
import { companyNameExtraWords } from "../company-name";
|
|
41
|
+
import { delistingLabel } from "../../market-data/quotes/delisting";
|
|
41
42
|
export type {
|
|
42
43
|
ResolvedTickerSearch,
|
|
43
44
|
TickerSearchCandidate,
|
|
@@ -58,6 +59,17 @@ const MAX_NAMED_LISTINGS = 6;
|
|
|
58
59
|
*/
|
|
59
60
|
export const LISTING_SEARCH_LIMIT = 30;
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A listing's name as a choice between listings gives it: a listing that no
|
|
64
|
+
* longer trades says so and when ("First Republic Bank · Last traded Apr 28,
|
|
65
|
+
* 2023"), and drops a name that only repeats its symbol.
|
|
66
|
+
*/
|
|
67
|
+
function listingChoiceName(result: InstrumentSearchResult): string {
|
|
68
|
+
if (!result.delisting) return result.name;
|
|
69
|
+
const name = result.name && normalizeTickerSymbol(result.name) !== normalizeTickerSymbol(result.symbol) ? result.name : "";
|
|
70
|
+
return [name, delistingLabel(result.delisting)].filter(Boolean).join(" · ");
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export class AmbiguousTickerError extends Error {
|
|
62
74
|
constructor(readonly query: string, readonly listings: readonly string[], readonly listingNames: Readonly<Record<string, string>> = {}) {
|
|
63
75
|
// Every listing is a choice: a list cut short could leave out the one meant.
|
|
@@ -467,7 +479,7 @@ async function resolveTickerSymbol(
|
|
|
467
479
|
}
|
|
468
480
|
const listings = new Set(matches.map((item) => publicTickerKey(item.symbol, listingExchange(item.result!))));
|
|
469
481
|
const ambiguity = (query: string) => new AmbiguousTickerError(query, [...listings], Object.fromEntries(matches.map((item) => [
|
|
470
|
-
publicTickerKey(item.symbol, listingExchange(item.result!)), item.result
|
|
482
|
+
publicTickerKey(item.symbol, listingExchange(item.result!)), listingChoiceName(item.result!),
|
|
471
483
|
])));
|
|
472
484
|
if (pickerQuery && listings.size > 1) throw ambiguity(pickerQuery);
|
|
473
485
|
// A code that names several venues (TSE) the symbol trades on more than one of is a choice.
|
|
@@ -485,7 +497,12 @@ async function resolveTickerSymbol(
|
|
|
485
497
|
});
|
|
486
498
|
const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
487
499
|
const currency = resolveCurrencyUnit(quote.currency).currency;
|
|
488
|
-
|
|
500
|
+
const ended = quote.delisting;
|
|
501
|
+
if (ended) {
|
|
502
|
+
// The symbol's own quote is a US listing that no longer trades (a bare FRC, whose
|
|
503
|
+
// venue is not on record): it is the run search offers with the same last session.
|
|
504
|
+
verified = matches.filter((item) => item.result?.delisting?.lastTradeDate === ended.lastTradeDate);
|
|
505
|
+
} else if (exchange && currency && Number.isFinite(quote.price) && quote.price !== 0
|
|
489
506
|
&& Number.isFinite(quote.lastUpdated) && quote.lastUpdated > 0) {
|
|
490
507
|
const sameSecurity = matches.filter((item) => {
|
|
491
508
|
const result = item.result!;
|
|
@@ -663,7 +680,11 @@ async function searchProviderResults(
|
|
|
663
680
|
|
|
664
681
|
// A sector word ("defense", which the server answers with rows tagged with it) lists its small companies by
|
|
665
682
|
// name; the server orders "defense ETF" by size, which a name search does not. Those funds lead the list.
|
|
666
|
-
|
|
683
|
+
// So do they when the funds are asked for by name: "defense etf" searches "defense", whose answer holds
|
|
684
|
+
// only the few funds tagged with it (not ITA), and "gold miners etf" a phrase no symbol is.
|
|
685
|
+
const fundsAsked = assetClass?.code === "ETF" || assetClass?.code === "FUND";
|
|
686
|
+
const theme = withThemeFunds && (!assetClass || fundsAsked)
|
|
687
|
+
&& (themeQueryWord([...byKey.values()], query) || (fundsAsked && /\s/.test(query.trim())));
|
|
667
688
|
if (theme) {
|
|
668
689
|
try {
|
|
669
690
|
const { onPartial: _ignored, ...context } = searchContext ?? {};
|
|
@@ -123,6 +123,41 @@ export function resolvePriceComparison(
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The bars a compared leg pans and zooms over: inside the view only its shared
|
|
128
|
+
* dates, as the chart shows them, and every loaded bar past either edge of the
|
|
129
|
+
* view. Clipping the whole buffer to the view left a pan or a zoom out nothing
|
|
130
|
+
* to move into until the next fetch, and none at all toward newer bars.
|
|
131
|
+
*/
|
|
132
|
+
export function comparedBufferPoints(
|
|
133
|
+
points: readonly TimeSeriesPoint[],
|
|
134
|
+
shared: { start: number | null; end: number | null },
|
|
135
|
+
view: { start: number | null; end: number | null },
|
|
136
|
+
): TimeSeriesPoint[] {
|
|
137
|
+
const { start, end } = shared;
|
|
138
|
+
if (start === null || end === null) return [];
|
|
139
|
+
const before = view.start === null ? Number.NEGATIVE_INFINITY : Math.min(start, view.start);
|
|
140
|
+
const after = view.end === null ? Number.POSITIVE_INFINITY : Math.max(end, view.end);
|
|
141
|
+
return points.filter((point) => {
|
|
142
|
+
const time = point.date.getTime();
|
|
143
|
+
return time < before || time > after || (time >= start && time <= end);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A compared leg's pan buffer around `view`; see comparedBufferPoints. */
|
|
148
|
+
export function bufferPriceComparison(
|
|
149
|
+
series: ResolvedSeries,
|
|
150
|
+
comparison: PriceComparison | null,
|
|
151
|
+
view: { start: number | null; end: number | null },
|
|
152
|
+
): ResolvedSeries {
|
|
153
|
+
if (!comparison?.seriesIds.includes(series.id)) return series;
|
|
154
|
+
return {
|
|
155
|
+
...series,
|
|
156
|
+
latestChangePercent: undefined,
|
|
157
|
+
points: comparedBufferPoints(series.points, priceComparisonBoundsForSeries(series, comparison)!, view),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
126
161
|
/** Clip only compared legs; raw observations and study inputs stay available in the source cache. */
|
|
127
162
|
export function clipPriceComparison(series: ResolvedSeries, comparison: PriceComparison | null): ResolvedSeries {
|
|
128
163
|
if (!comparison?.seriesIds.includes(series.id)) return series;
|