@gloom-sh/terminal 0.19.2 → 0.19.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/README.md +7 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +20 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +92 -23
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +9 -4
- package/src/cli/commands/search.ts +71 -12
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +5 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +154 -4
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +40 -5
- package/src/cli/listing-arg.ts +46 -9
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +20 -0
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +24 -1
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/header.tsx +8 -3
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +7 -2
- package/src/components/layout/pane/footer/model.ts +40 -5
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +54 -23
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +8 -1
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +34 -31
- package/src/data/sqlite/access.ts +19 -8
- package/src/data/sqlite/checkpointer.ts +95 -8
- package/src/data/sqlite/database.ts +40 -7
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/index.tsx +1 -2
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +2 -0
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +15 -2
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/index.tsx +7 -1
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +116 -13
- package/src/plugins/builtin/thirteenf/data.ts +132 -30
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
- package/src/plugins/builtin/thirteenf/types.ts +12 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +15 -4
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +2 -0
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +7 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +12 -7
- package/src/telemetry/stall-reports.ts +34 -5
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name.ts +19 -0
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +13 -2
- package/src/tickers/search/index.ts +4 -1
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +55 -6
- package/src/time-series/resolve.ts +12 -3
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +22 -0
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +11 -0
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/main-thread-stall.ts +75 -3
- package/src/utils/perf-marks.ts +36 -2
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/renderers/electrobun/launcher/preload.ts +0 -13
- package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
- package/src/time-series/studies.ts +0 -1628
|
@@ -42,6 +42,15 @@ export async function noListingCalendarNote(
|
|
|
42
42
|
return `${lead}; ADR date ${isoDate(event.earningsDate)} (estimate)`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* "BG, FPI: no earnings date found." for the listings at home a calendar came
|
|
47
|
+
* back without, each named once; null when it dated them all.
|
|
48
|
+
*/
|
|
49
|
+
export function undatedListingsNote(listings: readonly Pick<CliListing, "key">[]): string | null {
|
|
50
|
+
const keys = [...new Set(listings.map((listing) => listing.key))];
|
|
51
|
+
return keys.length > 0 ? `${keys.join(", ")}: no earnings date found.` : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
/**
|
|
46
55
|
* "SAN = Banco Santander, NYSE (different companies on EPA, LSE)" for a bare symbol whose
|
|
47
56
|
* calendar date is its US line's, when several companies list the symbol:
|
package/src/cli/entry.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import "./opentui-libc";
|
|
2
2
|
import { inferCliErrorOptions, printCliError } from "./errors";
|
|
3
3
|
import { isCliHelpFlag } from "./options";
|
|
4
|
-
import { plainByDefault, setCliPlainOverride } from "../utils/cli-output";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { cliAsciiEnabled, plainByDefault, setCliPlainOverride } from "../utils/cli-output";
|
|
5
|
+
import { localeMayNotReadUtf8 } from "../utils/ascii-text";
|
|
6
|
+
import { writeAsciiWhile } from "./ascii-output";
|
|
7
7
|
import { measurePerfAsync } from "../utils/perf-marks";
|
|
8
|
-
import { flushCrashReports, reportCrash } from "../telemetry/crash-reports";
|
|
9
|
-
import { CRASH_REPORT_EXIT_FLUSH_MS } from "../telemetry/crash-reports-node";
|
|
10
8
|
import type { CliLaunchRequest } from "../types/plugin";
|
|
11
9
|
import { detectUpdateAction } from "../updater";
|
|
12
10
|
import { linkStandaloneCommand } from "../updater/command-names";
|
|
@@ -24,6 +22,8 @@ import {
|
|
|
24
22
|
async function launchOpenTuiApp(cliLaunchRequest: CliLaunchRequest | null = null): Promise<void> {
|
|
25
23
|
// Before anything in the app can reach for the render library, so a temp
|
|
26
24
|
// folder that cannot run it is worked around (or named) first.
|
|
25
|
+
const { restoreExtractedPlugins } = await import("./restore-plugins");
|
|
26
|
+
const { loadExternalPlugins } = await import("../plugins/loader");
|
|
27
27
|
const { loadRenderLib } = await import("../renderers/opentui/render-lib");
|
|
28
28
|
await measurePerfAsync("startup.opentui.render-lib", loadRenderLib);
|
|
29
29
|
const { startOpenTuiApp } = await import("../renderers/opentui/start");
|
|
@@ -36,6 +36,8 @@ async function launchOpenTuiApp(cliLaunchRequest: CliLaunchRequest | null = null
|
|
|
36
36
|
} catch (error) {
|
|
37
37
|
// A failed launch still prints and exits as before; the report is only
|
|
38
38
|
// sent when the config was read far enough to install the reporter.
|
|
39
|
+
const { flushCrashReports, reportCrash } = await import("../telemetry/crash-reports");
|
|
40
|
+
const { CRASH_REPORT_EXIT_FLUSH_MS } = await import("../telemetry/crash-reports-node");
|
|
39
41
|
reportCrash(error, { kind: "uncaught" });
|
|
40
42
|
await flushCrashReports({ timeoutMs: CRASH_REPORT_EXIT_FLUSH_MS });
|
|
41
43
|
throw error;
|
|
@@ -74,21 +76,25 @@ export async function runCliEntrypoint(rawArgs = process.argv.slice(2)): Promise
|
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
//
|
|
78
|
-
// their modules are a noticeable part of what it would load before drawing.
|
|
79
|
-
const { dispatchCli, failUnknownCliCommand } = await import("./index");
|
|
80
|
-
// For a screen reader, a pipe or NO_COLOR: no rule lines, arrows in words, fixed table columns. Set
|
|
81
|
-
// for this process only; `--plain` / `--no-plain` decide it in dispatchCli, and commands run in-process keep theirs.
|
|
79
|
+
// For a screen reader, a pipe or NO_COLOR: no rule lines, arrows in words, fixed table columns.
|
|
82
80
|
setCliPlainOverride(plainByDefault());
|
|
83
|
-
|
|
84
|
-
const
|
|
81
|
+
if (inferCliErrorOptions(rawArgs).format === "text" && localeMayNotReadUtf8()) writeAsciiWhile(cliAsciiEnabled);
|
|
82
|
+
const { dispatchEarlyCommand } = await import("./early-command");
|
|
83
|
+
if (await dispatchEarlyCommand(rawArgs)) return;
|
|
84
|
+
|
|
85
|
+
const { dispatchCli, failUnknownCliCommand } = await import("./index");
|
|
86
|
+
let plugins: Promise<import("../plugins/loader").LoadedExternalPlugin[]> | undefined;
|
|
87
|
+
const options = {
|
|
88
|
+
loadExternalPlugins: () => plugins ??= import("../plugins/loader").then(({ loadExternalPlugins }) => loadExternalPlugins()),
|
|
89
|
+
};
|
|
90
|
+
const dispatchResult = await dispatchCli(rawArgs, options);
|
|
85
91
|
if (dispatchResult.kind === "handled") return;
|
|
86
92
|
if (dispatchResult.kind === "launch-ui") {
|
|
87
93
|
await launchOpenTuiApp(dispatchResult.request);
|
|
88
94
|
return;
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
await failUnknownCliCommand(rawArgs,
|
|
97
|
+
await failUnknownCliCommand(rawArgs, options);
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
runCliEntrypoint().catch((error) => {
|
package/src/cli/errors.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { AppPersistence } from "../data/app-persistence";
|
|
2
|
-
import { ApiRequestError } from "../api-client/errors";
|
|
2
|
+
import { ApiRequestError, wasAskedTwice } from "../api-client/errors";
|
|
3
3
|
import { DEFAULT_CLI_OPTIONS, type CliGlobalOptions } from "./options";
|
|
4
4
|
import { serializeCliError, type CliErrorObject } from "./result";
|
|
5
5
|
import { cliStyles, cliTerminalWidth, wrapText } from "../utils/cli-output";
|
|
6
6
|
import { withSearchHints, type FredSeriesCheck } from "./not-a-ticker";
|
|
7
|
-
import { isProviderBusy, SEARCH_BUSY_CODE, SEARCH_BUSY_MESSAGE } from "../sources/provider-errors";
|
|
7
|
+
import { isProviderBusy, isSourceFailure, SEARCH_BUSY_CODE, SEARCH_BUSY_MESSAGE, serviceDidNotAnswerMessage } from "../sources/provider-errors";
|
|
8
8
|
import { DATA_SERVICE_UNREACHABLE_MESSAGE, isNetworkFailure } from "../utils/network-failure";
|
|
9
9
|
|
|
10
10
|
const USAGE_PREFIX = "Usage: ";
|
|
@@ -14,6 +14,8 @@ export const CLI_ERROR_CODE = "cli_error";
|
|
|
14
14
|
export const NO_DATA_CODE = "no_data";
|
|
15
15
|
/** The code of a command whose requests never reached the data service. */
|
|
16
16
|
const SERVICE_UNREACHABLE_CODE = "service_unreachable";
|
|
17
|
+
/** The code of a command whose data no source could serve just now, one having failed rather than had none. */
|
|
18
|
+
const SOURCE_UNAVAILABLE_CODE = "source_unavailable";
|
|
17
19
|
|
|
18
20
|
/** What to check when nothing answered: the connection, and the proxy when one is set. */
|
|
19
21
|
function unreachableHint(): string {
|
|
@@ -121,6 +123,14 @@ function cliErrorObject(error: unknown): CliErrorObject {
|
|
|
121
123
|
if (isNetworkFailure(error)) {
|
|
122
124
|
return unreachableError();
|
|
123
125
|
}
|
|
126
|
+
// A source failed (a timeout, a rate limit) where none had the data to answer: asked again it may.
|
|
127
|
+
if (isSourceFailure(error)) {
|
|
128
|
+
return { code: SOURCE_UNAVAILABLE_CODE, message: error instanceof Error ? error.message : String(error), retryable: true };
|
|
129
|
+
}
|
|
130
|
+
// A listing read the data service failed to answer twice: said in Gloom's words. Any other 5xx keeps the server's.
|
|
131
|
+
if (wasAskedTwice(error)) {
|
|
132
|
+
return { code: SOURCE_UNAVAILABLE_CODE, message: serviceDidNotAnswerMessage(), retryable: true };
|
|
133
|
+
}
|
|
124
134
|
// Gloom Cloud refused what was asked (an unknown series, a bad date): the input was wrong, not the program.
|
|
125
135
|
if (error instanceof ApiRequestError && error.status === 400) {
|
|
126
136
|
return { code: CLI_ERROR_CODE, message: error.message };
|
package/src/cli/help.ts
CHANGED
|
@@ -54,7 +54,7 @@ const GLOBAL_OPTIONS: Array<[string, string]> = [
|
|
|
54
54
|
["-q, --quiet", "Print no results or errors in text mode, for commands that print results"],
|
|
55
55
|
["--color, --no-color", "Force or turn off colors (NO_COLOR also turns them off)"],
|
|
56
56
|
["--plain, --no-plain", "Plain text for a screen reader or large text: no rule lines, arrows as words, every table column kept with a dash in empty cells. On by default when NO_COLOR is set or output is piped; --no-plain turns it off"],
|
|
57
|
-
["--width <n>", "Fit tables to n columns (default: the terminal width; piped output is not fitted). shot takes its own --width in pixels"],
|
|
57
|
+
["--width <n>", "Fit tables to n columns (default: the terminal width; piped output fits COLUMNS when set, else is not fitted). shot takes its own --width in pixels"],
|
|
58
58
|
];
|
|
59
59
|
|
|
60
60
|
const EXIT_CODES: Array<[string, string]> = [
|
|
@@ -21,7 +21,7 @@ export interface HistoryListRow extends HistoryRow {
|
|
|
21
21
|
type ExportedRow = Record<string, unknown>;
|
|
22
22
|
|
|
23
23
|
/** A bar's mark as an export writes it: true or false on a bar, nothing on a symbol's row with no bar. */
|
|
24
|
-
function barMark(key: "partial" | "filled" | "unsettled") {
|
|
24
|
+
function barMark(key: "partial" | "filled" | "unsettled" | "roll" | "thin") {
|
|
25
25
|
return (row: ExportedRow) => row.date ? row[key] === true : "";
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -32,7 +32,7 @@ function barMark(key: "partial" | "filled" | "unsettled") {
|
|
|
32
32
|
* whether or not any bar of this run has one, so files of different symbols
|
|
33
33
|
* or days line up when joined.
|
|
34
34
|
*/
|
|
35
|
-
|
|
35
|
+
const HISTORY_EXPORT_COLUMNS: CliResultColumn<ExportedRow>[] = [
|
|
36
36
|
{ key: "date", header: "Date" },
|
|
37
37
|
{ key: "open", header: "Open" },
|
|
38
38
|
{ key: "high", header: "High" },
|
|
@@ -47,8 +47,28 @@ export const HISTORY_EXPORT_COLUMNS: CliResultColumn<ExportedRow>[] = [
|
|
|
47
47
|
{ key: "volumeNote", header: "Volume note" },
|
|
48
48
|
{ key: "venueVolume", header: "Venue volume" },
|
|
49
49
|
{ key: "volumeVenue", header: "Volume venue" },
|
|
50
|
+
{ key: "volumeBasis", header: "Volume basis" },
|
|
50
51
|
];
|
|
51
52
|
|
|
53
|
+
/** A continuous future's contract per bar, the bar it rolled at, and a bar that traded far below those around it. */
|
|
54
|
+
const CONTRACT_EXPORT_COLUMNS: CliResultColumn<ExportedRow>[] = [
|
|
55
|
+
{ key: "contract", header: "Contract" },
|
|
56
|
+
{ key: "roll", header: "Roll", value: barMark("roll") },
|
|
57
|
+
{ key: "thin", header: "Thin", value: barMark("thin") },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The columns of an export of these rows: HISTORY_EXPORT_COLUMNS, and when a
|
|
62
|
+
* bar names its contract (a continuous future such as NG=F) Contract, Roll and
|
|
63
|
+
* Thin after them, for every row of the export. A stock's or a coin's file is
|
|
64
|
+
* as it always was.
|
|
65
|
+
*/
|
|
66
|
+
export function historyExportColumns(rows: readonly Pick<HistoryRow, "contract">[]): CliResultColumn<ExportedRow>[] {
|
|
67
|
+
return rows.some((row) => typeof row.contract === "string" && row.contract !== "")
|
|
68
|
+
? [...HISTORY_EXPORT_COLUMNS, ...CONTRACT_EXPORT_COLUMNS]
|
|
69
|
+
: HISTORY_EXPORT_COLUMNS;
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
/** Flags of a break before a bar, whose prices stay; every other code left the prices blank. */
|
|
53
73
|
const BREAK_FLAGS = new Set(["gap", "jump"]);
|
|
54
74
|
|
package/src/cli/history-rows.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isWickFlag, latestTradeIndex, missingVolumeNotice, pricePointIntegrityFlags, pricePointValues, pricePointWickFlags,
|
|
19
19
|
} from "../utils/price-history-integrity";
|
|
20
20
|
import { formatUtcTime } from "../utils/utc-time";
|
|
21
|
+
import { maxDrawdown, type MaxDrawdown } from "../utils/max-drawdown";
|
|
21
22
|
import { historyCoverageNotice } from "../sources/history-coverage";
|
|
22
23
|
import { barLastSession, barPeriodToDate, type BarPeriodToDate } from "../market-data/market/bar-period";
|
|
23
24
|
import { barSizeFromGap, medianBarGap } from "../time-series/bar-spacing";
|
|
@@ -81,6 +82,17 @@ export interface HistoryRow {
|
|
|
81
82
|
/** A bar without the market's volume: the trades of one venue alone (`volumeVenue`), never its volume. */
|
|
82
83
|
venueVolume?: number;
|
|
83
84
|
volumeVenue?: string;
|
|
85
|
+
/**
|
|
86
|
+
* What the bar's volume counts, where the source says: `aggregate` across venues on a coin's daily bar,
|
|
87
|
+
* `3 spot venues` on its intraday bar, whose volume is summed over those venues.
|
|
88
|
+
*/
|
|
89
|
+
volumeBasis?: string;
|
|
90
|
+
/** A continuous future's bar (NG=F): the contract it closes on (NGZ26), where the source says. */
|
|
91
|
+
contract?: string;
|
|
92
|
+
/** The first bar on a contract after another's: the series rolled into `contract` at this bar. */
|
|
93
|
+
roll?: true;
|
|
94
|
+
/** The bar traded far below the bars around it (THIN_VOLUME_SHARE of their median): a price on few trades. */
|
|
95
|
+
thin?: true;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
/** A coin's volume: traded value in its quote currency, per UTC day on daily bars. */
|
|
@@ -306,6 +318,7 @@ export function historyRows(
|
|
|
306
318
|
...(values.volume == null && point.venueVolume
|
|
307
319
|
? { venueVolume: point.venueVolume.volume, volumeVenue: point.venueVolume.venue }
|
|
308
320
|
: {}),
|
|
321
|
+
...(values.volume != null && point.volumeBasis ? { volumeBasis: point.volumeBasis } : {}),
|
|
309
322
|
// A coin trades every day, so a bar without volume says why.
|
|
310
323
|
...(volume && values.volume == null
|
|
311
324
|
? {
|
|
@@ -316,12 +329,136 @@ export function historyRows(
|
|
|
316
329
|
};
|
|
317
330
|
});
|
|
318
331
|
const breaks = historyBreakFlags(times, rows.map((row) => row.close), interval);
|
|
332
|
+
const thin = thinVolumeBars(rows.map((row, index) => ({ ...row, contract: points[index]!.contract })));
|
|
333
|
+
let contract: string | null = null;
|
|
319
334
|
return rows.map((row, index) => {
|
|
320
335
|
const flags = [...flagCodes(row), ...breaks[index]!];
|
|
321
|
-
|
|
336
|
+
const own = points[index]!.contract;
|
|
337
|
+
// A roll is a change between two known contracts; a bar naming none (before the source knew) is no roll.
|
|
338
|
+
const rolled = !!own && contract != null && own !== contract;
|
|
339
|
+
if (own) contract = own;
|
|
340
|
+
const marked = {
|
|
341
|
+
...row,
|
|
342
|
+
...(own ? { contract: own } : {}),
|
|
343
|
+
...(rolled ? { roll: true as const } : {}),
|
|
344
|
+
...(thin[index] ? { thin: true as const } : {}),
|
|
345
|
+
};
|
|
346
|
+
return flags.length > 0 ? { ...marked, flag: flags.join(",") } : marked;
|
|
322
347
|
});
|
|
323
348
|
}
|
|
324
349
|
|
|
350
|
+
/** Bars on each side of a bar whose volumes it is held against. */
|
|
351
|
+
const THIN_NEIGHBOURS = 5;
|
|
352
|
+
/** A bar under this share of its neighbours' median volume traded thin. */
|
|
353
|
+
const THIN_VOLUME_SHARE = 0.05;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* For each bar of a continuous future, whether it traded far below the bars
|
|
357
|
+
* around it: a volume under THIN_VOLUME_SHARE of the median of up to
|
|
358
|
+
* THIN_NEIGHBOURS bars either side that have one (at least four). NG=F's
|
|
359
|
+
* 2026-10-28 bar on 15 lots between days of 150,000 is a price few trades
|
|
360
|
+
* made. Only a bar naming its contract is judged, against others that do: a
|
|
361
|
+
* stock's or a coin's quiet day is not a contract's thin one. A bar still
|
|
362
|
+
* forming, or with no trade at all (no volume, or 0), is not judged.
|
|
363
|
+
*/
|
|
364
|
+
function thinVolumeBars(rows: readonly (Pick<HistoryRow, "volume" | "partial"> & { contract?: string })[]): boolean[] {
|
|
365
|
+
const judged = (row: (typeof rows)[number]): row is (typeof rows)[number] & { volume: number } =>
|
|
366
|
+
!!row.contract && !row.partial && row.volume != null && Number.isFinite(row.volume) && row.volume > 0;
|
|
367
|
+
return rows.map((row, index) => {
|
|
368
|
+
if (!judged(row)) return false;
|
|
369
|
+
const around = [...rows.slice(Math.max(0, index - THIN_NEIGHBOURS), index), ...rows.slice(index + 1, index + 1 + THIN_NEIGHBOURS)]
|
|
370
|
+
.filter(judged)
|
|
371
|
+
.map((other) => other.volume)
|
|
372
|
+
.sort((left, right) => left - right);
|
|
373
|
+
if (around.length < 4) return false;
|
|
374
|
+
const middle = around.length / 2;
|
|
375
|
+
const median = around.length % 2 ? around[Math.floor(middle)]! : (around[middle - 1]! + around[middle]!) / 2;
|
|
376
|
+
return median > 0 && row.volume < THIN_VOLUME_SHARE * median;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** A roll of a continuous future: the bar it rolled at, and the contracts either side. */
|
|
381
|
+
interface HistoryRoll {
|
|
382
|
+
date: string;
|
|
383
|
+
from: string;
|
|
384
|
+
to: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Every roll in a history's bars (`roll`), oldest first. */
|
|
388
|
+
function historyRolls(rows: readonly Pick<HistoryRow, "date" | "contract" | "roll">[]): HistoryRoll[] {
|
|
389
|
+
let contract: string | null = null;
|
|
390
|
+
const rolls: HistoryRoll[] = [];
|
|
391
|
+
for (const row of rows) {
|
|
392
|
+
if (row.roll && contract && row.contract) rolls.push({ date: row.date, from: contract, to: row.contract });
|
|
393
|
+
if (row.contract) contract = row.contract;
|
|
394
|
+
}
|
|
395
|
+
return rolls;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* A history's bars split at every roll, each run one contract's, oldest
|
|
400
|
+
* first: what a drawdown or a change is read within, never across a roll.
|
|
401
|
+
* One run, named by no contract, for a history that names none.
|
|
402
|
+
*/
|
|
403
|
+
function historyContractRuns<Row extends Pick<HistoryRow, "contract" | "roll">>(rows: readonly Row[]): Array<{ contract: string | null; rows: Row[] }> {
|
|
404
|
+
const runs: Array<{ contract: string | null; rows: Row[] }> = [];
|
|
405
|
+
for (const row of rows) {
|
|
406
|
+
const current = runs.at(-1);
|
|
407
|
+
if (!current || row.roll) runs.push({ contract: row.contract ?? null, rows: [row] });
|
|
408
|
+
else {
|
|
409
|
+
current.rows.push(row);
|
|
410
|
+
current.contract ??= row.contract ?? null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return runs;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The largest drawdown of a history's bars within one contract: each run
|
|
418
|
+
* between rolls read on its own (historyContractRuns), the deepest kept and
|
|
419
|
+
* named by its contract when the bars roll. A history naming no contract is
|
|
420
|
+
* one run, its drawdown `maxDrawdown`'s.
|
|
421
|
+
*/
|
|
422
|
+
export function historyContractDrawdown(
|
|
423
|
+
rows: readonly Pick<HistoryRow, "date" | "close" | "contract" | "roll">[],
|
|
424
|
+
): { drawdown: MaxDrawdown; contract: string | null } | null {
|
|
425
|
+
const runs = historyContractRuns(rows);
|
|
426
|
+
let deepest: { drawdown: MaxDrawdown; contract: string | null } | null = null;
|
|
427
|
+
for (const run of runs) {
|
|
428
|
+
const drawdown = maxDrawdown(run.rows);
|
|
429
|
+
if (drawdown && (!deepest || drawdown.drawdown < deepest.drawdown.drawdown)) deepest = { drawdown, contract: runs.length > 1 ? run.contract : null };
|
|
430
|
+
}
|
|
431
|
+
return deepest;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* "NG=F is the front month: rolled from NGX26 to NGZ26 on 2026-10-26 (marked
|
|
436
|
+
* roll)": the rolls a continuous future's bars make, the latest few by date,
|
|
437
|
+
* the rest counted. Null without one.
|
|
438
|
+
*/
|
|
439
|
+
export function historyRollNote(symbol: string, rows: readonly Pick<HistoryRow, "date" | "contract" | "roll">[]): string | null {
|
|
440
|
+
const rolls = historyRolls(rows);
|
|
441
|
+
if (rolls.length === 0) return null;
|
|
442
|
+
const shown = rolls.slice(-LISTED_GAPS);
|
|
443
|
+
const earlier = rolls.length - shown.length;
|
|
444
|
+
const listed = shown.map((roll) => `from ${roll.from} to ${roll.to} on ${historyDate(roll.date)}`);
|
|
445
|
+
return `${symbol} is the front month: ${earlier > 0 ? `${earlier} earlier ${earlier === 1 ? "roll" : "rolls"}, then ` : ""}`
|
|
446
|
+
+ `rolled ${listItems(listed)} (marked roll); drawdowns are read within one contract`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* "2026-10-28 traded thin: under 5% of the volume of the bars around it", the
|
|
451
|
+
* bars marked `thin`, the latest few by date. Null without one.
|
|
452
|
+
*/
|
|
453
|
+
export function historyThinNote(rows: readonly Pick<HistoryRow, "date" | "thin">[]): string | null {
|
|
454
|
+
const thin = rows.filter((row) => row.thin).map((row) => historyDate(row.date));
|
|
455
|
+
if (thin.length === 0) return null;
|
|
456
|
+
const shown = thin.slice(-LISTED_GAPS);
|
|
457
|
+
const earlier = thin.length - shown.length;
|
|
458
|
+
const dates = listItems([...earlier > 0 ? [`${earlier} earlier ${earlier === 1 ? "bar" : "bars"}`] : [], ...shown]);
|
|
459
|
+
return `${dates} traded thin: under ${THIN_VOLUME_SHARE * 100}% of the volume of the bars around ${thin.length === 1 ? "it" : "them"}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
325
462
|
/**
|
|
326
463
|
* The newest weekly or monthly bar while its period has not ended at `now`
|
|
327
464
|
* (barPeriodToDate): marked `partial`, as a bar its source marks is, with
|
|
@@ -369,9 +506,22 @@ export function historyVolume(
|
|
|
369
506
|
return coin && unit.kind === "currency" && unit.currency ? { unit: unit.currency } : null;
|
|
370
507
|
}
|
|
371
508
|
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
|
|
509
|
+
/**
|
|
510
|
+
* `volume in USD per UTC day` for a coin's daily bars, `volume in USD` for others, with what it counts when every bar
|
|
511
|
+
* with a volume says the same (`historyVolumeBasis`): `volume in USD per UTC day, aggregate across venues`, `volume in
|
|
512
|
+
* USD summed over 3 spot venues`.
|
|
513
|
+
*/
|
|
514
|
+
export function historyVolumeText(volume: HistoryVolume, interval: ManualChartResolution | null, basis: string | null = null): string {
|
|
515
|
+
const text = interval === "1d" ? `volume in ${volume.unit} per UTC day` : `volume in ${volume.unit}`;
|
|
516
|
+
if (!basis) return text;
|
|
517
|
+
return basis === "aggregate" ? `${text}, aggregate across venues` : `${text} summed over ${basis}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** What the volume of every bar that has one counts, when they all say the same; null otherwise. */
|
|
521
|
+
export function historyVolumeBasis(rows: readonly Pick<HistoryRow, "volume" | "volumeBasis">[]): string | null {
|
|
522
|
+
const bases = new Set(rows.filter((row) => row.volume != null).map((row) => row.volumeBasis ?? null));
|
|
523
|
+
const [basis] = bases.size === 1 ? [...bases] : [null];
|
|
524
|
+
return basis ?? null;
|
|
375
525
|
}
|
|
376
526
|
|
|
377
527
|
function listDates(dates: readonly string[]): string {
|
package/src/cli/index.ts
CHANGED
|
@@ -408,6 +408,7 @@ const CLI_DESCRIPTION = "Market research and portfolio tracker for the terminal"
|
|
|
408
408
|
|
|
409
409
|
export interface DispatchCliOptions {
|
|
410
410
|
externalPlugins?: LoadedExternalPlugin[];
|
|
411
|
+
loadExternalPlugins?: () => Promise<LoadedExternalPlugin[]>;
|
|
411
412
|
}
|
|
412
413
|
|
|
413
414
|
/** Old names kept so existing scripts still run; help and suggestions leave them out. */
|
|
@@ -430,6 +431,7 @@ async function createRegistry(options: DispatchCliOptions = {}): Promise<CliComm
|
|
|
430
431
|
registry = buildCliCommandRegistry({
|
|
431
432
|
coreCommands,
|
|
432
433
|
externalPlugins: options.externalPlugins ?? [],
|
|
434
|
+
...(options.externalPlugins === undefined ? { loadExternalPlugins: options.loadExternalPlugins } : {}),
|
|
433
435
|
config,
|
|
434
436
|
});
|
|
435
437
|
return registry;
|
|
@@ -456,11 +458,25 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
|
|
|
456
458
|
}
|
|
457
459
|
|
|
458
460
|
const registry = await createRegistry(options);
|
|
459
|
-
const
|
|
461
|
+
const token = normalizeCliCommandToken(foldTypedText(command));
|
|
462
|
+
let resolved = registry.lookup.get(token);
|
|
463
|
+
if (!resolved) {
|
|
464
|
+
await registry.ensureExternalPlugins?.();
|
|
465
|
+
resolved = registry.lookup.get(token);
|
|
466
|
+
}
|
|
460
467
|
if (!resolved) {
|
|
461
468
|
return { kind: "unhandled" };
|
|
462
469
|
}
|
|
463
470
|
|
|
471
|
+
// The overview includes installed commands; one built-in command's help only
|
|
472
|
+
// needs that command. An unknown help topic may belong to an installed plugin.
|
|
473
|
+
if (resolved.command.name === "help") {
|
|
474
|
+
const topic = parsed.args[1];
|
|
475
|
+
if (!topic || !registry.lookup.has(normalizeCliCommandToken(foldTypedText(topic)))) {
|
|
476
|
+
await registry.ensureExternalPlugins?.();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
464
480
|
// Help flags never reach the command, where they would read as a symbol, a
|
|
465
481
|
// plugin name, or a note to write.
|
|
466
482
|
const helpOnly = parsed.help && resolved.command.name !== "help";
|
|
@@ -500,6 +516,7 @@ export async function failUnknownCliCommand(args: string[], options: DispatchCli
|
|
|
500
516
|
// Intentional ignore: dispatchCli already reports invalid flags.
|
|
501
517
|
}
|
|
502
518
|
const registry = await createRegistry(options);
|
|
519
|
+
await registry.ensureExternalPlugins?.();
|
|
503
520
|
const commands = listedCommands(registry).map((entry) => entry.command);
|
|
504
521
|
// A function's code typed as a command is pointed at `fn`, and a word the catalog knows at its
|
|
505
522
|
// functions, with the arguments the reader gave.
|
package/src/cli/known-gaps.ts
CHANGED
|
@@ -87,8 +87,15 @@ export const MUNI_LINE = "Municipal bonds: no AAA muni curve (MMD and BVAL are l
|
|
|
87
87
|
* `tickers` are terms that are also a real ticker (BDI is Black Diamond Group):
|
|
88
88
|
* a query that is only that word still gets the line, but `quote` adds it only
|
|
89
89
|
* to the exchange question or the failure of the bare symbol, never to a quote.
|
|
90
|
+
*
|
|
91
|
+
* An entry with `words` matches on tokens, not only the whole query: a query
|
|
92
|
+
* holding one of its terms, every other word a term or one of `words`
|
|
93
|
+
* ("panamax rates", "bulk freight", "freight cost"). A word outside both keeps
|
|
94
|
+
* it a different question ("freight forwarder stocks", "ocean freight
|
|
95
|
+
* carriers"). An entry without `words` matches the whole query alone, since
|
|
96
|
+
* its terms are words of company names too ("carbon", "second").
|
|
90
97
|
*/
|
|
91
|
-
const KNOWN_GAPS: ReadonlyArray<{ terms: readonly string[]; tickers?: readonly string[]; message: string }> = [
|
|
98
|
+
const KNOWN_GAPS: ReadonlyArray<{ terms: readonly string[]; words?: readonly string[]; tickers?: readonly string[]; message: string }> = [
|
|
92
99
|
{
|
|
93
100
|
terms: ["EUA", "EUAs", "EU ETS", "EU allowance", "EU allowances", "EU carbon", "carbon", "carbon allowance", "carbon allowances",
|
|
94
101
|
"carbon price", "carbon prices", "carbon credits", "emission allowances", "emissions allowances"],
|
|
@@ -107,8 +114,12 @@ const KNOWN_GAPS: ReadonlyArray<{ terms: readonly string[]; tickers?: readonly s
|
|
|
107
114
|
{
|
|
108
115
|
terms: ["Baltic", "Baltic Dry", "Baltic Dry Index", "Baltic Exchange", "Baltic index", "Baltic indices", "BDI", "BDIY", "freight",
|
|
109
116
|
"freight rate", "freight rates", "freight index", "freight futures", "freight forward", "freight forwards", "freight forward agreement",
|
|
110
|
-
"freight forward agreements", "FFA", "FFAs", "capesize", "panamax", "supramax", "
|
|
111
|
-
"dry bulk rates", "dry bulk freight"
|
|
117
|
+
"freight forward agreements", "FFA", "FFAs", "capesize", "panamax", "supramax", "handysize", "kamsarmax", "newcastlemax",
|
|
118
|
+
"charter rate", "charter rates", "dry bulk", "dry bulk rates", "dry bulk freight", "shipping rate", "shipping rates", "shipping cost",
|
|
119
|
+
"shipping costs", "time charter", "TCE"],
|
|
120
|
+
words: ["rate", "rates", "cost", "costs", "price", "prices", "index", "indices", "indexes", "level", "levels", "chart", "charts",
|
|
121
|
+
"history", "historical", "data", "spot", "today", "current", "latest", "daily", "weekly", "market", "markets", "bulk", "dry",
|
|
122
|
+
"bulker", "bulkers", "vessel", "vessels", "ship", "ships", "route", "routes", "earnings", "equivalent", "futures", "the", "of", "for"],
|
|
112
123
|
tickers: ["BDI"],
|
|
113
124
|
message: "Baltic indices (BDI, Capesize, Panamax, Supramax) and freight forwards (FFA) are not covered. Traded proxies: BDRY (freight futures ETF), "
|
|
114
125
|
+ "SBLK, GNK, DSX (dry-bulk shipowners); chokepoint transits: fn CHOKE; news \"dry bulk\".",
|
|
@@ -326,6 +337,14 @@ const COMMAND_POINTERS: ReadonlyArray<{
|
|
|
326
337
|
"change", "ranking", "rankings", "dashboard", "lending", "staking"],
|
|
327
338
|
line: "DeFi TVL, top protocols and chains with 7D and 30D change: gloom fn CRYP --list defi (by chain: --by chain)",
|
|
328
339
|
},
|
|
340
|
+
{
|
|
341
|
+
// Ahead of DIST, which "difficulty" finds by its description; the four series are listed after it.
|
|
342
|
+
names: ["hashrate", "hash rate", "hashprice", "hash price", "difficulty", "retarget", "mining", "miner", "miners"],
|
|
343
|
+
words: ["bitcoin", "btc", "network", "crypto", "stock", "stocks", "equities", "listed", "next",
|
|
344
|
+
"estimate", "adjustment", "chart", "history"],
|
|
345
|
+
line: "Bitcoin network: gloom fn G hashrate, difficulty or hashprice; listed miners: gloom fn THEM crypto",
|
|
346
|
+
leads: true,
|
|
347
|
+
},
|
|
329
348
|
];
|
|
330
349
|
|
|
331
350
|
/** The command for a query that names a setting ("light theme") or a portfolio report ("drip"), else null. */
|
|
@@ -398,7 +417,23 @@ export function knownGapForTicker(symbol: string): string | null {
|
|
|
398
417
|
}
|
|
399
418
|
|
|
400
419
|
/**
|
|
401
|
-
*
|
|
420
|
+
* Whether a query names a known subject: as a whole ("Baltic Dry", "dry-bulk"), or, for an entry with `words`,
|
|
421
|
+
* by a term among words that only qualify it ("panamax rates", "bulk freight").
|
|
422
|
+
*/
|
|
423
|
+
function namesKnownGap(gap: (typeof KNOWN_GAPS)[number], query: string): boolean {
|
|
424
|
+
const key = lookupKey(query);
|
|
425
|
+
if (gap.terms.some((term) => lookupKey(term) === key)) return true;
|
|
426
|
+
if (!gap.words) return false;
|
|
427
|
+
const asked = words(query);
|
|
428
|
+
const named = new Set(gap.terms.flatMap((term) => {
|
|
429
|
+
const parts = words(term);
|
|
430
|
+
return containsWords(asked, parts) ? parts : [];
|
|
431
|
+
}));
|
|
432
|
+
return named.size > 0 && asked.every((word) => named.has(word) || gap.words!.includes(word));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The line for a query that names a known subject ("carbon allowances", "EUA", "panamax rates"), a Treasury yield
|
|
402
437
|
* typed as a ticker (US10Y), a topic with no function ("defense budget"), or a macro release no series carries
|
|
403
438
|
* ("ISM manufacturing", "GDPNow"), else null.
|
|
404
439
|
*/
|
|
@@ -406,7 +441,7 @@ export function knownGapMessage(query: string): string | null {
|
|
|
406
441
|
const key = lookupKey(query);
|
|
407
442
|
if (!key) return null;
|
|
408
443
|
const tenor = treasuryYieldTenor(query);
|
|
409
|
-
return KNOWN_GAPS.find((gap) => gap
|
|
444
|
+
return KNOWN_GAPS.find((gap) => namesKnownGap(gap, query))?.message
|
|
410
445
|
?? (tenor ? treasuryYieldLine(tenor) : null)
|
|
411
446
|
?? topicPointer(query)?.message
|
|
412
447
|
?? macroReleaseTopic(query)?.line ?? null;
|
package/src/cli/listing-arg.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
|
|
18
18
|
import { findListingOfCompanyName, isCompanyNameInput, marksAnotherListing, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
|
|
19
19
|
import { movedAwayChange, startTickerChangeLookup, type MovedTickerChange } from "../tickers/ticker-changes";
|
|
20
|
-
import { lineMayHaveMoved, loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
|
|
20
|
+
import { lineMayHaveMoved, listingChangeLine, loadListingFate, MovedSymbolError, SUCCESSOR_CHANGE_WAIT_MS, type ListingFate } from "../tickers/listing-fate";
|
|
21
21
|
import { quotedListingName } from "../tickers/listing-name";
|
|
22
22
|
import { cliStyles } from "../utils/cli-output";
|
|
23
23
|
import { groupByCompany, namesDepositaryReceipt, sameCompany, sameListedName, shortCompanyName } from "../tickers/company-name";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
canonicalExchange,
|
|
26
26
|
exchangeCodeVenues,
|
|
27
27
|
exchangeLabel,
|
|
28
|
+
exchangeName,
|
|
28
29
|
isKnownExchangeCode,
|
|
29
30
|
isSharedExchangeCode,
|
|
30
31
|
isUsListingExchange,
|
|
@@ -36,7 +37,7 @@ import { withMarketData } from "./scoped-context";
|
|
|
36
37
|
import { failWithFields } from "./errors";
|
|
37
38
|
import { stripStrayEdges } from "./symbol-args";
|
|
38
39
|
import { isNoProviderMessage, isNoSourceAnswer, isProviderBusy } from "../sources/provider-errors";
|
|
39
|
-
import { dashedCoinPair, exchangeLineCoinListing, isSpotCommodityOrCurrencyPair, MAJOR_COIN_NAMES } from "../utils/crypto-pair";
|
|
40
|
+
import { dashedCoinPair, exchangeLineCoinListing, isSpotCommodityOrCurrencyPair, MAJOR_COIN_NAMES, TOP_COIN_NAMES } from "../utils/crypto-pair";
|
|
40
41
|
import { boardCoinFor, primeBoardCoins } from "./board-coins";
|
|
41
42
|
import { knownGapMessage } from "./known-gaps";
|
|
42
43
|
import { currencyPairSpelling } from "../utils/currency-pair";
|
|
@@ -60,7 +61,7 @@ import {
|
|
|
60
61
|
import { listingAbroadVenue } from "../utils/sec";
|
|
61
62
|
import { savedAbroadForBareSymbol, savedUsListingOf } from "../tickers/search/bare-symbol";
|
|
62
63
|
import { listingSuffixCandidate, listingSuffixVenue, tickerHasListingSuffix } from "../sources/listing-symbols";
|
|
63
|
-
import { notTheCoinNote } from "./commands/crypto-hints";
|
|
64
|
+
import { coinElsewhereNote, notTheCoinNote } from "./commands/crypto-hints";
|
|
64
65
|
|
|
65
66
|
/** The `--exchange` option of every command that takes a ticker. */
|
|
66
67
|
export const EXCHANGE_OPTION = {
|
|
@@ -414,9 +415,15 @@ export function listingChoiceNote(
|
|
|
414
415
|
.filter((exchange) => exchange !== chosen.exchange);
|
|
415
416
|
if (others.length === 0) return null;
|
|
416
417
|
const more = others.length - MAX_OTHER_LISTINGS;
|
|
417
|
-
const named = `${others.slice(0, MAX_OTHER_LISTINGS).join(", ")}${more > 0 ? ` and ${more} more: gloom search ${symbol}` : ""}`;
|
|
418
|
+
const named = `${others.slice(0, MAX_OTHER_LISTINGS).map(venueInProse).join(", ")}${more > 0 ? ` and ${more} more: gloom search ${symbol}` : ""}`;
|
|
418
419
|
const which = others.length === 1 ? "a different company" : "different companies";
|
|
419
|
-
return `${symbol} = ${name ? `${shortCompanyName(name)}, ` : ""}${chosen.exchange} (${which} on ${named})`;
|
|
420
|
+
return `${symbol} = ${name ? `${shortCompanyName(name)}, ` : ""}${exchangeName(chosen.exchange)} (${which} on ${named})`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** A venue as a sentence names it, in full: "the Warsaw Stock Exchange", "Xetra", "NASDAQ" (a name that is its code). */
|
|
424
|
+
function venueInProse(code: string): string {
|
|
425
|
+
const name = exchangeName(code);
|
|
426
|
+
return /\b(?:Exchange|Bourse|Market)$/.test(name) ? `the ${name}` : name;
|
|
420
427
|
}
|
|
421
428
|
|
|
422
429
|
// Quotes of these never name a company's listing, so they are never a choice between companies.
|
|
@@ -454,6 +461,19 @@ export function coinListingNotes(
|
|
|
454
461
|
return note ? [note] : [];
|
|
455
462
|
}
|
|
456
463
|
|
|
464
|
+
/**
|
|
465
|
+
* `coinListingNotes` for each listing of a list (CORR RIOT,BTC), asked only
|
|
466
|
+
* of a bare top coin code, whose quote alone says whether it is the coin.
|
|
467
|
+
*/
|
|
468
|
+
export async function listCoinNotes(
|
|
469
|
+
listings: ReadonlyArray<Pick<CliListing, "symbol" | "exchange" | "request">>,
|
|
470
|
+
deps: Pick<ListingDeps, "dataProvider">,
|
|
471
|
+
): Promise<string[]> {
|
|
472
|
+
const bare = listings.filter((listing) => !listing.exchange && TOP_COIN_NAMES.has(listing.symbol));
|
|
473
|
+
const quotes = await Promise.all(bare.map((listing) => loadListingQuote(deps.dataProvider, listing)));
|
|
474
|
+
return bare.flatMap((listing, index) => coinListingNotes(listing, quotes[index]));
|
|
475
|
+
}
|
|
476
|
+
|
|
457
477
|
/**
|
|
458
478
|
* `listingChoiceNote` for a listing a command resolved, from the quote it was
|
|
459
479
|
* read on: only a bare symbol went where the data service chose. A named
|
|
@@ -636,8 +656,12 @@ function describeOtherListings(
|
|
|
636
656
|
// Every other listing was asked and had nothing: they are named as such, with nothing to try.
|
|
637
657
|
const heading = choose ? "Listings" : lines.length === 0 ? `No ${request.noun} on its other listings either` : "Other listings";
|
|
638
658
|
const named = lines.length === 0 ? without : offered;
|
|
639
|
-
const
|
|
659
|
+
const listed = lines.length === 0 ? `${heading}: ${without.map((venue) => venueKey(listing.symbol, venue)).join(", ")}.`
|
|
640
660
|
: lines.length === 1 ? `${heading}: ${lines[0]}` : [`${heading}:`, ...lines.map((line) => ` ${line}`)].join("\n");
|
|
661
|
+
// A top coin's code (HYPE, SOL) none of the listings is: the coin may be what was meant.
|
|
662
|
+
const coin = coinElsewhereNote(listing.symbol, [...resolved ? [resolved] : [], ...others]
|
|
663
|
+
.map((venue) => ({ key: venueKey(listing.symbol, venue), name: venue.name })));
|
|
664
|
+
const details = coin ? `${listed}\n${coin}` : listed;
|
|
641
665
|
return {
|
|
642
666
|
// The symbol went to no venue, so none of them is said to lack the data.
|
|
643
667
|
message: choose
|
|
@@ -786,6 +810,11 @@ export interface ReadListing {
|
|
|
786
810
|
name: string | null;
|
|
787
811
|
/** A bare coin ticker read as a listing that is not the coin (`coinListingNotes`); null otherwise. */
|
|
788
812
|
coinNote: string | null;
|
|
813
|
+
/**
|
|
814
|
+
* What became of the company that had the symbol, as `quote` says it ("GOGL was Golden Ocean Group until
|
|
815
|
+
* 2025, now Cmb.Tech: CMBT"), when asked for (`successor`); null otherwise.
|
|
816
|
+
*/
|
|
817
|
+
successorNote?: string | null;
|
|
789
818
|
}
|
|
790
819
|
|
|
791
820
|
/**
|
|
@@ -801,13 +830,19 @@ export async function readBareListing(
|
|
|
801
830
|
symbol: string,
|
|
802
831
|
deps: ListingDeps,
|
|
803
832
|
request: ListingDataRequest,
|
|
804
|
-
|
|
833
|
+
/** `successor`: also say what became of the company that had the symbol, looked up beside its quote. */
|
|
834
|
+
{ refresh = false, successor = false }: { refresh?: boolean; successor?: boolean } = {},
|
|
805
835
|
): Promise<ReadListing | null> {
|
|
806
836
|
const listing = await resolveCliListing(symbol, null, deps);
|
|
807
837
|
if (listing.exchange) return null;
|
|
838
|
+
const changes = successor ? startTickerChangeLookup(deps.dataProvider, listing.symbol, listing.exchange) : null;
|
|
808
839
|
const answer = await loadListingQuoteAnswer(deps.dataProvider, listing);
|
|
809
840
|
if (answer.quote) {
|
|
810
|
-
|
|
841
|
+
const successorNote = changes
|
|
842
|
+
? listingChangeLine(await loadListingFate(listing, deps.dataProvider, { quote: answer.quote, changes, graceMs: SUCCESSOR_CHANGE_WAIT_MS }))
|
|
843
|
+
: null;
|
|
844
|
+
return { symbol: listing.symbol, exchange: listing.request.exchange, name: answer.quote.name?.trim() || null, coinNote: coinListingNotes(listing, answer.quote)[0] ?? null,
|
|
845
|
+
...successorNote ? { successorNote } : {} };
|
|
811
846
|
}
|
|
812
847
|
if (!isNoProviderError(answer.error)) return null;
|
|
813
848
|
const other = await quoteBareListing(listing, deps, request, { refresh });
|
|
@@ -861,10 +896,12 @@ export function listingFateLoader(
|
|
|
861
896
|
listing: Pick<CliListing, "symbol" | "exchange" | "request">,
|
|
862
897
|
deps: Pick<ListingDeps, "dataProvider">,
|
|
863
898
|
quote: Promise<Quote | null>,
|
|
899
|
+
/** `successor`: the command says what became of the company too (GOGL's merger), so the lookup is waited for. */
|
|
900
|
+
{ successor = false }: { successor?: boolean } = {},
|
|
864
901
|
): () => Promise<ListingFate> {
|
|
865
902
|
const changes = startTickerChangeLookup(deps.dataProvider, listing.symbol, listing.exchange);
|
|
866
903
|
let fate: Promise<ListingFate> | null = null;
|
|
867
|
-
return () => fate ??= loadListingFate(listing, deps.dataProvider, { quote, changes });
|
|
904
|
+
return () => fate ??= loadListingFate(listing, deps.dataProvider, { quote, changes, ...successor ? { graceMs: SUCCESSOR_CHANGE_WAIT_MS } : {} });
|
|
868
905
|
}
|
|
869
906
|
|
|
870
907
|
/**
|