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
|
@@ -8,11 +8,12 @@ import { describeFundamentalMarketCap, marketCapShareBasisNote, selectMarketCapi
|
|
|
8
8
|
import {
|
|
9
9
|
formatCompact,
|
|
10
10
|
formatCurrency,
|
|
11
|
+
formatCurrencyCode,
|
|
11
12
|
formatNumber,
|
|
12
13
|
formatPercent,
|
|
13
14
|
formatPercentRaw,
|
|
14
15
|
} from "../../utils/format";
|
|
15
|
-
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity,
|
|
16
|
+
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions, withCurrencyMinorDigits, type MarketFormatOptions } from "../../market-data/market/format";
|
|
16
17
|
import {
|
|
17
18
|
cliStyles,
|
|
18
19
|
cliTerminalWidth,
|
|
@@ -28,14 +29,17 @@ import {
|
|
|
28
29
|
getExtendedSessionDisplay,
|
|
29
30
|
getQuoteSessionFields,
|
|
30
31
|
getRegularSessionDisplay,
|
|
32
|
+
quoteBookSessionLabel,
|
|
31
33
|
quoteHeadlineTime,
|
|
32
34
|
quoteMarketStateLabel,
|
|
33
35
|
} from "../../market-data/market/status";
|
|
34
36
|
import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
|
|
35
|
-
import {
|
|
37
|
+
import { changeBasisJson, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
|
|
38
|
+
import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms, minorUnitPricePer } from "../../market-data/futures-contract";
|
|
39
|
+
import { currencyCodeLabel, heldCurrencyUnit } from "../../utils/currency-units";
|
|
40
|
+
import { quotedFxPair } from "../../utils/currency-pair";
|
|
36
41
|
import type { AppConfig } from "../../types/config";
|
|
37
42
|
import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
|
|
38
|
-
import { quoteTradingDay } from "../../market-data/quotes/day-range";
|
|
39
43
|
import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
|
|
40
44
|
import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
|
|
41
45
|
import { instrumentTypeLabel, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
|
|
@@ -46,7 +50,8 @@ import type { TickerRecord } from "../../types/ticker";
|
|
|
46
50
|
import type { CliCommandContext } from "../../types/plugin";
|
|
47
51
|
import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
|
|
48
52
|
import { isFuturesTicker } from "../../portfolio/futures-positions";
|
|
49
|
-
import { createBaseConverter } from "../base-converter";
|
|
53
|
+
import { createBaseConverter, type BaseConversion } from "../base-converter";
|
|
54
|
+
import { formatFxRate } from "../fx-pair";
|
|
50
55
|
import { initMarketData, withMarketData } from "../context";
|
|
51
56
|
import { fail } from "../errors";
|
|
52
57
|
import type { MarketContext } from "../types";
|
|
@@ -58,6 +63,7 @@ import {
|
|
|
58
63
|
formatSignedCurrency,
|
|
59
64
|
formatTimestamp,
|
|
60
65
|
formatWatchlistNames,
|
|
66
|
+
quoteChangeText,
|
|
61
67
|
} from "../helpers";
|
|
62
68
|
import { NotesFiles } from "../../plugins/builtin/notes/files";
|
|
63
69
|
import { isKnownNonUsListing, secFilingItemCodes } from "../../utils/sec";
|
|
@@ -68,6 +74,7 @@ import {
|
|
|
68
74
|
resolveCliListing, type CliListing, type ListingDataRequest,
|
|
69
75
|
} from "../listing-arg";
|
|
70
76
|
import { movedAwayChange } from "../../tickers/ticker-changes";
|
|
77
|
+
import { companyMemberships, loadCompanyMemberships, type CompanyMemberships, type ListMembership } from "../issuer-membership";
|
|
71
78
|
import { isNotATickerMessage } from "../not-a-ticker";
|
|
72
79
|
import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
|
|
73
80
|
import { cliFreshnessFooter } from "../result";
|
|
@@ -106,10 +113,6 @@ async function loadRecentSecFilings(
|
|
|
106
113
|
// Prose such as a company description stays readable on wide terminals.
|
|
107
114
|
const MAX_PROSE_WIDTH = 100;
|
|
108
115
|
const METADATA_SEPARATOR = " · ";
|
|
109
|
-
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
|
|
110
|
-
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
111
|
-
/** The states the header reads as CLOSED: the venue is between sessions, so nobody is quoting. */
|
|
112
|
-
const CLOSED_MARKET_STATES = new Set<string>(["CLOSED", "PREPRE", "POSTPOST"]);
|
|
113
116
|
|
|
114
117
|
interface TickerCommandDependencies {
|
|
115
118
|
/** `--exchange`: the listing, for a symbol that trades in several places. */
|
|
@@ -149,25 +152,22 @@ function appendMetricSection(lines: string[], title: string, metrics: Array<[str
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
155
|
+
* Out of the regular session the bid and ask say whose book they are: the pre-market or after-hours book beside
|
|
156
|
+
* Last and the extended print, or a closed market's last quote with its day (ITA's $202.00 against $208.74
|
|
157
|
+
* around a $206.65 close is no open book).
|
|
154
158
|
*/
|
|
155
159
|
function bidAskText(quote: Quote, options: MarketFormatOptions): string {
|
|
156
160
|
const text = formatBidAsk(quote.bid, quote.ask, quote.bidSize, quote.askSize, quote.currency, options);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const date = day ? new Date(`${day}T00:00:00Z`) : null;
|
|
160
|
-
const label = date && Number.isFinite(date.getTime())
|
|
161
|
-
? `, ${WEEKDAYS[date.getUTCDay()]} ${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]}` : "";
|
|
162
|
-
return `${text} ${cliStyles.muted(`(last quote${label})`)}`;
|
|
161
|
+
const label = text === "—" ? null : quoteBookSessionLabel(quote);
|
|
162
|
+
return label ? `${text} ${cliStyles.muted(`(${label})`)}` : text;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
function wrapProse(text: string): string {
|
|
166
166
|
return wrapText(text, Math.min(cliTerminalWidth() ?? MAX_PROSE_WIDTH, MAX_PROSE_WIDTH)).join("\n");
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
function buildStatementMetrics(statement: FinancialStatement, currency?: string): Array<[string, string]> {
|
|
170
|
-
const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare);
|
|
169
|
+
function buildStatementMetrics(statement: FinancialStatement, currency?: string, codes = false): Array<[string, string]> {
|
|
170
|
+
const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare, codes);
|
|
171
171
|
return [
|
|
172
172
|
["Revenue", money(statement.totalRevenue)],
|
|
173
173
|
["Gross Profit", money(statement.grossProfit)],
|
|
@@ -303,8 +303,11 @@ function shouldFetchSecFilings(
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
async function appendTickerPositions(lines: string[], tickerFile: TickerRecord | null, quote: TickerFinancials["quote"],
|
|
306
|
-
config: AppConfig, toBase:
|
|
306
|
+
config: AppConfig, toBase: ReportBaseConverter, rates: ReportRateNotes): Promise<void> {
|
|
307
307
|
const quoteCurrency = quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "";
|
|
308
|
+
const codes = writesCurrencyCodes(config);
|
|
309
|
+
const money = codes ? formatCurrencyCode : formatCurrency;
|
|
310
|
+
const converted = new Set<string>();
|
|
308
311
|
if (tickerFile && tickerFile.metadata.positions.length > 0) {
|
|
309
312
|
lines.push("");
|
|
310
313
|
lines.push(renderSection("Positions"));
|
|
@@ -319,11 +322,14 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
319
322
|
const currentPrice = getPortfolioQuoteDisplay(metrics, quote)?.price ?? null;
|
|
320
323
|
const currentPriceBase = currentPrice != null && quoteCurrency ? await toBase(currentPrice, quoteCurrency) : null;
|
|
321
324
|
const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
|
|
325
|
+
if (positionCurrency) converted.add(positionCurrency.toUpperCase());
|
|
326
|
+
if (quoteCurrency && currentPrice != null) converted.add(quoteCurrency.toUpperCase());
|
|
322
327
|
const baseMetrics = getPortfolioPositionMetrics({ ...tickerFile, metadata: { ...tickerFile.metadata, positions: [position] } }, undefined, quoteCurrency,
|
|
323
328
|
{ currency: config.baseCurrency, convert: value => value * positionRate }, quote);
|
|
324
|
-
// A cost bought at an entered rate into the base currency converts at it, as its P&L does; any other at today's.
|
|
329
|
+
// A cost bought at an entered rate into the base currency converts at it, as its P&L does, and says so; any other at today's.
|
|
330
|
+
const boughtRate = positionCurrency ? lotCostFxRate(position, positionCurrency, config.baseCurrency) : null;
|
|
325
331
|
const costBasisBase = !positionCurrency ? Number.NaN
|
|
326
|
-
:
|
|
332
|
+
: boughtRate !== null ? baseMetrics.signedCost
|
|
327
333
|
: await toBase(metrics.signedCost, positionCurrency);
|
|
328
334
|
const marketValueBase = resolvePortfolioMarketValue(baseMetrics, currentPriceBase)?.net ?? Number.NaN;
|
|
329
335
|
const selectedPnl = resolvePortfolioPositionPnl(baseMetrics, currentPriceBase);
|
|
@@ -334,26 +340,79 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
334
340
|
const stats: CliStatEntry[] = [
|
|
335
341
|
[
|
|
336
342
|
"Position",
|
|
337
|
-
`${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"}`,
|
|
343
|
+
`${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"}`,
|
|
338
344
|
],
|
|
339
|
-
["Cost Basis",
|
|
340
|
-
|
|
345
|
+
["Cost Basis", `${money(costBasisBase, config.baseCurrency)}${boughtRate !== null
|
|
346
|
+
? ` (${formatFxRate(boughtRate)} ${config.baseCurrency.trim().toUpperCase()} per ${positionCurrency} when bought)` : ""}`],
|
|
347
|
+
["Market Value", money(marketValueBase, config.baseCurrency)],
|
|
341
348
|
[
|
|
342
349
|
selectedPnl.basis === "broker-snapshot" ? "Broker P&L" : "P&L",
|
|
343
|
-
pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency), pnl),
|
|
350
|
+
pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency, codes), pnl),
|
|
344
351
|
],
|
|
345
352
|
];
|
|
346
353
|
if (position.markPrice != null) {
|
|
347
|
-
stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"]);
|
|
354
|
+
stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"]);
|
|
348
355
|
}
|
|
349
356
|
lines.push(renderStats(stats));
|
|
350
357
|
if (index < positions.length - 1) {
|
|
351
358
|
lines.push("");
|
|
352
359
|
}
|
|
353
360
|
}
|
|
361
|
+
for (const note of conversionNotes(toBase, converted, rates)) lines.push(cliStyles.muted(note));
|
|
354
362
|
}
|
|
355
363
|
}
|
|
356
364
|
|
|
365
|
+
/** The ticker report's converter: what `createBaseConverter` makes, whose rates a report states; a plain function in tests. */
|
|
366
|
+
type ReportBaseConverter = ((value: number, fromCurrency: string) => Promise<number>) & { conversions?(): BaseConversion[] };
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* A base currency other than USD reads every amount with its code (259.36
|
|
370
|
+
* USD, 878.15B CAD): $ alone is also the Canadian, Australian and other
|
|
371
|
+
* dollars. A USD base keeps the signs it always had.
|
|
372
|
+
*/
|
|
373
|
+
export function writesCurrencyCodes(config: Pick<AppConfig, "baseCurrency"> | undefined): boolean {
|
|
374
|
+
const base = config?.baseCurrency?.trim().toUpperCase();
|
|
375
|
+
return !!base && base !== "USD";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* What the report's rate notes have said and must say: each currency's rate
|
|
380
|
+
* is stated once, under the first section that converted it, and a lot whose
|
|
381
|
+
* cost converts at the rate entered when it was bought is left out of it.
|
|
382
|
+
*/
|
|
383
|
+
interface ReportRateNotes {
|
|
384
|
+
/** Currencies whose rate a note already gave. */
|
|
385
|
+
noted: Set<string>;
|
|
386
|
+
/** Currencies of lots whose Cost Basis is at their own entered rate. */
|
|
387
|
+
boughtAt: ReadonlySet<string>;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The currencies of the shown lots whose cost converts into the base currency at the rate they were bought at. */
|
|
391
|
+
function boughtRateCurrencies(tickerFile: TickerRecord | null, quoteCurrency: string, baseCurrency: string): Set<string> {
|
|
392
|
+
return new Set((tickerFile?.metadata.positions ?? []).flatMap((position) => {
|
|
393
|
+
const currency = (position.currency || quoteCurrency).trim();
|
|
394
|
+
return position.shares !== 0 && currency && lotCostFxRate(position, currency, baseCurrency) !== null ? [currency.toUpperCase()] : [];
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* "CAD figures at USD/CAD 1.40623, as of 2026-10-21 04:46 UTC.": the rate
|
|
400
|
+
* and its date for each currency in `currencies` the report converted into
|
|
401
|
+
* the base currency, as markets quote the pair (USD/JPY, not JPY/USD), so a
|
|
402
|
+
* converted figure is never without what it was converted at. A rate already
|
|
403
|
+
* stated is not repeated, and a cost basis at its own rate is excepted.
|
|
404
|
+
*/
|
|
405
|
+
function conversionNotes(toBase: ReportBaseConverter, currencies: ReadonlySet<string>, rates: ReportRateNotes): string[] {
|
|
406
|
+
return (toBase.conversions?.() ?? [])
|
|
407
|
+
.filter((conversion) => currencies.has(conversion.currency) && conversion.currency !== conversion.baseCurrency && !rates.noted.has(conversion.currency))
|
|
408
|
+
.map(({ currency, baseCurrency, rate, asOf }) => {
|
|
409
|
+
rates.noted.add(currency);
|
|
410
|
+
const quoted = quotedFxPair(currency, baseCurrency, rate);
|
|
411
|
+
const except = rates.boughtAt.has(currency) ? ", except a cost basis at the rate it was bought at" : "";
|
|
412
|
+
return `${baseCurrency} figures at ${quoted.pair} ${formatFxRate(quoted.rate)}${asOf != null ? `, as of ${formatTimestamp(asOf)}` : ""}${except}.`;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
357
416
|
/** A source's enterprise value is in the units of the capitalization it reports beside it,
|
|
358
417
|
* which without a declared unit is the listing's quote currency (as the overview labels it). */
|
|
359
418
|
function enterpriseValueCurrency(
|
|
@@ -378,9 +437,9 @@ interface FundamentalsMetric {
|
|
|
378
437
|
|
|
379
438
|
type MetricFigure = Omit<FundamentalsMetric, "label">;
|
|
380
439
|
|
|
381
|
-
/** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. */
|
|
382
|
-
function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false): MetricFigure {
|
|
383
|
-
return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
|
|
440
|
+
/** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. `codes`: per share too (6.54 USD). */
|
|
441
|
+
function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false, codes = false): MetricFigure {
|
|
442
|
+
return { text: formatReportedMoney(value, currency, perShare, codes), value, unit: currency?.trim() || "ccy?" };
|
|
384
443
|
}
|
|
385
444
|
|
|
386
445
|
/** The EV line: labelled "(provider)" when it is more than 5% from market cap + debt - cash, "unavailable" when withdrawn. */
|
|
@@ -570,6 +629,7 @@ function fundamentalsMetrics(
|
|
|
570
629
|
enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
|
|
571
630
|
distributions: DividendDistributions | null = null,
|
|
572
631
|
bond: FundBondFacts | null = null,
|
|
632
|
+
codes = false,
|
|
573
633
|
): FundamentalsMetric[] {
|
|
574
634
|
const signed = (value: number) => colorBySign(formatPercent(value), value);
|
|
575
635
|
const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
|
|
@@ -581,7 +641,7 @@ function fundamentalsMetrics(
|
|
|
581
641
|
{ label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
|
|
582
642
|
{ label: "PEG", ...multipleFigure(fundamentals?.pegRatio) },
|
|
583
643
|
// The flows and EPS are one trailing-twelve-month block, the same twelve months for each line.
|
|
584
|
-
{ label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true) },
|
|
644
|
+
{ label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true, codes) },
|
|
585
645
|
{
|
|
586
646
|
label: dividendYieldLabel(fundamentals?.dividendYieldBasis, bond),
|
|
587
647
|
...forwardYieldFigure(fundamentals, distributions),
|
|
@@ -684,6 +744,8 @@ type FundamentalsReportData = TickerFinancials & {
|
|
|
684
744
|
bank?: BankFiguresPayload | null;
|
|
685
745
|
/** In place of `bank`, for a bank whose figures could not be read. */
|
|
686
746
|
bankNote?: string | null;
|
|
747
|
+
/** A base currency other than USD: every amount reads with its code (`writesCurrencyCodes`). */
|
|
748
|
+
currencyCodes?: boolean;
|
|
687
749
|
};
|
|
688
750
|
|
|
689
751
|
/** "Latest quarter 2026-06-30, 10-Q filed 2026-08-06." and the newer quarter's results release, for the lines under a report's figures. */
|
|
@@ -723,7 +785,10 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
|
|
|
723
785
|
const quote = financials.quote;
|
|
724
786
|
const session = getRegularSessionDisplay(quote);
|
|
725
787
|
if (!quote || !session) return [];
|
|
726
|
-
const options = withCurrencyMinorDigits(
|
|
788
|
+
const options = withCurrencyMinorDigits({
|
|
789
|
+
...quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType),
|
|
790
|
+
...financials.currencyCodes ? { currencyCode: true } : {},
|
|
791
|
+
}, quote.currency);
|
|
727
792
|
const price = (value: number) => formatMarketPriceWithCurrency(value, quote.currency, options);
|
|
728
793
|
const extended = getExtendedSessionDisplay(quote);
|
|
729
794
|
const extendedLabel = extended ? EXTENDED_SESSION_LABELS[extended.session] : "";
|
|
@@ -732,7 +797,8 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
|
|
|
732
797
|
session.changePercent != null && Number.isFinite(session.changePercent)
|
|
733
798
|
? {
|
|
734
799
|
label: "Change",
|
|
735
|
-
|
|
800
|
+
// With what the move is measured from, as `ticker` and `quote` say it: "vs Fri 16 Oct close".
|
|
801
|
+
text: quoteChangeText(quote, session, options),
|
|
736
802
|
value: session.changePercent,
|
|
737
803
|
unit: "%",
|
|
738
804
|
}
|
|
@@ -767,7 +833,7 @@ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fund
|
|
|
767
833
|
? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
|
|
768
834
|
: { text: "—" };
|
|
769
835
|
const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials),
|
|
770
|
-
undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null);
|
|
836
|
+
undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null, snapshot.currencyCodes === true);
|
|
771
837
|
if (view === "fundamentals") {
|
|
772
838
|
return [...metrics, ...floatAndShortInterestMetrics(quote, fundamentals, financials.profile)]
|
|
773
839
|
.filter((metric) => metric.text !== "—");
|
|
@@ -912,8 +978,11 @@ export async function buildTickerReport({
|
|
|
912
978
|
distributions = null,
|
|
913
979
|
statementFilings = null,
|
|
914
980
|
bondFund = null,
|
|
981
|
+
memberships,
|
|
915
982
|
}: {
|
|
916
983
|
symbol: string;
|
|
984
|
+
/** The lists holding the company, by any of its listings (loadCompanyMemberships); the saved listing's own lists when absent. */
|
|
985
|
+
memberships?: CompanyMemberships;
|
|
917
986
|
/** The filings behind the latest statements, and a newer quarter's results release. */
|
|
918
987
|
statementFilings?: StatementFilingsReport | null;
|
|
919
988
|
/** A bond fund's SEC yield, yield to worst and duration (loadReportBondFacts). */
|
|
@@ -927,7 +996,7 @@ export async function buildTickerReport({
|
|
|
927
996
|
tickerFile: TickerRecord | null;
|
|
928
997
|
financials: TickerFinancials;
|
|
929
998
|
config: AppConfig;
|
|
930
|
-
toBase:
|
|
999
|
+
toBase: ReportBaseConverter;
|
|
931
1000
|
notes?: string;
|
|
932
1001
|
/** Why there is no quote, when a source said; "Quote unavailable." otherwise. */
|
|
933
1002
|
quoteNote?: string;
|
|
@@ -944,8 +1013,14 @@ export async function buildTickerReport({
|
|
|
944
1013
|
const profile = financials.profile;
|
|
945
1014
|
const name = quote?.name || tickerFile?.metadata.name || symbol;
|
|
946
1015
|
const baseQuoteOptions = quoteFormatOptions(quote, tickerFile?.metadata.assetCategory, financials.quoteMetadata?.instrumentType);
|
|
1016
|
+
const baseCurrency = config.baseCurrency.trim().toUpperCase();
|
|
1017
|
+
const codes = writesCurrencyCodes(config);
|
|
1018
|
+
const rates: ReportRateNotes = {
|
|
1019
|
+
noted: new Set(),
|
|
1020
|
+
boughtAt: boughtRateCurrencies(tickerFile, quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "", config.baseCurrency),
|
|
1021
|
+
};
|
|
947
1022
|
// Pad money prices to the currency's minor unit so a range reads £35.10 - £35.485, never past it (JPY has none).
|
|
948
|
-
const quoteOptions = withCurrencyMinorDigits(baseQuoteOptions, quote?.currency);
|
|
1023
|
+
const quoteOptions = withCurrencyMinorDigits(codes ? { ...baseQuoteOptions, currencyCode: true } : baseQuoteOptions, quote?.currency);
|
|
949
1024
|
const lines: string[] = [];
|
|
950
1025
|
|
|
951
1026
|
lines.push(`${cliStyles.accent(quote?.symbol ?? symbol)} ${cliStyles.bold(name)}`);
|
|
@@ -956,9 +1031,10 @@ export async function buildTickerReport({
|
|
|
956
1031
|
const indexLevel = isIndexLevel(quote?.instrumentType?.trim() || financials.quoteMetadata?.instrumentType?.trim() || tickerFile?.metadata.assetCategory);
|
|
957
1032
|
const summaryParts = [
|
|
958
1033
|
listingVenueLabel(listingExchange, quote, financials, tickerFile) || undefined,
|
|
959
|
-
indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${quoteCurrency}` : undefined,
|
|
1034
|
+
indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${currencyCodeLabel(quoteCurrency)}` : undefined,
|
|
960
1035
|
quoteMarketStateLabel(quote) ?? undefined,
|
|
961
|
-
|
|
1036
|
+
// A listing that no longer trades has no delay to speak of: its quote is its last session's close.
|
|
1037
|
+
quote?.dataSource && !quote.delisting ? `Source ${quote.dataSource.toUpperCase()}` : undefined,
|
|
962
1038
|
].filter((part): part is string => !!part);
|
|
963
1039
|
if (summaryParts.length > 0) {
|
|
964
1040
|
lines.push(cliStyles.muted(summaryParts.join(METADATA_SEPARATOR)));
|
|
@@ -977,14 +1053,15 @@ export async function buildTickerReport({
|
|
|
977
1053
|
lines.push(cliStyles.muted(metadataParts.join(METADATA_SEPARATOR)));
|
|
978
1054
|
}
|
|
979
1055
|
|
|
980
|
-
|
|
981
|
-
const
|
|
1056
|
+
// A list that holds the company as another of its listings says which: "Watchlists R (as RCI.B:TSX)".
|
|
1057
|
+
const lists = memberships ?? companyMemberships(config, tickerFile, [], null);
|
|
1058
|
+
const listNames = (held: readonly ListMembership[]) => held.map((list) => list.via ? `${list.name} (as ${list.via})` : list.name).join(", ");
|
|
982
1059
|
const membershipParts = [
|
|
983
|
-
|
|
984
|
-
? `Portfolios ${
|
|
1060
|
+
lists.portfolios.length > 0
|
|
1061
|
+
? `Portfolios ${listNames(lists.portfolios)}`
|
|
985
1062
|
: undefined,
|
|
986
|
-
|
|
987
|
-
? `Watchlists ${
|
|
1063
|
+
lists.watchlists.length > 0
|
|
1064
|
+
? `Watchlists ${listNames(lists.watchlists)}`
|
|
988
1065
|
: undefined,
|
|
989
1066
|
].filter((part): part is string => !!part);
|
|
990
1067
|
if (membershipParts.length > 0) {
|
|
@@ -994,21 +1071,27 @@ export async function buildTickerReport({
|
|
|
994
1071
|
// Market cap and the multiples are measured at the regular session price the Quote section heads with.
|
|
995
1072
|
const priced = atRegularSessionPrice(financials);
|
|
996
1073
|
const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
|
|
997
|
-
|
|
1074
|
+
// Market cap and EV read in the currency they are reported in, as the revenue and income beside them do,
|
|
1075
|
+
// with the base currency's figure after them when it differs: 627.2B USD (878.15B CAD).
|
|
1076
|
+
const convertedCurrencies = new Set<string>();
|
|
1077
|
+
const inBase = async (value: number, currency: string): Promise<string> => {
|
|
1078
|
+
// A minor unit converts as the hundredth of its currency it is: 2.75T GBp is 27.5B GBP, never 2.75T GBP.
|
|
1079
|
+
const unit = heldCurrencyUnit(currency);
|
|
1080
|
+
if (!unit.currency || (unit.currency === baseCurrency && unit.divisor === 1)) return "";
|
|
1081
|
+
const major = value / unit.divisor;
|
|
1082
|
+
const converted = unit.currency === baseCurrency ? major : await toBase(major, unit.currency);
|
|
1083
|
+
if (!Number.isFinite(converted)) return "";
|
|
1084
|
+
if (unit.currency !== baseCurrency) convertedCurrencies.add(unit.currency);
|
|
1085
|
+
return ` (${formatCompact(converted)} ${baseCurrency})`;
|
|
1086
|
+
};
|
|
998
1087
|
const marketCapText = capitalization
|
|
999
|
-
?
|
|
1000
|
-
? `${formatCompact(convertedMarketCap)} ${config.baseCurrency}`
|
|
1001
|
-
: `${formatCompact(capitalization.value)} ${capitalization.currency}`
|
|
1088
|
+
? `${formatCompact(capitalization.value)} ${capitalization.currency}${await inBase(capitalization.value, capitalization.currency)}`
|
|
1002
1089
|
: "—";
|
|
1003
1090
|
// Shown in the same currency as the market cap, so the two can be compared.
|
|
1004
1091
|
const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
|
|
1005
1092
|
const evCurrency = enterpriseValueCurrency(quote, fundamentals);
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
? await toBase(enterpriseValue, evCurrency) : Number.NaN;
|
|
1009
|
-
const enterpriseValueText = Number.isFinite(convertedEnterpriseValue)
|
|
1010
|
-
? `${formatCompact(convertedEnterpriseValue)} ${config.baseCurrency}`
|
|
1011
|
-
: formatReportedMoney(enterpriseValue, evCurrency);
|
|
1093
|
+
const enterpriseValueText = `${formatReportedMoney(enterpriseValue, evCurrency)}${enterpriseValue != null && evCurrency
|
|
1094
|
+
? await inBase(enterpriseValue, evCurrency) : ""}`;
|
|
1012
1095
|
|
|
1013
1096
|
if (quote) {
|
|
1014
1097
|
// As the Overview reads it: the regular session, then the extended-hours print against its close.
|
|
@@ -1023,13 +1106,16 @@ export async function buildTickerReport({
|
|
|
1023
1106
|
const volume = reportedQuoteVolume(quote);
|
|
1024
1107
|
// A futures price is its latest trade; the exchange settlement is its own row, dated by its session.
|
|
1025
1108
|
appendMetricSection(lines, "Quote", [
|
|
1026
|
-
|
|
1109
|
+
// A price in cents says what it is per: 314.95¢/lb.
|
|
1110
|
+
["Last", colorBySign(`${formatMarketPriceWithCurrency(session.price, quote.currency, quoteOptions)}${minorUnitPricePer(futuresContract, quote.currency)}`, session.change)],
|
|
1027
1111
|
...futuresContract
|
|
1028
1112
|
? [["Settle", futuresSettleText(futuresContract, quote)] as [string, string]]
|
|
1029
1113
|
: [],
|
|
1030
|
-
|
|
1114
|
+
// The move says what it is measured from: "vs Fri 16 Oct close", a future's "vs Fri 16 Oct settle".
|
|
1115
|
+
["Change", quoteChangeText(quote, session, quoteOptions, futuresContract)],
|
|
1031
1116
|
["Open", quote.open != null ? formatMarketPriceWithCurrency(quote.open, quote.currency, quoteOptions) : "—"],
|
|
1032
|
-
|
|
1117
|
+
// A listing that no longer trades: its last session's range, never today's.
|
|
1118
|
+
[quote.delisting ? "Last Session" : "Day Range", quote.low != null || quote.high != null
|
|
1033
1119
|
? formatPriceRange(quote.low, quote.high, quote.currency, quoteOptions)
|
|
1034
1120
|
: "—"],
|
|
1035
1121
|
["52W Range", quote.low52w != null || quote.high52w != null
|
|
@@ -1050,10 +1136,11 @@ export async function buildTickerReport({
|
|
|
1050
1136
|
}
|
|
1051
1137
|
|
|
1052
1138
|
const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
|
|
1053
|
-
{ text: enterpriseValueText }, distributions, bondFund);
|
|
1139
|
+
{ text: enterpriseValueText }, distributions, bondFund, codes);
|
|
1054
1140
|
const beforeFundamentals = lines.length;
|
|
1055
1141
|
appendMetricSection(lines, "Fundamentals", metricLines(metrics));
|
|
1056
1142
|
for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
|
|
1143
|
+
if (lines.length > beforeFundamentals) for (const note of conversionNotes(toBase, convertedCurrencies, rates)) lines.push(cliStyles.muted(note));
|
|
1057
1144
|
// Where a fund's facts would go: under its fundamentals, or in their place.
|
|
1058
1145
|
const fund = fundFactsNote(quote?.symbol ?? symbol,
|
|
1059
1146
|
quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
|
|
@@ -1071,13 +1158,13 @@ export async function buildTickerReport({
|
|
|
1071
1158
|
const latestAnnual = latestFinancialPeriod(financials.annualStatements, row => row.date);
|
|
1072
1159
|
if (latestAnnual) {
|
|
1073
1160
|
appendMetricSection(lines, `Latest Annual (${statementPeriodText(latestAnnual.date, statementFilings?.latestAnnual ?? null)})`,
|
|
1074
|
-
buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual)));
|
|
1161
|
+
buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual), codes));
|
|
1075
1162
|
}
|
|
1076
1163
|
|
|
1077
1164
|
const latestQuarter = latestFinancialPeriod(financials.quarterlyStatements, row => row.date);
|
|
1078
1165
|
if (latestQuarter) {
|
|
1079
1166
|
appendMetricSection(lines, `Latest Quarter (${statementPeriodText(latestQuarter.date, statementFilings?.latestQuarter ?? null)})`,
|
|
1080
|
-
buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter)));
|
|
1167
|
+
buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter), codes));
|
|
1081
1168
|
}
|
|
1082
1169
|
// The statements stop a quarter short of the results the company has released.
|
|
1083
1170
|
if (statementFilings?.resultsRelease && (latestAnnual || latestQuarter)) lines.push(cliStyles.muted(resultsReleaseText(statementFilings.resultsRelease)));
|
|
@@ -1113,7 +1200,7 @@ export async function buildTickerReport({
|
|
|
1113
1200
|
link: filing.filingUrl,
|
|
1114
1201
|
})), hiddenFilingsNote(hiddenSecFilings));
|
|
1115
1202
|
|
|
1116
|
-
await appendTickerPositions(lines, tickerFile, quote, config, toBase);
|
|
1203
|
+
await appendTickerPositions(lines, tickerFile, quote, config, toBase, rates);
|
|
1117
1204
|
|
|
1118
1205
|
return lines.join("\n");
|
|
1119
1206
|
}
|
|
@@ -1132,9 +1219,11 @@ function buildTickerStructuredData({
|
|
|
1132
1219
|
distributions,
|
|
1133
1220
|
statementFilings,
|
|
1134
1221
|
bondFund = null,
|
|
1222
|
+
memberships,
|
|
1135
1223
|
}: {
|
|
1136
1224
|
symbol: string;
|
|
1137
1225
|
listing: CliListing;
|
|
1226
|
+
memberships: CompanyMemberships;
|
|
1138
1227
|
statementFilings: StatementFilingsReport | null;
|
|
1139
1228
|
bondFund?: FundBondFacts | null;
|
|
1140
1229
|
futuresContract: FuturesContractTermsPayload | null;
|
|
@@ -1161,6 +1250,10 @@ function buildTickerStructuredData({
|
|
|
1161
1250
|
name: quote.name,
|
|
1162
1251
|
// Last and Change as the text report reads them, then the extended print from that close.
|
|
1163
1252
|
...getQuoteSessionFields(quote),
|
|
1253
|
+
// What Change is measured from ("vs Fri 16 Oct close"), and the quote's previous close with its exchange-local date.
|
|
1254
|
+
changeBasis: changeBasisJson(quoteChangeBasis(quote, { terms: futuresContract })),
|
|
1255
|
+
previousClose: quote.previousClose ?? null,
|
|
1256
|
+
previousCloseDate: quotePreviousCloseDate(quote),
|
|
1164
1257
|
priceBasis: quote.priceBasis ?? null,
|
|
1165
1258
|
currency: quote.currency,
|
|
1166
1259
|
marketCap: priced.quote?.marketCap ?? null,
|
|
@@ -1186,6 +1279,8 @@ function buildTickerStructuredData({
|
|
|
1186
1279
|
watchlists: formatWatchlistNames(config, tickerFile.metadata.watchlists),
|
|
1187
1280
|
positions: tickerFile.metadata.positions,
|
|
1188
1281
|
} : null,
|
|
1282
|
+
// The lists holding the company by any of its listings; `via` names the other listing a list holds it as.
|
|
1283
|
+
memberships,
|
|
1189
1284
|
fundamentals: financials.fundamentals || priceReturns.return1Y != null || priceReturns.return3Y != null ? exportedDividendFundamentals(exportedFundamentals({
|
|
1190
1285
|
...priced.fundamentals,
|
|
1191
1286
|
...priceReturns,
|
|
@@ -1318,6 +1413,15 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1318
1413
|
quarter: latestFinancialPeriod(resolvedFinancials.quarterlyStatements, (row) => row.date),
|
|
1319
1414
|
annual: latestFinancialPeriod(resolvedFinancials.annualStatements, (row) => row.date),
|
|
1320
1415
|
});
|
|
1416
|
+
// A list holding the company as another of its listings counts (a list of RCI.B:TSX holds RCI); read beside the rest. It never fails.
|
|
1417
|
+
const membershipsLoad = loadCompanyMemberships({
|
|
1418
|
+
config,
|
|
1419
|
+
own: tickerFile,
|
|
1420
|
+
symbol: listing.symbol,
|
|
1421
|
+
exchange: canonicalExchange(listing.exchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange),
|
|
1422
|
+
loadTickers: () => store.loadAllTickers(),
|
|
1423
|
+
dataProvider,
|
|
1424
|
+
});
|
|
1321
1425
|
const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
|
|
1322
1426
|
notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
|
|
1323
1427
|
dataProvider.getNews({
|
|
@@ -1351,6 +1455,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1351
1455
|
const coinNote = listing.exchange ? null
|
|
1352
1456
|
: notTheCoinNote(listing.symbol, quote ?? (tickerFile ? { name: tickerFile.metadata.name, instrumentType: tickerFile.metadata.assetCategory } : null));
|
|
1353
1457
|
const listingNote = [choiceNote, coinNote].filter((note): note is string => !!note).join("\n") || undefined;
|
|
1458
|
+
const memberships = await membershipsLoad;
|
|
1354
1459
|
if (dependencies.printResult) {
|
|
1355
1460
|
const warnings = [choiceNote, coinNote, quoteNote].filter((note): note is string => !!note);
|
|
1356
1461
|
dependencies.printResult({
|
|
@@ -1370,6 +1475,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1370
1475
|
distributions: await distributions,
|
|
1371
1476
|
statementFilings: await statementFilings,
|
|
1372
1477
|
bondFund: await bondFund,
|
|
1478
|
+
memberships,
|
|
1373
1479
|
}),
|
|
1374
1480
|
});
|
|
1375
1481
|
return;
|
|
@@ -1392,6 +1498,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1392
1498
|
distributions: await distributions,
|
|
1393
1499
|
statementFilings: await statementFilings,
|
|
1394
1500
|
bondFund: await bondFund,
|
|
1501
|
+
memberships,
|
|
1395
1502
|
}));
|
|
1396
1503
|
if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}`);
|
|
1397
1504
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isVenueAbroad } from "../tickers/listing-fate";
|
|
2
|
+
import type { Quote } from "../types/financials";
|
|
3
|
+
import { isCurrencyCode } from "../utils/currency-pair";
|
|
4
|
+
import { canonicalExchange } from "../utils/exchanges";
|
|
5
|
+
import { UNLISTED_TYPES, type CliListing } from "./listing-arg";
|
|
6
|
+
|
|
7
|
+
// Quoted as dollars per unit (EUR/USD); every other currency as units per dollar (USD/MXN).
|
|
8
|
+
const DOLLARS_PER_UNIT = new Set(["EUR", "GBP", "AUD", "NZD"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* "MXN is also a currency: fx MXN, quote USD/MXN" for a bare currency code
|
|
12
|
+
* that read as a listing abroad (MXN is mixi Inc on Stuttgart's exchange): the
|
|
13
|
+
* quote is that company's, and a currency was likely what was typed. Null for
|
|
14
|
+
* a code that is no currency, for the dollar (a US fund trades as USD, and
|
|
15
|
+
* there is no USD/USD), for a listing that named or saved its venue, and for
|
|
16
|
+
* one in the US.
|
|
17
|
+
*/
|
|
18
|
+
export function currencyCodeNote(
|
|
19
|
+
listing: Pick<CliListing, "symbol" | "exchange" | "saved">,
|
|
20
|
+
quote: Pick<Quote, "listingExchangeName" | "exchangeName" | "instrumentType"> | null | undefined,
|
|
21
|
+
): string | null {
|
|
22
|
+
const code = listing.symbol;
|
|
23
|
+
if (!quote || listing.exchange || listing.saved || code === "USD" || !/^[A-Z]{3}$/.test(code) || !isCurrencyCode(code)) return null;
|
|
24
|
+
if (UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return null;
|
|
25
|
+
if (!isVenueAbroad(canonicalExchange(quote.listingExchangeName || quote.exchangeName))) return null;
|
|
26
|
+
return `${code} is also a currency: fx ${code}, quote ${DOLLARS_PER_UNIT.has(code) ? `${code}/USD` : `USD/${code}`}`;
|
|
27
|
+
}
|
package/src/cli/fx-pair.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { currencyMinorDigits } from "../market-data/market/format";
|
|
2
2
|
import { parseTypedNumber, readCurrencyAmount } from "../utils/decimal-comma";
|
|
3
|
+
import { currenciesNamed } from "../utils/currency-names";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* What `gloomberb fx` is asked for. A bare code is one unit of it in the base
|
|
@@ -12,6 +13,8 @@ export interface FxRequest {
|
|
|
12
13
|
currency: string;
|
|
13
14
|
/** The currency the price is in: the base currency for a bare code, the quote of a pair. */
|
|
14
15
|
baseCurrency: string;
|
|
16
|
+
/** Each currency typed as its name (`yen`) and the code it was read as, said beside the answer. */
|
|
17
|
+
readAs?: string[];
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const CURRENCY_CODE = /^[A-Z]{3}$/;
|
|
@@ -30,19 +33,45 @@ function isKnownCurrency(code: string): boolean {
|
|
|
30
33
|
return knownCurrencies.has(code);
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A currency typed as its name: the code a name that is one currency's reads
|
|
38
|
+
* as, or the sentence listing the currencies a name several share (`peso`),
|
|
39
|
+
* with `retry` the argument that asks for the first of them. Null for text
|
|
40
|
+
* that names none.
|
|
41
|
+
*/
|
|
42
|
+
function readCurrencyName(typed: string, retry: (code: string) => string): { code: string } | { error: string } | null {
|
|
43
|
+
const named = currenciesNamed(typed);
|
|
44
|
+
if (named.length === 0) return null;
|
|
45
|
+
if (named.length === 1) return { code: named[0]!.code };
|
|
46
|
+
const choices = named.map(({ code, qualifier }) => (qualifier ? `${code} (${qualifier})` : code)).join(", ");
|
|
47
|
+
return { error: `${typed} names several currencies: ${choices}. Write one as its code, such as fx ${retry(named[0]!.code)}.` };
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
/** The request, or the sentence saying what is wrong with the argument. */
|
|
34
51
|
export function parseFxRequest(raw: string, baseCurrency: string): FxRequest | { error: string } {
|
|
52
|
+
const typed = raw.trim().split("/").map((part) => part.trim());
|
|
35
53
|
const text = raw.trim().toUpperCase();
|
|
36
|
-
const parts =
|
|
54
|
+
const parts = typed.map((part) => part.toUpperCase());
|
|
37
55
|
if (parts.length > 2 || parts.some((part) => !part)) return { error: `"${raw}" is not a currency or a pair such as USD/NGN.` };
|
|
38
56
|
// EURUSD is the pair typed without its slash: say how to write it.
|
|
39
57
|
const joined = parts.length === 1 && /^[A-Z]{6}$/.test(text) && isKnownCurrency(text.slice(0, 3)) && isKnownCurrency(text.slice(3));
|
|
40
58
|
if (joined) return { error: `${text} is not a currency code; write the pair as ${text.slice(0, 3)}/${text.slice(3)}.` };
|
|
59
|
+
// A name (peso, yen, Swiss franc) is a currency's code. Three letters that are a code stay one: only a name that is no code reads as one.
|
|
60
|
+
const readAs: string[] = [];
|
|
61
|
+
for (const [index, part] of parts.entries()) {
|
|
62
|
+
if (isKnownCurrency(part)) continue;
|
|
63
|
+
const named = readCurrencyName(typed[index]!, (code) => parts.map((other, at) => at === index ? code : other).join("/"));
|
|
64
|
+
if (!named) continue;
|
|
65
|
+
if ("error" in named) return named;
|
|
66
|
+
readAs.push(`Read ${typed[index]} as ${named.code}.`);
|
|
67
|
+
parts[index] = named.code;
|
|
68
|
+
}
|
|
41
69
|
const bad = parts.filter((part) => !CURRENCY_CODE.test(part));
|
|
42
70
|
if (bad.length > 0) return { error: `${bad.join(" and ")} ${bad.length > 1 ? "are not currency codes" : "is not a currency code"}; use three letters such as NGN.` };
|
|
43
|
-
|
|
71
|
+
const request = parts.length === 2
|
|
44
72
|
? { currency: parts[0]!, baseCurrency: parts[1]! }
|
|
45
73
|
: { currency: parts[0]!, baseCurrency: baseCurrency.trim().toUpperCase() };
|
|
74
|
+
return readAs.length > 0 ? { ...request, readAs } : request;
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
/** What `gloomberb fx` is asked for, with the amount to convert when one comes first. */
|
|
@@ -55,7 +84,14 @@ export interface FxArgs {
|
|
|
55
84
|
}
|
|
56
85
|
|
|
57
86
|
/** Every form `fx` takes, for its usage line. */
|
|
58
|
-
export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/<to> | fx <amount> <from> <to>";
|
|
87
|
+
export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/<to> | fx <amount> <from> <to> [--date <day>]";
|
|
88
|
+
|
|
89
|
+
/** A currency's name of several words (`hong kong dollar`, `100 swiss franc`) as the one argument it is. */
|
|
90
|
+
function joinNameWords(args: readonly string[]): readonly string[] {
|
|
91
|
+
const amount = args.length > 2 && parseTypedNumber(args[0]).kind !== "invalid" ? 1 : 0;
|
|
92
|
+
const name = args.slice(amount).join(" ");
|
|
93
|
+
return args.length - amount > 1 && currenciesNamed(name).length > 0 ? [...args.slice(0, amount), name] : args;
|
|
94
|
+
}
|
|
59
95
|
|
|
60
96
|
/**
|
|
61
97
|
* `fx` arguments: a currency or a pair (`EUR`, `USD/NGN`), or an amount and
|
|
@@ -66,7 +102,8 @@ export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/
|
|
|
66
102
|
* refused naming both readings. An amount and one currency converts to USD
|
|
67
103
|
* (`toUsdByDefault`). The error sentence otherwise.
|
|
68
104
|
*/
|
|
69
|
-
export function parseFxArgs(
|
|
105
|
+
export function parseFxArgs(words: readonly string[], baseCurrency: string): FxArgs | { error: string } {
|
|
106
|
+
const args = joinNameWords(words);
|
|
70
107
|
if (args.length === 1) {
|
|
71
108
|
const request = parseFxRequest(args[0]!, baseCurrency);
|
|
72
109
|
if (!("error" in request)) return { request };
|