gloomberb 0.18.0 → 0.18.2
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/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 +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/types.ts +14 -0
- 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/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/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +290 -159
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +105 -52
- 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/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +31 -7
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +51 -16
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +22 -7
- package/src/components/chart/composite/format.ts +13 -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 +64 -16
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -0
- package/src/components/index.ts +2 -0
- 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/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/onboarding/wizard-positions.ts +8 -0
- 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/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- 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 +57 -22
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -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/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- 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 +182 -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 +505 -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 +573 -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 +3 -2
- package/src/plugins/builtin/buildout/pane/status.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 +31 -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/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- 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 +36 -5
- 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 +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- 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/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/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/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- 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/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 +38 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- 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 +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- 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/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- 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 +37 -16
- 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/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- 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 +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- 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 +33 -13
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- 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/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/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- 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-feature-wall.tsx +9 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- 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/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- 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/pane.tsx +2 -2
- 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 +61 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- 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 +39 -7
- 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/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- 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/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -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 +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +63 -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/team/collections.ts +2 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/metadata.ts +10 -0
- 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 +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/ticker.ts +9 -0
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +41 -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
|
@@ -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,
|
|
@@ -235,7 +234,10 @@ import {
|
|
|
235
234
|
type HolderForm,
|
|
236
235
|
} from "../../plugins/builtin/holders/beneficial-report";
|
|
237
236
|
|
|
238
|
-
|
|
237
|
+
/** The ranges `history` takes: the chart ranges, and YTD, which runs from the prior year's last close. */
|
|
238
|
+
type HistoryRange = TimeRange | "YTD";
|
|
239
|
+
const HISTORY_RANGES: HistoryRange[] = [...TIME_RANGES.slice(0, TIME_RANGES.indexOf("1Y")), "YTD", ...TIME_RANGES.slice(TIME_RANGES.indexOf("1Y"))];
|
|
240
|
+
const VALID_RANGES = new Set<HistoryRange>(HISTORY_RANGES);
|
|
239
241
|
const VALID_NEWS_FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
|
|
240
242
|
|
|
241
243
|
type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
|
|
@@ -261,9 +263,9 @@ type FinancialsCliData = TickerFinancials & {
|
|
|
261
263
|
providerId: string | null;
|
|
262
264
|
};
|
|
263
265
|
|
|
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 ${
|
|
266
|
+
function parseRange(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): HistoryRange {
|
|
267
|
+
const range = (value ?? "1Y").toUpperCase() as HistoryRange;
|
|
268
|
+
if (!VALID_RANGES.has(range)) ctx.fail(`Unknown range "${value}".`, `Use one of ${HISTORY_RANGES.join(", ")}.`);
|
|
267
269
|
return range;
|
|
268
270
|
}
|
|
269
271
|
|
|
@@ -297,8 +299,9 @@ async function readSymbols(symbols: readonly string[], dataProvider: Parameters<
|
|
|
297
299
|
|
|
298
300
|
/**
|
|
299
301
|
* The symbols of `--file <path>` or standard input (`-`), with any typed
|
|
300
|
-
* beside them, read by the parser `watchlist add` uses
|
|
301
|
-
* only when asked for, so a script that
|
|
302
|
+
* beside them, read by the parser `watchlist add` uses, and its notes on what
|
|
303
|
+
* it left out. Standard input is read only when asked for, so a script that
|
|
304
|
+
* pipes in nothing never waits on it.
|
|
302
305
|
*/
|
|
303
306
|
async function readListedSymbols(
|
|
304
307
|
args: readonly string[],
|
|
@@ -306,13 +309,13 @@ async function readListedSymbols(
|
|
|
306
309
|
market: MarketContext,
|
|
307
310
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
308
311
|
usage: string,
|
|
309
|
-
): Promise<string[]> {
|
|
312
|
+
): Promise<{ symbols: string[]; notes: string[] }> {
|
|
310
313
|
try {
|
|
311
314
|
const list = await readSymbolList({
|
|
312
315
|
args, file, stdinPiped: false, readEntries: (entries) => readNameEntries(entries, market.dataProvider),
|
|
313
316
|
});
|
|
314
317
|
if (list.symbols.length === 0) ctx.fail(`No symbols in ${file && file !== "-" ? file : "standard input"}.`, usage);
|
|
315
|
-
return uniqueEntries(list.symbols);
|
|
318
|
+
return { symbols: uniqueEntries(list.symbols), notes: list.notes };
|
|
316
319
|
} catch (error) {
|
|
317
320
|
if (error instanceof SymbolListError) ctx.fail(error.message, usage);
|
|
318
321
|
throw error;
|
|
@@ -362,6 +365,28 @@ const QUOTE_LEAD_COLUMNS = [
|
|
|
362
365
|
|
|
363
366
|
type QuoteRow = ReturnType<typeof quoteRows>[number];
|
|
364
367
|
|
|
368
|
+
/**
|
|
369
|
+
* What Chg% is measured from, as every change line says it ("vs Fri 16 Oct
|
|
370
|
+
* close", "since 00:00 UTC"): the table sets it after Chg% and names the
|
|
371
|
+
* venue's holidays under it; an export adds it, and the holiday note, after
|
|
372
|
+
* its existing columns, so a column read by position stays where it was.
|
|
373
|
+
*/
|
|
374
|
+
const CHANGE_BASIS_COLUMN = {
|
|
375
|
+
key: "changeBasis", header: "Chg Basis", shrink: false, optional: true, format: (value: unknown) => cliStyles.muted(String(value ?? "")),
|
|
376
|
+
};
|
|
377
|
+
const CHANGE_BASIS_EXPORT_COLUMNS = [
|
|
378
|
+
{ key: "changeBasis", header: "Chg Basis" },
|
|
379
|
+
{ key: "changeBasisNote", header: "Chg Basis Note" },
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
/** The table's columns: Chg Basis follows Chg%, and the note is a line under the table. */
|
|
383
|
+
function quoteTextColumns<Column extends { key: string }>(columns: Column[]): Array<Column | typeof CHANGE_BASIS_COLUMN> {
|
|
384
|
+
const exported = new Set(CHANGE_BASIS_EXPORT_COLUMNS.map((column) => column.key));
|
|
385
|
+
const kept = columns.filter((column) => !exported.has(column.key));
|
|
386
|
+
const at = kept.findIndex((column) => column.key === "changePercent") + 1;
|
|
387
|
+
return [...kept.slice(0, at), CHANGE_BASIS_COLUMN, ...kept.slice(at)];
|
|
388
|
+
}
|
|
389
|
+
|
|
365
390
|
/** A Cboe yield index level is a yield: `5.24%`, not a price. */
|
|
366
391
|
function yieldLevelCell(value: unknown, row: QuoteRow): string {
|
|
367
392
|
const text = String(value ?? "");
|
|
@@ -397,10 +422,15 @@ function roundedPercent(value: number | undefined): number | null {
|
|
|
397
422
|
return value == null ? null : Number(value.toFixed(2));
|
|
398
423
|
}
|
|
399
424
|
|
|
425
|
+
/** The quote's currency code, US cents as US¢; an export keeps the code (USX). */
|
|
426
|
+
function currencyCell(value: unknown): string {
|
|
427
|
+
return currencyCodeLabel(typeof value === "string" ? value : "");
|
|
428
|
+
}
|
|
429
|
+
|
|
400
430
|
function quoteColumns() {
|
|
401
431
|
return [
|
|
402
432
|
...QUOTE_LEAD_COLUMNS,
|
|
403
|
-
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1 },
|
|
433
|
+
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1, format: currencyCell },
|
|
404
434
|
// Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
|
|
405
435
|
// row says so here, so the closing line's "N of M stale" always points at rows the table marks.
|
|
406
436
|
{
|
|
@@ -413,6 +443,7 @@ function quoteColumns() {
|
|
|
413
443
|
},
|
|
414
444
|
// In a narrow terminal the closing line's as-of stands in for each row's.
|
|
415
445
|
{ key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
|
|
446
|
+
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
416
447
|
];
|
|
417
448
|
}
|
|
418
449
|
|
|
@@ -423,15 +454,11 @@ function compareColumns() {
|
|
|
423
454
|
// A narrow terminal drops these before it cuts the names short.
|
|
424
455
|
{ key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
|
|
425
456
|
{ key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
|
|
426
|
-
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3 },
|
|
457
|
+
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3, format: currencyCell },
|
|
458
|
+
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
427
459
|
];
|
|
428
460
|
}
|
|
429
461
|
|
|
430
|
-
function errorMessage(error: unknown): string | null {
|
|
431
|
-
if (error == null) return null;
|
|
432
|
-
return error instanceof Error ? error.message : String(error);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
462
|
/** What the Feed cell says: stale wins over the feed's own live or delayed, as the closing line counts it. */
|
|
436
463
|
function quoteFeedState(quote: QuoteCliRecord["quote"], now: number): "stale" | "live" | "delayed" | "" {
|
|
437
464
|
if (!quote) return "";
|
|
@@ -454,6 +481,34 @@ function nearParCoin(quote: QuoteCliRecord["quote"]): boolean {
|
|
|
454
481
|
return reference != null && reference >= 0.9 && reference < 1.1;
|
|
455
482
|
}
|
|
456
483
|
|
|
484
|
+
/** What a row's move is measured from, worded as its Chg Basis cell: empty without a move. */
|
|
485
|
+
function rowChangeBasis(result: Pick<QuoteCliRecord, "quote" | "futuresContract">) {
|
|
486
|
+
return quoteChangeBasis(result.quote, { terms: result.futuresContract });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function basisLabel(result: Pick<QuoteCliRecord, "quote" | "futuresContract">, now: number): string {
|
|
490
|
+
const basis = rowChangeBasis(result);
|
|
491
|
+
return basis ? changeBasisLabel(basis, now) : "";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The lines under the table naming the days a row's venue was closed since
|
|
496
|
+
* the close its move is from ("HKEX closed Mon 19 Oct, holiday"), once each.
|
|
497
|
+
*/
|
|
498
|
+
function changeBasisNotes(results: QuoteCliRecord[], now = Date.now()): string[] {
|
|
499
|
+
return [...new Set(results.flatMap((result) => changeBasisNote(rowChangeBasis(result), now) ?? []))];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** The quote as `--json` gives it: `previousCloseDate`, the exchange-local date of its previous close, follows `previousClose`. */
|
|
503
|
+
function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { previousCloseDate?: string | null }) | null {
|
|
504
|
+
if (!quote) return null;
|
|
505
|
+
const previousCloseDate = quotePreviousCloseDate(quote);
|
|
506
|
+
const entries = Object.entries(quote);
|
|
507
|
+
const at = entries.findIndex(([key]) => key === "previousClose");
|
|
508
|
+
entries.splice(at < 0 ? entries.length : at + 1, 0, ["previousCloseDate", previousCloseDate]);
|
|
509
|
+
return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
|
|
510
|
+
}
|
|
511
|
+
|
|
457
512
|
function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
458
513
|
return results.map((result) => {
|
|
459
514
|
const quote = result.quote;
|
|
@@ -476,10 +531,12 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
476
531
|
? indexPoints ? formatMarketPrice(value, options) : formatMarketPriceWithCurrency(value, quote.currency, options)
|
|
477
532
|
: ""
|
|
478
533
|
);
|
|
534
|
+
// A price in cents says what it is per, as the contract terms word it: 314.95¢/lb.
|
|
535
|
+
const per = display?.price != null ? minorUnitPricePer(result.futuresContract, quote?.currency) : "";
|
|
479
536
|
return {
|
|
480
537
|
symbol: result.target.symbol,
|
|
481
538
|
name: quote?.name || result.listing?.name || "",
|
|
482
|
-
price: price(display?.price)
|
|
539
|
+
price: `${price(display?.price)}${per}`,
|
|
483
540
|
rawPrice: display?.price ?? null,
|
|
484
541
|
priceBasis: quote?.priceBasis ?? null,
|
|
485
542
|
instrumentType: quote?.instrumentType ?? null,
|
|
@@ -511,6 +568,9 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
511
568
|
// When the price Last shows was observed: in a pre-market nothing has traded in, the close it stands at.
|
|
512
569
|
updatedAt: quote?.lastUpdated ? new Date(quoteHeadlineTime(quote)).toISOString() : "",
|
|
513
570
|
error: result.error ?? "",
|
|
571
|
+
// Last, so a row read by position keeps every field where it was.
|
|
572
|
+
changeBasis: basisLabel(result, now),
|
|
573
|
+
changeBasisNote: changeBasisNote(rowChangeBasis(result), now) ?? "",
|
|
514
574
|
};
|
|
515
575
|
});
|
|
516
576
|
}
|
|
@@ -718,9 +778,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
718
778
|
await withMarketData(ctx, async (market) => {
|
|
719
779
|
const read = phrase ? await readPhraseArgument(phrase, market.dataProvider) : null;
|
|
720
780
|
if (read?.kind === "phrase") return answerPhrase(read.phrase, market, ctx);
|
|
781
|
+
// A list read from a file names what it left out: a stray separator, the cells beside a table's symbols.
|
|
782
|
+
const list = listed && !read ? await readListedSymbols(args, file, market, ctx, usage) : null;
|
|
721
783
|
const symbols = read?.kind === "name" ? [read.name]
|
|
722
784
|
: read?.kind === "symbols" ? uniqueEntries(read.symbols)
|
|
723
|
-
:
|
|
785
|
+
: list ? list.symbols
|
|
724
786
|
: await readSymbols(entries, market.dataProvider);
|
|
725
787
|
// With several symbols, --exchange is for the ones that name no exchange of their own.
|
|
726
788
|
const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
|
|
@@ -780,7 +842,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
780
842
|
listing: listingMetadata(listingIdentity(listing, result.quote)),
|
|
781
843
|
// The figures the table shows, beside the quote as the source sent it.
|
|
782
844
|
...getQuoteSessionFields(result.quote),
|
|
783
|
-
|
|
845
|
+
// What `change` is measured from, with the label and holiday note the table shows.
|
|
846
|
+
changeBasis: changeBasisJson(quoteChangeBasis(result.quote, { terms: contracts[index] }), now),
|
|
847
|
+
quote: withPreviousCloseDate(result.quote),
|
|
784
848
|
...contracts[index] ? { futuresContract: contracts[index] } : {},
|
|
785
849
|
...fronts[index] ? { futuresFront: fronts[index] } : {},
|
|
786
850
|
...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
|
|
@@ -791,6 +855,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
791
855
|
...formerIssuers[index] ? { formerIssuer: formerIssuers[index] } : {},
|
|
792
856
|
}));
|
|
793
857
|
const notes = [
|
|
858
|
+
...list?.notes ?? [],
|
|
794
859
|
// FB now lists another security; the company that was FB trades as META. A symbol a
|
|
795
860
|
// company renamed into (META) is what was typed: its change is in the row, not here.
|
|
796
861
|
// GOGL was Golden Ocean, which merged into CMB.Tech; a fund lists there now.
|
|
@@ -805,6 +870,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
805
870
|
...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
|
|
806
871
|
];
|
|
807
872
|
const columns = commandName === "compare" ? compareColumns() : quoteColumns();
|
|
873
|
+
const basisNotes = changeBasisNotes(data, now);
|
|
808
874
|
if (ctx.cliOptions.format !== "json") await primeBoardCoins(notes);
|
|
809
875
|
// Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
|
|
810
876
|
const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
|
|
@@ -822,8 +888,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
822
888
|
currencyOrUnit: row.currency || (row.yieldIndex ? "pct" : isIndexLevel(row.instrumentType) ? INDEX_POINTS_EXPORT_UNIT : null),
|
|
823
889
|
}),
|
|
824
890
|
details: quoteDetailsText,
|
|
891
|
+
...(basisNotes.length ? { footnote: basisNotes.join("\n") } : {}),
|
|
825
892
|
columns,
|
|
826
|
-
|
|
893
|
+
textColumns: quoteTextColumns(yieldsOnly
|
|
894
|
+
? columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column)
|
|
895
|
+
: columns),
|
|
827
896
|
});
|
|
828
897
|
// The table names each symbol it found nothing for; with none found, the exit status says so too.
|
|
829
898
|
if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
|
|
@@ -860,15 +929,11 @@ function quoteDetailsText(data: QuoteCliRecord[]): string {
|
|
|
860
929
|
|
|
861
930
|
/**
|
|
862
931
|
* "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
|
|
863
|
-
* mean AAPL (Apple Inc.)?"
|
|
864
|
-
* slip from a US ticker, worded as the not-a-ticker answers word it.
|
|
932
|
+
* mean AAPL (Apple Inc.)?" in the warning colour, for a row that has one.
|
|
865
933
|
*/
|
|
866
|
-
function notUsTickerLine(
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const name = quote.name || listing?.name;
|
|
870
|
-
const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
|
|
871
|
-
return cliStyles.warning(`${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`);
|
|
934
|
+
function notUsTickerLine(row: QuoteCliRecord): string | null {
|
|
935
|
+
const sentence = notUsTickerSentence(row);
|
|
936
|
+
return sentence && cliStyles.warning(sentence);
|
|
872
937
|
}
|
|
873
938
|
|
|
874
939
|
/** Each futures row's front contract quote, when its terms name a front other than its own contract; null otherwise. */
|
|
@@ -899,12 +964,17 @@ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
|
|
|
899
964
|
const terms = row.futuresContract;
|
|
900
965
|
if (!terms || !row.quote) return [];
|
|
901
966
|
const settle: [string, string] = ["Settle", futuresSettleText(terms, row.quote)];
|
|
967
|
+
// The move Chg% gives, in the quote's money and with what it is measured from: "vs Fri settle".
|
|
968
|
+
const session = getRegularSessionDisplay(row.quote);
|
|
969
|
+
const change: Array<[string, string]> = session?.change != null && Number.isFinite(session.change)
|
|
970
|
+
? [["Change", quoteChangeText(row.quote, session, withCurrencyMinorDigits(quoteFormatOptions(row.quote, "FUTURE"), row.quote.currency), terms)]]
|
|
971
|
+
: [];
|
|
902
972
|
const metrics = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
|
|
903
973
|
const rolls = futuresRollLines(terms, row.futuresFront).map((line): [string, string] => [line.label, line.value]);
|
|
904
974
|
const at = futuresContractLines(terms, row.quote.symbol || row.target.symbol)[0]?.key === "contract" ? 1 : 0;
|
|
905
975
|
const contract = [...metrics.slice(0, at), ...rolls, ...metrics.slice(at)];
|
|
906
|
-
if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...contract])}`];
|
|
907
|
-
return [[renderStats([settle]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
|
|
976
|
+
if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...change, ...contract])}`];
|
|
977
|
+
return [[renderStats([settle, ...change]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
|
|
908
978
|
}).join("\n\n");
|
|
909
979
|
}
|
|
910
980
|
|
|
@@ -919,91 +989,6 @@ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeB
|
|
|
919
989
|
return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
|
|
920
990
|
}
|
|
921
991
|
|
|
922
|
-
interface QuoteAnswer {
|
|
923
|
-
result: QuoteBatchResult;
|
|
924
|
-
/** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
|
|
925
|
-
listing: CliListing;
|
|
926
|
-
/** Why there is no quote, when there is more to say than the source's own error. */
|
|
927
|
-
failure?: { message: string; details?: string; oneLine: string };
|
|
928
|
-
notes: string[];
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
|
|
933
|
-
* is not listed on says so. A bare symbol the data service read on its home
|
|
934
|
-
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
|
|
935
|
-
* quoted on its listings (`quoteBareListing`), else names them, or asks for an
|
|
936
|
-
* exchange when several companies list it (2222); a bare index root (VIX) also
|
|
937
|
-
* names its index (^VIX), and a futures board code a security keeps (GC) its
|
|
938
|
-
* future (GC=F). Venues are only looked up for a
|
|
939
|
-
* symbol with no quote, so a request that succeeds costs no search.
|
|
940
|
-
*/
|
|
941
|
-
async function answerQuote(
|
|
942
|
-
result: QuoteBatchResult,
|
|
943
|
-
listing: CliListing,
|
|
944
|
-
market: MarketContext,
|
|
945
|
-
request: ListingDataRequest,
|
|
946
|
-
refresh: boolean,
|
|
947
|
-
venues: Promise<ListingVenue[]> | null,
|
|
948
|
-
): Promise<QuoteAnswer> {
|
|
949
|
-
const root = listing.exchange ? null : indexRootFor(listing.symbol);
|
|
950
|
-
// A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
|
|
951
|
-
const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
|
|
952
|
-
? [indexRootAlsoLine(root, request.command)] : [];
|
|
953
|
-
if (result.quote) {
|
|
954
|
-
// "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
|
|
955
|
-
const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
|
|
956
|
-
const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
|
|
957
|
-
// A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
|
|
958
|
-
const board = tradingBoardNote(listing, result.quote);
|
|
959
|
-
return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote)].flat() };
|
|
960
|
-
}
|
|
961
|
-
const notTraded = await notTradedMessage(listing, market);
|
|
962
|
-
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
963
|
-
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
964
|
-
const other = await quoteBareListing(listing, market, request, { refresh });
|
|
965
|
-
if (other?.kind === "quoted") {
|
|
966
|
-
return {
|
|
967
|
-
result: { target: other.listing.request, quote: other.quote },
|
|
968
|
-
listing: other.listing,
|
|
969
|
-
notes: [other.note, ...also(other.quote)],
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
// Other listings already name a futures board code's future (GC=F); with none, it is named here.
|
|
973
|
-
// A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
|
|
974
|
-
const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
|
|
975
|
-
const tryLines = [
|
|
976
|
-
yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
|
|
977
|
-
other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
|
|
978
|
-
// BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
|
|
979
|
-
listing.exchange ? null : knownGapForTicker(listing.symbol),
|
|
980
|
-
].filter((line): line is string => !!line);
|
|
981
|
-
if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
|
|
982
|
-
const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
|
|
983
|
-
const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
|
|
984
|
-
const oneLine = [
|
|
985
|
-
message,
|
|
986
|
-
other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
|
|
987
|
-
...tryLines,
|
|
988
|
-
].filter(Boolean).join(" ");
|
|
989
|
-
return { result, listing, failure: { message, details, oneLine }, notes: [] };
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* A symbol written like a currency pair that is not a ticker (USD_NGN, as some
|
|
994
|
-
* FX feeds write a pair) names the listing `gloomberb search` finds nearest to
|
|
995
|
-
* it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
|
|
996
|
-
* Naira)?". When search finds nothing near, the row keeps its own error and its
|
|
997
|
-
* search hint.
|
|
998
|
-
*/
|
|
999
|
-
async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
|
|
1000
|
-
const message = errorMessage(result.error);
|
|
1001
|
-
const symbol = message ? notATickerSymbol(message) : null;
|
|
1002
|
-
if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
|
|
1003
|
-
const suggestion = await suggestListing(symbol, market);
|
|
1004
|
-
return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
992
|
/** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
|
|
1008
993
|
function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
|
|
1009
994
|
if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
|
|
@@ -1012,6 +997,7 @@ function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandD
|
|
|
1012
997
|
|
|
1013
998
|
const HISTORY_USAGE = "history <symbol> [--range <range>]";
|
|
1014
999
|
const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
|
|
1000
|
+
const HISTORY_YTD_RANGE = "YTD starts at the prior year's last close, the one year-to-date returns are measured from";
|
|
1015
1001
|
/** Options people reach for to ask for dates, which history answers with a range. */
|
|
1016
1002
|
const DATE_WINDOW_OPTIONS = new Set(["--from", "--to", "--start", "--end", "--since", "--until", "--date", "--start-date", "--end-date"]);
|
|
1017
1003
|
/** Options people reach for to pick a bar size, which history derives from the range. */
|
|
@@ -1048,14 +1034,23 @@ function financialsOptionHint(flag: string, args: readonly string[]): string | n
|
|
|
1048
1034
|
}
|
|
1049
1035
|
|
|
1050
1036
|
function historyOptionHint(flag: string, args: readonly string[]): string | null {
|
|
1051
|
-
if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${
|
|
1037
|
+
if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${HISTORY_RANGES.join(", ")}. ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}.`;
|
|
1052
1038
|
if (BAR_SIZE_OPTIONS.has(flag)) return `history serves each --range in one bar size. ${subMinuteDataMessage(historySymbolArg(args))}`;
|
|
1053
1039
|
return null;
|
|
1054
1040
|
}
|
|
1055
1041
|
|
|
1042
|
+
/** "Asked YTD, data starts 2026-03-05 (first available bar)" when no bar reaches the prior year's last close. */
|
|
1043
|
+
function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
|
|
1044
|
+
const first = rows[0]?.date.slice(0, 10);
|
|
1045
|
+
const last = rows.at(-1)?.date.slice(0, 10);
|
|
1046
|
+
return first && last && first > `${Number(last.slice(0, 4)) - 1}-12-31` ? [`Asked YTD, data starts ${first} (first available bar)`] : [];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1056
1049
|
async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1057
1050
|
const args = [...rawArgs];
|
|
1058
1051
|
const range = parseRange(takeOption(args, "--range"), ctx);
|
|
1052
|
+
// YTD reads the year's daily bars and starts at the last one on or before 31 Dec.
|
|
1053
|
+
const loadRange: TimeRange = range === "YTD" ? "1Y" : range;
|
|
1059
1054
|
const requestedExchange = takeOption(args, "--exchange");
|
|
1060
1055
|
const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
|
|
1061
1056
|
await withMarketData(ctx, async (market) => {
|
|
@@ -1064,8 +1059,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1064
1059
|
const { symbol, exchange } = listing.request;
|
|
1065
1060
|
const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
|
|
1066
1061
|
const load = () => market.dataProvider.getPriceHistoryWithMetadata
|
|
1067
|
-
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange,
|
|
1068
|
-
: market.dataProvider.getPriceHistory(symbol, exchange,
|
|
1062
|
+
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
|
|
1063
|
+
: market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
|
|
1069
1064
|
const quoted = loadListingQuote(market.dataProvider, listing);
|
|
1070
1065
|
const fate = listingFateLoader(listing, market, quoted);
|
|
1071
1066
|
const [loaded, quote] = await Promise.all([
|
|
@@ -1088,7 +1083,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1088
1083
|
};
|
|
1089
1084
|
// A coin's volume is traded value in its quote currency: said in every row and on the status line.
|
|
1090
1085
|
const volume = historyVolume(unit, traded);
|
|
1091
|
-
const
|
|
1086
|
+
const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
|
|
1087
|
+
const served = range === "YTD" ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval))) : rows;
|
|
1092
1088
|
// Bars stop arriving on the listing's own trading days, not New York's.
|
|
1093
1089
|
const venue = quote?.listingExchangeName || exchange || undefined;
|
|
1094
1090
|
// October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
|
|
@@ -1118,7 +1114,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1118
1114
|
// FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
|
|
1119
1115
|
...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
|
|
1120
1116
|
...await listingChoiceNotes(listing, market, quote, venues),
|
|
1121
|
-
...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? [],
|
|
1117
|
+
...range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? []],
|
|
1122
1118
|
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1123
1119
|
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1124
1120
|
historyBarStatusNote(forming ? served : data) ?? [],
|
|
@@ -1279,7 +1275,9 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1279
1275
|
symbol: identity.symbol || listing.key, bond, rates, treasury: await treasury,
|
|
1280
1276
|
distribution: distributionBasis(priced.fundamentals?.dividendYield, priced.fundamentals?.dividendYieldBasis === "trailing" ? "trailing" : "forward"),
|
|
1281
1277
|
}) : null;
|
|
1282
|
-
const report = { ...data, distributions: await distributions, statementFilings, bank: bank.bank, bankNote: bank.note, bondFund: bond, taxEquivalent
|
|
1278
|
+
const report = { ...data, distributions: await distributions, statementFilings, bank: bank.bank, bankNote: bank.note, bondFund: bond, taxEquivalent,
|
|
1279
|
+
// A base currency other than USD reads every amount with its code, as `ticker` does.
|
|
1280
|
+
currencyCodes: writesCurrencyCodes(market.config) };
|
|
1283
1281
|
ctx.printResult({
|
|
1284
1282
|
// JSON reads a multiple over a loss as null with its reason, never as a number.
|
|
1285
1283
|
data: {
|
|
@@ -1355,22 +1353,33 @@ const NEWS_COLUMNS = [
|
|
|
1355
1353
|
{ key: "url", header: "URL", optional: true },
|
|
1356
1354
|
];
|
|
1357
1355
|
|
|
1358
|
-
/**
|
|
1356
|
+
/**
|
|
1357
|
+
* Stories matching text, printed as a symbol's news is, under a line saying
|
|
1358
|
+
* what they match. A text naming a commodity that matched nothing says so and
|
|
1359
|
+
* offers its future's news when that has stories; `commodity: false` for the
|
|
1360
|
+
* future's own news, which is that route. A search that failed (`failure`,
|
|
1361
|
+
* its message) says so, with `empty`, the lookup's own answer, rather than
|
|
1362
|
+
* that no story matches.
|
|
1363
|
+
*/
|
|
1359
1364
|
async function printNewsSearch(
|
|
1360
1365
|
text: string,
|
|
1361
1366
|
articles: NewsArticle[],
|
|
1362
|
-
{ reason, empty }: { reason?: string; empty?: string },
|
|
1367
|
+
{ reason, empty, commodity = true, failure }: { reason?: string; empty?: string; commodity?: boolean; failure?: string },
|
|
1363
1368
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1364
1369
|
) {
|
|
1370
|
+
const searched = failure === undefined;
|
|
1371
|
+
const named = searched && commodity && articles.length === 0 && commodityNamedIn(text) !== null;
|
|
1372
|
+
const route = named ? await commodityNewsRoute(text) : null;
|
|
1365
1373
|
ctx.printResult({
|
|
1366
1374
|
data: articles,
|
|
1367
|
-
|
|
1375
|
+
...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
|
|
1376
|
+
metadata: { ticker: null, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
|
|
1368
1377
|
freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
|
|
1369
1378
|
}, {
|
|
1370
1379
|
heading: newsSearchHeading(text, reason),
|
|
1371
1380
|
rows: newsRows,
|
|
1372
1381
|
columns: NEWS_COLUMNS,
|
|
1373
|
-
empty: empty ??
|
|
1382
|
+
empty: named ? noStoriesMessage(text, route) : empty ?? (searched ? noStoriesMessage(text, null) : "No stories to show."),
|
|
1374
1383
|
});
|
|
1375
1384
|
}
|
|
1376
1385
|
|
|
@@ -1405,18 +1414,29 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1405
1414
|
]);
|
|
1406
1415
|
const identity = listing ? listingIdentity(listing, quote) : null;
|
|
1407
1416
|
const resolved = quote !== null || identity?.name != null;
|
|
1417
|
+
const unfiled = !!listing && !listing.exchange && !feed && articles.length === 0;
|
|
1418
|
+
// A future's news is its commodity's: news is filed under company tickers, so KC=F (or a bare code
|
|
1419
|
+
// no security keeps, CT) searches the stories of coffee, which the service matches to KC=F's links too.
|
|
1420
|
+
const future = unfiled && isFuturesInstrument(listing!.symbol, quote?.instrumentType)
|
|
1421
|
+
? futuresBoardContract(listing!.symbol) ?? bareFuturesCode(args[0]) : null;
|
|
1422
|
+
const commodity = future ? commoditySearchName(future) : null;
|
|
1408
1423
|
// A bare word no listing answers to is text, and so is a futures board code (TTF), which resolves
|
|
1409
|
-
// to its future:
|
|
1410
|
-
const word =
|
|
1424
|
+
// to its future: the stories that mention the word are the news.
|
|
1425
|
+
const word = commodity ?? (unfiled ? newsFallbackText(args[0]!) : null);
|
|
1411
1426
|
const futuresCode = !!word && !!bareFuturesCode(word) && isFuturesInstrument(listing!.symbol, quote?.instrumentType);
|
|
1412
|
-
const reason =
|
|
1427
|
+
const reason = commodity ? `the commodity ${future!.symbol} trades`
|
|
1428
|
+
: !resolved ? "no ticker by that name" : futuresCode ? "a futures code, not a ticker" : null;
|
|
1413
1429
|
const emptyMessage = (searched: boolean) => listing && identity
|
|
1414
1430
|
? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
|
|
1415
1431
|
: undefined;
|
|
1416
1432
|
if (word && reason) {
|
|
1417
1433
|
// The lookup that found no ticker already answered; a search that fails leaves that answer.
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1434
|
+
const search = await searchNewsStories(word, limit).then((found) => ({ found, failure: undefined }), (error: unknown) => {
|
|
1435
|
+
logFailure("cli.commands-market", "Search news stories failed")(error);
|
|
1436
|
+
return { found: [], failure: errorMessage(error) ?? "no answer" };
|
|
1437
|
+
});
|
|
1438
|
+
const searched = search.failure === undefined;
|
|
1439
|
+
await printNewsSearch(word, search.found, { reason, empty: emptyMessage(searched && !commodity), commodity: !commodity, failure: search.failure }, ctx);
|
|
1420
1440
|
return;
|
|
1421
1441
|
}
|
|
1422
1442
|
const empty = emptyMessage(false);
|
|
@@ -2005,11 +2025,109 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2005
2025
|
/** What an options chain export's `interval` says: one quote snapshot, not bars. */
|
|
2006
2026
|
const OPTIONS_EXPORT_INTERVAL = "snapshot";
|
|
2007
2027
|
|
|
2028
|
+
/** `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. */
|
|
2029
|
+
function readFxDate(value: string, ctx: Parameters<CliCommandDef["execute"]>[1]): string {
|
|
2030
|
+
let date: string;
|
|
2031
|
+
try {
|
|
2032
|
+
date = isoDateOption(value, "date");
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
return ctx.fail(error instanceof Error ? error.message : String(error));
|
|
2035
|
+
}
|
|
2036
|
+
if (!date) return ctx.fail("--date takes a day, such as --date 2026-09-02.");
|
|
2037
|
+
const today = isoDateToday();
|
|
2038
|
+
if (date >= today) {
|
|
2039
|
+
return ctx.fail(`${date} has no daily close yet: --date takes a day before today (${today}).`, "Without --date, fx gives the latest rate.");
|
|
2040
|
+
}
|
|
2041
|
+
return date;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* `fx <pair> --date <day>`: the rate at that day's daily FX closes, read as
|
|
2046
|
+
* a position's purchase-date cost reads them (`fxCloseOnDate`): each
|
|
2047
|
+
* currency's USD pair at its close of the day, or of the last trading day
|
|
2048
|
+
* before it, which the answer then says, crossed through USD as the latest
|
|
2049
|
+
* rate is.
|
|
2050
|
+
*/
|
|
2051
|
+
async function runFxOnDate(
|
|
2052
|
+
parsed: Exclude<ReturnType<typeof parseFxArgs>, { error: string }>,
|
|
2053
|
+
date: string,
|
|
2054
|
+
args: readonly string[],
|
|
2055
|
+
market: MarketContext,
|
|
2056
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
2057
|
+
) {
|
|
2058
|
+
const { request, amount, toUsdByDefault } = parsed;
|
|
2059
|
+
const { currency, baseCurrency } = request;
|
|
2060
|
+
const pair = `${currency}/${baseCurrency}`;
|
|
2061
|
+
const history = market.fxHistory ?? apiClient;
|
|
2062
|
+
const codes = [...new Set([currency, baseCurrency].filter((code) => code !== "USD"))];
|
|
2063
|
+
const legs = new Map<string, FxCloseReading>([["USD", { rate: 1, date }]]);
|
|
2064
|
+
if (currency !== baseCurrency) {
|
|
2065
|
+
for (const code of codes) {
|
|
2066
|
+
const leg = dailyFxLeg(code);
|
|
2067
|
+
if (!leg) return ctx.fail(`${code} has no daily FX closes to read a rate for ${date} from.`);
|
|
2068
|
+
let reading: FxCloseReading | null;
|
|
2069
|
+
try {
|
|
2070
|
+
reading = await fxCloseOnDate(leg, date, history);
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
return ctx.fail(`The daily ${code} FX closes could not be read.`, error instanceof Error ? error.message : String(error));
|
|
2073
|
+
}
|
|
2074
|
+
if (!reading) {
|
|
2075
|
+
return ctx.fail(`No ${leg.invert ? `USD/${code}` : `${code}/USD`} daily close in the week up to ${date}.`,
|
|
2076
|
+
"Without --date, fx gives the latest rate.");
|
|
2077
|
+
}
|
|
2078
|
+
legs.set(code, reading);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const from = legs.get(currency), into = legs.get(baseCurrency);
|
|
2082
|
+
const rate = currency === baseCurrency ? 1 : crossRate(from?.rate, into?.rate);
|
|
2083
|
+
// A cross is as old as its older close; a weekend or holiday reads the trading day before it.
|
|
2084
|
+
const closeDate = [...legs.values()].map((leg) => leg.date).sort()[0]!;
|
|
2085
|
+
const converted = amount == null ? null : amount * rate;
|
|
2086
|
+
const row = {
|
|
2087
|
+
currency, baseCurrency, rate, asOf: closeDate, stale: false, pair, inverse: 1 / rate, date,
|
|
2088
|
+
...(amount == null ? {} : { amount, converted }),
|
|
2089
|
+
};
|
|
2090
|
+
const warnings = [
|
|
2091
|
+
...request.readAs ?? [],
|
|
2092
|
+
...closeDate < date ? [`No daily close on ${date}, a weekend or market holiday: this is the close of ${closeDate}, the trading day before it.`] : [],
|
|
2093
|
+
...toUsdByDefault && amount != null
|
|
2094
|
+
? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to> --date ${date}.`]
|
|
2095
|
+
: [],
|
|
2096
|
+
];
|
|
2097
|
+
ctx.printResult({
|
|
2098
|
+
data: [row],
|
|
2099
|
+
// A past close is what was asked for, never stale.
|
|
2100
|
+
freshness: rowsFreshness([], { status: "not-a-feed", basis: "daily FX close", asOf: closeDate }),
|
|
2101
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
2102
|
+
}, {
|
|
2103
|
+
text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
|
|
2104
|
+
exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "1day" },
|
|
2105
|
+
columns: [
|
|
2106
|
+
{ key: "currency", header: "Currency" },
|
|
2107
|
+
{ key: "baseCurrency", header: "Base" },
|
|
2108
|
+
{ key: "rate", header: "Rate", align: "right" },
|
|
2109
|
+
{ key: "date", header: "Date" },
|
|
2110
|
+
{ key: "asOf", header: "Close" },
|
|
2111
|
+
{ key: "stale", header: "Stale" },
|
|
2112
|
+
{ key: "pair", header: "Pair" },
|
|
2113
|
+
{ key: "inverse", header: "Inverse", align: "right" },
|
|
2114
|
+
...(amount == null ? [] : [
|
|
2115
|
+
{ key: "amount", header: "Amount", align: "right" as const },
|
|
2116
|
+
{ key: "converted", header: "Converted", align: "right" as const },
|
|
2117
|
+
]),
|
|
2118
|
+
],
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2008
2122
|
async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
2009
|
-
|
|
2123
|
+
const args = [...rawArgs];
|
|
2124
|
+
const dateOption = takeOption(args, "--date");
|
|
2125
|
+
if (args.length === 0) ctx.fail(`Usage: gloomberb ${FX_USAGE}`);
|
|
2126
|
+
const date = dateOption == null ? null : readFxDate(dateOption, ctx);
|
|
2010
2127
|
await withMarketData(ctx, async (market) => {
|
|
2011
|
-
const parsed = parseFxArgs(
|
|
2128
|
+
const parsed = parseFxArgs(args, market.config.baseCurrency);
|
|
2012
2129
|
if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloomberb ${FX_USAGE}`);
|
|
2130
|
+
if (date) return runFxOnDate(parsed, date, args, market, ctx);
|
|
2013
2131
|
const { request, amount, toUsdByDefault } = parsed;
|
|
2014
2132
|
const { currency, baseCurrency } = request;
|
|
2015
2133
|
const pair = `${currency}/${baseCurrency}`;
|
|
@@ -2049,10 +2167,14 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
|
|
|
2049
2167
|
}] as const] : [];
|
|
2050
2168
|
})));
|
|
2051
2169
|
// An amount with one currency names no target; the answer says it went to USD.
|
|
2052
|
-
const warnings =
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2170
|
+
const warnings = [
|
|
2171
|
+
// A currency typed as its name (yen) says which code it was read as.
|
|
2172
|
+
...request.readAs ?? [],
|
|
2173
|
+
...toUsdByDefault && amount != null
|
|
2174
|
+
? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to>.`]
|
|
2175
|
+
: [],
|
|
2176
|
+
];
|
|
2177
|
+
ctx.printResult({ data: [row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
|
|
2056
2178
|
// The conversion, then the rate it used; the source line under it dates that rate.
|
|
2057
2179
|
text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
|
|
2058
2180
|
// The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
|
|
@@ -2167,7 +2289,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2167
2289
|
group: CLI_COMMAND_GROUPS.research,
|
|
2168
2290
|
usage: [HISTORY_USAGE],
|
|
2169
2291
|
options: [
|
|
2170
|
-
{ flags: "--range <range>", description: `${
|
|
2292
|
+
{ flags: "--range <range>", description: `${HISTORY_RANGES.join(", ")} (default 1Y); ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}` },
|
|
2171
2293
|
EXCHANGE_OPTION,
|
|
2172
2294
|
],
|
|
2173
2295
|
sections: [{
|
|
@@ -2433,11 +2555,15 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2433
2555
|
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",
|
|
2434
2556
|
help: {
|
|
2435
2557
|
group: CLI_COMMAND_GROUPS.markets,
|
|
2436
|
-
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>"],
|
|
2558
|
+
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>"],
|
|
2559
|
+
options: [
|
|
2560
|
+
{ 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` },
|
|
2561
|
+
],
|
|
2437
2562
|
sections: [{
|
|
2438
2563
|
title: "Direction",
|
|
2439
2564
|
lines: [
|
|
2440
2565
|
"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.",
|
|
2566
|
+
"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.",
|
|
2441
2567
|
"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.",
|
|
2442
2568
|
],
|
|
2443
2569
|
}, {
|
|
@@ -2445,8 +2571,13 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2445
2571
|
lines: [
|
|
2446
2572
|
"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.",
|
|
2447
2573
|
],
|
|
2574
|
+
}, {
|
|
2575
|
+
title: "A past day",
|
|
2576
|
+
lines: [
|
|
2577
|
+
"--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).",
|
|
2578
|
+
],
|
|
2448
2579
|
}],
|
|
2449
|
-
examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx JPY --json"],
|
|
2580
|
+
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"],
|
|
2450
2581
|
},
|
|
2451
2582
|
execute: runFx,
|
|
2452
2583
|
},
|