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,179 @@
|
|
|
1
|
+
import type { QuoteBatchResult } from "../types/data-provider";
|
|
2
|
+
import type { Quote } from "../types/financials";
|
|
3
|
+
import { bareListingNote } from "../tickers/listing-fate";
|
|
4
|
+
import { notATickerSymbol } from "../sources/provider-errors";
|
|
5
|
+
import { looksLikeCurrencyPair } from "../utils/currency-pair";
|
|
6
|
+
import { canonicalExchange } from "../utils/exchanges";
|
|
7
|
+
import type { CliCommandDef } from "../types/plugin";
|
|
8
|
+
import { currencyCodeNote } from "./currency-hints";
|
|
9
|
+
import { describeSuggestion, suggestListing, suggestUsTicker, type ListingSuggestion } from "./listing-suggestion";
|
|
10
|
+
import { indexRootAlsoLine, indexRootFor, indexRootTryLine } from "./index-roots";
|
|
11
|
+
import { treasuryYieldLine, treasuryYieldTenor } from "./treasury-yields";
|
|
12
|
+
import { knownGapForTicker } from "./known-gaps";
|
|
13
|
+
import {
|
|
14
|
+
futuresTryLine,
|
|
15
|
+
isNoProviderError,
|
|
16
|
+
listingChoiceNotes,
|
|
17
|
+
listingReadNotes,
|
|
18
|
+
loadBareVenues,
|
|
19
|
+
notTradedMessage,
|
|
20
|
+
quoteBareListing,
|
|
21
|
+
requireCliListing,
|
|
22
|
+
tradingBoardNote,
|
|
23
|
+
type CliListing,
|
|
24
|
+
type ListingDataRequest,
|
|
25
|
+
type ListingVenue,
|
|
26
|
+
} from "./listing-arg";
|
|
27
|
+
import { withListingSuggestion } from "./not-a-ticker";
|
|
28
|
+
import type { MarketContext } from "./types";
|
|
29
|
+
|
|
30
|
+
/** The text of an error, or null for none. */
|
|
31
|
+
export function errorMessage(error: unknown): string | null {
|
|
32
|
+
if (error == null) return null;
|
|
33
|
+
return error instanceof Error ? error.message : String(error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
|
|
38
|
+
* mean AAPL (Apple Inc.)?", for a symbol read on a listing abroad that is one
|
|
39
|
+
* slip from a US ticker, worded as the not-a-ticker answers word it.
|
|
40
|
+
*/
|
|
41
|
+
export function notUsTickerSentence({ target, quote, listing, usSuggestion }: { target: { symbol: string }; quote: Quote | null; listing?: { name: string | null }; usSuggestion?: ListingSuggestion }): string | null {
|
|
42
|
+
if (!quote || !usSuggestion) return null;
|
|
43
|
+
const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
44
|
+
const name = quote.name || listing?.name;
|
|
45
|
+
const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
|
|
46
|
+
return `${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface QuoteAnswer {
|
|
50
|
+
result: QuoteBatchResult;
|
|
51
|
+
/** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
|
|
52
|
+
listing: CliListing;
|
|
53
|
+
/** Why there is no quote, when there is more to say than the source's own error. */
|
|
54
|
+
failure?: { message: string; details?: string; oneLine: string };
|
|
55
|
+
notes: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
|
|
60
|
+
* is not listed on says so. A bare symbol the data service read on its home
|
|
61
|
+
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
|
|
62
|
+
* quoted on its listings (`quoteBareListing`), else names them, or asks for an
|
|
63
|
+
* exchange when several companies list it (2222); a bare index root (VIX) also
|
|
64
|
+
* names its index (^VIX), and a futures board code a security keeps (GC) its
|
|
65
|
+
* future (GC=F). Venues are only looked up for a
|
|
66
|
+
* symbol with no quote, so a request that succeeds costs no search.
|
|
67
|
+
*/
|
|
68
|
+
export async function answerQuote(
|
|
69
|
+
result: QuoteBatchResult,
|
|
70
|
+
listing: CliListing,
|
|
71
|
+
market: MarketContext,
|
|
72
|
+
request: ListingDataRequest,
|
|
73
|
+
refresh: boolean,
|
|
74
|
+
venues: Promise<ListingVenue[]> | null,
|
|
75
|
+
): Promise<QuoteAnswer> {
|
|
76
|
+
const root = listing.exchange ? null : indexRootFor(listing.symbol);
|
|
77
|
+
// A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
|
|
78
|
+
const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
|
|
79
|
+
? [indexRootAlsoLine(root, request.command)] : [];
|
|
80
|
+
if (result.quote) {
|
|
81
|
+
// "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
|
|
82
|
+
const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
|
|
83
|
+
const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
|
|
84
|
+
// A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
|
|
85
|
+
const board = tradingBoardNote(listing, result.quote);
|
|
86
|
+
return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote), currencyCodeNote(listing, result.quote) ?? []].flat() };
|
|
87
|
+
}
|
|
88
|
+
const notTraded = await notTradedMessage(listing, market);
|
|
89
|
+
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
90
|
+
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
91
|
+
const other = await quoteBareListing(listing, market, request, { refresh });
|
|
92
|
+
if (other?.kind === "quoted") {
|
|
93
|
+
return {
|
|
94
|
+
result: { target: other.listing.request, quote: other.quote },
|
|
95
|
+
listing: other.listing,
|
|
96
|
+
notes: [other.note, ...also(other.quote), currencyCodeNote(listing, other.quote) ?? []].flat(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Other listings already name a futures board code's future (GC=F); with none, it is named here.
|
|
100
|
+
// A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
|
|
101
|
+
const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
|
|
102
|
+
const tryLines = [
|
|
103
|
+
yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
|
|
104
|
+
other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
|
|
105
|
+
// BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
|
|
106
|
+
listing.exchange ? null : knownGapForTicker(listing.symbol),
|
|
107
|
+
].filter((line): line is string => !!line);
|
|
108
|
+
if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
|
|
109
|
+
const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
|
|
110
|
+
const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
|
|
111
|
+
const oneLine = [
|
|
112
|
+
message,
|
|
113
|
+
other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
|
|
114
|
+
...tryLines,
|
|
115
|
+
].filter(Boolean).join(" ");
|
|
116
|
+
return { result, listing, failure: { message, details, oneLine }, notes: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A listing a command found a quote for, with what `quote` says beside it. */
|
|
120
|
+
interface QuotedListing {
|
|
121
|
+
/** The listing the quote is of: the one typed, or the venue a bare symbol was quoted on. */
|
|
122
|
+
listing: CliListing;
|
|
123
|
+
quote: Quote;
|
|
124
|
+
/** The venue a bare symbol was read on, the currency it may have meant, the US ticker it is one slip from. */
|
|
125
|
+
notes: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The listing a command that keeps a symbol (`alerts add`) was given, read as
|
|
130
|
+
* `quote` reads it: the same resolver and the same quote request, so what is
|
|
131
|
+
* kept is the listing `quote` prints (USDMXN and USD/MXN are USDMXN=X, MXN is
|
|
132
|
+
* MXN:FWB2) and `quote`'s own notes come back with it. A symbol nothing
|
|
133
|
+
* quotes fails the command with why, and the nearest listing search finds when
|
|
134
|
+
* it is near ("Not a ticker: ZZZQQ. Did you mean ZZZQ?"), so nothing is kept
|
|
135
|
+
* for it.
|
|
136
|
+
*/
|
|
137
|
+
export async function requireQuotedListing(
|
|
138
|
+
raw: string,
|
|
139
|
+
exchange: string | null | undefined,
|
|
140
|
+
market: MarketContext,
|
|
141
|
+
ctx: Pick<Parameters<CliCommandDef["execute"]>[1], "fail">,
|
|
142
|
+
command: string,
|
|
143
|
+
): Promise<QuotedListing> {
|
|
144
|
+
const listing = await requireCliListing(raw, exchange, market, ctx);
|
|
145
|
+
const venues = loadBareVenues(listing, market);
|
|
146
|
+
const [result] = await market.dataProvider.getQuotesBatch([listing.request], { revalidate: true });
|
|
147
|
+
const answer = await answerQuote(result ?? { target: listing.request, quote: null }, listing, market, { command, noun: "quote" }, false, venues);
|
|
148
|
+
const quote = answer.result.quote;
|
|
149
|
+
if (!quote) {
|
|
150
|
+
if (answer.failure) ctx.fail(answer.failure.message, answer.failure.details);
|
|
151
|
+
const message = errorMessage(answer.result.error) ?? `No quote found for ${listing.key}.`;
|
|
152
|
+
// The listing search names, else the US ticker it is one slip from (AAPLL for AAPL), which search does not list for a typo.
|
|
153
|
+
const suggestion = notATickerSymbol(message) && !message.includes(" Did you mean ")
|
|
154
|
+
? await suggestListing(listing.symbol, market) ?? await suggestUsTicker(listing.symbol, market)
|
|
155
|
+
: null;
|
|
156
|
+
ctx.fail(suggestion ? withListingSuggestion(message, describeSuggestion(suggestion)) : message);
|
|
157
|
+
}
|
|
158
|
+
// A bare symbol read on a listing abroad that is one slip from a US ticker says so, as `quote` does.
|
|
159
|
+
const venue = answer.listing.saved ? null : bareListingNote(answer.listing, quote);
|
|
160
|
+
const usSuggestion = venue && answer.notes.includes(venue) ? await suggestUsTicker(answer.listing.symbol, market) : null;
|
|
161
|
+
const notUs = notUsTickerSentence({ target: answer.result.target, quote, listing: { name: quote.name ?? null }, usSuggestion: usSuggestion ?? undefined });
|
|
162
|
+
return { listing: answer.listing, quote, notes: [...answer.notes, ...notUs ? [notUs] : []] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A symbol written like a currency pair that is not a ticker (USD_NGN, as some
|
|
167
|
+
* FX feeds write a pair) names the listing `gloomberb search` finds nearest to
|
|
168
|
+
* it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
|
|
169
|
+
* Naira)?". When search finds nothing near, the row keeps its own error and its
|
|
170
|
+
* search hint.
|
|
171
|
+
*/
|
|
172
|
+
async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
|
|
173
|
+
const message = errorMessage(result.error);
|
|
174
|
+
const symbol = message ? notATickerSymbol(message) : null;
|
|
175
|
+
if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
|
|
176
|
+
const suggestion = await suggestListing(symbol, market);
|
|
177
|
+
return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
|
|
178
|
+
}
|
|
179
|
+
|
package/src/cli/report-tables.ts
CHANGED
|
@@ -69,7 +69,8 @@ export interface CliReportEntry {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
type Cell =
|
|
72
|
-
|
|
72
|
+
/** `unit`: what the column draws a number of this row in, so a missing figure still names its unit. */
|
|
73
|
+
| { kind: "blank"; unit?: string }
|
|
73
74
|
| { kind: "text"; text: string; time?: boolean }
|
|
74
75
|
| { kind: "number"; value: number; unit: string }
|
|
75
76
|
/** Text that may read as a number, once the whole column is known. */
|
|
@@ -80,6 +81,8 @@ type ResolvedCell = Exclude<Cell, { kind: "textual" }>;
|
|
|
80
81
|
interface ResolvedColumn {
|
|
81
82
|
header: string;
|
|
82
83
|
cells: ResolvedCell[];
|
|
84
|
+
/** The unit the column draws its numbers in, for a table with no rows to say it. */
|
|
85
|
+
unit?: string;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
const BLANK = { kind: "blank" } as const;
|
|
@@ -265,8 +268,9 @@ function resolveColumn(header: string, cells: Cell[], parseText: boolean): Resol
|
|
|
265
268
|
};
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
/** The units a column's numbers are in, a missing figure's own among them, so the header does not change with which figures are missing. */
|
|
268
272
|
function numberUnits(cells: readonly ResolvedCell[]): Set<string> {
|
|
269
|
-
return new Set(cells.flatMap((cell) => cell.kind === "number" ? [cell.unit] : []));
|
|
273
|
+
return new Set(cells.flatMap((cell) => cell.kind === "number" ? [cell.unit] : cell.kind === "blank" && cell.unit != null ? [cell.unit] : []));
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
function cellValue(cell: ResolvedCell): CliReportValue {
|
|
@@ -312,8 +316,8 @@ function rowUnits(columns: readonly ResolvedColumn[], mixed: readonly boolean[],
|
|
|
312
316
|
* metrics whose rows each have one, and otherwise a `<header> unit` column
|
|
313
317
|
* beside a column that mixes them (a rate in % and a spread in bp).
|
|
314
318
|
*/
|
|
315
|
-
function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCount: number): CliReportTable {
|
|
316
|
-
const units = columns.map((column) => numberUnits(column.cells));
|
|
319
|
+
function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCount: number, status?: StatusColumn): CliReportTable {
|
|
320
|
+
const units = columns.map((column) => rowCount === 0 && column.unit != null ? new Set([column.unit]) : numberUnits(column.cells));
|
|
317
321
|
const mixed = units.map((columnUnits) => columnUnits.size > 1);
|
|
318
322
|
const byRow = mixed.some(Boolean) ? rowUnits(columns, mixed, rowCount) : null;
|
|
319
323
|
const layout = columns.flatMap((column, index) => {
|
|
@@ -338,6 +342,7 @@ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCou
|
|
|
338
342
|
},
|
|
339
343
|
];
|
|
340
344
|
});
|
|
345
|
+
if (status) layout.push({ header: status.header, value: (row: number) => status.values[row] ?? "" });
|
|
341
346
|
return {
|
|
342
347
|
title,
|
|
343
348
|
columns: layout.map((column) => column.header),
|
|
@@ -345,28 +350,141 @@ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCou
|
|
|
345
350
|
};
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
/** The `status` column a table of rows ends its own columns with: why a figure is missing, by row. */
|
|
354
|
+
interface StatusColumn {
|
|
355
|
+
header: string;
|
|
356
|
+
values: string[];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A number a probe draws through a column's format, to read the unit the
|
|
361
|
+
* column draws numbers in. A figure format rounds it; a format that prints
|
|
362
|
+
* it whole passes values through as text and draws no figures of its own.
|
|
363
|
+
*/
|
|
364
|
+
const UNIT_PROBE = 0.1234567891;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* A row with nothing in it that a format can still read through: every
|
|
368
|
+
* field, call and conversion of it is empty. For a format that reads fields of
|
|
369
|
+
* the row beyond its value and would throw on an empty one.
|
|
370
|
+
*/
|
|
371
|
+
const EMPTY_ROW: unknown = (() => {
|
|
372
|
+
const empty: unknown = new Proxy(function empty() {}, {
|
|
373
|
+
get: (_target, key) => key === Symbol.toPrimitive ? (hint: string) => (hint === "number" ? Number.NaN : "") : empty,
|
|
374
|
+
apply: () => empty,
|
|
375
|
+
});
|
|
376
|
+
return empty;
|
|
377
|
+
})();
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The unit a column draws a number of this row in (`%` for an IV, `""` for a
|
|
381
|
+
* plain figure), read from what its format draws for one; null when it draws
|
|
382
|
+
* no figure for one, passes the value through as it is, or throws.
|
|
383
|
+
*/
|
|
384
|
+
function drawnUnit<Row>(column: CliReportColumn<Row>, row: Row): string | null {
|
|
385
|
+
const unit = column.unitKey ? (row as Record<string, unknown> | undefined)?.[column.unitKey] : undefined;
|
|
386
|
+
if (typeof unit === "string" && unit) return unit;
|
|
387
|
+
if (!column.format) return null;
|
|
388
|
+
try {
|
|
389
|
+
const drawn = column.format(UNIT_PROBE, row);
|
|
390
|
+
if (typeof drawn !== "string" || drawn.trim() === String(UNIT_PROBE)) return null;
|
|
391
|
+
const read = readDisplay(drawn);
|
|
392
|
+
return read && read !== BLANK_DISPLAY ? read.unit : null;
|
|
393
|
+
} catch {
|
|
394
|
+
// Intentional ignore: a format that cannot draw a number for this row draws no unit for it.
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The unit a column draws its figures in whatever the rows, read without one:
|
|
401
|
+
* on an empty row, or through a row of nothing when the format throws on an
|
|
402
|
+
* empty one. The same with rows and without, so a header and the table's
|
|
403
|
+
* status column do not depend on what was loaded. Null for a column of text.
|
|
404
|
+
*/
|
|
405
|
+
function columnUnit<Row>(column: CliReportColumn<Row>): string | null {
|
|
406
|
+
if (column.unitKey) return "";
|
|
407
|
+
if (!column.format) return null;
|
|
408
|
+
try {
|
|
409
|
+
column.format(UNIT_PROBE, {} as Row);
|
|
410
|
+
return drawnUnit(column, {} as Row);
|
|
411
|
+
} catch {
|
|
412
|
+
// Intentional ignore: the format reads a field an empty row lacks; read it through a row of nothing.
|
|
413
|
+
return drawnUnit(column, EMPTY_ROW as Row);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** What a status cell says: each reason once, after the columns it is the reason for. */
|
|
418
|
+
function statusText(reasons: ReadonlyArray<{ header: string; text: string }>): string {
|
|
419
|
+
const byText = new Map<string, string[]>();
|
|
420
|
+
for (const { header, text } of reasons) byText.set(text, [...byText.get(text) ?? [], header]);
|
|
421
|
+
return [...byText].map(([text, headers]) => `${headers.join(", ")}: ${text}`).join("; ");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const isMissing = (value: unknown) => value == null || (typeof value === "number" && !Number.isFinite(value));
|
|
425
|
+
|
|
348
426
|
// Codes a spreadsheet must not turn into numbers, even when they are digits.
|
|
349
427
|
const IDENTIFIER_KEY = /^(?:id|cik|code|cusip|figi|isin|symbol|ticker)$|(?:Id|Cik|Code|Cusip|Figi|Isin|Symbol|Ticker)$/;
|
|
350
428
|
|
|
351
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* A table of rows under the columns the text view draws. A figure that is
|
|
431
|
+
* missing still names the unit its column draws it in, so a header is the same
|
|
432
|
+
* whichever figures a symbol has. With `status`, a reason the text draws in
|
|
433
|
+
* place of a missing figure (`not quoted`, `under 7d`) leaves the cell empty
|
|
434
|
+
* and goes to the `status` column the table ends its own columns with, so a
|
|
435
|
+
* column of figures holds only numbers. Only a row whose own value is missing,
|
|
436
|
+
* in a column that draws that row's figures, has a reason: text a format draws
|
|
437
|
+
* from the rest of the row (a range, N/M, ATM) stays. A table has the column
|
|
438
|
+
* when one of its columns draws figures through a format, read from the
|
|
439
|
+
* columns alone so it is there with or without rows, and a table of text has
|
|
440
|
+
* none; beside a column of its own named Status it is `value_status`.
|
|
441
|
+
*/
|
|
352
442
|
export function exportRowsTable<Row extends Record<string, unknown>>(
|
|
353
443
|
title: string,
|
|
354
444
|
columns: readonly CliReportColumn<Row>[],
|
|
355
445
|
rows: readonly Row[],
|
|
446
|
+
{ status = false }: { status?: boolean } = {},
|
|
356
447
|
): CliReportTable {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
448
|
+
const reasons = rows.map((): Array<{ header: string; text: string }> => []);
|
|
449
|
+
const units = columns.map(columnUnit);
|
|
450
|
+
// Decided by the columns alone, so a table has the column, or not, whatever rows it has.
|
|
451
|
+
const withStatus = status && units.some((unit) => unit != null);
|
|
452
|
+
const resolved = columns.map((column, columnIndex): ResolvedColumn => {
|
|
453
|
+
const rowUnits = rows.map((row) => drawnUnit(column, row));
|
|
454
|
+
const values = rows.map((row) => (row as Record<string, unknown> | undefined)?.[column.key]);
|
|
455
|
+
const { header, cells } = resolveColumn(
|
|
456
|
+
column.header,
|
|
457
|
+
rows.map((row, index): Cell => {
|
|
458
|
+
const value = values[index];
|
|
459
|
+
const unit = column.unitKey ? (row as Record<string, unknown> | undefined)?.[column.unitKey] : undefined;
|
|
460
|
+
if (typeof unit === "string" && unit && typeof value === "number" && Number.isFinite(value)) {
|
|
461
|
+
return { kind: "number", value: exportNumber(value), unit };
|
|
462
|
+
}
|
|
463
|
+
return exportCell(value, column.format ? column.format(value, row) : null, column.key);
|
|
464
|
+
}),
|
|
465
|
+
!IDENTIFIER_KEY.test(column.key),
|
|
466
|
+
);
|
|
467
|
+
const table: ResolvedColumn = {
|
|
468
|
+
header,
|
|
469
|
+
cells: cells.map((cell, index): ResolvedCell => {
|
|
470
|
+
const unit = rowUnits[index];
|
|
471
|
+
const blank: ResolvedCell = unit != null ? { kind: "blank", unit } : BLANK;
|
|
472
|
+
if (cell.kind === "blank") return cell.unit == null ? blank : cell;
|
|
473
|
+
// Text drawn in place of a figure this row does not have, where the column draws this row's figures: a reason.
|
|
474
|
+
// Text drawn from the rest of the row (a range, N/M, ATM) is no figure's absence and stays.
|
|
475
|
+
if (!withStatus || cell.kind !== "text" || cell.time || unit == null || !isMissing(values[index])) return cell;
|
|
476
|
+
reasons[index]!.push({ header: column.header, text: cell.text });
|
|
477
|
+
return blank;
|
|
478
|
+
}),
|
|
479
|
+
};
|
|
480
|
+
const unit = units[columnIndex];
|
|
481
|
+
return unit == null ? table : { ...table, unit };
|
|
482
|
+
});
|
|
483
|
+
const statusColumn = withStatus ? {
|
|
484
|
+
header: columns.some((column) => column.header.trim().toLowerCase() === "status") ? "value_status" : "status",
|
|
485
|
+
values: reasons.map(statusText),
|
|
486
|
+
} : undefined;
|
|
487
|
+
return assembleTable(title, resolved, rows.length, statusColumn);
|
|
370
488
|
}
|
|
371
489
|
|
|
372
490
|
/** Label/value lines as `Metric,Value`; a number's unit moves into its label. */
|
package/src/cli/result.ts
CHANGED
|
@@ -46,6 +46,12 @@ export interface CliResult<T = unknown> {
|
|
|
46
46
|
* `fear-greed` prints its row alone.
|
|
47
47
|
*/
|
|
48
48
|
exportFreshness?: ReportFreshness;
|
|
49
|
+
/**
|
|
50
|
+
* Why a result that still prints counts as a failure (a report with no
|
|
51
|
+
* data): the command exits 1, and JSON says `ok: false` with this as
|
|
52
|
+
* `error` beside the data, so the envelope and the exit status agree.
|
|
53
|
+
*/
|
|
54
|
+
failure?: CliErrorObject;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export interface CliErrorObject {
|
|
@@ -110,8 +116,9 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
|
|
|
110
116
|
exportNotes?: readonly string[];
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness"> {
|
|
114
|
-
ok:
|
|
119
|
+
interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness" | "failure"> {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
error?: CliErrorObject;
|
|
115
122
|
/** Where the data comes from, as CSV's closing `#` lines say it. */
|
|
116
123
|
source?: ExportSource;
|
|
117
124
|
columns?: Array<Pick<CliResultColumn, "key" | "header" | "align" | "width">>;
|
|
@@ -398,14 +405,15 @@ export function serializeCliResult<T, Row extends Record<string, unknown> = Reco
|
|
|
398
405
|
options: CliGlobalOptions,
|
|
399
406
|
renderOptions: CliResultRenderOptions<T, Row> = {},
|
|
400
407
|
): string {
|
|
401
|
-
const { freshness, exportFreshness, ...rest } = result;
|
|
408
|
+
const { freshness, exportFreshness, failure, ...rest } = result;
|
|
402
409
|
const cited = freshness ?? exportFreshness;
|
|
403
410
|
if (options.format === "json") {
|
|
404
411
|
const columns = serializeColumns(renderOptions.columns);
|
|
405
412
|
const window = Array.isArray(result.data) ? windowed(result.data as unknown[], options, renderOptions) : null;
|
|
406
413
|
const cut = window ? cutMetadata(window) : null;
|
|
407
414
|
const envelope: CliResultJsonEnvelope<T> = {
|
|
408
|
-
ok:
|
|
415
|
+
ok: !failure,
|
|
416
|
+
...(failure ? { error: failure } : {}),
|
|
409
417
|
...rest,
|
|
410
418
|
...(freshness || cut ? { metadata: { ...rest.metadata, ...(freshness ? { freshness } : {}), ...(cut ? { rows: cut } : {}) } } : {}),
|
|
411
419
|
data: window ? window.rows as T : result.data,
|
package/src/cli/series-search.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { seriesExpression } from "../series/registry";
|
|
2
2
|
import type { SeriesDefinition } from "../series/types";
|
|
3
3
|
import { cliStyles, renderSection, renderTable, wrapText } from "../utils/cli-output";
|
|
4
|
+
import { currencyCodeLabel } from "../utils/currency-units";
|
|
4
5
|
|
|
5
6
|
/** A data catalog series a query names, as the CLI lists it. */
|
|
6
7
|
export interface SeriesMatch {
|
|
@@ -108,7 +109,7 @@ export function renderSeriesMatches(
|
|
|
108
109
|
renderTable(
|
|
109
110
|
[{ header: "Series" }, { header: "Unit" }, { header: "Frequency", optional: true }, { header: "Publisher", optional: true },
|
|
110
111
|
{ header: "Chart", shrink: false }],
|
|
111
|
-
shown.map((match) => [match.name, match.unit, match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
|
|
112
|
+
shown.map((match) => [match.name, currencyCodeLabel(match.unit), match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
|
|
112
113
|
),
|
|
113
114
|
"",
|
|
114
115
|
...wrapText(hint, width).map((line) => cliStyles.muted(line)),
|
package/src/cli/symbol-list.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { splitSymbolArgs, splitSymbolWords, stripStrayEdges, uniqueSymbols } fro
|
|
|
4
4
|
|
|
5
5
|
/** Header cells that name a spreadsheet's symbol column, compared without case, spaces or punctuation. */
|
|
6
6
|
const SYMBOL_HEADERS = new Set(["symbol", "symbols", "ticker", "tickers", "code", "stockcode", "securitycode", "instrument"]);
|
|
7
|
+
/** A header naming a column of company names, read when no column names the symbols. */
|
|
8
|
+
const NAME_HEADERS = new Set(["name", "names", "company", "companies", "companyname", "security", "securityname", "issuer", "issuername"]);
|
|
7
9
|
|
|
8
10
|
/** One symbol as it can be typed: AAPL, $nvda, TTF.AS, SAN:EPA, BRK-B, 7203.T, ^GSPC, ES=F. */
|
|
9
11
|
const SYMBOL_TOKEN = /^\$?[\p{L}\p{N}^][\p{L}\p{N}.:\-^=/&]{0,23}$/u;
|
|
@@ -82,17 +84,32 @@ interface SymbolText {
|
|
|
82
84
|
symbols: string[];
|
|
83
85
|
/** Cells beside the symbol column of a table with no header, in reading order; empty for a list or a CSV with a header. */
|
|
84
86
|
ignored: string[];
|
|
87
|
+
/**
|
|
88
|
+
* The symbols are a list's entries, each a line or a cell of a list split
|
|
89
|
+
* by semicolons or commas, and are read as typed entries are
|
|
90
|
+
* (`readNameEntries`): one with spaces is a company's name (`Altria
|
|
91
|
+
* Group`), or symbols written on one line (`AAPL MSFT`).
|
|
92
|
+
*/
|
|
93
|
+
entries?: true;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/**
|
|
88
97
|
* The symbols in a file or piped text, in order. A spreadsheet export reads
|
|
89
|
-
* from the column its header names (Symbol, Ticker, Code)
|
|
98
|
+
* from the column its header names (Symbol, Ticker, Code), else from its
|
|
99
|
+
* Name or Company column, as `entries`. Without a header,
|
|
90
100
|
* every cell is a symbol unless some column after the first holds a name, a
|
|
91
101
|
* price or a weight: that table is read by its first column, and the cells
|
|
92
102
|
* beside it are returned as `ignored` so the command can name them. A list
|
|
93
103
|
* split by commas, spaces or newlines gives every entry, even when a line of
|
|
94
|
-
* it is ragged or carries a stray separator (`"BIMAS ,"`). A
|
|
95
|
-
*
|
|
104
|
+
* it is ragged or carries a stray separator (`"BIMAS ,"`). A list of lines
|
|
105
|
+
* gives each line whole, and a list of names split by semicolons or commas
|
|
106
|
+
* (one line, or one name a line) each cell, as `entries`: a line with spaces
|
|
107
|
+
* is a company's name (`Altria Group`) or symbols on one line, which reading
|
|
108
|
+
* the entries tells apart. Names beside other text (`Apple Inc,Technology`)
|
|
109
|
+
* are a table read by its first column, its names as `entries`. A list split
|
|
110
|
+
* by spaces with figures beside the symbols (`AAPL 150.00`) is a table. A
|
|
111
|
+
* byte order mark, Windows line ends, blank lines and lines starting with #
|
|
112
|
+
* or // are ignored.
|
|
96
113
|
*/
|
|
97
114
|
export function parseSymbolText(text: string): SymbolText {
|
|
98
115
|
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
@@ -105,13 +122,27 @@ export function parseSymbolText(text: string): SymbolText {
|
|
|
105
122
|
const exact = header.findIndex((cell) => SYMBOL_HEADERS.has(cell));
|
|
106
123
|
const column = exact >= 0 ? exact : header.findIndex((cell) => cell.includes("symbol") || cell.includes("ticker"));
|
|
107
124
|
if (column >= 0) return { symbols: records.slice(1).map((cells) => cells[column] ?? "").filter(Boolean), ignored: [] };
|
|
125
|
+
// Without one, a delimited export's Name or Company column holds the companies, read as typed names are.
|
|
126
|
+
const nameColumn = delimiter ? header.findIndex((cell) => NAME_HEADERS.has(cell)) : -1;
|
|
127
|
+
if (nameColumn >= 0) return { symbols: records.slice(1).map((cells) => cells[nameColumn] ?? "").filter(Boolean), ignored: [], entries: true };
|
|
108
128
|
// A separator or quote mark stuck to a cell is dropped (and named) when it is read, so it does not make the list a table.
|
|
109
129
|
const cleaned = records.map((cells) => cells.map((cell) => stripStrayEdges(cell).text));
|
|
130
|
+
const figures = cleaned.some((cells) => cells.some((cell, index) => index > 0 && NUMBER_CELL.test(cell) && /\d/.test(cell)));
|
|
131
|
+
// Lines split by spaces alone are entries, unless figures sit beside the symbols.
|
|
132
|
+
if (!delimiter && !figures) return { symbols: records.map((cells) => cells.join(" ")), ignored: [], entries: true };
|
|
110
133
|
const grid = cleaned.every((cells) => cells.every((cell, index) => !cell || (looksLikeSymbol(cell) && (index === 0 || !NUMBER_CELL.test(cell)))));
|
|
111
134
|
if (grid) return { symbols: records.flat().filter(Boolean), ignored: [] };
|
|
135
|
+
// Names split by semicolons or commas, with no figures: one pasted line, or one name a line with a separator left
|
|
136
|
+
// on it, is a list. A name beside a sector or any other text is a table, read by its first column below.
|
|
137
|
+
const names = cleaned.some((cells) => /\s/.test(cells[0] ?? ""));
|
|
138
|
+
if (!figures && (records.length === 1 || (names && cleaned.every((cells) => cells.filter(Boolean).length <= 1)))) {
|
|
139
|
+
return { symbols: records.flat().filter(Boolean), ignored: [], entries: true };
|
|
140
|
+
}
|
|
112
141
|
return {
|
|
113
142
|
symbols: records.map((cells) => cells[0] ?? "").filter(Boolean),
|
|
114
143
|
ignored: records.flatMap((cells) => cells.slice(1)).filter(Boolean),
|
|
144
|
+
// A first column of names (Apple Inc) is read as typed names are.
|
|
145
|
+
...names ? { entries: true as const } : {},
|
|
115
146
|
};
|
|
116
147
|
}
|
|
117
148
|
|
|
@@ -151,8 +182,10 @@ export interface SymbolListSource {
|
|
|
151
182
|
stdinPiped?: boolean;
|
|
152
183
|
/**
|
|
153
184
|
* Reads the typed entries (`splitSymbolArgs`) as symbols, such as
|
|
154
|
-
* `readNameEntries`, which keeps a company's name whole
|
|
155
|
-
*
|
|
185
|
+
* `readNameEntries`, which keeps a company's name whole, and then the
|
|
186
|
+
* entries of each list a file or standard input holds. Called first for
|
|
187
|
+
* what was typed, even when that is nothing. Without it every entry is its
|
|
188
|
+
* words, each a symbol.
|
|
156
189
|
*/
|
|
157
190
|
readEntries?: (entries: string[]) => Promise<string[]>;
|
|
158
191
|
}
|
|
@@ -176,17 +209,20 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
176
209
|
const readStdin = source.readStdin ?? (() => Bun.stdin.text());
|
|
177
210
|
const wantsStdin = source.args.includes("-") || source.file === "-";
|
|
178
211
|
const typedArgs = source.args.filter((arg) => arg !== "-");
|
|
212
|
+
const readEntries = (entries: string[]) => source.readEntries ? source.readEntries(entries) : Promise.resolve(splitSymbolWords(entries));
|
|
179
213
|
const typed = source.readEntries ? await source.readEntries(splitSymbolArgs(typedArgs)) : splitSymbolWords(typedArgs);
|
|
180
214
|
const collected = [...typed];
|
|
181
215
|
// A separator left on the end of what was typed: `UPST;`, or a list that ends in a comma.
|
|
182
216
|
const strays = new Set<string>(stripStrayEdges(typedArgs.join(" ")).notes);
|
|
183
217
|
const ignored: string[] = [];
|
|
184
|
-
const addCells = ({ symbols, ignored: beside }: SymbolText) => {
|
|
185
|
-
|
|
218
|
+
const addCells = async ({ symbols, ignored: beside, entries }: SymbolText) => {
|
|
219
|
+
const cells = symbols.map((cell) => {
|
|
186
220
|
const cleaned = stripStrayEdges(cell);
|
|
187
221
|
cleaned.notes.forEach((note) => strays.add(note));
|
|
188
|
-
|
|
189
|
-
}
|
|
222
|
+
return cleaned.text;
|
|
223
|
+
}).filter(Boolean);
|
|
224
|
+
// A list's lines and names are read as typed entries are: `Altria Group` stays one company.
|
|
225
|
+
collected.push(...entries ? await readEntries(cells) : cells);
|
|
190
226
|
ignored.push(...beside);
|
|
191
227
|
};
|
|
192
228
|
if (source.file != null && source.file !== "-") {
|
|
@@ -198,10 +234,10 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
198
234
|
const code = (error as NodeJS.ErrnoException).code;
|
|
199
235
|
throw new SymbolListError(code === "ENOENT" ? `No file at ${source.file}.` : `Could not read ${source.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
236
|
}
|
|
201
|
-
addCells(parseSymbolText(text));
|
|
237
|
+
await addCells(parseSymbolText(text));
|
|
202
238
|
}
|
|
203
239
|
if (wantsStdin || (typed.length === 0 && source.file == null && (source.stdinPiped ?? stdinIsPiped()))) {
|
|
204
|
-
addCells(parseSymbolText(await readStdin()));
|
|
240
|
+
await addCells(parseSymbolText(await readStdin()));
|
|
205
241
|
}
|
|
206
242
|
const ignoredNote = ignoredCellsNote(ignored);
|
|
207
243
|
return { ...uniqueSymbols(collected), notes: [...strays, ...ignoredNote ? [ignoredNote] : []] };
|
package/src/cli/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { AssetDataRouter } from "../sources/provider-router";
|
|
|
5
5
|
import type { AppServices } from "../core/app-services";
|
|
6
6
|
import type { CapabilityInvoker } from "../capabilities/types";
|
|
7
7
|
import type { StatementReportSources } from "./statement-filings";
|
|
8
|
+
import type { FxHistoryClient } from "../market-data/fx-closes";
|
|
8
9
|
|
|
9
10
|
export type ConfigContext = {
|
|
10
11
|
config: AppConfig;
|
|
@@ -15,12 +16,15 @@ export type ConfigContext = {
|
|
|
15
16
|
|
|
16
17
|
export type MarketContext = ConfigContext & {
|
|
17
18
|
dataProvider: AssetDataRouter;
|
|
19
|
+
resolveBroker?: (brokerType: string) => import("../types/broker").BrokerAdapter | null;
|
|
18
20
|
/** Charts catalog series in a report run without the plugin runtime. */
|
|
19
21
|
seriesCapabilities?: CapabilityInvoker;
|
|
20
22
|
/** `--refresh`: prefetched data bypasses the local cache. */
|
|
21
23
|
refresh?: boolean;
|
|
22
24
|
/** Filings behind the statements and a bank's figures, read from Gloom Cloud; absent where the data service is a stand-in. */
|
|
23
25
|
statementSources?: StatementReportSources;
|
|
26
|
+
/** Daily FX closes (`fx --date`, converted INS values); Gloom Cloud unless a test stands in for it. */
|
|
27
|
+
fxHistory?: FxHistoryClient;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export type CliServicesContext = {
|