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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logFailure } from "../../utils/log-failure";
|
|
2
2
|
import type { CliCommandDef } from "../../types/plugin";
|
|
3
3
|
import { TIME_RANGES, type TimeRange } from "../../time-series/range";
|
|
4
|
+
import { yearToDateStartIndex } from "../../market-data/market/year-to-date";
|
|
4
5
|
import type { EarningsEvent, QuoteBatchResult, QuoteSubscriptionTarget, SecFilingItem } from "../../types/data-provider";
|
|
5
6
|
import type { NewsArticle, NewsFeed, NewsQuery } from "../../news/types";
|
|
6
7
|
import type {
|
|
@@ -11,8 +12,9 @@ import type {
|
|
|
11
12
|
Quote,
|
|
12
13
|
TickerFinancials,
|
|
13
14
|
} from "../../types/financials";
|
|
14
|
-
import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, resolveAssetDisplayKind } from "../../market-data/market/format";
|
|
15
|
+
import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, resolveAssetDisplayKind, withCurrencyMinorDigits } from "../../market-data/market/format";
|
|
15
16
|
import { INDEX_POINTS_EXPORT_UNIT, isIndexLevel, reportedQuoteVolume } from "../../market-data/market/index-level";
|
|
17
|
+
import { changeBasisJson, changeBasisLabel, changeBasisNote, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
|
|
16
18
|
import {
|
|
17
19
|
EXTENDED_SESSION_LABELS,
|
|
18
20
|
getExtendedSessionDisplay,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
formatCountCell,
|
|
35
37
|
formatFractionPercentCell,
|
|
36
38
|
formatPriceRange,
|
|
39
|
+
quoteChangeText,
|
|
37
40
|
} from "../helpers";
|
|
38
41
|
import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
|
|
39
42
|
import { statementHistoryNotes } from "../../utils/financial-statements";
|
|
@@ -51,13 +54,14 @@ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshnes
|
|
|
51
54
|
import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
|
|
52
55
|
import {
|
|
53
56
|
cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions, loadReportBondFacts,
|
|
54
|
-
renderFundamentalsReport,
|
|
57
|
+
renderFundamentalsReport, writesCurrencyCodes,
|
|
55
58
|
} from "./ticker";
|
|
56
59
|
import { loadStatementFilings } from "../statement-filings";
|
|
57
60
|
import { loadBankFigures } from "../bank-section";
|
|
58
61
|
import { latestFinancialPeriod } from "../../utils/latest-financial-period";
|
|
59
62
|
import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
|
|
60
|
-
import {
|
|
63
|
+
import { currencyCodeLabel } from "../../utils/currency-units";
|
|
64
|
+
import { futuresContractLines, futuresRollLines, futuresSettleText, isFuturesInstrument, minorUnitPricePer } from "../../market-data/futures-contract";
|
|
61
65
|
import {
|
|
62
66
|
chainHasExpiry,
|
|
63
67
|
formatExpiryList,
|
|
@@ -77,7 +81,9 @@ import {
|
|
|
77
81
|
type OptionsChainBasis,
|
|
78
82
|
} from "./options-chain";
|
|
79
83
|
import { missingExpiryMessage, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../utils/option-expiry";
|
|
80
|
-
import { DATE_OPTION_EXAMPLES } from "../../utils/calendar-date";
|
|
84
|
+
import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
|
|
85
|
+
import { apiClient } from "../../api-client";
|
|
86
|
+
import { dailyFxLeg, fxCloseOnDate, type FxCloseReading } from "../../market-data/fx-closes";
|
|
81
87
|
import { declaredDividends } from "../../utils/declared-dividends";
|
|
82
88
|
import {
|
|
83
89
|
DEFAULT_LEAPS_CRITERIA, LEAPS_EXTRA_COLUMNS, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions,
|
|
@@ -113,9 +119,10 @@ import {
|
|
|
113
119
|
} from "../history-rows";
|
|
114
120
|
import { MISSING_VOLUME_TEXT } from "../../utils/price-history-integrity";
|
|
115
121
|
import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
|
|
116
|
-
import { bareFuturesCode } from "../../market-data/futures-board";
|
|
122
|
+
import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures-board";
|
|
123
|
+
import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
|
|
117
124
|
import { newsEmptyMessage } from "./news-hints";
|
|
118
|
-
import { commonWordCandidate, newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, quotedSearchCommand, searchNewsStories, wordMeansMore } from "./news-search";
|
|
125
|
+
import { commodityNewsRoute, commonWordCandidate, newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, noStoriesMessage, quotedSearchCommand, searchNewsStories, wordMeansMore } from "./news-search";
|
|
119
126
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
120
127
|
import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
|
|
121
128
|
import { isFiniteNumber } from "../../utils/guards";
|
|
@@ -141,10 +148,7 @@ import {
|
|
|
141
148
|
showsEpsBasis,
|
|
142
149
|
} from "../../plugins/builtin/shared/earnings-report";
|
|
143
150
|
import {
|
|
144
|
-
tradingBoardNote,
|
|
145
151
|
EXCHANGE_OPTION,
|
|
146
|
-
futuresTryLine,
|
|
147
|
-
isNoProviderError,
|
|
148
152
|
listingHeading,
|
|
149
153
|
listingIdentity,
|
|
150
154
|
listingTitle,
|
|
@@ -152,48 +156,43 @@ import {
|
|
|
152
156
|
listingChoiceNotes,
|
|
153
157
|
listingReadNotes,
|
|
154
158
|
loadBareVenues,
|
|
155
|
-
type ListingVenue,
|
|
156
159
|
loadForListing,
|
|
157
160
|
loadListingQuote,
|
|
158
161
|
listingFateLoader,
|
|
159
162
|
movedSymbolLine,
|
|
160
|
-
notTradedMessage,
|
|
161
|
-
quoteBareListing,
|
|
162
163
|
requireCliListing,
|
|
163
164
|
type CliListing,
|
|
164
|
-
type ListingDataRequest,
|
|
165
165
|
type ListingIdentity,
|
|
166
166
|
} from "../listing-arg";
|
|
167
167
|
import { bareListingNote, listingFateNotes, noLongerTradingNote, tradingListing } from "../../tickers/listing-fate";
|
|
168
|
-
import {
|
|
169
|
-
import { treasuryYieldLine, treasuryYieldTenor } from "../treasury-yields";
|
|
168
|
+
import { isYieldIndexSymbol } from "../index-roots";
|
|
170
169
|
import {
|
|
171
170
|
analyzeTaxEquivalent, distributionBasis, loadTreasuryTenors, parseTaxRates, TAX_RATE_HELP_OPTIONS, taxEquivalentJson, type TaxRates,
|
|
172
171
|
} from "../../market-data/tax-equivalent";
|
|
173
172
|
import { CBOE_YIELD_INDEX_CLOSE } from "../../plugins/builtin/shared/treasury-cutoffs";
|
|
174
173
|
import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
|
|
175
174
|
import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
|
|
176
|
-
import {
|
|
177
|
-
import { isNotATickerMessage
|
|
175
|
+
import { providerMissReason } from "../../sources/provider-errors";
|
|
176
|
+
import { isNotATickerMessage } from "../not-a-ticker";
|
|
178
177
|
import { primeBoardCoins } from "../board-coins";
|
|
179
|
-
import {
|
|
178
|
+
import { suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
|
|
180
179
|
import { quotedPhrase, splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-args";
|
|
181
180
|
import { isCompanyNameInput } from "../../tickers/company-name-listing";
|
|
182
181
|
import { readNameEntries, readPhraseArgument } from "../name-entries";
|
|
183
182
|
import { readSymbolList, SymbolListError } from "../symbol-list";
|
|
184
183
|
import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
|
|
185
|
-
import {
|
|
184
|
+
import { knownGapMessage, subMinuteDataMessage } from "../known-gaps";
|
|
186
185
|
import { phraseSearchReport } from "./search";
|
|
187
186
|
import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
|
|
188
187
|
import { isCryptoPairSymbol } from "../../utils/crypto-pair";
|
|
189
|
-
import {
|
|
190
|
-
import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
|
|
188
|
+
import { isUsListingExchange } from "../../utils/exchanges";
|
|
191
189
|
import {
|
|
192
190
|
describeFormerIssuer, describeTickerChange, movedAwayChange, startTickerChangeLookup, tickerChangeLine, type FormerIssuer, type TickerChange,
|
|
193
191
|
} from "../../tickers/ticker-changes";
|
|
194
192
|
import { noSecReportsNotice, nonUsSecListingVenue, secFilingItemCodes } from "../../utils/sec";
|
|
195
193
|
import type { MarketContext } from "../types";
|
|
196
194
|
import { windowRows } from "../row-window";
|
|
195
|
+
import { answerQuote, errorMessage, notUsTickerSentence } from "../quote-answer";
|
|
197
196
|
import { crossRate, describeFxConversion, describeFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
|
|
198
197
|
import {
|
|
199
198
|
holderListFacts,
|
|
@@ -219,8 +218,9 @@ import { centralBankEventsMessage } from "../central-bank-events";
|
|
|
219
218
|
import { classSharesToday } from "../../plugins/builtin/holders/client";
|
|
220
219
|
import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
|
|
221
220
|
import { currentReportItemsLabel } from "../../utils/sec-filings";
|
|
222
|
-
import { loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
223
|
-
import { insiderEmptyNotice } from "../../plugins/builtin/insider/issuer";
|
|
221
|
+
import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
222
|
+
import { insiderEmptyNotice, loadHomeInsiderReportsNote } from "../../plugins/builtin/insider/issuer";
|
|
223
|
+
import { buildInsiderSummary } from "../../plugins/builtin/insider/model";
|
|
224
224
|
import {
|
|
225
225
|
BENEFICIAL_REPORT_COLUMNS,
|
|
226
226
|
beneficialCoverageNotices,
|
|
@@ -235,7 +235,10 @@ import {
|
|
|
235
235
|
type HolderForm,
|
|
236
236
|
} from "../../plugins/builtin/holders/beneficial-report";
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
/** The ranges `history` takes: the chart ranges, and YTD, which runs from the prior year's last close. */
|
|
239
|
+
type HistoryRange = TimeRange | "YTD";
|
|
240
|
+
const HISTORY_RANGES: HistoryRange[] = [...TIME_RANGES.slice(0, TIME_RANGES.indexOf("1Y")), "YTD", ...TIME_RANGES.slice(TIME_RANGES.indexOf("1Y"))];
|
|
241
|
+
const VALID_RANGES = new Set<HistoryRange>(HISTORY_RANGES);
|
|
239
242
|
const VALID_NEWS_FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
|
|
240
243
|
|
|
241
244
|
type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
|
|
@@ -261,9 +264,9 @@ type FinancialsCliData = TickerFinancials & {
|
|
|
261
264
|
providerId: string | null;
|
|
262
265
|
};
|
|
263
266
|
|
|
264
|
-
function parseRange(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]):
|
|
265
|
-
const range = (value ?? "1Y").toUpperCase() as
|
|
266
|
-
if (!VALID_RANGES.has(range)) ctx.fail(`Unknown range "${value}".`, `Use one of ${
|
|
267
|
+
function parseRange(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): HistoryRange {
|
|
268
|
+
const range = (value ?? "1Y").toUpperCase() as HistoryRange;
|
|
269
|
+
if (!VALID_RANGES.has(range)) ctx.fail(`Unknown range "${value}".`, `Use one of ${HISTORY_RANGES.join(", ")}.`);
|
|
267
270
|
return range;
|
|
268
271
|
}
|
|
269
272
|
|
|
@@ -288,8 +291,8 @@ function uniqueEntries(entries: readonly string[]): string[] {
|
|
|
288
291
|
|
|
289
292
|
/**
|
|
290
293
|
* The symbols to resolve: each entry with a space read as a company's name
|
|
291
|
-
* when search finds one, else as its words
|
|
292
|
-
* with the repeats that leaves dropped. A name
|
|
294
|
+
* when search finds one, else as its words when they are symbols
|
|
295
|
+
* (`readNameEntries`), with the repeats that leaves dropped. A name stays whole.
|
|
293
296
|
*/
|
|
294
297
|
async function readSymbols(symbols: readonly string[], dataProvider: Parameters<typeof readNameEntries>[1]): Promise<string[]> {
|
|
295
298
|
return uniqueEntries(await readNameEntries(symbols, dataProvider));
|
|
@@ -363,6 +366,28 @@ const QUOTE_LEAD_COLUMNS = [
|
|
|
363
366
|
|
|
364
367
|
type QuoteRow = ReturnType<typeof quoteRows>[number];
|
|
365
368
|
|
|
369
|
+
/**
|
|
370
|
+
* What Chg% is measured from, as every change line says it ("vs Fri 16 Oct
|
|
371
|
+
* close", "since 00:00 UTC"): the table sets it after Chg% and names the
|
|
372
|
+
* venue's holidays under it; an export adds it, and the holiday note, after
|
|
373
|
+
* its existing columns, so a column read by position stays where it was.
|
|
374
|
+
*/
|
|
375
|
+
const CHANGE_BASIS_COLUMN = {
|
|
376
|
+
key: "changeBasis", header: "Chg Basis", shrink: false, optional: true, format: (value: unknown) => cliStyles.muted(String(value ?? "")),
|
|
377
|
+
};
|
|
378
|
+
const CHANGE_BASIS_EXPORT_COLUMNS = [
|
|
379
|
+
{ key: "changeBasis", header: "Chg Basis" },
|
|
380
|
+
{ key: "changeBasisNote", header: "Chg Basis Note" },
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
/** The table's columns: Chg Basis follows Chg%, and the note is a line under the table. */
|
|
384
|
+
function quoteTextColumns<Column extends { key: string }>(columns: Column[]): Array<Column | typeof CHANGE_BASIS_COLUMN> {
|
|
385
|
+
const exported = new Set(CHANGE_BASIS_EXPORT_COLUMNS.map((column) => column.key));
|
|
386
|
+
const kept = columns.filter((column) => !exported.has(column.key));
|
|
387
|
+
const at = kept.findIndex((column) => column.key === "changePercent") + 1;
|
|
388
|
+
return [...kept.slice(0, at), CHANGE_BASIS_COLUMN, ...kept.slice(at)];
|
|
389
|
+
}
|
|
390
|
+
|
|
366
391
|
/** A Cboe yield index level is a yield: `5.24%`, not a price. */
|
|
367
392
|
function yieldLevelCell(value: unknown, row: QuoteRow): string {
|
|
368
393
|
const text = String(value ?? "");
|
|
@@ -398,10 +423,15 @@ function roundedPercent(value: number | undefined): number | null {
|
|
|
398
423
|
return value == null ? null : Number(value.toFixed(2));
|
|
399
424
|
}
|
|
400
425
|
|
|
426
|
+
/** The quote's currency code, US cents as US¢; an export keeps the code (USX). */
|
|
427
|
+
function currencyCell(value: unknown): string {
|
|
428
|
+
return currencyCodeLabel(typeof value === "string" ? value : "");
|
|
429
|
+
}
|
|
430
|
+
|
|
401
431
|
function quoteColumns() {
|
|
402
432
|
return [
|
|
403
433
|
...QUOTE_LEAD_COLUMNS,
|
|
404
|
-
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1 },
|
|
434
|
+
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1, format: currencyCell },
|
|
405
435
|
// Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
|
|
406
436
|
// row says so here, so the closing line's "N of M stale" always points at rows the table marks.
|
|
407
437
|
{
|
|
@@ -414,6 +444,7 @@ function quoteColumns() {
|
|
|
414
444
|
},
|
|
415
445
|
// In a narrow terminal the closing line's as-of stands in for each row's.
|
|
416
446
|
{ key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
|
|
447
|
+
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
417
448
|
];
|
|
418
449
|
}
|
|
419
450
|
|
|
@@ -424,15 +455,11 @@ function compareColumns() {
|
|
|
424
455
|
// A narrow terminal drops these before it cuts the names short.
|
|
425
456
|
{ key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
|
|
426
457
|
{ key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
|
|
427
|
-
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3 },
|
|
458
|
+
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3, format: currencyCell },
|
|
459
|
+
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
428
460
|
];
|
|
429
461
|
}
|
|
430
462
|
|
|
431
|
-
function errorMessage(error: unknown): string | null {
|
|
432
|
-
if (error == null) return null;
|
|
433
|
-
return error instanceof Error ? error.message : String(error);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
463
|
/** What the Feed cell says: stale wins over the feed's own live or delayed, as the closing line counts it. */
|
|
437
464
|
function quoteFeedState(quote: QuoteCliRecord["quote"], now: number): "stale" | "live" | "delayed" | "" {
|
|
438
465
|
if (!quote) return "";
|
|
@@ -455,6 +482,34 @@ function nearParCoin(quote: QuoteCliRecord["quote"]): boolean {
|
|
|
455
482
|
return reference != null && reference >= 0.9 && reference < 1.1;
|
|
456
483
|
}
|
|
457
484
|
|
|
485
|
+
/** What a row's move is measured from, worded as its Chg Basis cell: empty without a move. */
|
|
486
|
+
function rowChangeBasis(result: Pick<QuoteCliRecord, "quote" | "futuresContract">) {
|
|
487
|
+
return quoteChangeBasis(result.quote, { terms: result.futuresContract });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function basisLabel(result: Pick<QuoteCliRecord, "quote" | "futuresContract">, now: number): string {
|
|
491
|
+
const basis = rowChangeBasis(result);
|
|
492
|
+
return basis ? changeBasisLabel(basis, now) : "";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The lines under the table naming the days a row's venue was closed since
|
|
497
|
+
* the close its move is from ("HKEX closed Mon 19 Oct, holiday"), once each.
|
|
498
|
+
*/
|
|
499
|
+
function changeBasisNotes(results: QuoteCliRecord[], now = Date.now()): string[] {
|
|
500
|
+
return [...new Set(results.flatMap((result) => changeBasisNote(rowChangeBasis(result), now) ?? []))];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** The quote as `--json` gives it: `previousCloseDate`, the exchange-local date of its previous close, follows `previousClose`. */
|
|
504
|
+
function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { previousCloseDate?: string | null }) | null {
|
|
505
|
+
if (!quote) return null;
|
|
506
|
+
const previousCloseDate = quotePreviousCloseDate(quote);
|
|
507
|
+
const entries = Object.entries(quote);
|
|
508
|
+
const at = entries.findIndex(([key]) => key === "previousClose");
|
|
509
|
+
entries.splice(at < 0 ? entries.length : at + 1, 0, ["previousCloseDate", previousCloseDate]);
|
|
510
|
+
return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
|
|
511
|
+
}
|
|
512
|
+
|
|
458
513
|
function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
459
514
|
return results.map((result) => {
|
|
460
515
|
const quote = result.quote;
|
|
@@ -477,10 +532,12 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
477
532
|
? indexPoints ? formatMarketPrice(value, options) : formatMarketPriceWithCurrency(value, quote.currency, options)
|
|
478
533
|
: ""
|
|
479
534
|
);
|
|
535
|
+
// A price in cents says what it is per, as the contract terms word it: 314.95¢/lb.
|
|
536
|
+
const per = display?.price != null ? minorUnitPricePer(result.futuresContract, quote?.currency) : "";
|
|
480
537
|
return {
|
|
481
538
|
symbol: result.target.symbol,
|
|
482
539
|
name: quote?.name || result.listing?.name || "",
|
|
483
|
-
price: price(display?.price)
|
|
540
|
+
price: `${price(display?.price)}${per}`,
|
|
484
541
|
rawPrice: display?.price ?? null,
|
|
485
542
|
priceBasis: quote?.priceBasis ?? null,
|
|
486
543
|
instrumentType: quote?.instrumentType ?? null,
|
|
@@ -512,6 +569,9 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
512
569
|
// When the price Last shows was observed: in a pre-market nothing has traded in, the close it stands at.
|
|
513
570
|
updatedAt: quote?.lastUpdated ? new Date(quoteHeadlineTime(quote)).toISOString() : "",
|
|
514
571
|
error: result.error ?? "",
|
|
572
|
+
// Last, so a row read by position keeps every field where it was.
|
|
573
|
+
changeBasis: basisLabel(result, now),
|
|
574
|
+
changeBasisNote: changeBasisNote(rowChangeBasis(result), now) ?? "",
|
|
515
575
|
};
|
|
516
576
|
});
|
|
517
577
|
}
|
|
@@ -783,7 +843,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
783
843
|
listing: listingMetadata(listingIdentity(listing, result.quote)),
|
|
784
844
|
// The figures the table shows, beside the quote as the source sent it.
|
|
785
845
|
...getQuoteSessionFields(result.quote),
|
|
786
|
-
|
|
846
|
+
// What `change` is measured from, with the label and holiday note the table shows.
|
|
847
|
+
changeBasis: changeBasisJson(quoteChangeBasis(result.quote, { terms: contracts[index] }), now),
|
|
848
|
+
quote: withPreviousCloseDate(result.quote),
|
|
787
849
|
...contracts[index] ? { futuresContract: contracts[index] } : {},
|
|
788
850
|
...fronts[index] ? { futuresFront: fronts[index] } : {},
|
|
789
851
|
...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
|
|
@@ -809,6 +871,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
809
871
|
...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
|
|
810
872
|
];
|
|
811
873
|
const columns = commandName === "compare" ? compareColumns() : quoteColumns();
|
|
874
|
+
const basisNotes = changeBasisNotes(data, now);
|
|
812
875
|
if (ctx.cliOptions.format !== "json") await primeBoardCoins(notes);
|
|
813
876
|
// Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
|
|
814
877
|
const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
|
|
@@ -826,8 +889,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
826
889
|
currencyOrUnit: row.currency || (row.yieldIndex ? "pct" : isIndexLevel(row.instrumentType) ? INDEX_POINTS_EXPORT_UNIT : null),
|
|
827
890
|
}),
|
|
828
891
|
details: quoteDetailsText,
|
|
892
|
+
...(basisNotes.length ? { footnote: basisNotes.join("\n") } : {}),
|
|
829
893
|
columns,
|
|
830
|
-
|
|
894
|
+
textColumns: quoteTextColumns(yieldsOnly
|
|
895
|
+
? columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column)
|
|
896
|
+
: columns),
|
|
831
897
|
});
|
|
832
898
|
// The table names each symbol it found nothing for; with none found, the exit status says so too.
|
|
833
899
|
if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
|
|
@@ -864,15 +930,11 @@ function quoteDetailsText(data: QuoteCliRecord[]): string {
|
|
|
864
930
|
|
|
865
931
|
/**
|
|
866
932
|
* "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
|
|
867
|
-
* mean AAPL (Apple Inc.)?"
|
|
868
|
-
* slip from a US ticker, worded as the not-a-ticker answers word it.
|
|
933
|
+
* mean AAPL (Apple Inc.)?" in the warning colour, for a row that has one.
|
|
869
934
|
*/
|
|
870
|
-
function notUsTickerLine(
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const name = quote.name || listing?.name;
|
|
874
|
-
const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
|
|
875
|
-
return cliStyles.warning(`${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`);
|
|
935
|
+
function notUsTickerLine(row: QuoteCliRecord): string | null {
|
|
936
|
+
const sentence = notUsTickerSentence(row);
|
|
937
|
+
return sentence && cliStyles.warning(sentence);
|
|
876
938
|
}
|
|
877
939
|
|
|
878
940
|
/** Each futures row's front contract quote, when its terms name a front other than its own contract; null otherwise. */
|
|
@@ -903,12 +965,17 @@ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
|
|
|
903
965
|
const terms = row.futuresContract;
|
|
904
966
|
if (!terms || !row.quote) return [];
|
|
905
967
|
const settle: [string, string] = ["Settle", futuresSettleText(terms, row.quote)];
|
|
968
|
+
// The move Chg% gives, in the quote's money and with what it is measured from: "vs Fri settle".
|
|
969
|
+
const session = getRegularSessionDisplay(row.quote);
|
|
970
|
+
const change: Array<[string, string]> = session?.change != null && Number.isFinite(session.change)
|
|
971
|
+
? [["Change", quoteChangeText(row.quote, session, withCurrencyMinorDigits(quoteFormatOptions(row.quote, "FUTURE"), row.quote.currency), terms)]]
|
|
972
|
+
: [];
|
|
906
973
|
const metrics = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
|
|
907
974
|
const rolls = futuresRollLines(terms, row.futuresFront).map((line): [string, string] => [line.label, line.value]);
|
|
908
975
|
const at = futuresContractLines(terms, row.quote.symbol || row.target.symbol)[0]?.key === "contract" ? 1 : 0;
|
|
909
976
|
const contract = [...metrics.slice(0, at), ...rolls, ...metrics.slice(at)];
|
|
910
|
-
if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...contract])}`];
|
|
911
|
-
return [[renderStats([settle]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
|
|
977
|
+
if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...change, ...contract])}`];
|
|
978
|
+
return [[renderStats([settle, ...change]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
|
|
912
979
|
}).join("\n\n");
|
|
913
980
|
}
|
|
914
981
|
|
|
@@ -923,91 +990,6 @@ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeB
|
|
|
923
990
|
return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
|
|
924
991
|
}
|
|
925
992
|
|
|
926
|
-
interface QuoteAnswer {
|
|
927
|
-
result: QuoteBatchResult;
|
|
928
|
-
/** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
|
|
929
|
-
listing: CliListing;
|
|
930
|
-
/** Why there is no quote, when there is more to say than the source's own error. */
|
|
931
|
-
failure?: { message: string; details?: string; oneLine: string };
|
|
932
|
-
notes: string[];
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
/**
|
|
936
|
-
* One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
|
|
937
|
-
* is not listed on says so. A bare symbol the data service read on its home
|
|
938
|
-
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
|
|
939
|
-
* quoted on its listings (`quoteBareListing`), else names them, or asks for an
|
|
940
|
-
* exchange when several companies list it (2222); a bare index root (VIX) also
|
|
941
|
-
* names its index (^VIX), and a futures board code a security keeps (GC) its
|
|
942
|
-
* future (GC=F). Venues are only looked up for a
|
|
943
|
-
* symbol with no quote, so a request that succeeds costs no search.
|
|
944
|
-
*/
|
|
945
|
-
async function answerQuote(
|
|
946
|
-
result: QuoteBatchResult,
|
|
947
|
-
listing: CliListing,
|
|
948
|
-
market: MarketContext,
|
|
949
|
-
request: ListingDataRequest,
|
|
950
|
-
refresh: boolean,
|
|
951
|
-
venues: Promise<ListingVenue[]> | null,
|
|
952
|
-
): Promise<QuoteAnswer> {
|
|
953
|
-
const root = listing.exchange ? null : indexRootFor(listing.symbol);
|
|
954
|
-
// A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
|
|
955
|
-
const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
|
|
956
|
-
? [indexRootAlsoLine(root, request.command)] : [];
|
|
957
|
-
if (result.quote) {
|
|
958
|
-
// "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
|
|
959
|
-
const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
|
|
960
|
-
const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
|
|
961
|
-
// A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
|
|
962
|
-
const board = tradingBoardNote(listing, result.quote);
|
|
963
|
-
return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote)].flat() };
|
|
964
|
-
}
|
|
965
|
-
const notTraded = await notTradedMessage(listing, market);
|
|
966
|
-
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
967
|
-
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
968
|
-
const other = await quoteBareListing(listing, market, request, { refresh });
|
|
969
|
-
if (other?.kind === "quoted") {
|
|
970
|
-
return {
|
|
971
|
-
result: { target: other.listing.request, quote: other.quote },
|
|
972
|
-
listing: other.listing,
|
|
973
|
-
notes: [other.note, ...also(other.quote)],
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
// Other listings already name a futures board code's future (GC=F); with none, it is named here.
|
|
977
|
-
// A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
|
|
978
|
-
const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
|
|
979
|
-
const tryLines = [
|
|
980
|
-
yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
|
|
981
|
-
other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
|
|
982
|
-
// BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
|
|
983
|
-
listing.exchange ? null : knownGapForTicker(listing.symbol),
|
|
984
|
-
].filter((line): line is string => !!line);
|
|
985
|
-
if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
|
|
986
|
-
const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
|
|
987
|
-
const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
|
|
988
|
-
const oneLine = [
|
|
989
|
-
message,
|
|
990
|
-
other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
|
|
991
|
-
...tryLines,
|
|
992
|
-
].filter(Boolean).join(" ");
|
|
993
|
-
return { result, listing, failure: { message, details, oneLine }, notes: [] };
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* A symbol written like a currency pair that is not a ticker (USD_NGN, as some
|
|
998
|
-
* FX feeds write a pair) names the listing `gloomberb search` finds nearest to
|
|
999
|
-
* it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
|
|
1000
|
-
* Naira)?". When search finds nothing near, the row keeps its own error and its
|
|
1001
|
-
* search hint.
|
|
1002
|
-
*/
|
|
1003
|
-
async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
|
|
1004
|
-
const message = errorMessage(result.error);
|
|
1005
|
-
const symbol = message ? notATickerSymbol(message) : null;
|
|
1006
|
-
if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
|
|
1007
|
-
const suggestion = await suggestListing(symbol, market);
|
|
1008
|
-
return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
993
|
/** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
|
|
1012
994
|
function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
|
|
1013
995
|
if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
|
|
@@ -1016,6 +998,7 @@ function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandD
|
|
|
1016
998
|
|
|
1017
999
|
const HISTORY_USAGE = "history <symbol> [--range <range>]";
|
|
1018
1000
|
const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
|
|
1001
|
+
const HISTORY_YTD_RANGE = "YTD starts at the prior year's last close, the one year-to-date returns are measured from";
|
|
1019
1002
|
/** Options people reach for to ask for dates, which history answers with a range. */
|
|
1020
1003
|
const DATE_WINDOW_OPTIONS = new Set(["--from", "--to", "--start", "--end", "--since", "--until", "--date", "--start-date", "--end-date"]);
|
|
1021
1004
|
/** Options people reach for to pick a bar size, which history derives from the range. */
|
|
@@ -1052,14 +1035,23 @@ function financialsOptionHint(flag: string, args: readonly string[]): string | n
|
|
|
1052
1035
|
}
|
|
1053
1036
|
|
|
1054
1037
|
function historyOptionHint(flag: string, args: readonly string[]): string | null {
|
|
1055
|
-
if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${
|
|
1038
|
+
if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${HISTORY_RANGES.join(", ")}. ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}.`;
|
|
1056
1039
|
if (BAR_SIZE_OPTIONS.has(flag)) return `history serves each --range in one bar size. ${subMinuteDataMessage(historySymbolArg(args))}`;
|
|
1057
1040
|
return null;
|
|
1058
1041
|
}
|
|
1059
1042
|
|
|
1043
|
+
/** "Asked YTD, data starts 2026-03-05 (first available bar)" when no bar reaches the prior year's last close. */
|
|
1044
|
+
function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
|
|
1045
|
+
const first = rows[0]?.date.slice(0, 10);
|
|
1046
|
+
const last = rows.at(-1)?.date.slice(0, 10);
|
|
1047
|
+
return first && last && first > `${Number(last.slice(0, 4)) - 1}-12-31` ? [`Asked YTD, data starts ${first} (first available bar)`] : [];
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1060
1050
|
async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1061
1051
|
const args = [...rawArgs];
|
|
1062
1052
|
const range = parseRange(takeOption(args, "--range"), ctx);
|
|
1053
|
+
// YTD reads the year's daily bars and starts at the last one on or before 31 Dec.
|
|
1054
|
+
const loadRange: TimeRange = range === "YTD" ? "1Y" : range;
|
|
1063
1055
|
const requestedExchange = takeOption(args, "--exchange");
|
|
1064
1056
|
const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
|
|
1065
1057
|
await withMarketData(ctx, async (market) => {
|
|
@@ -1068,8 +1060,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1068
1060
|
const { symbol, exchange } = listing.request;
|
|
1069
1061
|
const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
|
|
1070
1062
|
const load = () => market.dataProvider.getPriceHistoryWithMetadata
|
|
1071
|
-
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange,
|
|
1072
|
-
: market.dataProvider.getPriceHistory(symbol, exchange,
|
|
1063
|
+
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
|
|
1064
|
+
: market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
|
|
1073
1065
|
const quoted = loadListingQuote(market.dataProvider, listing);
|
|
1074
1066
|
const fate = listingFateLoader(listing, market, quoted);
|
|
1075
1067
|
const [loaded, quote] = await Promise.all([
|
|
@@ -1092,7 +1084,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1092
1084
|
};
|
|
1093
1085
|
// A coin's volume is traded value in its quote currency: said in every row and on the status line.
|
|
1094
1086
|
const volume = historyVolume(unit, traded);
|
|
1095
|
-
const
|
|
1087
|
+
const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
|
|
1088
|
+
const served = range === "YTD" ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval))) : rows;
|
|
1096
1089
|
// Bars stop arriving on the listing's own trading days, not New York's.
|
|
1097
1090
|
const venue = quote?.listingExchangeName || exchange || undefined;
|
|
1098
1091
|
// October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
|
|
@@ -1122,7 +1115,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1122
1115
|
// FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
|
|
1123
1116
|
...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
|
|
1124
1117
|
...await listingChoiceNotes(listing, market, quote, venues),
|
|
1125
|
-
...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? [],
|
|
1118
|
+
...range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? []],
|
|
1126
1119
|
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1127
1120
|
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1128
1121
|
historyBarStatusNote(forming ? served : data) ?? [],
|
|
@@ -1283,7 +1276,9 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1283
1276
|
symbol: identity.symbol || listing.key, bond, rates, treasury: await treasury,
|
|
1284
1277
|
distribution: distributionBasis(priced.fundamentals?.dividendYield, priced.fundamentals?.dividendYieldBasis === "trailing" ? "trailing" : "forward"),
|
|
1285
1278
|
}) : null;
|
|
1286
|
-
const report = { ...data, distributions: await distributions, statementFilings, bank: bank.bank, bankNote: bank.note, bondFund: bond, taxEquivalent
|
|
1279
|
+
const report = { ...data, distributions: await distributions, statementFilings, bank: bank.bank, bankNote: bank.note, bondFund: bond, taxEquivalent,
|
|
1280
|
+
// A base currency other than USD reads every amount with its code, as `ticker` does.
|
|
1281
|
+
currencyCodes: writesCurrencyCodes(market.config) };
|
|
1287
1282
|
ctx.printResult({
|
|
1288
1283
|
// JSON reads a multiple over a loss as null with its reason, never as a number.
|
|
1289
1284
|
data: {
|
|
@@ -1359,22 +1354,33 @@ const NEWS_COLUMNS = [
|
|
|
1359
1354
|
{ key: "url", header: "URL", optional: true },
|
|
1360
1355
|
];
|
|
1361
1356
|
|
|
1362
|
-
/**
|
|
1357
|
+
/**
|
|
1358
|
+
* Stories matching text, printed as a symbol's news is, under a line saying
|
|
1359
|
+
* what they match. A text naming a commodity that matched nothing says so and
|
|
1360
|
+
* offers its future's news when that has stories; `commodity: false` for the
|
|
1361
|
+
* future's own news, which is that route. A search that failed (`failure`,
|
|
1362
|
+
* its message) says so, with `empty`, the lookup's own answer, rather than
|
|
1363
|
+
* that no story matches.
|
|
1364
|
+
*/
|
|
1363
1365
|
async function printNewsSearch(
|
|
1364
1366
|
text: string,
|
|
1365
1367
|
articles: NewsArticle[],
|
|
1366
|
-
{ reason, empty }: { reason?: string; empty?: string },
|
|
1368
|
+
{ reason, empty, commodity = true, failure }: { reason?: string; empty?: string; commodity?: boolean; failure?: string },
|
|
1367
1369
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1368
1370
|
) {
|
|
1371
|
+
const searched = failure === undefined;
|
|
1372
|
+
const named = searched && commodity && articles.length === 0 && commodityNamedIn(text) !== null;
|
|
1373
|
+
const route = named ? await commodityNewsRoute(text) : null;
|
|
1369
1374
|
ctx.printResult({
|
|
1370
1375
|
data: articles,
|
|
1371
|
-
|
|
1376
|
+
...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
|
|
1377
|
+
metadata: { ticker: null, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
|
|
1372
1378
|
freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
|
|
1373
1379
|
}, {
|
|
1374
1380
|
heading: newsSearchHeading(text, reason),
|
|
1375
1381
|
rows: newsRows,
|
|
1376
1382
|
columns: NEWS_COLUMNS,
|
|
1377
|
-
empty: empty ??
|
|
1383
|
+
empty: named ? noStoriesMessage(text, route) : empty ?? (searched ? noStoriesMessage(text, null) : "No stories to show."),
|
|
1378
1384
|
});
|
|
1379
1385
|
}
|
|
1380
1386
|
|
|
@@ -1409,18 +1415,29 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1409
1415
|
]);
|
|
1410
1416
|
const identity = listing ? listingIdentity(listing, quote) : null;
|
|
1411
1417
|
const resolved = quote !== null || identity?.name != null;
|
|
1418
|
+
const unfiled = !!listing && !listing.exchange && !feed && articles.length === 0;
|
|
1419
|
+
// A future's news is its commodity's: news is filed under company tickers, so KC=F (or a bare code
|
|
1420
|
+
// no security keeps, CT) searches the stories of coffee, which the service matches to KC=F's links too.
|
|
1421
|
+
const future = unfiled && isFuturesInstrument(listing!.symbol, quote?.instrumentType)
|
|
1422
|
+
? futuresBoardContract(listing!.symbol) ?? bareFuturesCode(args[0]) : null;
|
|
1423
|
+
const commodity = future ? commoditySearchName(future) : null;
|
|
1412
1424
|
// A bare word no listing answers to is text, and so is a futures board code (TTF), which resolves
|
|
1413
|
-
// to its future:
|
|
1414
|
-
const word =
|
|
1425
|
+
// to its future: the stories that mention the word are the news.
|
|
1426
|
+
const word = commodity ?? (unfiled ? newsFallbackText(args[0]!) : null);
|
|
1415
1427
|
const futuresCode = !!word && !!bareFuturesCode(word) && isFuturesInstrument(listing!.symbol, quote?.instrumentType);
|
|
1416
|
-
const reason =
|
|
1428
|
+
const reason = commodity ? `the commodity ${future!.symbol} trades`
|
|
1429
|
+
: !resolved ? "no ticker by that name" : futuresCode ? "a futures code, not a ticker" : null;
|
|
1417
1430
|
const emptyMessage = (searched: boolean) => listing && identity
|
|
1418
1431
|
? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
|
|
1419
1432
|
: undefined;
|
|
1420
1433
|
if (word && reason) {
|
|
1421
1434
|
// The lookup that found no ticker already answered; a search that fails leaves that answer.
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1435
|
+
const search = await searchNewsStories(word, limit).then((found) => ({ found, failure: undefined }), (error: unknown) => {
|
|
1436
|
+
logFailure("cli.commands-market", "Search news stories failed")(error);
|
|
1437
|
+
return { found: [], failure: errorMessage(error) ?? "no answer" };
|
|
1438
|
+
});
|
|
1439
|
+
const searched = search.failure === undefined;
|
|
1440
|
+
await printNewsSearch(word, search.found, { reason, empty: emptyMessage(searched && !commodity), commodity: !commodity, failure: search.failure }, ctx);
|
|
1424
1441
|
return;
|
|
1425
1442
|
}
|
|
1426
1443
|
const empty = emptyMessage(false);
|
|
@@ -1533,50 +1550,81 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1533
1550
|
|
|
1534
1551
|
type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
|
|
1535
1552
|
|
|
1553
|
+
/** The company's own Form 4 filings as `fn INS` reads them: whether it has any, and its summary of them. */
|
|
1554
|
+
interface Form4Report {
|
|
1555
|
+
status: Form4Insiders;
|
|
1556
|
+
/** `fn INS`'s line of the last 90 days' open-market buys and sales in the filings read; null when it has none. */
|
|
1557
|
+
summary: string | null;
|
|
1558
|
+
/** The Form 4 lines in the filings read, and how many filings that was. */
|
|
1559
|
+
lines: number;
|
|
1560
|
+
filings: number;
|
|
1561
|
+
/** The newest filing's date (YYYY-MM-DD), which dates the report. */
|
|
1562
|
+
newestFiled: string | null;
|
|
1563
|
+
/** "Canadian insider reports (SEDI) not covered." beside the reason a company from abroad has none. */
|
|
1564
|
+
home: string | null;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1536
1567
|
/**
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
* nothing for files no SEC
|
|
1568
|
+
* The company's Form 4 filings, read as `fn INS` reads its first page (its
|
|
1569
|
+
* latest 20 of its own) and summed up as it sums them; null when they could
|
|
1570
|
+
* not be read. A listing abroad the SEC lists nothing for files no SEC
|
|
1571
|
+
* reports at all, and a company from abroad with none names its home
|
|
1572
|
+
* market's insider reports as not covered either.
|
|
1540
1573
|
*/
|
|
1541
|
-
async function
|
|
1574
|
+
async function loadForm4Report(market: MarketContext, listing: CliListing): Promise<Form4Report | null> {
|
|
1575
|
+
const { symbol, exchange } = listing.request;
|
|
1542
1576
|
try {
|
|
1543
|
-
const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider,
|
|
1544
|
-
limit:
|
|
1545
|
-
exchange
|
|
1577
|
+
const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider, symbol, {
|
|
1578
|
+
limit: INSIDER_REPORT_FILINGS,
|
|
1579
|
+
exchange,
|
|
1546
1580
|
});
|
|
1547
|
-
if (parsed.length > 0)
|
|
1548
|
-
|
|
1549
|
-
|
|
1581
|
+
if (parsed.length > 0) {
|
|
1582
|
+
const filed = parsed.map(({ filing }) => new Date(filing.filingDate).getTime()).filter(Number.isFinite);
|
|
1583
|
+
return {
|
|
1584
|
+
status: "available",
|
|
1585
|
+
summary: buildInsiderSummary(parsed, Date.now(), parsed),
|
|
1586
|
+
lines: parsed.filter(({ transaction }) => transaction != null).length,
|
|
1587
|
+
filings: new Set(parsed.map(({ filing }) => filing.accessionNumber)).size,
|
|
1588
|
+
newestFiled: filed.length > 0 ? new Date(Math.max(...filed)).toISOString().slice(0, 10) : null,
|
|
1589
|
+
home: null,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
const status: Form4Insiders = noFilings && nonUsSecListingVenue(symbol, exchange) ? "no-sec-reports"
|
|
1593
|
+
: foreignPrivateIssuer ? "foreign-private-issuer" : "none";
|
|
1594
|
+
const home = await loadHomeInsiderReportsNote(market.dataProvider, listing.key, exchange, foreignPrivateIssuer);
|
|
1595
|
+
return { status, summary: null, lines: 0, filings: 0, newestFiled: null, home };
|
|
1550
1596
|
} catch (error) {
|
|
1551
|
-
logFailure("cli.commands-market", "Read insider
|
|
1597
|
+
logFailure("cli.commands-market", "Read insider filings failed")(error);
|
|
1552
1598
|
return null;
|
|
1553
1599
|
}
|
|
1554
1600
|
}
|
|
1555
1601
|
|
|
1556
1602
|
/**
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
* Form 4
|
|
1560
|
-
*
|
|
1603
|
+
* The insider lines under `insider`'s holders. A company's insiders file
|
|
1604
|
+
* Form 4, which 13F holder lists rarely name, so the report leads with
|
|
1605
|
+
* `fn INS`'s summary of the Form 4 filings and points to the transactions;
|
|
1606
|
+
* a company with none says why, in the words `fn INS` uses. The insider share
|
|
1607
|
+
* of the company follows when the holder data reports it.
|
|
1561
1608
|
*/
|
|
1562
1609
|
function insiderSummary(
|
|
1563
1610
|
data: HolderData,
|
|
1564
|
-
ownerTypes: Set<string>,
|
|
1565
1611
|
listing: Pick<CliListing, "key">,
|
|
1566
1612
|
identity: ListingIdentity,
|
|
1567
|
-
form4:
|
|
1613
|
+
form4: Form4Report | null,
|
|
1568
1614
|
): string {
|
|
1569
1615
|
const held = data.summary?.insidersPercentHeld;
|
|
1570
1616
|
const share = held == null || !Number.isFinite(held)
|
|
1571
1617
|
? ""
|
|
1572
1618
|
: `Insiders hold ${formatFractionPercentCell(held)} of shares outstanding.`;
|
|
1573
|
-
const listed = data.holders.some((holder) => ownerTypes.has(holder.ownerType));
|
|
1574
1619
|
// The listing as the command read it (CAR:ASX), not the source's spelling of it (CAR.AX), which fn does not take.
|
|
1575
|
-
const transactions = form4 === "available"
|
|
1576
|
-
?
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1620
|
+
const transactions = form4?.status === "available"
|
|
1621
|
+
? [
|
|
1622
|
+
...form4.summary ? [form4.summary] : [],
|
|
1623
|
+
`${form4.lines} Form 4 ${form4.lines === 1 ? "line" : "lines"} in the latest ${form4.filings} ${form4.filings === 1 ? "filing" : "filings"}${form4.newestFiled ? `, newest filed ${form4.newestFiled}` : ""}: gloomberb fn INS ${listing.key}`,
|
|
1624
|
+
].join("\n")
|
|
1625
|
+
: form4?.status === "no-sec-reports" ? [noSecReportsNotice(identity), form4.home].filter(Boolean).join(" ")
|
|
1626
|
+
: form4 ? insiderEmptyNotice(form4.status === "foreign-private-issuer", form4.home) : "";
|
|
1627
|
+
return [transactions, share].filter(Boolean).join("\n");
|
|
1580
1628
|
}
|
|
1581
1629
|
|
|
1582
1630
|
/** `holders --form 13d|13g|all`: the 13D/13G beneficial owners, joined by name to the 13F holders. */
|
|
@@ -1666,7 +1714,7 @@ async function runHolders(
|
|
|
1666
1714
|
{ command: commandName, noun: "holder data" },
|
|
1667
1715
|
),
|
|
1668
1716
|
loadListingQuote(market.dataProvider, listing),
|
|
1669
|
-
insiderCommand ?
|
|
1717
|
+
insiderCommand ? loadForm4Report(market, listing) : null,
|
|
1670
1718
|
]);
|
|
1671
1719
|
const identity = listingIdentity(listing, quote);
|
|
1672
1720
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
@@ -1688,7 +1736,9 @@ async function runHolders(
|
|
|
1688
1736
|
const markedRows = rows.some((row) => holderShareBasisMarker(row));
|
|
1689
1737
|
// The heading names the listing; its unit, date and how much of the list follows on the same line.
|
|
1690
1738
|
// One "as of" only when every row shown is of that date; each row carries its own.
|
|
1691
|
-
|
|
1739
|
+
// An insider list the 13F filings name nobody in is not "none reported": the Form 4 summary under it says what insiders did.
|
|
1740
|
+
const form4Only = insiderCommand && rows.length === 0;
|
|
1741
|
+
const facts = form4Only ? [] : holderListFacts({
|
|
1692
1742
|
currency: valueCurrency,
|
|
1693
1743
|
listingCurrency: data.currency,
|
|
1694
1744
|
period: holderListPeriod(shownRows, data.asOf),
|
|
@@ -1699,8 +1749,10 @@ async function runHolders(
|
|
|
1699
1749
|
});
|
|
1700
1750
|
const unvaluedNote = unvaluedHoldersNote(shownRows);
|
|
1701
1751
|
const columns = holderReportColumns(shownRows, valueCurrency);
|
|
1702
|
-
// Dated as HDS dates it: by the list's as-of.
|
|
1703
|
-
const freshness =
|
|
1752
|
+
// Dated as HDS dates it: by the list's as-of; an insider report of Form 4 filings alone as INS dates it, by the newest.
|
|
1753
|
+
const freshness = form4Only
|
|
1754
|
+
? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" })
|
|
1755
|
+
: rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
|
|
1704
1756
|
// Named as fn HDS takes it, so the closing notes point to the same listing.
|
|
1705
1757
|
const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
|
|
1706
1758
|
const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
|
|
@@ -1722,7 +1774,11 @@ async function runHolders(
|
|
|
1722
1774
|
truncated: shown < (total ?? rows.length),
|
|
1723
1775
|
valueBasis,
|
|
1724
1776
|
positionsBasis,
|
|
1725
|
-
...(insiderCommand ? {
|
|
1777
|
+
...(insiderCommand ? {
|
|
1778
|
+
form4Insiders: form4?.status ?? null,
|
|
1779
|
+
// INS's summary of the latest Form 4 filings, as `fn INS` gives it in its metadata.
|
|
1780
|
+
form4: form4 ? { summary: form4.summary, lines: form4.lines, filings: form4.filings, newestFiled: form4.newestFiled, notCovered: form4.home } : null,
|
|
1781
|
+
} : {}),
|
|
1726
1782
|
},
|
|
1727
1783
|
...(choice.length > 0 ? { warnings: choice } : {}),
|
|
1728
1784
|
}, {
|
|
@@ -1756,7 +1812,7 @@ async function runHolders(
|
|
|
1756
1812
|
exportIdentity: { symbolOrSeries: listing.key },
|
|
1757
1813
|
summary: () => [
|
|
1758
1814
|
[valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
|
|
1759
|
-
insiderCommand ? insiderSummary(data,
|
|
1815
|
+
insiderCommand ? insiderSummary(data, listing, identity, form4) : "",
|
|
1760
1816
|
].filter(Boolean).join("\n"),
|
|
1761
1817
|
empty: `No holders reported for ${listingTitle(identity)}.`,
|
|
1762
1818
|
});
|
|
@@ -2009,11 +2065,109 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2009
2065
|
/** What an options chain export's `interval` says: one quote snapshot, not bars. */
|
|
2010
2066
|
const OPTIONS_EXPORT_INTERVAL = "snapshot";
|
|
2011
2067
|
|
|
2068
|
+
/** `fx --date`: a past day as YYYY-MM-DD; today's close is not in yet, so it is refused, as is a day ahead. */
|
|
2069
|
+
function readFxDate(value: string, ctx: Parameters<CliCommandDef["execute"]>[1]): string {
|
|
2070
|
+
let date: string;
|
|
2071
|
+
try {
|
|
2072
|
+
date = isoDateOption(value, "date");
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
return ctx.fail(error instanceof Error ? error.message : String(error));
|
|
2075
|
+
}
|
|
2076
|
+
if (!date) return ctx.fail("--date takes a day, such as --date 2026-09-02.");
|
|
2077
|
+
const today = isoDateToday();
|
|
2078
|
+
if (date >= today) {
|
|
2079
|
+
return ctx.fail(`${date} has no daily close yet: --date takes a day before today (${today}).`, "Without --date, fx gives the latest rate.");
|
|
2080
|
+
}
|
|
2081
|
+
return date;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* `fx <pair> --date <day>`: the rate at that day's daily FX closes, read as
|
|
2086
|
+
* a position's purchase-date cost reads them (`fxCloseOnDate`): each
|
|
2087
|
+
* currency's USD pair at its close of the day, or of the last trading day
|
|
2088
|
+
* before it, which the answer then says, crossed through USD as the latest
|
|
2089
|
+
* rate is.
|
|
2090
|
+
*/
|
|
2091
|
+
async function runFxOnDate(
|
|
2092
|
+
parsed: Exclude<ReturnType<typeof parseFxArgs>, { error: string }>,
|
|
2093
|
+
date: string,
|
|
2094
|
+
args: readonly string[],
|
|
2095
|
+
market: MarketContext,
|
|
2096
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
2097
|
+
) {
|
|
2098
|
+
const { request, amount, toUsdByDefault } = parsed;
|
|
2099
|
+
const { currency, baseCurrency } = request;
|
|
2100
|
+
const pair = `${currency}/${baseCurrency}`;
|
|
2101
|
+
const history = market.fxHistory ?? apiClient;
|
|
2102
|
+
const codes = [...new Set([currency, baseCurrency].filter((code) => code !== "USD"))];
|
|
2103
|
+
const legs = new Map<string, FxCloseReading>([["USD", { rate: 1, date }]]);
|
|
2104
|
+
if (currency !== baseCurrency) {
|
|
2105
|
+
for (const code of codes) {
|
|
2106
|
+
const leg = dailyFxLeg(code);
|
|
2107
|
+
if (!leg) return ctx.fail(`${code} has no daily FX closes to read a rate for ${date} from.`);
|
|
2108
|
+
let reading: FxCloseReading | null;
|
|
2109
|
+
try {
|
|
2110
|
+
reading = await fxCloseOnDate(leg, date, history);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
return ctx.fail(`The daily ${code} FX closes could not be read.`, error instanceof Error ? error.message : String(error));
|
|
2113
|
+
}
|
|
2114
|
+
if (!reading) {
|
|
2115
|
+
return ctx.fail(`No ${leg.invert ? `USD/${code}` : `${code}/USD`} daily close in the week up to ${date}.`,
|
|
2116
|
+
"Without --date, fx gives the latest rate.");
|
|
2117
|
+
}
|
|
2118
|
+
legs.set(code, reading);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const from = legs.get(currency), into = legs.get(baseCurrency);
|
|
2122
|
+
const rate = currency === baseCurrency ? 1 : crossRate(from?.rate, into?.rate);
|
|
2123
|
+
// A cross is as old as its older close; a weekend or holiday reads the trading day before it.
|
|
2124
|
+
const closeDate = [...legs.values()].map((leg) => leg.date).sort()[0]!;
|
|
2125
|
+
const converted = amount == null ? null : amount * rate;
|
|
2126
|
+
const row = {
|
|
2127
|
+
currency, baseCurrency, rate, asOf: closeDate, stale: false, pair, inverse: 1 / rate, date,
|
|
2128
|
+
...(amount == null ? {} : { amount, converted }),
|
|
2129
|
+
};
|
|
2130
|
+
const warnings = [
|
|
2131
|
+
...request.readAs ?? [],
|
|
2132
|
+
...closeDate < date ? [`No daily close on ${date}, a weekend or market holiday: this is the close of ${closeDate}, the trading day before it.`] : [],
|
|
2133
|
+
...toUsdByDefault && amount != null
|
|
2134
|
+
? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to> --date ${date}.`]
|
|
2135
|
+
: [],
|
|
2136
|
+
];
|
|
2137
|
+
ctx.printResult({
|
|
2138
|
+
data: [row],
|
|
2139
|
+
// A past close is what was asked for, never stale.
|
|
2140
|
+
freshness: rowsFreshness([], { status: "not-a-feed", basis: "daily FX close", asOf: closeDate }),
|
|
2141
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
2142
|
+
}, {
|
|
2143
|
+
text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
|
|
2144
|
+
exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "1day" },
|
|
2145
|
+
columns: [
|
|
2146
|
+
{ key: "currency", header: "Currency" },
|
|
2147
|
+
{ key: "baseCurrency", header: "Base" },
|
|
2148
|
+
{ key: "rate", header: "Rate", align: "right" },
|
|
2149
|
+
{ key: "date", header: "Date" },
|
|
2150
|
+
{ key: "asOf", header: "Close" },
|
|
2151
|
+
{ key: "stale", header: "Stale" },
|
|
2152
|
+
{ key: "pair", header: "Pair" },
|
|
2153
|
+
{ key: "inverse", header: "Inverse", align: "right" },
|
|
2154
|
+
...(amount == null ? [] : [
|
|
2155
|
+
{ key: "amount", header: "Amount", align: "right" as const },
|
|
2156
|
+
{ key: "converted", header: "Converted", align: "right" as const },
|
|
2157
|
+
]),
|
|
2158
|
+
],
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2012
2162
|
async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
2013
|
-
|
|
2163
|
+
const args = [...rawArgs];
|
|
2164
|
+
const dateOption = takeOption(args, "--date");
|
|
2165
|
+
if (args.length === 0) ctx.fail(`Usage: gloomberb ${FX_USAGE}`);
|
|
2166
|
+
const date = dateOption == null ? null : readFxDate(dateOption, ctx);
|
|
2014
2167
|
await withMarketData(ctx, async (market) => {
|
|
2015
|
-
const parsed = parseFxArgs(
|
|
2168
|
+
const parsed = parseFxArgs(args, market.config.baseCurrency);
|
|
2016
2169
|
if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloomberb ${FX_USAGE}`);
|
|
2170
|
+
if (date) return runFxOnDate(parsed, date, args, market, ctx);
|
|
2017
2171
|
const { request, amount, toUsdByDefault } = parsed;
|
|
2018
2172
|
const { currency, baseCurrency } = request;
|
|
2019
2173
|
const pair = `${currency}/${baseCurrency}`;
|
|
@@ -2053,10 +2207,14 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
|
|
|
2053
2207
|
}] as const] : [];
|
|
2054
2208
|
})));
|
|
2055
2209
|
// An amount with one currency names no target; the answer says it went to USD.
|
|
2056
|
-
const warnings =
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2210
|
+
const warnings = [
|
|
2211
|
+
// A currency typed as its name (yen) says which code it was read as.
|
|
2212
|
+
...request.readAs ?? [],
|
|
2213
|
+
...toUsdByDefault && amount != null
|
|
2214
|
+
? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to>.`]
|
|
2215
|
+
: [],
|
|
2216
|
+
];
|
|
2217
|
+
ctx.printResult({ data: [row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
|
|
2060
2218
|
// The conversion, then the rate it used; the source line under it dates that rate.
|
|
2061
2219
|
text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
|
|
2062
2220
|
// The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
|
|
@@ -2171,7 +2329,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2171
2329
|
group: CLI_COMMAND_GROUPS.research,
|
|
2172
2330
|
usage: [HISTORY_USAGE],
|
|
2173
2331
|
options: [
|
|
2174
|
-
{ flags: "--range <range>", description: `${
|
|
2332
|
+
{ flags: "--range <range>", description: `${HISTORY_RANGES.join(", ")} (default 1Y); ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}` },
|
|
2175
2333
|
EXCHANGE_OPTION,
|
|
2176
2334
|
],
|
|
2177
2335
|
sections: [{
|
|
@@ -2437,11 +2595,15 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2437
2595
|
description: "Convert a currency into your base currency, quote a pair such as USD/NGN, or convert an amount such as fx 1234.5 EUR/USD",
|
|
2438
2596
|
help: {
|
|
2439
2597
|
group: CLI_COMMAND_GROUPS.markets,
|
|
2440
|
-
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>"],
|
|
2598
|
+
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>"],
|
|
2599
|
+
options: [
|
|
2600
|
+
{ flags: "--date <day>", description: `The rate at that day's daily FX close, such as a transaction date (${DATE_OPTION_EXAMPLES}); a weekend or holiday reads the trading day before it` },
|
|
2601
|
+
],
|
|
2441
2602
|
sections: [{
|
|
2442
2603
|
title: "Direction",
|
|
2443
2604
|
lines: [
|
|
2444
2605
|
"A bare code is one unit of it in your base currency. A pair reads as markets quote it: USD/NGN is how many NGN one USD buys. Two codes neither of which is USD, such as ZAR/NGN, cross through their USD rates.",
|
|
2606
|
+
"A currency's name stands for its code: fx yen is JPY, fx rand ZAR, fx Swiss franc CHF, and the answer says which it read. A name several currencies share (peso, dollar, franc, krona, rupee) lists them: write one as its code.",
|
|
2445
2607
|
"The answer states both directions: 1 NGN = 0.000752791 USD (USD/NGN 1328.39). --json, --csv and --ndjson carry rate (base currency per currency), pair, inverse, asOf and stale.",
|
|
2446
2608
|
],
|
|
2447
2609
|
}, {
|
|
@@ -2449,8 +2611,13 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2449
2611
|
lines: [
|
|
2450
2612
|
"An amount in front converts it from the first currency to the second: fx 1234.5 EUR/USD, or fx 1234,5 EUR USD. It takes either decimal mark (1.234,5 and 1,234.5 are 1234.5) and reads as an amount of the first currency: 1,234 or 1.234 EUR are thousands, a third decimal being below a cent, while in a currency with three decimals (KWD) fx asks which. An amount and one currency converts to USD. The answer gives the converted amount, then the rate it used, dated by the source line; --json adds amount and converted.",
|
|
2451
2613
|
],
|
|
2614
|
+
}, {
|
|
2615
|
+
title: "A past day",
|
|
2616
|
+
lines: [
|
|
2617
|
+
"--date gives the rate at that day's daily FX close instead of the latest: fx USD/CAD --date 2026-09-02, or with an amount, fx 1250 USD/CAD --date 2026-09-02. Each currency's USD pair is read at its close of that day and crossed through USD, as the latest rate is. A weekend or market holiday has no close: the answer then gives the trading day before it and says so. The source line dates the close; --json adds date (the day asked for) and asOf (the close used).",
|
|
2618
|
+
],
|
|
2452
2619
|
}],
|
|
2453
|
-
examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx JPY --json"],
|
|
2620
|
+
examples: ["fx EUR", "fx NGN", "fx yen", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx USD/CAD --date 2026-09-02", "fx JPY --json"],
|
|
2454
2621
|
},
|
|
2455
2622
|
execute: runFx,
|
|
2456
2623
|
},
|