gloomberb 0.18.3 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/README.zh-CN.md +11 -11
- package/bin/{gloomberb → gloom} +3 -1
- package/package.json +6 -2
- package/patches/electrobun@1.18.1.patch +32 -1
- package/src/account/auth-model.ts +7 -3
- package/src/account/checkout-handoff.tsx +225 -0
- package/src/account/device-signin-dialog.tsx +2 -2
- package/src/account/device-signin.ts +2 -2
- package/src/account/upgrade-dialog.tsx +1 -1
- package/src/api-client/askg.ts +2 -2
- package/src/api-client/auth.ts +11 -6
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/central-bank-rates.ts +6 -0
- package/src/api-client/collections.ts +5 -1
- package/src/api-client/cot.ts +2 -0
- package/src/api-client/crypto-etf-flows.ts +37 -0
- package/src/api-client/crypto-markets.ts +60 -0
- package/src/api-client/data.ts +98 -5
- package/src/api-client/doe.ts +15 -0
- package/src/api-client/equity-screener.ts +28 -0
- package/src/api-client/exchange-announcements.ts +38 -0
- package/src/api-client/fund-facts.ts +39 -0
- package/src/api-client/futures-curve.ts +9 -1
- package/src/api-client/hiring.ts +301 -0
- package/src/api-client/index.ts +182 -16
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/market-discovery.ts +14 -0
- package/src/api-client/members.ts +18 -0
- package/src/api-client/mna.ts +6 -0
- package/src/api-client/paths.ts +18 -1
- package/src/api-client/request.ts +224 -26
- package/src/api-client/research-activity.ts +3 -10
- package/src/api-client/short-side-publications.ts +37 -0
- package/src/api-client/socket.ts +64 -4
- package/src/api-client/swap-curve.ts +35 -0
- package/src/api-client/themes.ts +7 -1
- package/src/api-client/types.ts +96 -2
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +9 -2
- package/src/app/runtime/broker-import.ts +43 -26
- package/src/app/runtime/desktop-deeplink.ts +5 -5
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +13 -0
- package/src/app/star-prompt/status-line.tsx +2 -2
- package/src/architecture/core-builtin-allowlist.json +5 -2
- package/src/architecture/ui-literals-allowlist.json +3 -3
- package/src/architecture/ui-tokens-allowlist.json +1 -1
- package/src/brokers/broker-portfolio-sync.ts +37 -0
- package/src/brokers/directory.ts +11 -0
- package/src/brokers/logos.ts +3 -1
- package/src/brokers/signed-in/adapter.ts +304 -26
- package/src/brokers/signed-in/client.ts +14 -2
- package/src/brokers/signed-in/connect.ts +10 -2
- package/src/brokers/sync-broker-instance.ts +9 -1
- package/src/capabilities/chart-series.ts +22 -6
- package/src/cli/bank-section.ts +1 -1
- package/src/cli/base-converter.ts +23 -14
- package/src/cli/central-bank-events.ts +1 -1
- package/src/cli/command-options.ts +59 -4
- package/src/cli/commands/api.ts +9 -6
- package/src/cli/commands/broker.ts +2 -2
- package/src/cli/commands/command-utils.ts +2 -2
- package/src/cli/commands/crypto-hints.ts +7 -20
- package/src/cli/commands/fred-series.ts +9 -2
- package/src/cli/commands/keybindings.ts +25 -2
- package/src/cli/commands/market.ts +1177 -278
- package/src/cli/commands/news-hints.ts +2 -2
- package/src/cli/commands/news-search.ts +59 -6
- package/src/cli/commands/options-chain.ts +59 -3
- package/src/cli/commands/options-leaps.ts +41 -26
- package/src/cli/commands/overview.ts +222 -61
- package/src/cli/commands/plugins.ts +9 -9
- package/src/cli/commands/remote.ts +13 -10
- package/src/cli/commands/search.ts +82 -46
- package/src/cli/commands/system.ts +27 -19
- package/src/cli/commands/themes.ts +1 -1
- package/src/cli/commands/ticker.ts +194 -48
- package/src/cli/context.ts +3 -2
- package/src/cli/desktop-pane-shot-view.ts +2 -2
- package/src/cli/desktop-pane-shot.ts +1 -1
- package/src/cli/earnings-calendar.ts +4 -4
- package/src/cli/entry.ts +18 -1
- package/src/cli/errors.ts +21 -3
- package/src/cli/exit-codes.ts +1 -1
- package/src/cli/export-source.ts +38 -4
- package/src/cli/freshness.ts +2 -8
- package/src/cli/fx-forward.ts +130 -0
- package/src/cli/fx-pair.ts +10 -9
- package/src/cli/glossary.ts +113 -20
- package/src/cli/help.ts +19 -13
- package/src/cli/history-export.ts +126 -0
- package/src/cli/history-rows.ts +226 -50
- package/src/cli/index-roots.ts +5 -45
- package/src/cli/index.ts +24 -18
- package/src/cli/known-gaps.ts +123 -27
- package/src/cli/listing-arg.ts +316 -46
- package/src/cli/name-entries.ts +30 -18
- package/src/cli/native-smoke.ts +2 -0
- package/src/cli/not-a-ticker.ts +19 -9
- package/src/cli/opentui-libc.ts +5 -0
- package/src/cli/options.ts +12 -0
- package/src/cli/pane-functions/capabilities.ts +75 -20
- package/src/cli/pane-functions/catalog.ts +24 -23
- package/src/cli/pane-functions/discovery.ts +1 -1
- package/src/cli/pane-functions/dom.ts +10 -7
- package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
- package/src/cli/pane-functions/freshness.ts +193 -30
- package/src/cli/pane-functions/function-help.ts +76 -30
- package/src/cli/pane-functions/headless.ts +59 -12
- package/src/cli/pane-functions/index.ts +28 -19
- package/src/cli/pane-functions/market-session.ts +15 -5
- package/src/cli/pane-functions/options.ts +22 -2
- package/src/cli/pane-functions/report-notices.ts +12 -4
- package/src/cli/pane-functions/report.ts +10 -6
- package/src/cli/pane-functions/resolver.ts +64 -11
- package/src/cli/pane-functions/screenshot-assessment.ts +322 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +152 -0
- package/src/cli/pane-functions/screenshot-chart-evidence.ts +396 -0
- package/src/cli/pane-functions/screenshot-data-evidence.ts +383 -0
- package/src/cli/pane-functions/screenshot-evidence.ts +1 -1
- package/src/cli/pane-functions/screenshot-payload.ts +453 -0
- package/src/cli/pane-functions/screenshot-render.ts +316 -0
- package/src/cli/pane-functions/screenshot-types.ts +162 -0
- package/src/cli/pane-functions/shot-options.ts +8 -3
- package/src/cli/peso-dollar-rates.ts +79 -0
- package/src/cli/quote-answer.ts +45 -4
- package/src/cli/report-tables.ts +75 -19
- package/src/cli/restore-plugins.ts +42 -15
- package/src/cli/result.ts +164 -23
- package/src/cli/series-search.ts +5 -5
- package/src/cli/statement-filings.ts +19 -0
- package/src/cli/suffixed-listing-rows.ts +49 -0
- package/src/cli/symbol-args.ts +50 -15
- package/src/cli/symbol-cells.ts +120 -0
- package/src/cli/symbol-list.ts +198 -68
- package/src/cli/symbol-rows.ts +131 -25
- package/src/cli/ticker-resolution.ts +108 -11
- package/src/cli/treasury-yields.ts +5 -5
- package/src/cli/types.ts +2 -0
- package/src/cli/uncovered-instrument.ts +3 -3
- package/src/cli/unknown-command.ts +6 -6
- package/src/components/chart/composite/chart-layout.ts +131 -0
- package/src/components/chart/composite/composite-chart.tsx +395 -20
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/interactions.ts +7 -2
- package/src/components/chart/composite/legend.tsx +75 -3
- package/src/components/chart/composite/levels.ts +7 -0
- package/src/components/chart/composite/panel-gestures.ts +41 -0
- package/src/components/chart/composite/panel-labels.ts +2 -1
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +314 -13
- package/src/components/chart/composite/price-axis-labels.tsx +27 -3
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/rasterizer.ts +44 -19
- package/src/components/chart/composite/scene.ts +20 -4
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/text-renderer.ts +48 -21
- package/src/components/chart/composite/toolbar.tsx +52 -5
- package/src/components/chart/composite/types.ts +17 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +120 -13
- package/src/components/chart/composite/use-composite-value-scales.ts +44 -0
- package/src/components/chart/composite/use-composite-viewport.ts +12 -3
- package/src/components/chart/composite/value-scale.ts +149 -0
- package/src/components/chart/composite/volume-profile-paint.ts +6 -3
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart-table/index.ts +2 -0
- package/src/components/chart-table/read.tsx +74 -0
- package/src/components/command-bar/commands/collection-list.ts +41 -19
- package/src/components/command-bar/commands/collection.ts +9 -3
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/commands/direct/runtime.ts +3 -2
- package/src/components/command-bar/pane-templates/items.ts +59 -6
- package/src/components/command-bar/pane-templates/workflow.ts +10 -1
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/routes/root/plugin-install.ts +1 -1
- package/src/components/command-bar/routes/root/provider-search.ts +42 -11
- package/src/components/command-bar/routes/root/results.ts +18 -4
- package/src/components/command-bar/routes/root/runtime.ts +15 -3
- package/src/components/command-bar/routes/root/search-providers.ts +25 -2
- package/src/components/command-bar/routes/root/selection.ts +6 -1
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +4 -1
- package/src/components/command-bar/routes/root/shortcuts.ts +30 -3
- package/src/components/command-bar/routes/ticker-search/results.ts +73 -4
- package/src/components/command-bar/selection-runtime.ts +50 -10
- package/src/components/command-bar/surface/index.tsx +24 -2
- package/src/components/command-bar/view-model.ts +7 -0
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/command-bar/workflow/broker.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +46 -7
- package/src/components/command-bar/workflow/collection-ticker.ts +32 -4
- package/src/components/command-bar/workflow/fields.ts +8 -2
- package/src/components/command-bar/workflow/ops.ts +18 -4
- package/src/components/command-bar/workflow/portfolio-position.ts +53 -4
- package/src/components/command-bar/workflow/submit.ts +3 -2
- package/src/components/command-bar/workflow/tickers.ts +5 -1
- package/src/components/company-logo.tsx +39 -0
- package/src/components/data-table/sections.ts +4 -2
- package/src/components/form-modal/confirm.tsx +2 -1
- package/src/components/form-modal/request.ts +5 -0
- package/src/components/form-modal/routes.ts +4 -1
- package/src/components/index.ts +12 -6
- package/src/components/layout/header.tsx +33 -46
- package/src/components/layout/market-summary.ts +9 -6
- package/src/components/layout/pane/capture-status.ts +1 -1
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +38 -5
- package/src/components/layout/shell/chrome.ts +19 -6
- package/src/components/layout/shell/index.tsx +2 -1
- package/src/components/layout/shell/pane/layers.tsx +19 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/market-board/index.tsx +11 -3
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.tsx +1 -0
- package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +2 -2
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
- package/src/components/onboarding/wizard-broker-sync.ts +4 -1
- package/src/components/pane-settings-dialog/desktop.tsx +20 -7
- package/src/components/pane-settings-dialog/tui.tsx +17 -9
- package/src/components/pane-settings-dialog/value.ts +8 -1
- package/src/components/pane-settings-dialog.tsx +44 -13
- package/src/components/sign-in-code-panel.tsx +27 -19
- package/src/components/ui/column-grid.tsx +48 -4
- package/src/components/ui/country-flag-art.tsx +180 -0
- package/src/components/ui/country-flag.tsx +118 -12
- package/src/components/ui/data-table/opentui/index.tsx +61 -2
- package/src/components/ui/data-table/opentui/model.ts +109 -2
- package/src/components/ui/data-table/types.ts +1 -1
- package/src/components/ui/display.tsx +10 -2
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/logo-mark.tsx +11 -2
- package/src/components/ui/query-bar.tsx +1 -1
- package/src/components/ui/row-notes.ts +79 -0
- package/src/core/state/app/layout.ts +6 -4
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/cached-query.ts +8 -1
- package/src/data/config/chart-settings.ts +3 -3
- package/src/data/config/home.ts +10 -2
- package/src/data/config/layout.ts +4 -1
- package/src/data/config/store/node.ts +4 -0
- package/src/data/config/store/normalize.ts +13 -3
- package/src/data/config/store/portfolio-as-of-column.ts +2 -2
- package/src/data/json-storage.ts +2 -0
- package/src/data/saved-copies.ts +123 -0
- package/src/docs/snapshot.json +422 -112
- package/src/i18n/es.ts +30 -13
- package/src/i18n/ja.ts +30 -13
- package/src/i18n/ko.ts +30 -13
- package/src/i18n/zh-cn.ts +30 -13
- package/src/i18n/zh-tw.ts +30 -13
- package/src/layout/desks.ts +1 -1
- package/src/layout/launch-layout.ts +1 -1
- package/src/layout/pane-manager/floating-actions.ts +5 -3
- package/src/layout/pane-manager/floating.ts +43 -1
- package/src/layout/pane-manager/index.ts +2 -0
- package/src/layout/selected-ticker.ts +74 -0
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/depositary-pair.ts +251 -0
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +124 -40
- package/src/market-data/fx-closes.ts +17 -2
- package/src/market-data/hooks.tsx +19 -11
- package/src/market-data/market/change-basis.ts +81 -11
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/freshness.ts +34 -5
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/period-return.ts +163 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/market/trading-sessions.ts +13 -0
- package/src/market-data/market/year-to-date.ts +8 -13
- package/src/market-data/options-alternatives.ts +60 -0
- package/src/market-data/performance.ts +20 -14
- package/src/market-data/perpetual-premium.ts +32 -0
- package/src/market-data/published-apac-sessions.ts +2 -1
- package/src/market-data/published-futures-sessions.ts +69 -0
- package/src/market-data/quotes/contributions.ts +25 -3
- package/src/market-data/quotes/freshness.ts +42 -0
- package/src/market-data/quotes/intraday-bar.ts +37 -0
- package/src/market-data/quotes/regular-session-valuation.ts +13 -8
- package/src/market-data/quotes/resolution.ts +7 -2
- package/src/market-data/selectors.ts +3 -0
- package/src/market-data/tax-equivalent.ts +1 -1
- package/src/market-data/ticker-price-returns.ts +3 -1
- package/src/news/aggregator.ts +5 -1
- package/src/news/news-model.ts +4 -0
- package/src/news/types.ts +11 -0
- package/src/notifications/app-notifier.ts +1 -1
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/auto-update.ts +2 -2
- package/src/plugins/builtin/account-management/index.tsx +1 -0
- package/src/plugins/builtin/account-management/pane.tsx +9 -8
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -20
- package/src/plugins/builtin/alerts/calendar-events.ts +429 -0
- package/src/plugins/builtin/alerts/cli.ts +533 -85
- package/src/plugins/builtin/alerts/command.ts +12 -10
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/event-rows.ts +9 -5
- package/src/plugins/builtin/alerts/events-pane.tsx +71 -56
- package/src/plugins/builtin/alerts/events.ts +107 -5
- package/src/plugins/builtin/alerts/fired-log.ts +119 -0
- package/src/plugins/builtin/alerts/format.ts +10 -3
- package/src/plugins/builtin/alerts/headless.ts +107 -41
- package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
- package/src/plugins/builtin/alerts/history-rows.ts +92 -0
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +107 -30
- package/src/plugins/builtin/alerts/iv-level.ts +365 -0
- package/src/plugins/builtin/alerts/levels.ts +3 -3
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +61 -0
- package/src/plugins/builtin/alerts/metric-data.ts +70 -0
- package/src/plugins/builtin/alerts/metric-model.ts +175 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +130 -49
- package/src/plugins/builtin/alerts/price.ts +35 -0
- package/src/plugins/builtin/alerts/quotes.ts +24 -0
- package/src/plugins/builtin/alerts/research-builder.ts +84 -11
- package/src/plugins/builtin/alerts/research-rules.ts +76 -3
- package/src/plugins/builtin/alerts/scheduled-check.ts +206 -32
- package/src/plugins/builtin/alerts/scheduled-sources.ts +26 -3
- package/src/plugins/builtin/alerts/scheduled.ts +188 -29
- package/src/plugins/builtin/alerts/storage.ts +38 -12
- package/src/plugins/builtin/alerts/targets.ts +12 -3
- package/src/plugins/builtin/alerts/types.ts +3 -0
- package/src/plugins/builtin/analytics/index.tsx +8 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -1
- package/src/plugins/builtin/analytics/risk-book.ts +215 -0
- package/src/plugins/builtin/analytics/risk-client.ts +13 -6
- package/src/plugins/builtin/analytics/risk-headless.ts +55 -21
- package/src/plugins/builtin/analytics/risk-math.ts +6 -3
- package/src/plugins/builtin/analytics/risk-model.ts +297 -104
- package/src/plugins/builtin/analytics/risk-options.ts +17 -2
- package/src/plugins/builtin/analytics/risk-pane.tsx +97 -36
- package/src/plugins/builtin/attention/pane.tsx +6 -2
- package/src/plugins/builtin/awards/headless.ts +7 -3
- package/src/plugins/builtin/awards/model.ts +18 -0
- package/src/plugins/builtin/awards/pane.tsx +6 -3
- package/src/plugins/builtin/backtest/headless.ts +13 -8
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/backtest/rules.ts +41 -5
- package/src/plugins/builtin/broker-manager/add-flow.tsx +12 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +47 -19
- package/src/plugins/builtin/broker-manager/footer.ts +27 -11
- package/src/plugins/builtin/broker-manager/index.tsx +24 -14
- package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
- package/src/plugins/builtin/broker-manager/model.ts +18 -3
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +183 -57
- package/src/plugins/builtin/broker-manager/table.ts +28 -12
- package/src/plugins/builtin/broker-trading/account-picker.tsx +2 -1
- package/src/plugins/builtin/broker-trading/chart-levels.ts +82 -13
- package/src/plugins/builtin/broker-trading/controller.ts +66 -24
- package/src/plugins/builtin/broker-trading/dock.tsx +22 -4
- package/src/plugins/builtin/broker-trading/headless.ts +2 -1
- package/src/plugins/builtin/broker-trading/index.tsx +19 -1
- package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +83 -10
- package/src/plugins/builtin/broker-trading/orders.tsx +109 -31
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +9 -3
- package/src/plugins/builtin/broker-trading/pane-model.ts +30 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +111 -24
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
- package/src/plugins/builtin/broker-trading/ticket.tsx +112 -31
- package/src/plugins/builtin/buildout/cells.tsx +4 -0
- package/src/plugins/builtin/buildout/detail/index.tsx +5 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +7 -4
- package/src/plugins/builtin/buildout/pane/index.tsx +25 -12
- package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +2 -2
- package/src/plugins/builtin/catalysts/detail.tsx +3 -2
- package/src/plugins/builtin/catalysts/index.tsx +1 -1
- package/src/plugins/builtin/catalysts/model.ts +51 -5
- package/src/plugins/builtin/catalysts/pane.tsx +6 -8
- package/src/plugins/builtin/central-bank-rates/client.ts +5 -0
- package/src/plugins/builtin/central-bank-rates/headless.ts +36 -10
- package/src/plugins/builtin/central-bank-rates/model.ts +33 -8
- package/src/plugins/builtin/changelog/index.tsx +1 -0
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +4 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +3 -2
- package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +7 -10
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -3
- package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +9 -4
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +42 -29
- package/src/plugins/builtin/chart-composer/headless.ts +266 -37
- package/src/plugins/builtin/chart-composer/index.tsx +12 -2
- package/src/plugins/builtin/chart-composer/leverage-overlay.ts +142 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +144 -30
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/series-catalog.ts +1 -0
- package/src/plugins/builtin/chart-composer/series-expression.ts +65 -6
- package/src/plugins/builtin/chart-composer/session-history.ts +31 -28
- package/src/plugins/builtin/chart-composer/session-range.ts +4 -2
- package/src/plugins/builtin/chart-composer/settings.ts +31 -19
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +44 -31
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +18 -8
- package/src/plugins/builtin/chat/attachments/model.ts +3 -1
- package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +51 -22
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +9 -2
- package/src/plugins/builtin/chat/people.ts +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/clinical-trials/client.ts +5 -2
- package/src/plugins/builtin/clinical-trials/model.ts +13 -4
- package/src/plugins/builtin/clinical-trials/pane.tsx +32 -12
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +17 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +1 -1
- package/src/plugins/builtin/cloud/askg/lock-note.ts +1 -1
- package/src/plugins/builtin/cloud/askg/tool-display.ts +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +3 -3
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +105 -47
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +42 -4
- package/src/plugins/builtin/cloud/team/pane.tsx +204 -64
- package/src/plugins/builtin/cloud/thesis/store.ts +45 -22
- package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
- package/src/plugins/builtin/commodities-energy/index.tsx +29 -2
- package/src/plugins/builtin/commodities-energy/view-state.ts +2 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +11 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +7 -5
- package/src/plugins/builtin/company-attention/building-model.ts +222 -0
- package/src/plugins/builtin/company-attention/building.tsx +172 -0
- package/src/plugins/builtin/company-attention/client.ts +117 -1
- package/src/plugins/builtin/company-attention/evidence.ts +16 -3
- package/src/plugins/builtin/company-attention/headless.ts +101 -5
- package/src/plugins/builtin/company-attention/index.tsx +6 -3
- package/src/plugins/builtin/company-attention/model.ts +33 -7
- package/src/plugins/builtin/company-attention/moves-model.ts +253 -0
- package/src/plugins/builtin/company-attention/moves.tsx +159 -0
- package/src/plugins/builtin/company-attention/pane.tsx +79 -23
- package/src/plugins/builtin/company-attention/signals-model.ts +229 -0
- package/src/plugins/builtin/company-attention/signals.tsx +160 -0
- package/src/plugins/builtin/company-kpis/headless.ts +3 -2
- package/src/plugins/builtin/company-kpis/model.ts +2 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +1 -1
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/congress-trades/headless.ts +1 -1
- package/src/plugins/builtin/connections/index.ts +1 -0
- package/src/plugins/builtin/correlation/headless-schema.ts +3 -2
- package/src/plugins/builtin/correlation/headless.ts +5 -3
- package/src/plugins/builtin/cot/headless.ts +28 -6
- package/src/plugins/builtin/cot/model.ts +12 -0
- package/src/plugins/builtin/cot/pane.tsx +3 -2
- package/src/plugins/builtin/cpi/headless.ts +5 -1
- package/src/plugins/builtin/cpi/pane.tsx +11 -9
- package/src/plugins/builtin/credit-boards/model.ts +2 -0
- package/src/plugins/builtin/credit-documents/headless.ts +1 -1
- package/src/plugins/builtin/crypto-board/client.ts +86 -0
- package/src/plugins/builtin/crypto-board/defi-view.tsx +218 -0
- package/src/plugins/builtin/crypto-board/defi.ts +239 -0
- package/src/plugins/builtin/crypto-board/headless.ts +117 -6
- package/src/plugins/builtin/crypto-board/index.tsx +8 -3
- package/src/plugins/builtin/crypto-board/model.ts +40 -8
- package/src/plugins/builtin/crypto-board/pane.tsx +55 -21
- package/src/plugins/builtin/crypto-board/series.ts +19 -1
- package/src/plugins/builtin/crypto-etf-flows/client.ts +60 -0
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +132 -0
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +44 -0
- package/src/plugins/builtin/crypto-etf-flows/model.ts +121 -0
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +193 -0
- package/src/plugins/builtin/debug/index.tsx +1 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/docs/index.tsx +1 -1
- package/src/plugins/builtin/docs/pane.tsx +2 -2
- package/src/plugins/builtin/docs/targets.ts +1 -1
- package/src/plugins/builtin/doe/client.ts +5 -1
- package/src/plugins/builtin/doe/headless.ts +12 -6
- package/src/plugins/builtin/doe/model.ts +43 -6
- package/src/plugins/builtin/doe/pane.tsx +18 -11
- package/src/plugins/builtin/earnings/board-model.ts +13 -6
- package/src/plugins/builtin/earnings/board-table.ts +4 -1
- package/src/plugins/builtin/earnings/board.tsx +253 -50
- package/src/plugins/builtin/earnings/calendar-model.ts +188 -0
- package/src/plugins/builtin/earnings/calendar.tsx +338 -0
- package/src/plugins/builtin/earnings/client.ts +4 -3
- package/src/plugins/builtin/earnings/headless.ts +53 -15
- package/src/plugins/builtin/earnings/history-model.ts +53 -36
- package/src/plugins/builtin/earnings/index.tsx +15 -22
- package/src/plugins/builtin/earnings/model.ts +17 -12
- package/src/plugins/builtin/earnings/scope.ts +89 -0
- package/src/plugins/builtin/earnings-calls/calendar.tsx +310 -0
- package/src/plugins/builtin/earnings-calls/data.ts +146 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +151 -9
- package/src/plugins/builtin/earnings-calls/index.tsx +3 -3
- package/src/plugins/builtin/earnings-calls/lock-note.ts +1 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +149 -31
- package/src/plugins/builtin/earnings-calls/upcoming.ts +195 -0
- package/src/plugins/builtin/earnings-ripple/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +144 -27
- package/src/plugins/builtin/econ/detail-view.tsx +48 -10
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/fred-catalog.ts +23 -6
- package/src/plugins/builtin/econ/fred-series-map.ts +4 -0
- package/src/plugins/builtin/econ/index.tsx +144 -27
- package/src/plugins/builtin/econ/print-watch.ts +2 -1
- package/src/plugins/builtin/econ/types.ts +34 -1
- package/src/plugins/builtin/econ-statistics/client.ts +16 -3
- package/src/plugins/builtin/econ-statistics/defs.ts +4 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +2 -2
- package/src/plugins/builtin/econ-statistics/headless.ts +87 -28
- package/src/plugins/builtin/econ-statistics/index.tsx +3 -3
- package/src/plugins/builtin/econ-statistics/pane.tsx +2 -1
- package/src/plugins/builtin/econ-statistics/stats.ts +104 -0
- package/src/plugins/builtin/econ-statistics/view.ts +19 -1
- package/src/plugins/builtin/equity-screener/client.ts +21 -3
- package/src/plugins/builtin/equity-screener/headless.ts +68 -6
- package/src/plugins/builtin/equity-screener/live.ts +1 -1
- package/src/plugins/builtin/equity-screener/model.ts +6 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +47 -12
- package/src/plugins/builtin/equity-screener/screen-check.ts +90 -0
- package/src/plugins/builtin/equity-screener/ticker-check.tsx +148 -0
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -1
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +3 -3
- package/src/plugins/builtin/estimate-revisions/model.ts +8 -10
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/headless.ts +51 -6
- package/src/plugins/builtin/exposure/index.tsx +28 -2
- package/src/plugins/builtin/exposure/model.ts +20 -1
- package/src/plugins/builtin/exposure/pane.tsx +10 -5
- package/src/plugins/builtin/fear-greed/index.tsx +1 -1
- package/src/plugins/builtin/filing-events/index.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +12 -10
- package/src/plugins/builtin/futures/returns.ts +8 -36
- package/src/plugins/builtin/futures/table.tsx +3 -3
- package/src/plugins/builtin/futures-curve/client.ts +40 -9
- package/src/plugins/builtin/futures-curve/headless.ts +39 -17
- package/src/plugins/builtin/futures-curve/model.ts +258 -69
- package/src/plugins/builtin/futures-curve/pane.tsx +46 -17
- package/src/plugins/builtin/fx-matrix/client.ts +20 -8
- package/src/plugins/builtin/fx-matrix/headless.ts +37 -16
- package/src/plugins/builtin/fx-matrix/index.tsx +25 -13
- package/src/plugins/builtin/fx-matrix/live-legs.ts +34 -0
- package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
- package/src/plugins/builtin/gpu/headless.ts +9 -4
- package/src/plugins/builtin/help/index.tsx +13 -3
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/holders/beneficial-model.ts +8 -11
- package/src/plugins/builtin/holders/beneficial-report.ts +6 -5
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/client.ts +13 -1
- package/src/plugins/builtin/holders/format.ts +28 -17
- package/src/plugins/builtin/holders/headless.ts +46 -7
- package/src/plugins/builtin/holders/index-holders.ts +264 -0
- package/src/plugins/builtin/holders/pane.tsx +79 -15
- package/src/plugins/builtin/holders/report-rows.ts +35 -7
- package/src/plugins/builtin/holders/table-model.ts +2 -1
- package/src/plugins/builtin/holders/treemap.tsx +2 -2
- package/src/plugins/builtin/insider/headless.ts +1 -1
- package/src/plugins/builtin/insider/signals-client.ts +1 -1
- package/src/plugins/builtin/insider/signals.tsx +1 -1
- package/src/plugins/builtin/ipo-calendar/pane.tsx +7 -4
- package/src/plugins/builtin/iv-history/client.ts +20 -0
- package/src/plugins/builtin/iv-history/headless.ts +7 -4
- package/src/plugins/builtin/iv-history/index.tsx +1 -1
- package/src/plugins/builtin/iv-history/model.ts +13 -4
- package/src/plugins/builtin/iv-history/series.ts +3 -1
- package/src/plugins/builtin/jobs/lock-note.ts +1 -1
- package/src/plugins/builtin/jobs/model.ts +4 -15
- package/src/plugins/builtin/jobs/pane.tsx +7 -5
- package/src/plugins/builtin/jobs/share-bars.tsx +11 -5
- package/src/plugins/builtin/jodi/pane.tsx +10 -4
- package/src/plugins/builtin/layout-manager/index.tsx +1 -0
- package/src/plugins/builtin/macro-day/headless.ts +20 -7
- package/src/plugins/builtin/macro-day/index.tsx +3 -2
- package/src/plugins/builtin/macro-day/model.ts +29 -8
- package/src/plugins/builtin/macro-day/pane.tsx +51 -13
- package/src/plugins/builtin/macro-day/releases.ts +55 -13
- package/src/plugins/builtin/market-heatmap/data.ts +9 -2
- package/src/plugins/builtin/market-heatmap/evidence.ts +1 -1
- package/src/plugins/builtin/market-heatmap/headless.ts +9 -6
- package/src/plugins/builtin/market-heatmap/index.tsx +3 -3
- package/src/plugins/builtin/market-heatmap/model.ts +10 -7
- package/src/plugins/builtin/market-movers/client.ts +33 -19
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/market-movers/headless.ts +8 -2
- package/src/plugins/builtin/market-movers/index.tsx +16 -10
- package/src/plugins/builtin/market-movers/screener.ts +29 -8
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -2
- package/src/plugins/builtin/market-movers/session-table.ts +4 -2
- package/src/plugins/builtin/market-movers/table.ts +4 -1
- package/src/plugins/builtin/market-valuation/headless.ts +2 -0
- package/src/plugins/builtin/market-valuation/view.ts +5 -0
- package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +24 -10
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +31 -14
- package/src/plugins/builtin/mna/client.ts +1 -0
- package/src/plugins/builtin/mna/detail.tsx +20 -32
- package/src/plugins/builtin/mna/distress/detail.tsx +16 -4
- package/src/plugins/builtin/mna/distress/model.ts +3 -2
- package/src/plugins/builtin/mna/distress/pane.tsx +7 -4
- package/src/plugins/builtin/mna/headless.ts +6 -13
- package/src/plugins/builtin/mna/model.ts +93 -15
- package/src/plugins/builtin/mna/pane.tsx +5 -4
- package/src/plugins/builtin/news/headless.ts +12 -6
- package/src/plugins/builtin/news/index.tsx +10 -1
- package/src/plugins/builtin/news/wire/index.tsx +3 -2
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +26 -10
- package/src/plugins/builtin/news/wire/news/query-presets.ts +6 -2
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -3
- package/src/plugins/builtin/notes/cli.ts +49 -14
- package/src/plugins/builtin/openfda/pane.tsx +1 -1
- package/src/plugins/builtin/options/analytics.ts +26 -2
- package/src/plugins/builtin/options/carry.ts +2 -2
- package/src/plugins/builtin/options/index.tsx +1 -1
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +1 -7
- package/src/plugins/builtin/options/view.tsx +30 -13
- package/src/plugins/builtin/options-calculator/headless.ts +46 -7
- package/src/plugins/builtin/options-positioning/client.ts +19 -0
- package/src/plugins/builtin/options-positioning/headless.ts +54 -14
- package/src/plugins/builtin/options-positioning/index.tsx +3 -3
- package/src/plugins/builtin/options-positioning/model.ts +47 -8
- package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
- package/src/plugins/builtin/options-scenario/client.ts +123 -28
- package/src/plugins/builtin/options-scenario/collar.ts +8 -6
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +127 -0
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +165 -0
- package/src/plugins/builtin/options-scenario/csp-screen.ts +258 -0
- package/src/plugins/builtin/options-scenario/csp.ts +101 -21
- package/src/plugins/builtin/options-scenario/headless.ts +36 -15
- package/src/plugins/builtin/options-scenario/hedge.ts +20 -14
- package/src/plugins/builtin/options-scenario/index.tsx +28 -3
- package/src/plugins/builtin/options-scenario/model.ts +13 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +9 -5
- package/src/plugins/builtin/perps/equity-row.tsx +11 -3
- package/src/plugins/builtin/perps/freshness.ts +14 -9
- package/src/plugins/builtin/perps/headless.ts +3 -1
- package/src/plugins/builtin/perps/list-perpetuals.ts +25 -4
- package/src/plugins/builtin/perps/live-rows.ts +168 -0
- package/src/plugins/builtin/perps/markets-section.tsx +154 -0
- package/src/plugins/builtin/perps/pane.tsx +49 -26
- package/src/plugins/builtin/perps/venue-markets.ts +96 -0
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -1
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +143 -59
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +72 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
- package/src/plugins/builtin/portfolio-list/cli/render.ts +54 -22
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +3 -3
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +29 -24
- package/src/plugins/builtin/portfolio-list/headless.ts +7 -4
- package/src/plugins/builtin/portfolio-list/income.ts +13 -2
- package/src/plugins/builtin/portfolio-list/index.tsx +9 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +21 -8
- package/src/plugins/builtin/portfolio-list/pane/purchase-date-fx.ts +46 -0
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +16 -9
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
- package/src/plugins/builtin/power/client.ts +4 -1
- package/src/plugins/builtin/power/evidence.ts +22 -7
- package/src/plugins/builtin/power/gas.tsx +5 -1
- package/src/plugins/builtin/power/pane.tsx +8 -1
- package/src/plugins/builtin/power/prices.tsx +10 -3
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/research/analyst-headless.ts +25 -8
- package/src/plugins/builtin/research/analyst-model.ts +68 -3
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +63 -14
- package/src/plugins/builtin/research/event-model.ts +3 -1
- package/src/plugins/builtin/research/event-sources.ts +12 -4
- package/src/plugins/builtin/research/index.tsx +2 -1
- package/src/plugins/builtin/research/relative-valuation-basis.ts +58 -0
- package/src/plugins/builtin/research/relative-valuation-columns.ts +31 -2
- package/src/plugins/builtin/research/relative-valuation-currency.ts +12 -5
- package/src/plugins/builtin/research/relative-valuation-headless.ts +84 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +88 -19
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +80 -13
- package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +6 -2
- package/src/plugins/builtin/risk-factors/index.tsx +2 -2
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/index.ts +24 -1
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +114 -0
- package/src/plugins/builtin/scanner/live-model.ts +351 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/scanner/lock-note.ts +1 -1
- package/src/plugins/builtin/sec/forms.ts +13 -0
- package/src/plugins/builtin/sec/fund-abroad.ts +32 -0
- package/src/plugins/builtin/sec/fund-facts.ts +68 -8
- package/src/plugins/builtin/sec/headless.ts +85 -8
- package/src/plugins/builtin/sec/index.tsx +58 -12
- package/src/plugins/builtin/sectors/client.ts +2 -1
- package/src/plugins/builtin/sectors/sector-model.ts +11 -15
- package/src/plugins/builtin/series-inventory.ts +3 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
- package/src/plugins/builtin/shared/cloud-upgrade.ts +34 -6
- package/src/plugins/builtin/shared/earnings-calendar.ts +375 -0
- package/src/plugins/builtin/{earnings/estimate-basis.ts → shared/earnings-estimate-basis.ts} +1 -1
- package/src/plugins/builtin/shared/earnings-report.ts +3 -3
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +23 -1
- package/src/plugins/builtin/shared/job-posting.ts +129 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +3 -3
- package/src/plugins/builtin/shared/report-freshness.ts +3 -0
- package/src/plugins/builtin/shared/research-trading.tsx +21 -0
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/series-cache.ts +2 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
- package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
- package/src/plugins/builtin/short-interest/client.ts +2 -0
- package/src/plugins/builtin/short-interest/headless.ts +41 -12
- package/src/plugins/builtin/short-interest/index.tsx +23 -1
- package/src/plugins/builtin/short-interest/model.ts +70 -19
- package/src/plugins/builtin/short-interest/pane.tsx +19 -2
- package/src/plugins/builtin/short-interest/publications-client.ts +82 -0
- package/src/plugins/builtin/short-interest/publications-headless.ts +29 -0
- package/src/plugins/builtin/short-interest/publications-model.ts +50 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +66 -0
- package/src/plugins/builtin/short-interest/schedule.ts +35 -2
- package/src/plugins/builtin/short-interest/surface.tsx +11 -4
- package/src/plugins/builtin/short-interest/types.ts +9 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +14 -8
- package/src/plugins/builtin/short-interest/watch-model.ts +12 -25
- package/src/plugins/builtin/short-interest/watch-pane.tsx +5 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +33 -9
- package/src/plugins/builtin/supply-chain/model.ts +51 -3
- package/src/plugins/builtin/supply-chain/pane.tsx +78 -39
- package/src/plugins/builtin/supply-chain/row-evidence.tsx +1 -1
- package/src/plugins/builtin/themes/headless.ts +11 -6
- package/src/plugins/builtin/themes/model.ts +15 -5
- package/src/plugins/builtin/themes/pane.tsx +27 -8
- package/src/plugins/builtin/thirteenf/headless.ts +5 -5
- package/src/plugins/builtin/thirteenf/signals.ts +3 -1
- package/src/plugins/builtin/ticker-detail/adr-pair/headless.ts +123 -0
- package/src/plugins/builtin/ticker-detail/adr-pair/model.ts +132 -0
- package/src/plugins/builtin/ticker-detail/adr-pair/tab.tsx +87 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +40 -26
- package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +8 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +67 -3
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +112 -5
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +17 -2
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +21 -7
- package/src/plugins/builtin/ticker-detail/financials/model.ts +36 -1
- package/src/plugins/builtin/ticker-detail/financials/offerings.ts +56 -0
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +62 -13
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +30 -4
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +14 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +148 -35
- package/src/plugins/builtin/ticker-detail/index.tsx +37 -5
- package/src/plugins/builtin/ticker-detail/overview/model.ts +65 -10
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +9 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +68 -11
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -0
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +14 -4
- package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +7 -4
- package/src/plugins/builtin/vol-surface/headless.ts +71 -13
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +26 -9
- package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/model.ts +5 -10
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +4 -3
- package/src/plugins/builtin/world-venue-map/basemap.ts +3 -1
- package/src/plugins/builtin/world-venue-map/chokepoint-chart.ts +40 -0
- package/src/plugins/builtin/world-venue-map/evidence.ts +1 -1
- package/src/plugins/builtin/world-venue-map/geo-series.ts +90 -11
- package/src/plugins/builtin/world-venue-map/headless.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +3 -26
- package/src/plugins/builtin/world-venue-map/layers.ts +1 -1
- package/src/plugins/builtin/world-venue-map/map.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/pane.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/venue-header.tsx +15 -4
- package/src/plugins/builtin/yield-curve/client.ts +25 -0
- package/src/plugins/builtin/yield-curve/curves.ts +7 -4
- package/src/plugins/builtin/yield-curve/headless.ts +38 -4
- package/src/plugins/builtin/yield-curve/history.ts +4 -3
- package/src/plugins/builtin/yield-curve/index.tsx +12 -6
- package/src/plugins/builtin/yield-curve/swaps-model.ts +93 -0
- package/src/plugins/builtin/yield-curve/swaps.tsx +99 -0
- package/src/plugins/bundle.ts +1 -1
- package/src/plugins/catalog-browser.ts +15 -9
- package/src/plugins/compat.ts +3 -3
- package/src/plugins/dependencies.ts +75 -14
- package/src/plugins/extracted-plugins.ts +132 -34
- package/src/plugins/host-link.ts +149 -34
- package/src/plugins/installer.ts +35 -10
- package/src/plugins/loader.ts +206 -38
- package/src/plugins/manager-node.ts +2 -2
- package/src/plugins/marketplace/deactivation.ts +22 -0
- package/src/plugins/marketplace/model.ts +2 -2
- package/src/plugins/marketplace/packs.ts +1 -1
- package/src/plugins/ownership.ts +1 -1
- package/src/plugins/peer-retry.ts +155 -0
- package/src/plugins/registry/contributions.ts +3 -1
- package/src/plugins/registry/index.ts +4 -3
- package/src/plugins/registry/plugin-state.ts +3 -0
- package/src/plugins/seed-notice.ts +20 -0
- package/src/plugins/shortcut-argument.ts +39 -0
- package/src/portfolio/allocation.ts +1 -1
- package/src/portfolio/collection-name.ts +31 -0
- package/src/portfolio/column-values.ts +15 -13
- package/src/portfolio/cost-fx.ts +296 -19
- package/src/portfolio/manual-lots.ts +75 -0
- package/src/portfolio/mutations.ts +60 -22
- package/src/portfolio/suffixed-listings.ts +209 -0
- package/src/portfolio/totals-currency.ts +24 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +15 -0
- package/src/public/time-series.ts +11 -8
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +5 -55
- package/src/renderers/browser/main.tsx +30 -60
- package/src/renderers/dom/boot-frame-page.ts +187 -0
- package/src/renderers/dom/boot-frame.ts +237 -0
- package/src/renderers/dom/build-assets.ts +46 -8
- package/src/renderers/dom/dom-ui-host.tsx +4 -29
- package/src/renderers/dom/host/ascii-text.ts +16 -2
- package/src/renderers/dom/host/box.tsx +43 -2
- package/src/renderers/dom/host/chart-surface.tsx +2 -0
- package/src/renderers/dom/host/hls.chunk.ts +3 -0
- package/src/renderers/dom/host/image-surface.tsx +49 -0
- package/src/renderers/dom/host/media-surface.tsx +24 -18
- package/src/renderers/dom/host/mouse.ts +30 -0
- package/src/renderers/dom/host/size-observer.ts +1 -1
- package/src/renderers/dom/host/style.ts +1 -1
- package/src/renderers/dom/styles.css +76 -0
- package/src/renderers/dom/test-utils.tsx +2 -1
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +56 -3
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +1 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -3
- package/src/renderers/electrobun/bun/desktop/initialization.ts +81 -18
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
- package/src/renderers/electrobun/bun/index.ts +36 -2
- package/src/renderers/electrobun/bun/window/boot-frame.ts +67 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -1
- package/src/renderers/electrobun/shared/app-name.ts +17 -0
- package/src/renderers/electrobun/shared/boot-frame.ts +7 -0
- package/src/renderers/electrobun/shared/protocol.ts +6 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/config-host.ts +3 -0
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/electrobun/view/fatal-screen.tsx +1 -1
- package/src/renderers/electrobun/view/main.tsx +30 -4
- package/src/renderers/electrobun/view/news-source.ts +5 -5
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/browser-open.ts +88 -0
- package/src/renderers/opentui/host.tsx +5 -14
- package/src/renderers/opentui/image/loader.ts +3 -2
- package/src/renderers/opentui/native-smoke.ts +10 -1
- package/src/renderers/opentui/pane-image/index.ts +2 -1
- package/src/renderers/opentui/render-lib.ts +253 -0
- package/src/renderers/opentui/render-timing-smoke.ts +1 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/renderers/share/layout-view.tsx +2 -2
- package/src/renderers/share/main.tsx +4 -4
- package/src/renderers/share/view.tsx +1 -1
- package/src/series/resolve.ts +22 -6
- package/src/series/standalone.ts +30 -2
- package/src/series/types.ts +8 -1
- package/src/shares/api.ts +4 -1
- package/src/shares/pane.ts +1 -1
- package/src/sources/gloomberb-cloud/fred-series.ts +46 -1
- package/src/sources/gloomberb-cloud/index.ts +32 -16
- package/src/sources/gloomberb-cloud/news.ts +39 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +16 -1
- package/src/sources/gloomberb-cloud/perpetuals.ts +49 -1
- package/src/sources/listing-symbols.ts +37 -0
- package/src/sources/provider-errors.ts +16 -2
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +21 -3
- package/src/sources/provider-router/financials.ts +15 -2
- package/src/sources/provider-router/history.ts +58 -15
- package/src/sources/provider-router/index.ts +146 -10
- package/src/sources/provider-router/primary.ts +1 -1
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/bootstrap.ts +23 -0
- package/src/state/app/context/index.tsx +3 -0
- package/src/state/hooks/inline-tickers.ts +30 -5
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +67 -17
- package/src/sync/core-contributors.ts +46 -14
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/team/collections-sync.ts +2 -1
- package/src/team/collections.ts +178 -17
- package/src/telemetry/crash-reports.ts +17 -4
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/test-support/builtin-panes.ts +1 -1
- package/src/theme/colors.ts +11 -30
- package/src/theme/ui-tokens.ts +37 -1
- package/src/tickers/company-name-listing.ts +210 -33
- package/src/tickers/company-name.ts +59 -0
- package/src/tickers/index-roots.ts +47 -0
- package/src/tickers/instrument-label.ts +5 -1
- package/src/tickers/listing-venues.ts +23 -0
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/perpetual-listing.ts +5 -2
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/home-listing.ts +75 -0
- package/src/tickers/search/index.ts +111 -33
- package/src/tickers/search/ranking.ts +64 -5
- package/src/tickers/search/types.ts +5 -0
- package/src/tickers/search/upsert.ts +29 -0
- package/src/tickers/terminal-codes.ts +416 -40
- package/src/time-series/bar-aggregation.ts +243 -0
- package/src/time-series/base-series.ts +315 -0
- package/src/time-series/chart-bounds.ts +349 -0
- package/src/time-series/chart-seed.ts +142 -0
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/economic.ts +5 -0
- package/src/time-series/field-catalog.ts +1 -3
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +173 -10
- package/src/time-series/history-acquisition.ts +499 -0
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/leverage.ts +100 -0
- package/src/time-series/market-resolution.ts +4 -2
- package/src/time-series/market.ts +2 -1
- package/src/time-series/price-comparison.ts +175 -30
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/range.ts +9 -1
- package/src/time-series/resolution.ts +174 -16
- package/src/time-series/resolve-context.ts +161 -0
- package/src/time-series/resolve.ts +185 -1587
- package/src/time-series/series-name.ts +38 -0
- package/src/time-series/series-presentation.ts +242 -0
- package/src/time-series/spec.ts +13 -7
- package/src/time-series/studies.ts +377 -82
- package/src/time-series/tail-reconcile.ts +121 -0
- package/src/time-series/transforms.ts +1 -0
- package/src/time-series/types.ts +34 -2
- package/src/time-series/use-chart-resolution.ts +4 -5
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/broker.ts +10 -0
- package/src/types/config.ts +14 -0
- package/src/types/data-provider.ts +23 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/exchange-rate.ts +6 -0
- package/src/types/financials.ts +145 -8
- package/src/types/headless.ts +117 -9
- package/src/types/instrument.ts +6 -0
- package/src/types/plugin.ts +66 -9
- package/src/types/price-history.ts +2 -0
- package/src/types/ticker.ts +13 -0
- package/src/types/trading.ts +59 -1
- package/src/ui/ascii-font.ts +13 -1
- package/src/ui/host.tsx +6 -1
- package/src/updater/command-names.ts +180 -0
- package/src/updater/index.ts +10 -5
- package/src/utils/calendar-date.ts +32 -15
- package/src/utils/cash-runway.ts +241 -0
- package/src/utils/central-banks.ts +42 -0
- package/src/utils/cli-output.ts +259 -80
- package/src/utils/contract-month.ts +16 -0
- package/src/utils/crypto-pair.ts +61 -3
- package/src/utils/currency-units.ts +29 -0
- package/src/utils/datetime-format.ts +10 -0
- package/src/utils/debug-log.ts +1 -1
- package/src/utils/decimal-comma.ts +39 -6
- package/src/utils/exchange-announcements.ts +71 -0
- package/src/utils/exchange-rate-snapshot.ts +10 -0
- package/src/utils/financial-statements.ts +89 -5
- package/src/utils/format.ts +34 -16
- package/src/utils/fundamentals.ts +150 -34
- package/src/utils/fx-market-hours.ts +10 -1
- package/src/utils/latest-financial-period.ts +8 -0
- package/src/utils/linux-libc.ts +54 -0
- package/src/utils/market-capitalization.ts +111 -15
- package/src/utils/max-drawdown.ts +64 -0
- package/src/utils/network-failure.ts +86 -0
- package/src/utils/option-expiry.ts +186 -7
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-earnings.ts +11 -5
- package/src/utils/price-history.ts +23 -8
- package/src/utils/price-window.ts +131 -0
- package/src/utils/release-date.ts +47 -0
- package/src/utils/sec.ts +3 -0
- package/src/utils/throttled-fetch.ts +11 -4
- package/src/utils/trailing-eps.ts +67 -0
- package/src/utils/typed-text.ts +21 -0
- package/src/utils/utc-time.ts +24 -1
- package/src/version.ts +6 -0
- package/src/cli/pane-functions/screenshot.ts +0 -2122
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "../../types/financials";
|
|
15
15
|
import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, resolveAssetDisplayKind, withCurrencyMinorDigits } from "../../market-data/market/format";
|
|
16
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";
|
|
17
|
+
import { changeBasisJson, changeBasisLabel, changeBasisNote, quoteChangeBasis, quotePreviousCloseDate, quoteSettleText } from "../../market-data/market/change-basis";
|
|
18
18
|
import {
|
|
19
19
|
EXTENDED_SESSION_LABELS,
|
|
20
20
|
getExtendedSessionDisplay,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
quoteSessionStateLabel,
|
|
26
26
|
type ExtendedSession,
|
|
27
27
|
} from "../../market-data/market/status";
|
|
28
|
-
import { formatCompact, formatCurrency, formatDistributionAmount, formatNumber, formatPercent
|
|
28
|
+
import { formatCompact, formatCurrency, formatDistributionAmount, formatNumber, formatPercent } from "../../utils/format";
|
|
29
29
|
import { withCliServices, withMarketData } from "../context";
|
|
30
30
|
import { isoDate, parsePositiveInt, requireOneArg, takeFlag, takeOption } from "./command-utils";
|
|
31
31
|
import type { BuiltinCliCommandDef } from "../command-options";
|
|
@@ -38,8 +38,8 @@ import {
|
|
|
38
38
|
formatPriceRange,
|
|
39
39
|
quoteChangeText,
|
|
40
40
|
} from "../helpers";
|
|
41
|
-
import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
|
|
42
|
-
import { statementHistoryNotes } from "../../utils/financial-statements";
|
|
41
|
+
import { cliArrow, cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
|
|
42
|
+
import { filledPeriodsNote, stalePeriodNote, statementHistoryNotes } from "../../utils/financial-statements";
|
|
43
43
|
import { formatPerShareNumber } from "../../utils/reported-money";
|
|
44
44
|
import {
|
|
45
45
|
analystTargetCurrency,
|
|
@@ -47,29 +47,42 @@ import {
|
|
|
47
47
|
formatRatingLabel,
|
|
48
48
|
formatRecommendationMix,
|
|
49
49
|
recommendationTotal,
|
|
50
|
-
|
|
50
|
+
analystReferenceMetadata,
|
|
51
|
+
analystUpside,
|
|
52
|
+
formatAnalystReference,
|
|
51
53
|
} from "../../plugins/builtin/research/analyst-model";
|
|
52
54
|
import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
|
|
53
|
-
import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
54
|
-
import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
|
|
55
|
+
import { isQuoteStaleForCurrentSession, noPrintsTodayText } from "../../market-data/quotes/freshness";
|
|
55
56
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
coinOptionsAlternativesText, isCoinWithoutOptions, isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle,
|
|
58
|
+
} from "../../market-data/options-alternatives";
|
|
59
|
+
import {
|
|
60
|
+
atReportTtmRate, cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions,
|
|
61
|
+
loadReportBondFacts, renderFundamentalsReport, writesCurrencyCodes,
|
|
58
62
|
} from "./ticker";
|
|
63
|
+
import { createBaseConverter } from "../base-converter";
|
|
59
64
|
import { loadStatementFilings } from "../statement-filings";
|
|
60
65
|
import { loadBankFigures } from "../bank-section";
|
|
61
66
|
import { latestFinancialPeriod } from "../../utils/latest-financial-period";
|
|
62
67
|
import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
|
|
63
68
|
import { currencyCodeLabel } from "../../utils/currency-units";
|
|
64
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
futuresContractLines, futuresPricePer, futuresRollLines, isFuturesInstrument, loadUsdPerMmbtu, usdPerMmbtuText, type UsdPerMmbtu,
|
|
71
|
+
} from "../../market-data/futures-contract";
|
|
65
72
|
import {
|
|
66
73
|
chainHasExpiry,
|
|
67
74
|
formatExpiryList,
|
|
68
75
|
loadOptionModelInputs,
|
|
69
76
|
loadOptionsChainOpenInterest,
|
|
77
|
+
chainCoinCurrency,
|
|
78
|
+
formatCoinPremiumCell,
|
|
70
79
|
formatOptionDeltaCell,
|
|
80
|
+
formatOptionDollarCell,
|
|
81
|
+
formatOptionGammaCell,
|
|
71
82
|
formatOptionIvCell,
|
|
72
83
|
formatOptionQuoteCell,
|
|
84
|
+
isMarkedChain,
|
|
85
|
+
MARKED_OPTION_FIELDS,
|
|
73
86
|
OPTION_MODEL_RATE,
|
|
74
87
|
OPTION_VOLATILITY_FIELDS,
|
|
75
88
|
OPTIONS_USAGE,
|
|
@@ -80,9 +93,14 @@ import {
|
|
|
80
93
|
valueChainAtModelSpot,
|
|
81
94
|
type OptionsChainBasis,
|
|
82
95
|
} from "./options-chain";
|
|
83
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
expirationHint, expiredExpiryText, expiryIsoDate, isLiveOptionExpiry, liveDefaultExpiry, liveOptionExpiries, missingExpiryMessage,
|
|
98
|
+
OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, optionExpiryVenue, withLiveExpiryList,
|
|
99
|
+
} from "../../utils/option-expiry";
|
|
84
100
|
import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
|
|
85
101
|
import { apiClient } from "../../api-client";
|
|
102
|
+
import type { ExchangeAnnouncementsPayload } from "../../api-client/exchange-announcements";
|
|
103
|
+
import { exchangeAnnouncementsPage, listsExchangeAnnouncements, type ExchangeAnnouncementsPage } from "../../utils/exchange-announcements";
|
|
86
104
|
import { dailyFxLeg, fxCloseOnDate, type FxCloseReading } from "../../market-data/fx-closes";
|
|
87
105
|
import { declaredDividends } from "../../utils/declared-dividends";
|
|
88
106
|
import {
|
|
@@ -91,15 +109,21 @@ import {
|
|
|
91
109
|
import type { CliResultColumn } from "../result";
|
|
92
110
|
import type { PriceHistoryResult } from "../../types/price-history";
|
|
93
111
|
import {
|
|
112
|
+
exportNumber,
|
|
94
113
|
exportRowsTable,
|
|
95
114
|
reportFooterLines,
|
|
96
115
|
selectReportTables,
|
|
97
116
|
type CliReportTables,
|
|
98
117
|
} from "../report-tables";
|
|
118
|
+
import { historyCoverageNotice } from "../../sources/history-coverage";
|
|
119
|
+
import { maxDrawdown, maxDrawdownText } from "../../utils/max-drawdown";
|
|
120
|
+
import { isPastPriceWindow, loadPriceWindowHistory, PRICE_WINDOW_FROM_HELP, PRICE_WINDOW_TO_HELP, priceWindowText, readPriceWindow, type PriceWindow } from "../../utils/price-window";
|
|
99
121
|
import {
|
|
122
|
+
BAR_INTERVAL_LABELS,
|
|
100
123
|
closedPeriodBarDay,
|
|
101
124
|
formingPeriodBar,
|
|
102
125
|
historyBarStatusNote,
|
|
126
|
+
venueTradingDay,
|
|
103
127
|
historyCoverageWarning,
|
|
104
128
|
historyFacts,
|
|
105
129
|
historyFlagNote,
|
|
@@ -109,6 +133,7 @@ import {
|
|
|
109
133
|
historyAllRangeYears,
|
|
110
134
|
historyIntervalsByRange,
|
|
111
135
|
historyMissingVolumeNote,
|
|
136
|
+
historyVenueVolumeNote,
|
|
112
137
|
historyNotes,
|
|
113
138
|
historyReportsVolume,
|
|
114
139
|
historyPriceDecimals,
|
|
@@ -116,20 +141,54 @@ import {
|
|
|
116
141
|
historyUnit,
|
|
117
142
|
historyVolume,
|
|
118
143
|
historyVolumeText,
|
|
144
|
+
type HistoryRow,
|
|
145
|
+
type HistoryUnit,
|
|
146
|
+
type HistoryVolume,
|
|
119
147
|
} from "../history-rows";
|
|
148
|
+
import {
|
|
149
|
+
HISTORY_EXPORT_COLUMNS,
|
|
150
|
+
historyExportStatus,
|
|
151
|
+
historyFailureRow,
|
|
152
|
+
oneLineFailure,
|
|
153
|
+
symbolListNotes,
|
|
154
|
+
type HistoryListRow,
|
|
155
|
+
} from "../history-export";
|
|
156
|
+
import { isCliFailure } from "../errors";
|
|
157
|
+
import { mapWithConcurrency } from "../../utils/concurrency";
|
|
120
158
|
import { MISSING_VOLUME_TEXT } from "../../utils/price-history-integrity";
|
|
121
159
|
import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
|
|
122
160
|
import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures-board";
|
|
123
161
|
import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
|
|
124
162
|
import { newsEmptyMessage } from "./news-hints";
|
|
125
|
-
import {
|
|
163
|
+
import {
|
|
164
|
+
commodityNewsRoute,
|
|
165
|
+
commonWordCandidate,
|
|
166
|
+
METALS_NEWS_TOPIC,
|
|
167
|
+
metalCodeNewsRoute,
|
|
168
|
+
type MetalNewsRoute,
|
|
169
|
+
metalNewsHeading,
|
|
170
|
+
metalNewsRoute,
|
|
171
|
+
newsFallbackText,
|
|
172
|
+
newsSearchHeading,
|
|
173
|
+
newsSearchText,
|
|
174
|
+
newsSearchTextProblem,
|
|
175
|
+
noMetalStoriesMessage,
|
|
176
|
+
noStoriesMessage,
|
|
177
|
+
quotedSearchCommand,
|
|
178
|
+
searchNewsStories,
|
|
179
|
+
wordMeansMore,
|
|
180
|
+
} from "./news-search";
|
|
126
181
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
127
182
|
import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
|
|
128
183
|
import { isFiniteNumber } from "../../utils/guards";
|
|
129
184
|
import { exportedFundamentals } from "../../utils/price-earnings";
|
|
130
185
|
import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
|
|
131
|
-
import {
|
|
132
|
-
import {
|
|
186
|
+
import { fxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
|
|
187
|
+
import { quoteBarNotes } from "../../market-data/quotes/intraday-bar";
|
|
188
|
+
import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
|
|
189
|
+
import { isPesoDollarPair, loadPesoDollarRates, pesoDollarRateLines, pesoDollarRatesJson } from "../peso-dollar-rates";
|
|
190
|
+
import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
|
|
191
|
+
import { viewMarketCapitalization } from "../../utils/market-capitalization";
|
|
133
192
|
import {
|
|
134
193
|
barHistoryFreshness,
|
|
135
194
|
barResolutionFromDates,
|
|
@@ -147,6 +206,14 @@ import {
|
|
|
147
206
|
quarterLabel,
|
|
148
207
|
showsEpsBasis,
|
|
149
208
|
} from "../../plugins/builtin/shared/earnings-report";
|
|
209
|
+
import {
|
|
210
|
+
calendarEventOf,
|
|
211
|
+
calendarStatus,
|
|
212
|
+
newYorkDay,
|
|
213
|
+
resolveListingEvents,
|
|
214
|
+
withResolvedEarnings,
|
|
215
|
+
type ResolvedEarningsEvent,
|
|
216
|
+
} from "../../plugins/builtin/shared/earnings-calendar";
|
|
150
217
|
import {
|
|
151
218
|
EXCHANGE_OPTION,
|
|
152
219
|
listingHeading,
|
|
@@ -155,12 +222,17 @@ import {
|
|
|
155
222
|
listingVenues,
|
|
156
223
|
listingChoiceNotes,
|
|
157
224
|
listingReadNotes,
|
|
225
|
+
failUnchosenListing,
|
|
158
226
|
loadBareVenues,
|
|
159
227
|
loadForListing,
|
|
160
228
|
loadListingQuote,
|
|
229
|
+
loadListingQuoteAnswer,
|
|
161
230
|
listingFateLoader,
|
|
231
|
+
meantQuestion,
|
|
162
232
|
movedSymbolLine,
|
|
233
|
+
ListingArgError,
|
|
163
234
|
requireCliListing,
|
|
235
|
+
resolveCliListing,
|
|
164
236
|
type CliListing,
|
|
165
237
|
type ListingIdentity,
|
|
166
238
|
} from "../listing-arg";
|
|
@@ -173,6 +245,8 @@ import { CBOE_YIELD_INDEX_CLOSE } from "../../plugins/builtin/shared/treasury-cu
|
|
|
173
245
|
import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
|
|
174
246
|
import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
|
|
175
247
|
import { providerMissReason } from "../../sources/provider-errors";
|
|
248
|
+
import { isNetworkFailure } from "../../utils/network-failure";
|
|
249
|
+
import { withSavedCopy, type ReportFreshness } from "../pane-functions/freshness";
|
|
176
250
|
import { isNotATickerMessage } from "../not-a-ticker";
|
|
177
251
|
import { primeBoardCoins } from "../board-coins";
|
|
178
252
|
import { suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
|
|
@@ -180,11 +254,12 @@ import { quotedPhrase, splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-
|
|
|
180
254
|
import { isCompanyNameInput } from "../../tickers/company-name-listing";
|
|
181
255
|
import { readNameEntries, readPhraseArgument } from "../name-entries";
|
|
182
256
|
import { readSymbolList, SymbolListError } from "../symbol-list";
|
|
257
|
+
import { notASymbolError } from "../symbol-cells";
|
|
183
258
|
import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
|
|
184
259
|
import { knownGapMessage, subMinuteDataMessage } from "../known-gaps";
|
|
185
260
|
import { phraseSearchReport } from "./search";
|
|
186
261
|
import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
|
|
187
|
-
import { isCryptoPairSymbol } from "../../utils/crypto-pair";
|
|
262
|
+
import { isCryptoPairSymbol, isRoundTheClockCoin } from "../../utils/crypto-pair";
|
|
188
263
|
import { isUsListingExchange } from "../../utils/exchanges";
|
|
189
264
|
import {
|
|
190
265
|
describeFormerIssuer, describeTickerChange, movedAwayChange, startTickerChangeLookup, tickerChangeLine, type FormerIssuer, type TickerChange,
|
|
@@ -193,7 +268,21 @@ import { noSecReportsNotice, nonUsSecListingVenue, secFilingItemCodes } from "..
|
|
|
193
268
|
import type { MarketContext } from "../types";
|
|
194
269
|
import { windowRows } from "../row-window";
|
|
195
270
|
import { answerQuote, errorMessage, notUsTickerSentence } from "../quote-answer";
|
|
196
|
-
import { crossRate, describeFxConversion, describeFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
|
|
271
|
+
import { crossRate, describeFxConversion, describeFxRate, formatFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
|
|
272
|
+
import {
|
|
273
|
+
describeShortRate,
|
|
274
|
+
formatForwardPoints,
|
|
275
|
+
formatRatePercent,
|
|
276
|
+
formatSignedPercent,
|
|
277
|
+
IMPLIED_FORWARD_LABEL,
|
|
278
|
+
impliedForward,
|
|
279
|
+
parseForwardTenor,
|
|
280
|
+
shortRateFor,
|
|
281
|
+
} from "../fx-forward";
|
|
282
|
+
import { unavailableOnServer } from "../../plugins/builtin/shared/cloud-resource";
|
|
283
|
+
|
|
284
|
+
/** `fx`'s forward form, for its usage line. */
|
|
285
|
+
const FX_FORWARD_USAGE = "fx <base>/<quote> --forward <tenor>";
|
|
197
286
|
import {
|
|
198
287
|
holderListFacts,
|
|
199
288
|
holderListPeriod,
|
|
@@ -215,7 +304,15 @@ import {
|
|
|
215
304
|
} from "../../plugins/builtin/holders/report-rows";
|
|
216
305
|
import { fetchBeneficialOwners } from "../../plugins/builtin/holders/beneficial-client";
|
|
217
306
|
import { centralBankEventsMessage } from "../central-bank-events";
|
|
218
|
-
import { classSharesToday } from "../../plugins/builtin/holders/client";
|
|
307
|
+
import { classSharesAsOf, classSharesToday } from "../../plugins/builtin/holders/client";
|
|
308
|
+
import {
|
|
309
|
+
HOLDER_ROWS_MAX,
|
|
310
|
+
indexedHoldersPartialNote,
|
|
311
|
+
indexedHolderUnits,
|
|
312
|
+
indexHolderTicker,
|
|
313
|
+
loadIndexedHolders,
|
|
314
|
+
withIndexedHolders,
|
|
315
|
+
} from "../../plugins/builtin/holders/index-holders";
|
|
219
316
|
import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
|
|
220
317
|
import { currentReportItemsLabel } from "../../utils/sec-filings";
|
|
221
318
|
import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
@@ -249,6 +346,11 @@ type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
|
|
|
249
346
|
futuresContract?: FuturesContractTermsPayload;
|
|
250
347
|
/** The quote of the root's front contract, for a futures row that prices another one (NGF27, NG2). */
|
|
251
348
|
futuresFront?: Quote;
|
|
349
|
+
/**
|
|
350
|
+
* A per-MWh gas price (Dutch TTF) in dollars per MMBtu, the unit Henry Hub
|
|
351
|
+
* trades in, converted at the day's rate: one unit of the quote's currency in dollars.
|
|
352
|
+
*/
|
|
353
|
+
usdPerMmbtu?: UsdPerMmbtu;
|
|
252
354
|
/** A perpetual row's market, funding per eight hours and open interest. */
|
|
253
355
|
perpetualContract?: PerpetualContractPayload;
|
|
254
356
|
/** The symbol changed: its issuer moved to another symbol, or its holder had another before. */
|
|
@@ -299,10 +401,11 @@ async function readSymbols(symbols: readonly string[], dataProvider: Parameters<
|
|
|
299
401
|
}
|
|
300
402
|
|
|
301
403
|
/**
|
|
302
|
-
* The symbols of `--file <path>`
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* pipes in nothing never
|
|
404
|
+
* The symbols typed, of `--file <path>` and of standard input (`-`), read by
|
|
405
|
+
* the reader `watchlist add` uses, and its notes on what it left out (a
|
|
406
|
+
* stray separator, the prices beside the symbols, in one line). Standard
|
|
407
|
+
* input is read only when asked for, so a script that pipes in nothing never
|
|
408
|
+
* waits on it.
|
|
306
409
|
*/
|
|
307
410
|
async function readListedSymbols(
|
|
308
411
|
args: readonly string[],
|
|
@@ -315,7 +418,11 @@ async function readListedSymbols(
|
|
|
315
418
|
const list = await readSymbolList({
|
|
316
419
|
args, file, stdinPiped: false, readEntries: (entries) => readNameEntries(entries, market.dataProvider),
|
|
317
420
|
});
|
|
318
|
-
|
|
421
|
+
// Only numbers, or nothing: the message says what was left out (`Ignored 1.899, a number, not a symbol.`).
|
|
422
|
+
if (list.symbols.length === 0) {
|
|
423
|
+
const where = file && file !== "-" ? file : args.includes("-") ? "standard input" : "what was given";
|
|
424
|
+
ctx.fail([`No symbols in ${where}.`, ...list.notes].join(" "), usage);
|
|
425
|
+
}
|
|
319
426
|
return { symbols: uniqueEntries(list.symbols), notes: list.notes };
|
|
320
427
|
} catch (error) {
|
|
321
428
|
if (error instanceof SymbolListError) ctx.fail(error.message, usage);
|
|
@@ -323,6 +430,15 @@ async function readListedSymbols(
|
|
|
323
430
|
}
|
|
324
431
|
}
|
|
325
432
|
|
|
433
|
+
/** What `quote --csv` and `compare --csv` write, for `help quote` and `help compare`. */
|
|
434
|
+
const QUOTE_CSV_SECTION = {
|
|
435
|
+
title: "CSV",
|
|
436
|
+
lines: [
|
|
437
|
+
"Last, Chg%, Prev Close and each extended print are plain numbers, the currency in Cur and currency_or_unit. A pre-market, after-hours or overnight print's move is in Pre-Market Chg%, After Hours Chg% or Overnight Chg%, and compare's day range in Day Low and Day High, after the columns the export already had.",
|
|
438
|
+
"A row with no figure says why in its status column, never NaN; a symbol with no quote is also named on standard error.",
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
|
|
326
442
|
/** `--file` of quote and compare, as `watchlist add` takes it. */
|
|
327
443
|
const LIST_FILE_OPTION = {
|
|
328
444
|
flags: "--file <path>",
|
|
@@ -334,8 +450,8 @@ function listingMetadata(identity: ListingIdentity) {
|
|
|
334
450
|
return { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name };
|
|
335
451
|
}
|
|
336
452
|
|
|
337
|
-
// Fitted to a narrow width,
|
|
338
|
-
// What a user types or reads the price by stays whole.
|
|
453
|
+
// Fitted to a narrow width, quote's Updated, Feed, Cur, Chg Basis and session prints move to labelled lines under
|
|
454
|
+
// their row, in that order, before Name wraps: quote never drops a column. What a user types or reads the price by stays whole.
|
|
339
455
|
const QUOTE_LEAD_COLUMNS = [
|
|
340
456
|
{ key: "symbol", header: "Symbol", shrink: false },
|
|
341
457
|
{ key: "name", header: "Name" },
|
|
@@ -343,18 +459,24 @@ const QUOTE_LEAD_COLUMNS = [
|
|
|
343
459
|
key: "price",
|
|
344
460
|
header: "Last",
|
|
345
461
|
align: "right" as const,
|
|
346
|
-
|
|
347
|
-
|
|
462
|
+
// An export writes the number; the row's Cur and currency_or_unit name what it is in.
|
|
463
|
+
value: (row: QuoteRow) => exportFigure(row.rawPrice),
|
|
464
|
+
format: (_value: unknown, row: QuoteRow) => (
|
|
465
|
+
row.error && !row.price
|
|
348
466
|
// A typo is not an outage: say what is wrong with the symbol itself.
|
|
349
467
|
? isNotATickerMessage(row.error) ? cliStyles.warning("not a ticker") : cliStyles.danger("unavailable")
|
|
350
|
-
: yieldLevelCell(
|
|
468
|
+
: yieldLevelCell(row.price, row)
|
|
351
469
|
),
|
|
352
470
|
},
|
|
353
471
|
{
|
|
354
472
|
key: "changePercent",
|
|
355
473
|
header: "Chg%",
|
|
356
474
|
align: "right" as const,
|
|
357
|
-
format: (value: unknown, row: QuoteRow) =>
|
|
475
|
+
format: (value: unknown, row: QuoteRow) => {
|
|
476
|
+
const move = row.yieldIndex ? yieldChangeCell(row) : formatChangePercentCell(value);
|
|
477
|
+
// Keep a rolling move's short label with the value when narrow tables drop Chg Basis.
|
|
478
|
+
return move && row.changeBasis === "24h" ? `${move} ${cliStyles.muted("24h")}` : move;
|
|
479
|
+
},
|
|
358
480
|
},
|
|
359
481
|
// Text drops a column no row fills, so a table without an extended print stays as narrow as before.
|
|
360
482
|
extendedColumn("PRE"),
|
|
@@ -373,16 +495,21 @@ type QuoteRow = ReturnType<typeof quoteRows>[number];
|
|
|
373
495
|
* its existing columns, so a column read by position stays where it was.
|
|
374
496
|
*/
|
|
375
497
|
const CHANGE_BASIS_COLUMN = {
|
|
376
|
-
key: "changeBasis", header: "Chg Basis", shrink: false,
|
|
498
|
+
key: "changeBasis", header: "Chg Basis", shrink: false, stackPriority: 2,
|
|
499
|
+
format: (value: unknown) => value === "24h" ? "" : cliStyles.muted(String(value ?? "")),
|
|
377
500
|
};
|
|
378
501
|
const CHANGE_BASIS_EXPORT_COLUMNS = [
|
|
379
502
|
{ key: "changeBasis", header: "Chg Basis" },
|
|
380
503
|
{ key: "changeBasisNote", header: "Chg Basis Note" },
|
|
381
504
|
];
|
|
382
505
|
|
|
383
|
-
/**
|
|
506
|
+
/**
|
|
507
|
+
* The table's columns: Chg Basis follows Chg%, and the note is a line under
|
|
508
|
+
* the table. The figures an export adds (an extended print's move, the day's
|
|
509
|
+
* low and high) are already in the table's own cells.
|
|
510
|
+
*/
|
|
384
511
|
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));
|
|
512
|
+
const exported = new Set([...CHANGE_BASIS_EXPORT_COLUMNS, ...EXTENDED_CHANGE_EXPORT_COLUMNS, ...DAY_RANGE_EXPORT_COLUMNS].map((column) => column.key));
|
|
386
513
|
const kept = columns.filter((column) => !exported.has(column.key));
|
|
387
514
|
const at = kept.findIndex((column) => column.key === "changePercent") + 1;
|
|
388
515
|
return [...kept.slice(0, at), CHANGE_BASIS_COLUMN, ...kept.slice(at)];
|
|
@@ -399,28 +526,70 @@ function yieldChangeCell(row: QuoteRow): string {
|
|
|
399
526
|
return row.changeBasisPoints == null ? "" : colorBySign(formatBasisPoints(row.changeBasisPoints / 100, 1), row.changeBasisPoints);
|
|
400
527
|
}
|
|
401
528
|
|
|
529
|
+
/** A figure as an export writes it: the number without float noise, nothing for none. */
|
|
530
|
+
function exportFigure(value: number | null | undefined): number | null {
|
|
531
|
+
return value == null ? null : Number.isFinite(value) ? exportNumber(value) : value;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const EXTENDED_SESSIONS = ["PRE", "POST", "OVERNIGHT"] as const satisfies readonly ExtendedSession[];
|
|
535
|
+
|
|
536
|
+
function extendedKey(session: ExtendedSession): string {
|
|
537
|
+
return session === "PRE" ? "preMarket" : session === "POST" ? "afterHours" : "overnight";
|
|
538
|
+
}
|
|
539
|
+
|
|
402
540
|
/**
|
|
403
541
|
* The pre-market, after-hours or overnight print and its move from the
|
|
404
542
|
* regular close, in its own column; "mid" when it is a bid and ask midpoint.
|
|
543
|
+
* An export writes the price alone, its move going to `<session> Chg%`
|
|
544
|
+
* (EXTENDED_CHANGE_EXPORT_COLUMNS) and a midpoint to a closing note.
|
|
405
545
|
*/
|
|
406
546
|
function extendedColumn(session: ExtendedSession) {
|
|
407
547
|
const price = (row: QuoteRow) => row.extendedMark ? `${row.extendedPrice} mid` : row.extendedPrice;
|
|
408
548
|
return {
|
|
409
|
-
key: session
|
|
549
|
+
key: extendedKey(session),
|
|
410
550
|
header: EXTENDED_SESSION_LABELS[session],
|
|
411
551
|
align: "right" as const,
|
|
412
|
-
value: (row: QuoteRow) => row.extendedSession === session
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
552
|
+
value: (row: QuoteRow) => row.extendedSession === session ? exportFigure(row.rawExtendedPrice) : null,
|
|
553
|
+
format: (_value: unknown, row: QuoteRow) => (
|
|
554
|
+
row.extendedSession === session && row.extendedPrice
|
|
555
|
+
? [price(row), formatChangePercentCell(row.extendedChangePercent)].join(" ").trim()
|
|
556
|
+
: ""
|
|
417
557
|
),
|
|
418
558
|
};
|
|
419
559
|
}
|
|
420
560
|
|
|
421
|
-
/**
|
|
561
|
+
/**
|
|
562
|
+
* Each extended print's move from the regular close, as a number: the
|
|
563
|
+
* columns an export adds after those it had, so a column read by position
|
|
564
|
+
* stays where it was. The table keeps the move beside the price.
|
|
565
|
+
*/
|
|
566
|
+
const EXTENDED_CHANGE_EXPORT_COLUMNS = EXTENDED_SESSIONS.map((session) => ({
|
|
567
|
+
key: `${extendedKey(session)}ChangePercent`,
|
|
568
|
+
header: `${EXTENDED_SESSION_LABELS[session]} Chg%`,
|
|
569
|
+
value: (row: QuoteRow) => row.extendedSession === session ? row.extendedChangePercent : null,
|
|
570
|
+
}));
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Why an exported quote row's figures are empty, for its status column: the
|
|
574
|
+
* symbol's failure as the warning says it, or the figure it has no value for.
|
|
575
|
+
*/
|
|
576
|
+
function quoteExportStatus(row: QuoteRow): string {
|
|
577
|
+
if (row.rawPrice == null || !Number.isFinite(row.rawPrice)) return row.error || "Last: no price";
|
|
578
|
+
if (row.changePercent == null || !Number.isFinite(row.changePercent)) return "Chg%: no previous close";
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** The closing note of an export with an extended price that is a bid and ask midpoint: the cell holds the bare number. */
|
|
583
|
+
function extendedMidNotes(data: readonly QuoteCliRecord[]): string[] {
|
|
584
|
+
return data.flatMap((row) => {
|
|
585
|
+
const extended = getExtendedSessionDisplay(row.quote);
|
|
586
|
+
return extended?.mark ? [`${row.target.symbol} ${EXTENDED_SESSION_LABELS[extended.session]} is the bid and ask midpoint; nothing has traded in that session`] : [];
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** A percent to two decimals, as the table prints it; none for a move that is no number (a zero close). */
|
|
422
591
|
function roundedPercent(value: number | undefined): number | null {
|
|
423
|
-
return value == null ? null : Number(value.toFixed(2));
|
|
592
|
+
return value == null || !Number.isFinite(value) ? null : Number(value.toFixed(2));
|
|
424
593
|
}
|
|
425
594
|
|
|
426
595
|
/** The quote's currency code, US cents as US¢; an export keeps the code (USX). */
|
|
@@ -428,35 +597,54 @@ function currencyCell(value: unknown): string {
|
|
|
428
597
|
return currencyCodeLabel(typeof value === "string" ? value : "");
|
|
429
598
|
}
|
|
430
599
|
|
|
600
|
+
/** The session prints of a narrow quote table move under their row after its other columns, before the name. */
|
|
601
|
+
const EXTENDED_KEYS = new Set(["preMarket", "afterHours", "overnight"]);
|
|
602
|
+
|
|
431
603
|
function quoteColumns() {
|
|
432
604
|
return [
|
|
433
|
-
...QUOTE_LEAD_COLUMNS,
|
|
434
|
-
{ key: "currency", header: "Cur", shrink: false,
|
|
605
|
+
...QUOTE_LEAD_COLUMNS.map((column) => EXTENDED_KEYS.has(column.key) ? { ...column, stackPriority: 1 } : column),
|
|
606
|
+
{ key: "currency", header: "Cur", shrink: false, stackPriority: 3, format: currencyCell },
|
|
435
607
|
// Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
|
|
436
608
|
// row says so here, so the closing line's "N of M stale" always points at rows the table marks.
|
|
437
609
|
{
|
|
438
610
|
key: "feed",
|
|
439
611
|
header: "Feed",
|
|
440
612
|
shrink: false,
|
|
441
|
-
|
|
442
|
-
dropPriority: 2,
|
|
613
|
+
stackPriority: 4,
|
|
443
614
|
format: (value: unknown) => value === "stale" ? cliStyles.warning(value) : String(value ?? ""),
|
|
444
615
|
},
|
|
445
|
-
|
|
446
|
-
{ key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
|
|
616
|
+
{ key: "updatedAt", header: "Updated", shrink: false, stackPriority: 5 },
|
|
447
617
|
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
618
|
+
...EXTENDED_CHANGE_EXPORT_COLUMNS,
|
|
448
619
|
];
|
|
449
620
|
}
|
|
450
621
|
|
|
622
|
+
/** The day's low and high as numbers, after the columns an export already had. */
|
|
623
|
+
const DAY_RANGE_EXPORT_COLUMNS = [
|
|
624
|
+
{ key: "dayLow", header: "Day Low", value: (row: QuoteRow) => exportFigure(row.rawDayLow) },
|
|
625
|
+
{ key: "dayHigh", header: "Day High", value: (row: QuoteRow) => exportFigure(row.rawDayHigh) },
|
|
626
|
+
];
|
|
627
|
+
|
|
451
628
|
function compareColumns() {
|
|
452
629
|
return [
|
|
453
630
|
...QUOTE_LEAD_COLUMNS,
|
|
454
|
-
{
|
|
631
|
+
{
|
|
632
|
+
key: "previousClose", header: "Prev Close", align: "right" as const,
|
|
633
|
+
value: (row: QuoteRow) => exportFigure(row.rawPreviousClose),
|
|
634
|
+
format: (_value: unknown, row: QuoteRow) => yieldLevelCell(row.previousClose, row),
|
|
635
|
+
},
|
|
455
636
|
// A narrow terminal drops these before it cuts the names short.
|
|
456
|
-
|
|
637
|
+
// An export writes the range as plain numbers (12.5-13.1), and each end as a number in Day Low and Day High.
|
|
638
|
+
{
|
|
639
|
+
key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1,
|
|
640
|
+
value: (row: QuoteRow) => row.rawDayLow != null && row.rawDayHigh != null ? `${exportFigure(row.rawDayLow)}-${exportFigure(row.rawDayHigh)}` : "",
|
|
641
|
+
format: (_value: unknown, row: QuoteRow) => row.dayRange,
|
|
642
|
+
},
|
|
457
643
|
{ key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
|
|
458
644
|
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3, format: currencyCell },
|
|
459
645
|
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
646
|
+
...EXTENDED_CHANGE_EXPORT_COLUMNS,
|
|
647
|
+
...DAY_RANGE_EXPORT_COLUMNS,
|
|
460
648
|
];
|
|
461
649
|
}
|
|
462
650
|
|
|
@@ -504,18 +692,24 @@ function changeBasisNotes(results: QuoteCliRecord[], now = Date.now()): string[]
|
|
|
504
692
|
function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { previousCloseDate?: string | null }) | null {
|
|
505
693
|
if (!quote) return null;
|
|
506
694
|
const previousCloseDate = quotePreviousCloseDate(quote);
|
|
507
|
-
const entries = Object.entries(quote);
|
|
695
|
+
const entries = Object.entries(quote).filter(([key]) => key !== "previousCloseDate");
|
|
508
696
|
const at = entries.findIndex(([key]) => key === "previousClose");
|
|
509
697
|
entries.splice(at < 0 ? entries.length : at + 1, 0, ["previousCloseDate", previousCloseDate]);
|
|
510
698
|
return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
|
|
511
699
|
}
|
|
512
700
|
|
|
701
|
+
/** An explicit missing comparison wins over any leftover numeric move in a CLI quote. */
|
|
702
|
+
function quoteSessionFields(quote: Quote | null | undefined) {
|
|
703
|
+
const fields = getQuoteSessionFields(quote);
|
|
704
|
+
return quote?.changeBasis === "none" ? { ...fields, change: null, changePercent: null } : fields;
|
|
705
|
+
}
|
|
706
|
+
|
|
513
707
|
function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
514
708
|
return results.map((result) => {
|
|
515
709
|
const quote = result.quote;
|
|
516
710
|
// Last and Chg% are the regular session, as `ticker` and the quote monitor read it; a
|
|
517
711
|
// pre-market or after-hours print is its own column, measured from that session's close.
|
|
518
|
-
const display =
|
|
712
|
+
const display = quoteSessionFields(quote);
|
|
519
713
|
const extended = getExtendedSessionDisplay(quote);
|
|
520
714
|
const yieldIndex = isYieldIndexSymbol(result.target.symbol);
|
|
521
715
|
// An index level is in points, not in the currency its members trade in.
|
|
@@ -527,25 +721,26 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
527
721
|
minimumFractionDigits: indexPoints ? 2 : currencyPriceMinDigits(quote?.currency),
|
|
528
722
|
...nearParCoin(quote) ? { fixedFractionDigits: STABLECOIN_PRICE_DIGITS } : {},
|
|
529
723
|
};
|
|
530
|
-
const price = (value: number | undefined) => (
|
|
724
|
+
const price = (value: number | null | undefined) => (
|
|
531
725
|
quote && value != null
|
|
532
726
|
? indexPoints ? formatMarketPrice(value, options) : formatMarketPriceWithCurrency(value, quote.currency, options)
|
|
533
727
|
: ""
|
|
534
728
|
);
|
|
535
729
|
// A price in cents says what it is per, as the contract terms word it: 314.95¢/lb.
|
|
536
|
-
const per = display
|
|
730
|
+
const per = display.price != null ? futuresPricePer(result.futuresContract, quote?.currency) : "";
|
|
731
|
+
const previousClose = display.price != null && display.change != null ? display.price - display.change : quote?.previousClose;
|
|
537
732
|
return {
|
|
538
733
|
symbol: result.target.symbol,
|
|
539
734
|
name: quote?.name || result.listing?.name || "",
|
|
540
|
-
price: `${price(display
|
|
541
|
-
rawPrice: display
|
|
735
|
+
price: `${price(display.price)}${per}`,
|
|
736
|
+
rawPrice: display.price,
|
|
542
737
|
priceBasis: quote?.priceBasis ?? null,
|
|
543
738
|
instrumentType: quote?.instrumentType ?? null,
|
|
544
|
-
change: display
|
|
545
|
-
changePercent: roundedPercent(display
|
|
739
|
+
change: display.change,
|
|
740
|
+
changePercent: roundedPercent(display.changePercent ?? undefined),
|
|
546
741
|
// A Cboe yield index is read as a yield in percent and moves in basis points.
|
|
547
742
|
yieldIndex,
|
|
548
|
-
changeBasisPoints: yieldIndex && display
|
|
743
|
+
changeBasisPoints: yieldIndex && display.change != null ? toBasisPoints(display.change, 1) : null,
|
|
549
744
|
extendedSession: extended?.session ?? null,
|
|
550
745
|
extendedPrice: price(extended?.price),
|
|
551
746
|
rawExtendedPrice: extended?.price ?? null,
|
|
@@ -556,7 +751,10 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
556
751
|
session: quoteSessionStateLabel(quote, now) ?? "",
|
|
557
752
|
sessionLabel: quoteSessionLabel(quote, now) ?? "",
|
|
558
753
|
// The close the shown move is measured from: the one before the session Last is.
|
|
559
|
-
previousClose: price(
|
|
754
|
+
previousClose: price(previousClose),
|
|
755
|
+
rawPreviousClose: previousClose ?? null,
|
|
756
|
+
rawDayLow: quote?.low != null && quote.high != null ? quote.low : null,
|
|
757
|
+
rawDayHigh: quote?.low != null && quote.high != null ? quote.high : null,
|
|
560
758
|
dayRange: quote?.low != null && quote.high != null
|
|
561
759
|
? indexPoints ? `${price(quote.low)}-${price(quote.high)}` : formatPriceRange(quote.low, quote.high, quote.currency, options, "-")
|
|
562
760
|
: "",
|
|
@@ -625,15 +823,17 @@ function filingsWithItemLabels(filings: SecFilingItem[]) {
|
|
|
625
823
|
});
|
|
626
824
|
}
|
|
627
825
|
|
|
628
|
-
|
|
826
|
+
/** The summary over the ratings: the upside from Last, and the price and time it is measured from, as `fn ANR` gives them. */
|
|
827
|
+
function analystSummary(data: AnalystResearchData, quote: Quote | null): string {
|
|
629
828
|
const target = data.priceTarget;
|
|
630
829
|
const currency = analystTargetCurrency(data);
|
|
631
|
-
const upside =
|
|
830
|
+
const { upside, reference } = analystUpside(data, quote);
|
|
632
831
|
const analysts = recommendationTotal(data);
|
|
633
832
|
const mix = formatRecommendationMix(data);
|
|
634
833
|
const entries: Array<[string, string]> = [
|
|
635
834
|
["Average Target", formatAnalystPrice(target?.average, currency)],
|
|
636
835
|
["Upside", upside == null ? "-" : formatPercent(upside)],
|
|
836
|
+
["Upside vs", upside == null ? "-" : formatAnalystReference(reference, currency)],
|
|
637
837
|
["Low / Median / High", target && [target.low, target.median, target.high].some((value) => value != null)
|
|
638
838
|
? [target.low, target.median, target.high].map((value) => formatAnalystPrice(value, currency)).join(" / ")
|
|
639
839
|
: "-"],
|
|
@@ -734,13 +934,13 @@ function formatEpsCell(value: unknown): string {
|
|
|
734
934
|
return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "";
|
|
735
935
|
}
|
|
736
936
|
|
|
737
|
-
function earningsRows(events:
|
|
937
|
+
function earningsRows(events: ResolvedEarningsEvent[]) {
|
|
738
938
|
return events.map((event) => ({
|
|
739
939
|
symbol: event.symbol,
|
|
740
940
|
name: event.name,
|
|
741
941
|
date: isoDate(event.earningsDate).slice(0, 10),
|
|
742
|
-
//
|
|
743
|
-
status: earningsDateLabel(event),
|
|
942
|
+
// Out, announced by the company, or projected from its past reports.
|
|
943
|
+
status: earningsDateLabel(event.dateStatus),
|
|
744
944
|
timing: event.timing,
|
|
745
945
|
epsEstimate: event.epsEstimate,
|
|
746
946
|
epsActual: event.epsActual,
|
|
@@ -756,17 +956,17 @@ const MAX_CHOICE_LOOKUPS = 10;
|
|
|
756
956
|
* The line `quote` prints beside a symbol that changed, as `history` and
|
|
757
957
|
* `ticker` word it: a company that moved, with nothing new listed there,
|
|
758
958
|
* names the quote of its new symbol ("HFC became DINO on 2022-03-15:
|
|
759
|
-
* `
|
|
959
|
+
* `gloom quote DINO`"); its old line's last price stays in the row.
|
|
760
960
|
*/
|
|
761
961
|
function quoteChangeLine(change: TickerChange | null | undefined, formerIssuer?: FormerIssuer | null): string | null {
|
|
762
962
|
const moved = movedAwayChange(change);
|
|
763
963
|
// One line per symbol: a company that left it, else the company that merged away from it (GOGL).
|
|
764
|
-
return (moved ? movedSymbolLine(moved, `
|
|
964
|
+
return (moved ? movedSymbolLine(moved, `gloom quote ${moved.tradesAs}`) : tickerChangeLine(change)) ?? formerIssuer?.note ?? null;
|
|
765
965
|
}
|
|
766
966
|
|
|
767
967
|
async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], commandName: "quote" | "compare") {
|
|
768
968
|
const args = [...rawArgs];
|
|
769
|
-
const usage = `Usage:
|
|
969
|
+
const usage = `Usage: gloom ${commandName} <symbol...> [--file <path>]`;
|
|
770
970
|
const exchange = takeOption(args, "--exchange") ?? "";
|
|
771
971
|
// A list from a file or standard input (`-`), read as `watchlist add` reads one.
|
|
772
972
|
const file = args.some((arg) => arg === "--file" || arg.startsWith("--file=")) ? takeOption(args, "--file") ?? "" : undefined;
|
|
@@ -779,16 +979,33 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
779
979
|
await withMarketData(ctx, async (market) => {
|
|
780
980
|
const read = phrase ? await readPhraseArgument(phrase, market.dataProvider) : null;
|
|
781
981
|
if (read?.kind === "phrase") return answerPhrase(read.phrase, market, ctx);
|
|
782
|
-
//
|
|
783
|
-
const list =
|
|
982
|
+
// Typed or from a file, one reader names what it left out: a stray separator, the prices beside the symbols.
|
|
983
|
+
const list = read ? null : await readListedSymbols(args, file, market, ctx, usage);
|
|
784
984
|
const symbols = read?.kind === "name" ? [read.name]
|
|
785
985
|
: read?.kind === "symbols" ? uniqueEntries(read.symbols)
|
|
786
|
-
: list
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
986
|
+
: list!.symbols;
|
|
987
|
+
// What cannot be a symbol (N/A, $$$, a price alone) is never looked up; one line names it.
|
|
988
|
+
const refused = symbols.flatMap((symbol) => notASymbolError(symbol) ?? []);
|
|
989
|
+
if (refused.length === symbols.length) {
|
|
990
|
+
ctx.fail(refused.length === 1 ? refused[0]!.message : `Nothing to quote: ${refusedList(refused)} are not symbols.`, usage);
|
|
991
|
+
}
|
|
992
|
+
// With several symbols, --exchange is for the ones that name no exchange of their own, and one that names no
|
|
993
|
+
// listing (EQT:NYSE:US) is named in a line while the others are quoted.
|
|
994
|
+
const resolvedListings = await Promise.all(symbols.filter((symbol) => !notASymbolError(symbol)).map(async (symbol) => {
|
|
995
|
+
if (symbols.length === 1) return { symbol, listing: await requireCliListing(symbol, exchange, market, ctx) };
|
|
996
|
+
try {
|
|
997
|
+
return { symbol, listing: await resolveCliListing(symbol, exchange, market, { ownExchangeWins: true }) };
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (error instanceof ListingArgError) return { symbol, error };
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}));
|
|
1003
|
+
const unread = resolvedListings.flatMap((entry) => "error" in entry && entry.error ? [{ symbol: entry.symbol, error: entry.error }] : []);
|
|
1004
|
+
const listings = resolvedListings.flatMap((entry) => "listing" in entry && entry.listing ? [entry.listing] : []);
|
|
1005
|
+
if (listings.length === 0) {
|
|
1006
|
+
const [first] = unread;
|
|
1007
|
+
ctx.fail(first!.error.message, [...unread.slice(1).map(unreadLine), ...refused.length > 0 ? [refusedLine(refused)] : []].join("\n") || first!.error.details);
|
|
1008
|
+
}
|
|
792
1009
|
const targets: QuoteSubscriptionTarget[] = listings.map((listing) => ({ symbol: listing.request.symbol, exchange: listing.request.exchange }));
|
|
793
1010
|
// A bare symbol's venues load beside its quote, to name the company it was read as when several list it.
|
|
794
1011
|
// Each costs a search, so a long list (a portfolio check, not a lookup) names none.
|
|
@@ -798,14 +1015,15 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
798
1015
|
const changes = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
|
|
799
1016
|
: listings.map((listing) => [listing, startTickerChangeLookup(market.dataProvider, listing.symbol, listing.exchange)] as const));
|
|
800
1017
|
// The service's quote of the moment, not each form's local copy from whenever it was last asked for:
|
|
801
|
-
// BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote
|
|
802
|
-
|
|
1018
|
+
// BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote. A local copy answers only offline.
|
|
1019
|
+
// Each row prints what its move is from (Chg Basis).
|
|
1020
|
+
const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true, datesChangeBasis: true });
|
|
803
1021
|
const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
|
|
804
1022
|
const asked = results.map(listingOf);
|
|
805
1023
|
const request = { command: commandName, noun: "quote" };
|
|
806
1024
|
const answers = await Promise.all(results.map((result, index) => {
|
|
807
1025
|
const listing = asked[index]!;
|
|
808
|
-
return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null);
|
|
1026
|
+
return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null, { datesChangeBasis: true });
|
|
809
1027
|
}));
|
|
810
1028
|
const now = Date.now();
|
|
811
1029
|
const symbolChanges = await Promise.all(answers.map(async ({ result }, index) => {
|
|
@@ -819,7 +1037,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
819
1037
|
const tickerChanges = symbolChanges.map(({ change }) => change);
|
|
820
1038
|
const formerIssuers = symbolChanges.map(({ formerIssuer }) => formerIssuer);
|
|
821
1039
|
const failed = answers[0]?.failure;
|
|
822
|
-
if (
|
|
1040
|
+
if (symbols.length === 1 && failed) {
|
|
823
1041
|
ctx.fail(failed.message, [quoteChangeLine(tickerChanges[0], formerIssuers[0]), failed.details].filter(Boolean).join("\n") || undefined);
|
|
824
1042
|
}
|
|
825
1043
|
// A futures quote adds its dated settlement and contract terms, as `ticker` prints them; `compare` stays a table.
|
|
@@ -828,6 +1046,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
828
1046
|
: null));
|
|
829
1047
|
// A contract that is not its root's front (NGF27, NG2) prints the front's own price beside it, read in one batch.
|
|
830
1048
|
const fronts = await loadFuturesFrontQuotes(contracts, market.dataProvider);
|
|
1049
|
+
// A per-MWh gas price (TTF) adds its dollars per MMBtu at the day's rate, to read beside Henry Hub.
|
|
1050
|
+
const gasInUsd = await Promise.all(answers.map(({ result }, index) => loadUsdPerMmbtu(
|
|
1051
|
+
getRegularSessionDisplay(result.quote)?.price, contracts[index], result.quote?.currency, (currency) => market.dataProvider.getExchangeRate(currency))));
|
|
831
1052
|
// A perpetual adds its funding per eight hours and open interest, the figures PERP lists beside its mark.
|
|
832
1053
|
const perpetualContracts = await Promise.all(answers.map(({ result }) => commandName === "quote" && result.quote
|
|
833
1054
|
? cliPerpetualContract(result.quote)
|
|
@@ -842,12 +1063,13 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
842
1063
|
target: result.target,
|
|
843
1064
|
listing: listingMetadata(listingIdentity(listing, result.quote)),
|
|
844
1065
|
// The figures the table shows, beside the quote as the source sent it.
|
|
845
|
-
...
|
|
1066
|
+
...quoteSessionFields(result.quote),
|
|
846
1067
|
// What `change` is measured from, with the label and holiday note the table shows.
|
|
847
1068
|
changeBasis: changeBasisJson(quoteChangeBasis(result.quote, { terms: contracts[index] }), now),
|
|
848
1069
|
quote: withPreviousCloseDate(result.quote),
|
|
849
1070
|
...contracts[index] ? { futuresContract: contracts[index] } : {},
|
|
850
1071
|
...fronts[index] ? { futuresFront: fronts[index] } : {},
|
|
1072
|
+
...gasInUsd[index] ? { usdPerMmbtu: gasInUsd[index] } : {},
|
|
851
1073
|
...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
|
|
852
1074
|
...usSuggestions[index] ? { usSuggestion: usSuggestions[index] } : {},
|
|
853
1075
|
error: failure?.oneLine ?? errorMessage(result.error),
|
|
@@ -857,21 +1079,34 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
857
1079
|
}));
|
|
858
1080
|
const notes = [
|
|
859
1081
|
...list?.notes ?? [],
|
|
1082
|
+
...refused.length > 0 ? [refusedLine(refused)] : [],
|
|
1083
|
+
...unread.map(unreadLine),
|
|
860
1084
|
// FB now lists another security; the company that was FB trades as META. A symbol a
|
|
861
1085
|
// company renamed into (META) is what was typed: its change is in the row, not here.
|
|
862
1086
|
// GOGL was Golden Ocean, which merged into CMB.Tech; a fund lists there now.
|
|
863
1087
|
...tickerChanges.flatMap((change, index) => quoteChangeLine(change, formerIssuers[index]) ?? []),
|
|
864
1088
|
// Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
|
|
865
1089
|
...answers.flatMap((answer) => answer.notes),
|
|
866
|
-
// A row that stopped trading long ago reads as a quote that is merely stale
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1090
|
+
// A row that stopped trading long ago reads as a quote that is merely stale, and one with
|
|
1091
|
+
// nothing printed in a session the calendar has open (a feed that stalled, a typhoon day) says since when.
|
|
1092
|
+
...data.flatMap((row) => {
|
|
1093
|
+
if (!row.quote) return [];
|
|
1094
|
+
const label = `${row.target.symbol}${row.listing.name ? ` (${row.listing.name})` : ""}`;
|
|
1095
|
+
const ended = noLongerTradingNote(label, row.quote, now);
|
|
1096
|
+
const quiet = ended ? null : noPrintsTodayText(row.quote, now);
|
|
1097
|
+
return [ended ?? (quiet ? `${label}: ${quiet}` : []), quoteBarNotes(label, row.quote, now)].flat();
|
|
1098
|
+
}),
|
|
1099
|
+
// A receipt and its home line each say the ratio, as `ticker` does.
|
|
1100
|
+
...data.flatMap((row) => {
|
|
1101
|
+
const pair = listingDepositaryPair({ quote: row.quote });
|
|
1102
|
+
return pair ? [`${row.target.symbol}: ${depositaryPairText(pair, isReceiptSide({ quote: row.quote }))}.`] : [];
|
|
1103
|
+
}),
|
|
1104
|
+
// A cell shows only "unavailable", and an exported row only its status, so stderr names each failure; the JSON rows carry each reason.
|
|
1105
|
+
...ctx.cliOptions.format !== "json" ? quoteNotes(data, { exchange }) : [],
|
|
872
1106
|
];
|
|
873
1107
|
const columns = commandName === "compare" ? compareColumns() : quoteColumns();
|
|
874
1108
|
const basisNotes = changeBasisNotes(data, now);
|
|
1109
|
+
const midNotes = extendedMidNotes(data);
|
|
875
1110
|
if (ctx.cliOptions.format !== "json") await primeBoardCoins(notes);
|
|
876
1111
|
// Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
|
|
877
1112
|
const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
|
|
@@ -880,7 +1115,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
880
1115
|
warnings: notes.length > 0 ? notes : undefined,
|
|
881
1116
|
// A Cboe yield index stops at its 15:00 ET close, before the Treasury's curve is taken: the line says so, so its
|
|
882
1117
|
// close does not read as a contradiction of DGS10 or GC for the same day.
|
|
883
|
-
|
|
1118
|
+
// Dated by the quotes as served, which say whether one is the saved copy.
|
|
1119
|
+
freshness: withSavedCopy(
|
|
1120
|
+
quotesFreshness(data.map((row) => row.quote), yieldsOnly ? { schedule: `Cboe close ${CBOE_YIELD_INDEX_CLOSE}` } : undefined),
|
|
1121
|
+
answers.map(({ result }) => result.quote),
|
|
1122
|
+
),
|
|
884
1123
|
}, {
|
|
885
1124
|
rows: quoteRows,
|
|
886
1125
|
// An index row's Cur is blank, its level having no currency; the export names what it is in instead.
|
|
@@ -888,6 +1127,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
888
1127
|
symbolOrSeries: row.symbol,
|
|
889
1128
|
currencyOrUnit: row.currency || (row.yieldIndex ? "pct" : isIndexLevel(row.instrumentType) ? INDEX_POINTS_EXPORT_UNIT : null),
|
|
890
1129
|
}),
|
|
1130
|
+
// A row with no price or move says why in its status column, as the warnings do.
|
|
1131
|
+
exportStatus: quoteExportStatus,
|
|
1132
|
+
...(midNotes.length > 0 ? { exportNotes: midNotes } : {}),
|
|
891
1133
|
details: quoteDetailsText,
|
|
892
1134
|
...(basisNotes.length ? { footnote: basisNotes.join("\n") } : {}),
|
|
893
1135
|
columns,
|
|
@@ -895,11 +1137,27 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
895
1137
|
? columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column)
|
|
896
1138
|
: columns),
|
|
897
1139
|
});
|
|
898
|
-
// The table
|
|
1140
|
+
// The table and the warnings name each symbol it found nothing for; with none found, the exit status says so too.
|
|
899
1141
|
if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
|
|
900
1142
|
});
|
|
901
1143
|
}
|
|
902
1144
|
|
|
1145
|
+
/** The entries refused before any lookup, named as written: `$$$, N/A`, split by semicolons when one has a comma. */
|
|
1146
|
+
function refusedList(refused: ReadonlyArray<{ input: string }>): string {
|
|
1147
|
+
const inputs = refused.map((error) => error.input.trim());
|
|
1148
|
+
return inputs.join(inputs.some((input) => input.includes(",")) ? "; " : ", ");
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** The one line a quote prints for what it did not look up: `Not a symbol, not quoted: $$$, N/A.` */
|
|
1152
|
+
function refusedLine(refused: ReadonlyArray<{ input: string }>): string {
|
|
1153
|
+
return `Not ${refused.length === 1 ? "a symbol" : "symbols"}, not quoted: ${refusedList(refused)}.`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** A symbol of a list that names no listing: `Not quoted: EQT:NYSE:US (Unknown exchange US for EQT:NYSE).` */
|
|
1157
|
+
function unreadLine({ symbol, error }: { symbol: string; error: ListingArgError }): string {
|
|
1158
|
+
return `Not quoted: ${symbol} (${error.message.replace(/\.$/, "")}).`;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
903
1161
|
/**
|
|
904
1162
|
* `quote "10 year yield"`: a phrase that is no ticker and no company's name
|
|
905
1163
|
* is answered with what search finds for it and the catalog, never quoted
|
|
@@ -909,7 +1167,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
909
1167
|
async function answerPhrase(phrase: string, market: MarketContext, ctx: Parameters<CliCommandDef["execute"]>[1]): Promise<void> {
|
|
910
1168
|
const said = `"${phrase}" is not a ticker or a company's name.`;
|
|
911
1169
|
if (ctx.cliOptions.format !== "text") {
|
|
912
|
-
ctx.fail(said, [knownGapMessage(phrase), `Search for it:
|
|
1170
|
+
ctx.fail(said, [knownGapMessage(phrase), `Search for it: gloom search "${phrase}"`, `Functions and data: gloom catalog "${phrase}"`]
|
|
913
1171
|
.filter(Boolean).join("\n"));
|
|
914
1172
|
}
|
|
915
1173
|
process.exitCode = CLI_EXIT_CODES.error;
|
|
@@ -964,12 +1222,15 @@ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
|
|
|
964
1222
|
}
|
|
965
1223
|
const terms = row.futuresContract;
|
|
966
1224
|
if (!terms || !row.quote) return [];
|
|
967
|
-
const settle: [string, string] = ["Settle",
|
|
1225
|
+
const settle: [string, string] = ["Settle", quoteSettleText(row.quote, terms)];
|
|
968
1226
|
// The move Chg% gives, in the quote's money and with what it is measured from: "vs Fri settle".
|
|
969
1227
|
const session = getRegularSessionDisplay(row.quote);
|
|
970
1228
|
const change: Array<[string, string]> = session?.change != null && Number.isFinite(session.change)
|
|
971
1229
|
? [["Change", quoteChangeText(row.quote, session, withCurrencyMinorDigits(quoteFormatOptions(row.quote, "FUTURE"), row.quote.currency), terms)]]
|
|
972
1230
|
: [];
|
|
1231
|
+
// European gas beside Henry Hub: "≈ $30.832/MMBtu at EUR/USD 1.1650", a conversion that says so.
|
|
1232
|
+
const inUsd = row.usdPerMmbtu;
|
|
1233
|
+
if (inUsd) change.push(["In USD", usdPerMmbtuText(inUsd)]);
|
|
973
1234
|
const metrics = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
|
|
974
1235
|
const rolls = futuresRollLines(terms, row.futuresFront).map((line): [string, string] => [line.label, line.value]);
|
|
975
1236
|
const at = futuresContractLines(terms, row.quote.symbol || row.target.symbol)[0]?.key === "contract" ? 1 : 0;
|
|
@@ -990,17 +1251,21 @@ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeB
|
|
|
990
1251
|
return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
|
|
991
1252
|
}
|
|
992
1253
|
|
|
993
|
-
/**
|
|
1254
|
+
/**
|
|
1255
|
+
* A crypto pair that will not load points at the crypto board. Structured
|
|
1256
|
+
* formats keep the error as thrown, and so does no connection at all, which
|
|
1257
|
+
* the board cannot get past either.
|
|
1258
|
+
*/
|
|
994
1259
|
function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
|
|
995
|
-
if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
|
|
1260
|
+
if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol) || isNetworkFailure(error)) throw error;
|
|
996
1261
|
return ctx.fail(errorMessage(error) ?? `No history available for ${symbol}`, CRYPTO_BOARD_HINT);
|
|
997
1262
|
}
|
|
998
1263
|
|
|
999
|
-
const HISTORY_USAGE = "history <symbol
|
|
1264
|
+
const HISTORY_USAGE = "history <symbol...> [--range <range> | --from <year|date> [--to <year|date>]]";
|
|
1000
1265
|
const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
|
|
1001
1266
|
const HISTORY_YTD_RANGE = "YTD starts at the prior year's last close, the one year-to-date returns are measured from";
|
|
1002
|
-
/** Options people reach for to ask for dates, which history answers with
|
|
1003
|
-
const DATE_WINDOW_OPTIONS = new Set(["--
|
|
1267
|
+
/** Options people reach for to ask for dates, which history answers with --from and --to (or --start and --end). */
|
|
1268
|
+
const DATE_WINDOW_OPTIONS = new Set(["--since", "--until", "--date", "--start-date", "--end-date"]);
|
|
1004
1269
|
/** Options people reach for to pick a bar size, which history derives from the range. */
|
|
1005
1270
|
const BAR_SIZE_OPTIONS = new Set(["--interval", "--resolution", "--bar", "--bars", "--bar-size", "--granularity", "--timeframe", "--frequency", "--freq", "--tick", "--ticks"]);
|
|
1006
1271
|
|
|
@@ -1030,16 +1295,26 @@ function financialsSymbolArg(args: readonly string[]): string | undefined {
|
|
|
1030
1295
|
function financialsOptionHint(flag: string, args: readonly string[]): string | null {
|
|
1031
1296
|
if (!QUARTERLY_OPTIONS.has(flag)) return null;
|
|
1032
1297
|
const symbol = financialsSymbolArg(args) ?? "<symbol>";
|
|
1033
|
-
return `financials lists fiscal years. For quarters:
|
|
1034
|
-
+ `or
|
|
1298
|
+
return `financials lists fiscal years. For quarters: gloom fn GF ${symbol} --period quarterly --periods 40 (one metric at a time), `
|
|
1299
|
+
+ `or gloom fn FA ${symbol} --period quarterly (the statements).`;
|
|
1035
1300
|
}
|
|
1036
1301
|
|
|
1037
1302
|
function historyOptionHint(flag: string, args: readonly string[]): string | null {
|
|
1038
|
-
if (DATE_WINDOW_OPTIONS.has(flag))
|
|
1303
|
+
if (DATE_WINDOW_OPTIONS.has(flag)) {
|
|
1304
|
+
return `history takes dates as --from and --to (or --start and --end), each a year or a date (--from 2007 --to 2009), or a --range: ${HISTORY_RANGES.join(", ")}.`;
|
|
1305
|
+
}
|
|
1039
1306
|
if (BAR_SIZE_OPTIONS.has(flag)) return `history serves each --range in one bar size. ${subMinuteDataMessage(historySymbolArg(args))}`;
|
|
1040
1307
|
return null;
|
|
1041
1308
|
}
|
|
1042
1309
|
|
|
1310
|
+
/** "Asked from 1990-01-01, data starts 1995-01-03 (first available bar)" when the bars start over a week into the window. */
|
|
1311
|
+
function windowHistoryNotes(window: PriceWindow, rows: readonly { date: string }[]): string[] {
|
|
1312
|
+
const first = rows[0]?.date.slice(0, 10);
|
|
1313
|
+
if (!first) return [];
|
|
1314
|
+
return Date.parse(`${first}T00:00:00Z`) - Date.parse(`${window.from}T00:00:00Z`) > 7 * 86_400_000
|
|
1315
|
+
? [`Asked from ${window.from}, data starts ${first} (first available bar)`] : [];
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1043
1318
|
/** "Asked YTD, data starts 2026-03-05 (first available bar)" when no bar reaches the prior year's last close. */
|
|
1044
1319
|
function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
|
|
1045
1320
|
const first = rows[0]?.date.slice(0, 10);
|
|
@@ -1047,116 +1322,246 @@ function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
|
|
|
1047
1322
|
return first && last && first > `${Number(last.slice(0, 4)) - 1}-12-31` ? [`Asked YTD, data starts ${first} (first available bar)`] : [];
|
|
1048
1323
|
}
|
|
1049
1324
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1325
|
+
/** What `history` was asked for: a range, or a window of dates in its place. */
|
|
1326
|
+
interface HistoryAsk {
|
|
1327
|
+
range: HistoryRange;
|
|
1328
|
+
window: PriceWindow | null;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** One listing's bars as `history` prints them, with what its heading, warnings and closing line say. */
|
|
1332
|
+
interface ListingHistory {
|
|
1333
|
+
listing: CliListing;
|
|
1334
|
+
identity: ListingIdentity;
|
|
1335
|
+
unit: HistoryUnit;
|
|
1336
|
+
data: HistoryRow[];
|
|
1337
|
+
resolution: PriceHistoryResult["resolution"];
|
|
1338
|
+
interval: HistoryRow["interval"];
|
|
1339
|
+
decimals: number;
|
|
1340
|
+
/** Intraday bars, which print in UTC as times. */
|
|
1341
|
+
intraday: boolean;
|
|
1342
|
+
volume: HistoryVolume | null;
|
|
1343
|
+
freshness: ReportFreshness | undefined;
|
|
1344
|
+
notes: string[];
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/** The command a listing that cannot be served is pointed back at: `history`, `history --range 5Y`. */
|
|
1348
|
+
function historyCommand({ range, window }: HistoryAsk): string {
|
|
1349
|
+
// A window that runs to today retries to the day it is run on, not the one it was first run on.
|
|
1350
|
+
return window ? `history --from ${window.from}${isPastPriceWindow(window) ? ` --to ${window.to}` : ""}` : range === "1Y" ? "history" : `history --range ${range}`;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* One listing's bars, dated and checked as `history` prints them. Without
|
|
1355
|
+
* `lookups` it does not look up the other venues of a bare symbol or whether
|
|
1356
|
+
* its company moved, which a long list skips, as `quote` does.
|
|
1357
|
+
*/
|
|
1358
|
+
async function loadListingHistory(
|
|
1359
|
+
listing: CliListing,
|
|
1360
|
+
market: MarketContext,
|
|
1361
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1362
|
+
ask: HistoryAsk,
|
|
1363
|
+
{ lookups }: { lookups: boolean },
|
|
1364
|
+
): Promise<ListingHistory> {
|
|
1365
|
+
const { range, window } = ask;
|
|
1053
1366
|
// YTD reads the year's daily bars and starts at the last one on or before 31 Dec.
|
|
1054
1367
|
const loadRange: TimeRange = range === "YTD" ? "1Y" : range;
|
|
1368
|
+
const venues = lookups ? loadBareVenues(listing, market) : null;
|
|
1369
|
+
const { symbol, exchange } = listing.request;
|
|
1370
|
+
const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
|
|
1371
|
+
const load = () => window ? loadPriceWindowHistory(market.dataProvider, symbol, exchange, window, context)
|
|
1372
|
+
: market.dataProvider.getPriceHistoryWithMetadata
|
|
1373
|
+
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
|
|
1374
|
+
: market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
|
|
1375
|
+
const quoteAnswer = loadListingQuoteAnswer(market.dataProvider, listing);
|
|
1376
|
+
const quoted = quoteAnswer.then((answer) => answer.quote);
|
|
1377
|
+
const fate = lookups ? listingFateLoader(listing, market, quoted) : null;
|
|
1378
|
+
const request = { command: historyCommand(ask), noun: "history", ...(fate ? { fate } : {}) };
|
|
1379
|
+
const [loaded, quote] = await Promise.all([
|
|
1380
|
+
loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, request)
|
|
1381
|
+
.catch((error) => failHistory(error, listing.key, ctx)),
|
|
1382
|
+
quoted,
|
|
1383
|
+
]);
|
|
1384
|
+
// A bare symbol several companies list is never served from the one the data service picked, in a range or a window:
|
|
1385
|
+
// it asks for an exchange as `quote` does, and in a list that is the symbol's row alone.
|
|
1386
|
+
await failUnchosenListing(listing, market, ctx, request, (await quoteAnswer).error);
|
|
1387
|
+
const { resolution } = loaded;
|
|
1388
|
+
// While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
|
|
1389
|
+
// A window that ended before today has no session in progress.
|
|
1390
|
+
const points = window && isPastPriceWindow(window) ? loaded.points
|
|
1391
|
+
: withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
|
|
1392
|
+
const fromQuote = points !== loaded.points;
|
|
1393
|
+
const identity = listingIdentity(listing, quote);
|
|
1394
|
+
// Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
|
|
1395
|
+
const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(logFailure("cli.commands-market", "Read quote metadata for history failed", null));
|
|
1396
|
+
const unit = historyUnit(listing.key, listed ?? quote);
|
|
1397
|
+
const traded = {
|
|
1398
|
+
listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
|
|
1399
|
+
};
|
|
1400
|
+
// A coin's volume is traded value in its quote currency: said in every row and on the status line.
|
|
1401
|
+
const volume = historyVolume(unit, traded);
|
|
1402
|
+
const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
|
|
1403
|
+
// Bars stop arriving on the listing's own trading days, not New York's.
|
|
1404
|
+
const venue = quote?.listingExchangeName || exchange || undefined;
|
|
1405
|
+
const served = range === "YTD" && !window ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval, venue))) : rows;
|
|
1406
|
+
// October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
|
|
1407
|
+
const forming = formingPeriodBar(served, venue);
|
|
1408
|
+
// A complete one is dated by the day its last session closed, the day its close is from.
|
|
1409
|
+
const closedDay = forming ? null : closedPeriodBarDay(served, venue);
|
|
1410
|
+
const data = forming?.rows ?? served;
|
|
1411
|
+
const interval = data[0]?.interval ?? null;
|
|
1412
|
+
// Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
|
|
1413
|
+
const intraday = data.some((row) => row.date.length > 10);
|
|
1414
|
+
// A bar history: dated by its last bar, stale once bars of its size stop arriving.
|
|
1415
|
+
// The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
|
|
1416
|
+
const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
|
|
1417
|
+
// The source that served the bars, a broker or another source when Gloom Cloud could not.
|
|
1418
|
+
const source = loaded.sourceName ? { source: loaded.sourceName } : {};
|
|
1419
|
+
// A window that closed before today is the history asked for: no later bar makes it stale.
|
|
1420
|
+
const freshness = withSavedCopy(window && isPastPriceWindow(window) ? rowsFreshness(data, {
|
|
1421
|
+
...source,
|
|
1422
|
+
status: "not-a-feed", basis: `${interval ? BAR_INTERVAL_LABELS[interval] : "price"} bars, ${priceWindowText(window)}`, observedKey: "date", oldest: null,
|
|
1423
|
+
}) : rowsFreshness(data, {
|
|
1424
|
+
...source,
|
|
1425
|
+
...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
|
|
1426
|
+
observedKey: "date",
|
|
1427
|
+
oldest: null,
|
|
1428
|
+
cadenceExchange: venue,
|
|
1429
|
+
...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
|
|
1430
|
+
...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
|
|
1431
|
+
...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
|
|
1432
|
+
// The rows are drawn from the bars served, which say whether they are the saved copy.
|
|
1433
|
+
}), loaded);
|
|
1434
|
+
const gaps = historyGapNote(data, traded);
|
|
1435
|
+
const coverage = window ? historyCoverageNotice(loaded.coverageStart, Date.parse(`${window.from}T00:00:00Z`), loaded.coverageNote)
|
|
1436
|
+
: range === "YTD" ? null : historyCoverageWarning(range, loaded);
|
|
1437
|
+
const notes = [
|
|
1438
|
+
// A window the source has no bar in (a typo, a weekend, a listing that had not started) is said, not an empty table alone.
|
|
1439
|
+
...data.length === 0 ? [emptyHistoryNote(listing.key, ask)] : [],
|
|
1440
|
+
// FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
|
|
1441
|
+
...fate ? listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }) : [],
|
|
1442
|
+
...lookups ? await listingChoiceNotes(listing, market, quote, venues) : [],
|
|
1443
|
+
// ALL says where the source's bars start (BTC-USD's in 2014-09), unless the coverage line does.
|
|
1444
|
+
...window ? [...windowHistoryNotes(window, data), coverage ?? []]
|
|
1445
|
+
: range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date, Date.now(), coverage != null), coverage ?? []],
|
|
1446
|
+
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1447
|
+
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1448
|
+
historyVenueVolumeNote(data) ?? [],
|
|
1449
|
+
historyBarStatusNote(forming ? served : data, { tradingDay: venueTradingDay(venue, Date.now()) }) ?? [],
|
|
1450
|
+
].flat();
|
|
1451
|
+
return {
|
|
1452
|
+
listing, identity, unit, data, resolution, interval, intraday, volume, freshness, notes,
|
|
1453
|
+
decimals: historyPriceDecimals(data, listing.saved?.metadata.assetCategory),
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** What a listing with no bar for what was asked says, in a warning and in a list's status column. */
|
|
1458
|
+
function emptyHistoryNote(symbol: string, { range, window }: HistoryAsk): string {
|
|
1459
|
+
return window ? `No bars for ${symbol} from ${priceWindowText(window)}.` : `No bars for ${symbol} in ${range}.`;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/** A bar's prices to the decimals its history prints them to. */
|
|
1463
|
+
function historyPrice(decimals: number) {
|
|
1464
|
+
return (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/** The columns a history table prints: Time in UTC for intraday bars, prices to `price`'s decimals. */
|
|
1468
|
+
function historyTextColumns(intraday: boolean, price: (value: unknown, row: Record<string, unknown>) => string): CliResultColumn[] {
|
|
1469
|
+
return [
|
|
1470
|
+
intraday
|
|
1471
|
+
? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
|
|
1472
|
+
: { key: "date", header: "Date" },
|
|
1473
|
+
{ key: "open", header: "Open", align: "right", format: price },
|
|
1474
|
+
{ key: "high", header: "High", align: "right", format: price },
|
|
1475
|
+
{ key: "low", header: "Low", align: "right", format: price },
|
|
1476
|
+
{ key: "close", header: "Close", align: "right", format: price },
|
|
1477
|
+
{ key: "volume", header: "Volume", align: "right", format: formatCountCell },
|
|
1478
|
+
];
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* The volume a table prints: a bar with one venue's count in place of the
|
|
1483
|
+
* market's shows it beside the venue, never as its volume, and a bar without
|
|
1484
|
+
* volume in a history that reports it prints as missing, never as 0 or blank.
|
|
1485
|
+
*/
|
|
1486
|
+
function historyVolumeCell(missingVolume: (row: Record<string, unknown>) => string) {
|
|
1487
|
+
return (value: unknown, row: Record<string, unknown>) => formatCountCell(value)
|
|
1488
|
+
|| (typeof row.venueVolume === "number"
|
|
1489
|
+
? cliStyles.muted(`${formatCountCell(row.venueVolume)} ${typeof row.volumeVenue === "string" ? row.volumeVenue : ""}`.trim())
|
|
1490
|
+
: missingVolume(row));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/** Text marks a bar still forming, rebuilt from hourly bars or closing before its settlement beside its flag; exports keep them as columns. */
|
|
1494
|
+
const HISTORY_FLAG_TEXT_COLUMN: CliResultColumn = {
|
|
1495
|
+
key: "flag",
|
|
1496
|
+
header: "Flag",
|
|
1497
|
+
format: (value, row) => [
|
|
1498
|
+
typeof value === "string" ? cliStyles.warning(value) : "",
|
|
1499
|
+
row.partial ? cliStyles.muted("partial") : "",
|
|
1500
|
+
row.filled ? cliStyles.muted("filled") : "",
|
|
1501
|
+
row.unsettled ? cliStyles.muted("unsettled") : "",
|
|
1502
|
+
typeof row.status === "string" ? cliStyles.warning(row.status) : "",
|
|
1503
|
+
].filter(Boolean).join(" "),
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
/** The dates `--from` and `--to` (or `--start` and `--end`) name, each given once. */
|
|
1507
|
+
function historyWindowOption(args: string[], rangeOption: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): PriceWindow | null {
|
|
1508
|
+
const bound = (flag: string, alias: string) => {
|
|
1509
|
+
const value = takeOption(args, flag);
|
|
1510
|
+
const aliased = takeOption(args, alias);
|
|
1511
|
+
if (value != null && aliased != null) ctx.fail(`${alias} is ${flag}; give one of them.`, `Usage: gloom ${HISTORY_USAGE}`);
|
|
1512
|
+
return { value: value ?? aliased, flag: aliased != null ? alias : flag };
|
|
1513
|
+
};
|
|
1514
|
+
const typedFrom = bound("--from", "--start");
|
|
1515
|
+
const typedTo = bound("--to", "--end");
|
|
1516
|
+
// Every message names the options as typed: an --end alone asks for a --start, not a --from.
|
|
1517
|
+
const from = { ...typedFrom, flag: typedFrom.value == null && typedTo.flag === "--end" ? "--start" : typedFrom.flag };
|
|
1518
|
+
const to = { ...typedTo, flag: typedTo.value == null && from.flag === "--start" ? "--end" : typedTo.flag };
|
|
1519
|
+
let window: PriceWindow | null;
|
|
1520
|
+
try {
|
|
1521
|
+
window = readPriceWindow(from.value, to.value, new Date(), undefined, { from: from.flag, to: to.flag });
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
return ctx.fail(errorMessage(error) ?? `Invalid ${from.flag} or ${to.flag}.`, `Usage: gloom ${HISTORY_USAGE}`);
|
|
1524
|
+
}
|
|
1525
|
+
if (window && rangeOption != null) ctx.fail(`${from.flag} replaces --range; give one of them.`, `Usage: gloom ${HISTORY_USAGE}`);
|
|
1526
|
+
return window;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** How many symbols of a list load at once: enough that a few hundred take a minute or two, not one call after another. */
|
|
1530
|
+
const HISTORY_LIST_CONCURRENCY = 8;
|
|
1531
|
+
|
|
1532
|
+
async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1533
|
+
const args = [...rawArgs];
|
|
1534
|
+
const rangeOption = takeOption(args, "--range");
|
|
1535
|
+
// --from 2007 --to 2009: those years' bars, daily up to five years, in place of a range ending now.
|
|
1536
|
+
const window = historyWindowOption(args, rangeOption, ctx);
|
|
1537
|
+
const ask: HistoryAsk = { range: parseRange(rangeOption, ctx), window };
|
|
1055
1538
|
const requestedExchange = takeOption(args, "--exchange");
|
|
1539
|
+
// A list from a file or standard input (`-`), or several symbols typed (AAPL MSFT, AAPL,MSFT), is one long table.
|
|
1540
|
+
// One argument with spaces stays one, as before: it may be a company's name.
|
|
1541
|
+
const file = args.some((arg) => arg === "--file" || arg.startsWith("--file=")) ? takeOption(args, "--file") ?? "" : undefined;
|
|
1542
|
+
if (file != null || args.includes("-") || uniqueEntries(args).length > 1 || args.some((arg) => /[,;]/.test(arg))) {
|
|
1543
|
+
return runHistoryList(args, file, requestedExchange, ask, ctx);
|
|
1544
|
+
}
|
|
1056
1545
|
const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
|
|
1057
1546
|
await withMarketData(ctx, async (market) => {
|
|
1058
1547
|
const listing = await requireCliListing(raw, requestedExchange, market, ctx);
|
|
1059
|
-
const
|
|
1060
|
-
const {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
|
|
1064
|
-
: market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
|
|
1065
|
-
const quoted = loadListingQuote(market.dataProvider, listing);
|
|
1066
|
-
const fate = listingFateLoader(listing, market, quoted);
|
|
1067
|
-
const [loaded, quote] = await Promise.all([
|
|
1068
|
-
loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
|
|
1069
|
-
command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history", fate,
|
|
1070
|
-
})
|
|
1071
|
-
.catch((error) => failHistory(error, listing.key, ctx)),
|
|
1072
|
-
quoted,
|
|
1073
|
-
]);
|
|
1074
|
-
const { resolution } = loaded;
|
|
1075
|
-
// While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
|
|
1076
|
-
const points = withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
|
|
1077
|
-
const fromQuote = points !== loaded.points;
|
|
1078
|
-
const identity = listingIdentity(listing, quote);
|
|
1079
|
-
// Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
|
|
1080
|
-
const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(logFailure("cli.commands-market", "Read quote metadata for history failed", null));
|
|
1081
|
-
const unit = historyUnit(listing.key, listed ?? quote);
|
|
1082
|
-
const traded = {
|
|
1083
|
-
listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
|
|
1084
|
-
};
|
|
1085
|
-
// A coin's volume is traded value in its quote currency: said in every row and on the status line.
|
|
1086
|
-
const volume = historyVolume(unit, traded);
|
|
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;
|
|
1089
|
-
// Bars stop arriving on the listing's own trading days, not New York's.
|
|
1090
|
-
const venue = quote?.listingExchangeName || exchange || undefined;
|
|
1091
|
-
// October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
|
|
1092
|
-
const forming = formingPeriodBar(served, venue);
|
|
1093
|
-
// A complete one is dated by the day its last session closed, the day its close is from.
|
|
1094
|
-
const closedDay = forming ? null : closedPeriodBarDay(served, venue);
|
|
1095
|
-
const data = forming?.rows ?? served;
|
|
1096
|
-
const interval = data[0]?.interval ?? null;
|
|
1097
|
-
const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
|
|
1098
|
-
const price = (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
|
|
1099
|
-
// Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
|
|
1100
|
-
const intraday = data.some((row) => row.date.length > 10);
|
|
1101
|
-
// A bar history: dated by its last bar, stale once bars of its size stop arriving.
|
|
1102
|
-
// The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
|
|
1103
|
-
const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
|
|
1104
|
-
const freshness = rowsFreshness(data, {
|
|
1105
|
-
...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
|
|
1106
|
-
observedKey: "date",
|
|
1107
|
-
oldest: null,
|
|
1108
|
-
cadenceExchange: venue,
|
|
1109
|
-
...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
|
|
1110
|
-
...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
|
|
1111
|
-
...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
|
|
1112
|
-
});
|
|
1113
|
-
const gaps = historyGapNote(data, traded);
|
|
1114
|
-
const notes = [
|
|
1115
|
-
// FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
|
|
1116
|
-
...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
|
|
1117
|
-
...await listingChoiceNotes(listing, market, quote, venues),
|
|
1118
|
-
...range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? []],
|
|
1119
|
-
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1120
|
-
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1121
|
-
historyBarStatusNote(forming ? served : data) ?? [],
|
|
1122
|
-
].flat();
|
|
1123
|
-
const priceColumns: CliResultColumn[] = [
|
|
1124
|
-
intraday
|
|
1125
|
-
? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
|
|
1126
|
-
: { key: "date", header: "Date" },
|
|
1127
|
-
{ key: "open", header: "Open", align: "right", format: price },
|
|
1128
|
-
{ key: "high", header: "High", align: "right", format: price },
|
|
1129
|
-
{ key: "low", header: "Low", align: "right", format: price },
|
|
1130
|
-
{ key: "close", header: "Close", align: "right", format: price },
|
|
1131
|
-
{ key: "volume", header: "Volume", align: "right", format: formatCountCell },
|
|
1132
|
-
];
|
|
1133
|
-
// A bar without volume in a history that reports it prints as missing, never as 0 or blank beside the others.
|
|
1548
|
+
const history = await loadListingHistory(listing, market, ctx, ask, { lookups: true });
|
|
1549
|
+
const { identity, unit, data, interval, intraday, volume, freshness, notes } = history;
|
|
1550
|
+
const price = historyPrice(history.decimals);
|
|
1551
|
+
// A bar without volume in a history that reports it prints as missing.
|
|
1134
1552
|
const missingVolume = historyReportsVolume(data) ? MISSING_VOLUME_TEXT : "";
|
|
1135
|
-
const
|
|
1136
|
-
: { ...column, format: (
|
|
1137
|
-
//
|
|
1138
|
-
const
|
|
1139
|
-
key: "flag", header: "Flag", format: (value) => typeof value === "string" ? cliStyles.warning(value) : "",
|
|
1140
|
-
};
|
|
1141
|
-
// Text also marks a bar still forming, rebuilt from hourly bars or closing before its settlement; exports keep them as fields.
|
|
1142
|
-
const flagTextColumn: CliResultColumn = {
|
|
1143
|
-
...flagColumn,
|
|
1144
|
-
format: (value, row) => [
|
|
1145
|
-
typeof value === "string" ? cliStyles.warning(value) : "",
|
|
1146
|
-
row.partial ? cliStyles.muted("partial") : "",
|
|
1147
|
-
row.filled ? cliStyles.muted("filled") : "",
|
|
1148
|
-
row.unsettled ? cliStyles.muted("unsettled") : "",
|
|
1149
|
-
].filter(Boolean).join(" "),
|
|
1150
|
-
};
|
|
1151
|
-
const marked = data.some((row) => row.partial || row.filled);
|
|
1152
|
-
const unsettled = data.some((row) => row.unsettled);
|
|
1553
|
+
const textColumns = historyTextColumns(intraday, price).map((column) => column.key !== "volume" ? column
|
|
1554
|
+
: { ...column, format: historyVolumeCell(() => missingVolume) });
|
|
1555
|
+
// The largest fall from a close to a later one in the bars shown, said under the heading.
|
|
1556
|
+
const drawdown = maxDrawdown(data);
|
|
1153
1557
|
ctx.printResult({
|
|
1154
1558
|
data,
|
|
1155
1559
|
metadata: {
|
|
1156
1560
|
...listingMetadata(identity),
|
|
1157
|
-
range,
|
|
1158
|
-
resolution,
|
|
1159
|
-
requestedRange: range,
|
|
1561
|
+
range: window ? null : ask.range,
|
|
1562
|
+
resolution: history.resolution,
|
|
1563
|
+
requestedRange: window ? null : ask.range,
|
|
1564
|
+
...(window ? { requestedWindow: window } : {}),
|
|
1160
1565
|
currency: unit.currency,
|
|
1161
1566
|
unit: unit.unit,
|
|
1162
1567
|
interval,
|
|
@@ -1165,25 +1570,141 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1165
1570
|
// The first bar after the last gap or jump; the first bar when there is none.
|
|
1166
1571
|
unbrokenFrom: historyUnbrokenFrom(data) ?? data[0]?.date ?? null,
|
|
1167
1572
|
bars: data.length,
|
|
1573
|
+
maxDrawdown: drawdown,
|
|
1168
1574
|
asOf: freshness?.asOf ?? null,
|
|
1169
1575
|
...(volume ? { volumeUnit: volume.unit, volumePeriod: interval === "1d" ? "UTC day" : interval } : {}),
|
|
1170
1576
|
},
|
|
1171
1577
|
...(notes.length > 0 ? { warnings: notes } : {}),
|
|
1172
1578
|
freshness,
|
|
1173
1579
|
}, {
|
|
1174
|
-
heading:
|
|
1580
|
+
heading: [listingHeading(identity), cliStyles.muted(historyFacts(unit, data).join(" · ")),
|
|
1581
|
+
...drawdown && interval ? [cliStyles.muted(maxDrawdownText(drawdown, { basis: `${BAR_INTERVAL_LABELS[interval]} closes`, price,
|
|
1582
|
+
date: (text) => intraday ? formatUtcTime(text) : text.slice(0, 10) }))] : []].join("\n"),
|
|
1175
1583
|
dateKey: "date",
|
|
1176
|
-
textColumns: [...
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
...marked ? [{ key: "partial", header: "Partial" }, { key: "filled", header: "Filled" }] : [],
|
|
1181
|
-
...unsettled ? [{ key: "unsettled", header: "Unsettled" }] : [],
|
|
1182
|
-
...volume ? [{ key: "volumeUnit", header: "Volume unit" }, { key: "volumeNote", header: "Volume note" }] : [],
|
|
1183
|
-
],
|
|
1584
|
+
textColumns: [...textColumns, HISTORY_FLAG_TEXT_COLUMN],
|
|
1585
|
+
// One header whatever the market is doing, for one symbol as for a list (HISTORY_EXPORT_COLUMNS).
|
|
1586
|
+
columns: HISTORY_EXPORT_COLUMNS,
|
|
1587
|
+
exportStatus: historyExportStatus,
|
|
1184
1588
|
// Every exported row says what it is a bar of, the currency a price is in and its bar size, so it survives head and concatenation.
|
|
1185
|
-
exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
|
|
1589
|
+
exportIdentity: { symbolOrSeries: history.listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
|
|
1590
|
+
});
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** A symbol of a list: its bars, or why it has none. */
|
|
1595
|
+
type ListedHistory = { symbol: string; history: ListingHistory } | { symbol: string; error: string };
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* `history --file holdings.txt` (or several symbols, or `-` for standard
|
|
1599
|
+
* input): every symbol's bars in one long table, `HISTORY_LIST_CONCURRENCY`
|
|
1600
|
+
* symbols loading at once. A symbol that serves nothing is one row with its
|
|
1601
|
+
* reason in `status`, and a warning; the command fails only when none served.
|
|
1602
|
+
*/
|
|
1603
|
+
async function runHistoryList(
|
|
1604
|
+
args: string[],
|
|
1605
|
+
file: string | undefined,
|
|
1606
|
+
exchange: string | undefined,
|
|
1607
|
+
ask: HistoryAsk,
|
|
1608
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1609
|
+
) {
|
|
1610
|
+
const usage = `Usage: gloom ${HISTORY_USAGE}`;
|
|
1611
|
+
await withMarketData(ctx, async (market) => {
|
|
1612
|
+
const list = await readListedSymbols(args, file, market, ctx, usage);
|
|
1613
|
+
const refused = list.symbols.flatMap((symbol) => notASymbolError(symbol) ?? []);
|
|
1614
|
+
const symbols = list.symbols.filter((symbol) => !notASymbolError(symbol));
|
|
1615
|
+
if (symbols.length === 0) ctx.fail(refused.length === 1 ? refused[0]!.message : `No history to read: ${refusedList(refused)} are not symbols.`, usage);
|
|
1616
|
+
// A long list is a batch, not a lookup: it skips the per-symbol venue and moved-company lookups, as `quote` does.
|
|
1617
|
+
const lookups = symbols.length <= MAX_CHOICE_LOOKUPS;
|
|
1618
|
+
const listed = await mapWithConcurrency(symbols, HISTORY_LIST_CONCURRENCY, async (symbol): Promise<ListedHistory> => {
|
|
1619
|
+
try {
|
|
1620
|
+
const listing = await resolveCliListing(symbol, exchange, market, { ownExchangeWins: true });
|
|
1621
|
+
return { symbol, history: await loadListingHistory(listing, market, ctx, ask, { lookups }) };
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
const message = errorMessage(error) ?? `No history for ${symbol}`;
|
|
1624
|
+
return { symbol, error: isCliFailure(error) ? oneLineFailure(message, error.details) : message };
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
const served = listed.flatMap((entry) => "history" in entry && entry.history.data.length > 0 ? [entry.history] : []);
|
|
1628
|
+
// A symbol with no bars, whether its request failed or came back empty, is one row with the reason.
|
|
1629
|
+
const rows: HistoryListRow[] = listed.flatMap((entry) => "history" in entry
|
|
1630
|
+
? entry.history.data.length > 0
|
|
1631
|
+
? entry.history.data.map((row) => ({ symbol: entry.history.listing.key, ...row }))
|
|
1632
|
+
: [historyFailureRow(entry.history.listing.key, emptyHistoryNote(entry.history.listing.key, ask))]
|
|
1633
|
+
: [historyFailureRow(entry.symbol, entry.error)]);
|
|
1634
|
+
const bySymbol = new Map(served.map((history) => [history.listing.key, history] as const));
|
|
1635
|
+
const notes = [
|
|
1636
|
+
...list.notes,
|
|
1637
|
+
...refused.length > 0 ? [`Not ${refused.length === 1 ? "a symbol" : "symbols"}, no history read: ${refusedList(refused)}.`] : [],
|
|
1638
|
+
...symbolListNotes(listed.map((entry) => ({
|
|
1639
|
+
symbol: "history" in entry ? entry.history.listing.key : entry.symbol,
|
|
1640
|
+
notes: "history" in entry ? entry.history.notes : [entry.error],
|
|
1641
|
+
}))),
|
|
1642
|
+
];
|
|
1643
|
+
// The closing lines date the list by its newest bar; each row's as_of is its own symbol's.
|
|
1644
|
+
const lastBars = served.flatMap((history) => history.data.at(-1) ?? []);
|
|
1645
|
+
const pastWindow = ask.window && isPastPriceWindow(ask.window);
|
|
1646
|
+
const listFreshness = served.length === 0 ? undefined : rowsFreshness(lastBars, pastWindow && ask.window ? {
|
|
1647
|
+
status: "not-a-feed", basis: `price bars, ${priceWindowText(ask.window)}`, observedKey: "date", oldest: null,
|
|
1648
|
+
} : {
|
|
1649
|
+
...barHistoryFreshness(served.some((history) => history.intraday) ? null : served[0]!.interval ?? "1d"),
|
|
1650
|
+
observedKey: "date",
|
|
1651
|
+
oldest: null,
|
|
1652
|
+
});
|
|
1653
|
+
// A saved copy is marked on the bars a symbol loaded, not on the rows drawn from them (loadListingHistory
|
|
1654
|
+
// reads it there): the list ends "couldn't refresh; saved copy from <time>" for the oldest copy among its symbols.
|
|
1655
|
+
const savedAt = served.flatMap((history) => history.freshness?.savedAt ?? []).sort()[0];
|
|
1656
|
+
const freshness = listFreshness && savedAt && !listFreshness.savedAt ? { ...listFreshness, savedAt } : listFreshness;
|
|
1657
|
+
const decimals = (row: Record<string, unknown>) => bySymbol.get(String(row.symbol))?.decimals ?? 2;
|
|
1658
|
+
const intraday = served.some((history) => history.intraday);
|
|
1659
|
+
const textColumns = historyTextColumns(intraday, (value, row) => historyPrice(decimals(row))(value)).map((column) => column.key !== "volume" ? column
|
|
1660
|
+
: { ...column, format: historyVolumeCell((row) => {
|
|
1661
|
+
const history = bySymbol.get(String(row.symbol));
|
|
1662
|
+
return history && row.date && historyReportsVolume(history.data) ? MISSING_VOLUME_TEXT : "";
|
|
1663
|
+
}) });
|
|
1664
|
+
ctx.printResult({
|
|
1665
|
+
data: rows,
|
|
1666
|
+
metadata: {
|
|
1667
|
+
range: ask.window ? null : ask.range,
|
|
1668
|
+
requestedRange: ask.window ? null : ask.range,
|
|
1669
|
+
...(ask.window ? { requestedWindow: ask.window } : {}),
|
|
1670
|
+
// Each symbol as a single `history` describes it, or the reason it has no bars.
|
|
1671
|
+
symbols: listed.map((entry) => "history" in entry ? {
|
|
1672
|
+
...listingMetadata(entry.history.identity),
|
|
1673
|
+
currency: entry.history.unit.currency,
|
|
1674
|
+
unit: entry.history.unit.unit,
|
|
1675
|
+
interval: entry.history.interval,
|
|
1676
|
+
firstDate: entry.history.data[0]?.date ?? null,
|
|
1677
|
+
lastDate: entry.history.data.at(-1)?.date ?? null,
|
|
1678
|
+
bars: entry.history.data.length,
|
|
1679
|
+
asOf: entry.history.freshness?.asOf ?? null,
|
|
1680
|
+
} : { symbol: entry.symbol, error: entry.error }),
|
|
1681
|
+
},
|
|
1682
|
+
...(notes.length > 0 ? { warnings: notes } : {}),
|
|
1683
|
+
...(served.length === 0 ? { failure: { code: "cli_error", message: `No history for ${symbols.join(", ")}.` } } : {}),
|
|
1684
|
+
freshness,
|
|
1685
|
+
// With no bars to date, an export still ends its rows with what each is of, under the same header.
|
|
1686
|
+
...(freshness ? {} : { exportFreshness: rowsFreshness([], { status: "not-a-feed", basis: "price bars" }) }),
|
|
1687
|
+
}, {
|
|
1688
|
+
seriesKey: "symbol",
|
|
1689
|
+
dateKey: "date",
|
|
1690
|
+
textColumns: [{ key: "symbol", header: "Symbol", shrink: false }, ...textColumns,
|
|
1691
|
+
{ key: "currency", header: "Cur", shrink: false, format: currencyCell }, HISTORY_FLAG_TEXT_COLUMN],
|
|
1692
|
+
columns: HISTORY_EXPORT_COLUMNS,
|
|
1693
|
+
exportStatus: historyExportStatus,
|
|
1694
|
+
// Every row names its symbol, the currency its prices are in, its bar size and the as-of of its own symbol's bars.
|
|
1695
|
+
exportIdentity: (row) => {
|
|
1696
|
+
const history = bySymbol.get(String(row.symbol));
|
|
1697
|
+
return {
|
|
1698
|
+
symbolOrSeries: String(row.symbol),
|
|
1699
|
+
currencyOrUnit: history ? history.unit.currency ?? history.unit.unit : null,
|
|
1700
|
+
interval: history?.interval ?? null,
|
|
1701
|
+
// A symbol with no bars has no as-of of its own.
|
|
1702
|
+
asOf: history ? history.freshness?.asOf ?? undefined : null,
|
|
1703
|
+
};
|
|
1704
|
+
},
|
|
1186
1705
|
});
|
|
1706
|
+
// The table and the warnings name each symbol with no bars; with none served, the exit status says so too.
|
|
1707
|
+
if (served.length === 0) process.exitCode = CLI_EXIT_CODES.error;
|
|
1187
1708
|
});
|
|
1188
1709
|
}
|
|
1189
1710
|
|
|
@@ -1234,9 +1755,12 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1234
1755
|
await withMarketData(ctx, async (market) => {
|
|
1235
1756
|
const listing = await requireCliListing(raw, exchangeOption, market, ctx);
|
|
1236
1757
|
const venues = loadBareVenues(listing, market);
|
|
1237
|
-
const
|
|
1758
|
+
const loaded = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
|
|
1238
1759
|
listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
|
|
1239
1760
|
), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
|
|
1761
|
+
// A converted TTM block at the rate `fx` reports, as `ticker` shows it; only the US dollar legs are read.
|
|
1762
|
+
const financials = view === "statements" ? loaded
|
|
1763
|
+
: await atReportTtmRate(loaded, createBaseConverter(market.dataProvider, "USD"));
|
|
1240
1764
|
// A trailing yield is measured from the cash history as DVD measures it, a forward yield dated ahead names the
|
|
1241
1765
|
// declared distribution it annualizes (not in JSON), and one whose dividend stopped says so; read beside the listing notes.
|
|
1242
1766
|
const distributions = view === "statements" ? null : loadDividendDistributions(
|
|
@@ -1259,7 +1783,8 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1259
1783
|
const freshness = view === "valuation" ? valuationFreshness(financials) : fundamentalsFreshness(financials);
|
|
1260
1784
|
// Market cap, the multiples and the yield at the regular session price, as the text gives them; the quote stays as sent.
|
|
1261
1785
|
const priced = atRegularSessionPrice(financials);
|
|
1262
|
-
|
|
1786
|
+
// One market cap per view: the share count times the price where the provider's sits more than 2% from it.
|
|
1787
|
+
const capitalization = viewMarketCapitalization(priced.quote, priced.fundamentals, regularSessionValuationPrice(priced.quote));
|
|
1263
1788
|
// The filings behind the latest statements, and for a bank its deposits, uninsured share and CRE.
|
|
1264
1789
|
const [statementFilings, bank] = view === "fundamentals"
|
|
1265
1790
|
? await Promise.all([
|
|
@@ -1307,8 +1832,13 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1307
1832
|
const freshness = rowsFreshness(rows, {
|
|
1308
1833
|
...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
|
|
1309
1834
|
});
|
|
1310
|
-
// The rows are the annual years served; older ones are not, and the closing lines say so
|
|
1311
|
-
|
|
1835
|
+
// The rows are the annual years served; older ones are not, and the closing lines say so, after how old
|
|
1836
|
+
// the latest year is when it ended over 15 months ago.
|
|
1837
|
+
const stale = stalePeriodNote(rows.map((row) => ({ date: row.date, lead: row.revenue != null })));
|
|
1838
|
+
// Years filled from the listing's exchange, for the revenue and net income columns.
|
|
1839
|
+
const filled = filledPeriodsNote(financials.annualStatements.filter((statement) => rows.some((row) => row.date === statement.date)),
|
|
1840
|
+
new Set(["totalRevenue", "netIncome"]));
|
|
1841
|
+
const historyNotes = [...stale ? [stale] : [], ...filled ? [filled] : [], ...statementHistoryNotes("annual", rows.map((row) => row.date), listing.key)];
|
|
1312
1842
|
// NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
|
|
1313
1843
|
const tables = tabular
|
|
1314
1844
|
? selectTables({
|
|
@@ -1348,9 +1878,9 @@ const NEWS_COLUMNS = [
|
|
|
1348
1878
|
header: "Published",
|
|
1349
1879
|
format: (value: unknown) => typeof value === "string" ? formatUtcTime(value) : "",
|
|
1350
1880
|
},
|
|
1351
|
-
{ key: "source", header: "Source"
|
|
1881
|
+
{ key: "source", header: "Source" },
|
|
1352
1882
|
{ key: "title", header: "Title" },
|
|
1353
|
-
{ key: "tickers", header: "Tickers"
|
|
1883
|
+
{ key: "tickers", header: "Tickers" },
|
|
1354
1884
|
{ key: "url", header: "URL", optional: true },
|
|
1355
1885
|
];
|
|
1356
1886
|
|
|
@@ -1365,7 +1895,14 @@ const NEWS_COLUMNS = [
|
|
|
1365
1895
|
async function printNewsSearch(
|
|
1366
1896
|
text: string,
|
|
1367
1897
|
articles: NewsArticle[],
|
|
1368
|
-
{ reason, empty, commodity = true, failure }: {
|
|
1898
|
+
{ reason, empty, commodity = true, failure, ticker = null }: {
|
|
1899
|
+
reason?: string;
|
|
1900
|
+
empty?: string;
|
|
1901
|
+
commodity?: boolean;
|
|
1902
|
+
failure?: string;
|
|
1903
|
+
/** The listing a coin pair's search stands in for. */
|
|
1904
|
+
ticker?: string | null;
|
|
1905
|
+
},
|
|
1369
1906
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1370
1907
|
) {
|
|
1371
1908
|
const searched = failure === undefined;
|
|
@@ -1374,7 +1911,7 @@ async function printNewsSearch(
|
|
|
1374
1911
|
ctx.printResult({
|
|
1375
1912
|
data: articles,
|
|
1376
1913
|
...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
|
|
1377
|
-
metadata: { ticker
|
|
1914
|
+
metadata: { ticker, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
|
|
1378
1915
|
freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
|
|
1379
1916
|
}, {
|
|
1380
1917
|
heading: newsSearchHeading(text, reason),
|
|
@@ -1384,6 +1921,50 @@ async function printNewsSearch(
|
|
|
1384
1921
|
});
|
|
1385
1922
|
}
|
|
1386
1923
|
|
|
1924
|
+
/** The ticker a metal's name is in capitals, when one answers to exactly that: GOLD, Gold.com Inc., for "gold". */
|
|
1925
|
+
async function tickerOfWord(word: string, market: MarketContext): Promise<{ symbol: string; name: string | null } | null> {
|
|
1926
|
+
const symbol = word.toUpperCase();
|
|
1927
|
+
const listing = await resolveCliListing(symbol, null, market).catch(() => {
|
|
1928
|
+
// Intentional ignore: a word no listing answers to offers no ticker under the metal's news.
|
|
1929
|
+
return null;
|
|
1930
|
+
});
|
|
1931
|
+
if (!listing || listing.symbol.toUpperCase() !== symbol) return null;
|
|
1932
|
+
const quote = await loadListingQuote(market.dataProvider, listing);
|
|
1933
|
+
const identity = listingIdentity(listing, quote);
|
|
1934
|
+
return quote !== null || identity.name != null ? { symbol: listing.symbol, name: identity.name ?? null } : null;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* A metal's market news: the stories tagged METALS that mention
|
|
1939
|
+
* it, for its future, its board code or its name. Under them, the ticker its
|
|
1940
|
+
* name is in capitals, whose company news `news GOLD` keeps.
|
|
1941
|
+
*/
|
|
1942
|
+
async function printMetalNews(route: MetalNewsRoute, limit: number, ctx: Parameters<CliCommandDef["execute"]>[1], market?: MarketContext) {
|
|
1943
|
+
const search = searchNewsStories(route.metal, limit, [METALS_NEWS_TOPIC]).then((found) => ({ found, failure: undefined }), (error: unknown) => {
|
|
1944
|
+
logFailure("cli.commands-market", "Search metals news failed")(error);
|
|
1945
|
+
return { found: [] as NewsArticle[], failure: errorMessage(error) ?? "no answer" };
|
|
1946
|
+
});
|
|
1947
|
+
const word = route.word;
|
|
1948
|
+
const ticker = word
|
|
1949
|
+
? await (market ? tickerOfWord(word, market) : withMarketData(ctx, (context) => tickerOfWord(word, context)))
|
|
1950
|
+
.catch(logFailure("cli.commands-market", "Read the ticker a metal's name is failed", null))
|
|
1951
|
+
: null;
|
|
1952
|
+
const { found, failure } = await search;
|
|
1953
|
+
const companyNews = ticker ? `gloom news ${ticker.symbol}` : null;
|
|
1954
|
+
ctx.printResult({
|
|
1955
|
+
data: found,
|
|
1956
|
+
...(failure === undefined ? {} : { warnings: [`Searching metals stories for "${route.metal}" failed: ${failure}`] }),
|
|
1957
|
+
metadata: { ticker: null, search: route.metal, topics: [METALS_NEWS_TOPIC], feed: null, ...(companyNews ? { companyNews } : {}) },
|
|
1958
|
+
freshness: rowsFreshness(newsRows(found), cloudNewsFreshness(await cloudRealtimeAccess())),
|
|
1959
|
+
}, {
|
|
1960
|
+
heading: metalNewsHeading(route),
|
|
1961
|
+
rows: newsRows,
|
|
1962
|
+
columns: NEWS_COLUMNS,
|
|
1963
|
+
empty: failure === undefined ? noMetalStoriesMessage(route) : "No stories to show.",
|
|
1964
|
+
...(ticker ? { footnote: `${word} ${cliArrow("→")} ${route.metal}'s market news. For ${ticker.name ?? ticker.symbol} (ticker ${ticker.symbol}), try: ${companyNews}` } : {}),
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1387
1968
|
async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1388
1969
|
const args = [...rawArgs];
|
|
1389
1970
|
const feed = parseNewsFeed(takeOption(args, "--feed"));
|
|
@@ -1392,27 +1973,42 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1392
1973
|
const limit = ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 20;
|
|
1393
1974
|
const text = newsSearchText(args);
|
|
1394
1975
|
if (text != null) {
|
|
1395
|
-
if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage:
|
|
1976
|
+
if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloom news <query>");
|
|
1396
1977
|
const problem = newsSearchTextProblem(text);
|
|
1397
1978
|
if (problem) ctx.fail(problem);
|
|
1398
1979
|
await printNewsSearch(text, await searchNewsStories(text, limit), {}, ctx);
|
|
1399
1980
|
return;
|
|
1400
1981
|
}
|
|
1401
|
-
|
|
1982
|
+
// A metal's future or name is its market's news, not a company's: GOLD in capitals stays Gold.com's.
|
|
1983
|
+
const metal = args.length === 1 && !exchangeOption && !feed ? metalNewsRoute(args[0]!) : null;
|
|
1984
|
+
if (metal) {
|
|
1985
|
+
await printMetalNews(metal, limit, ctx);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloom news <symbol> --exchange <code>");
|
|
1402
1989
|
await withMarketData(ctx, async (market) => {
|
|
1403
1990
|
const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
|
|
1404
|
-
const
|
|
1991
|
+
const quoteLoad = listing ? loadListingQuote(market.dataProvider, listing) : Promise.resolve(null);
|
|
1992
|
+
// A coin pair's feed searches for the coin when no story is filed under the pair, by the name its quote gives.
|
|
1993
|
+
const coin = !!listing && !feed && isRoundTheClockCoin(listing.request.symbol, listing.request.exchange);
|
|
1994
|
+
const loadNews = async () => market.dataProvider.getNews({
|
|
1405
1995
|
feed: feed ?? (listing ? "ticker" : "latest"),
|
|
1406
1996
|
// Still set for news plugins that read the deprecated scope.
|
|
1407
1997
|
scope: listing ? "ticker" : "global",
|
|
1408
1998
|
ticker: listing?.request.symbol,
|
|
1409
1999
|
exchange: listing?.request.exchange || undefined,
|
|
1410
2000
|
limit,
|
|
2001
|
+
...(coin ? { listingName: (await quoteLoad)?.name || listing!.saved?.metadata.name || undefined } : {}),
|
|
1411
2002
|
});
|
|
1412
2003
|
const [articles, quote] = await Promise.all([
|
|
1413
2004
|
listing ? loadForListing(listing, market, ctx, loadNews, (found) => found.length === 0) : loadNews(),
|
|
1414
|
-
|
|
2005
|
+
quoteLoad,
|
|
1415
2006
|
]);
|
|
2007
|
+
const searchedFor = listing ? articles[0]?.searchedFor : undefined;
|
|
2008
|
+
if (listing && searchedFor) {
|
|
2009
|
+
await printNewsSearch(searchedFor, articles, { reason: `no stories filed under ${listing.symbol}`, ticker: listing.key }, ctx);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
1416
2012
|
const identity = listing ? listingIdentity(listing, quote) : null;
|
|
1417
2013
|
const resolved = quote !== null || identity?.name != null;
|
|
1418
2014
|
const unfiled = !!listing && !listing.exchange && !feed && articles.length === 0;
|
|
@@ -1430,6 +2026,12 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1430
2026
|
const emptyMessage = (searched: boolean) => listing && identity
|
|
1431
2027
|
? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
|
|
1432
2028
|
: undefined;
|
|
2029
|
+
// A metal's board code no listing answers to (GC, SI) is the metal's market news.
|
|
2030
|
+
const metalCode = !resolved && unfiled && !exchangeOption ? metalCodeNewsRoute(args[0]!) : null;
|
|
2031
|
+
if (metalCode) {
|
|
2032
|
+
await printMetalNews(metalCode, limit, ctx, market);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
1433
2035
|
if (word && reason) {
|
|
1434
2036
|
// The lookup that found no ticker already answered; a search that fails leaves that answer.
|
|
1435
2037
|
const search = await searchNewsStories(word, limit).then((found) => ({ found, failure: undefined }), (error: unknown) => {
|
|
@@ -1447,7 +2049,7 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1447
2049
|
const typed = args[0]?.trim() ?? "";
|
|
1448
2050
|
const wordHint = listing && resolved && !listing.exchange && !feed && !exchangeOption && commonWordCandidate(typed, identity?.name)
|
|
1449
2051
|
&& wordMeansMore(await searchNewsStories(typed, 20).catch(logFailure("cli.commands-market", "Search news stories for a ticker word failed", [])), listing.symbol)
|
|
1450
|
-
? `${typed} → ${identity?.name ?? listing.symbol} (ticker ${listing.symbol}). For stories that mention ${typed.toLowerCase()}, try: ${quotedSearchCommand(typed.toLowerCase())}`
|
|
2052
|
+
? `${typed} ${cliArrow("→")} ${identity?.name ?? listing.symbol} (ticker ${listing.symbol}). For stories that mention ${typed.toLowerCase()}, try: ${quotedSearchCommand(typed.toLowerCase())}`
|
|
1451
2053
|
: null;
|
|
1452
2054
|
ctx.printResult({
|
|
1453
2055
|
data: articles,
|
|
@@ -1490,6 +2092,27 @@ async function registrantUsExchange(market: MarketContext, symbol: string, regis
|
|
|
1490
2092
|
return (venues.find((venue) => isUsListingExchange(venue.exchange)) ?? venues[0])?.exchange ?? null;
|
|
1491
2093
|
}
|
|
1492
2094
|
|
|
2095
|
+
/** ASX announcements as `filings` lists them for a listing that files nothing with the SEC. */
|
|
2096
|
+
const ANNOUNCEMENT_COLUMNS = [
|
|
2097
|
+
{ key: "date", header: "Released", format: (value: unknown) => typeof value === "string" ? `${value.slice(0, 16).replace("T", " ")} UTC` : "" },
|
|
2098
|
+
{ key: "headline", header: "Headline", maxWidth: 60 },
|
|
2099
|
+
{ key: "type", header: "Type", maxWidth: 36 },
|
|
2100
|
+
{ key: "priceSensitive", header: "Price sensitive", format: (value: unknown) => value === true ? "yes" : "" },
|
|
2101
|
+
{ key: "url", header: "URL", optional: true },
|
|
2102
|
+
];
|
|
2103
|
+
|
|
2104
|
+
const ASX_ANNOUNCEMENTS = { source: "ASX company announcements", status: "not-a-feed", basis: "exchange announcements", oldest: null } as const;
|
|
2105
|
+
|
|
2106
|
+
function announcementRows(payload: ExchangeAnnouncementsPayload) {
|
|
2107
|
+
return payload.announcements.map((item) => ({
|
|
2108
|
+
date: item.date,
|
|
2109
|
+
headline: item.headline,
|
|
2110
|
+
type: item.types.join("; "),
|
|
2111
|
+
priceSensitive: item.priceSensitive,
|
|
2112
|
+
url: item.url,
|
|
2113
|
+
}));
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1493
2116
|
async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1494
2117
|
const args = [...rawArgs];
|
|
1495
2118
|
// Filings come newest first, so --tail asks for the same first n as --limit.
|
|
@@ -1498,12 +2121,16 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1498
2121
|
const form = takeOption(args, "--form")?.trim() || null;
|
|
1499
2122
|
const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
|
|
1500
2123
|
await withMarketData(ctx, async (market) => {
|
|
1501
|
-
|
|
2124
|
+
// NST.AX is NST:ASX, as quote, valuation and the pane functions read it: the SEC is asked by venue.
|
|
2125
|
+
const listing = await requireCliListing(raw, exchangeOption, market, ctx, { venueSuffix: true });
|
|
1502
2126
|
const venues = loadBareVenues(listing, market);
|
|
1503
2127
|
const { symbol, exchange } = listing.request;
|
|
1504
2128
|
// A listing outside the US sends its company to the lookup, which needs its quote first.
|
|
1505
2129
|
const quotePromise = loadListingQuote(market.dataProvider, listing);
|
|
1506
2130
|
const identity = nonUsSecListingVenue(symbol, exchange) ? listingIdentity(listing, await quotePromise) : null;
|
|
2131
|
+
// Where a listing abroad publishes its announcements, whatever the SEC holds for it.
|
|
2132
|
+
const page = identity ? exchangeAnnouncementsPage(identity.symbol, identity.exchange) : null;
|
|
2133
|
+
const pageLine = page ? `${page.name}: ${page.url}` : null;
|
|
1507
2134
|
try {
|
|
1508
2135
|
const context = identity?.name ? { listingName: identity.name } : undefined;
|
|
1509
2136
|
const [filings, quote] = await Promise.all([
|
|
@@ -1517,9 +2144,17 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1517
2144
|
]);
|
|
1518
2145
|
const resolved = identity ?? listingIdentity(listing, quote);
|
|
1519
2146
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
2147
|
+
// A company on ASX that files nothing with the SEC lists its ASX announcements instead.
|
|
2148
|
+
if (identity && filings.length === 0 && !form && listsExchangeAnnouncements(identity.exchange)) {
|
|
2149
|
+
await printExchangeAnnouncements(ctx, { resolved, count, choice, page: page! });
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
1520
2152
|
ctx.printResult({
|
|
1521
2153
|
data: filingsWithItemLabels(filings),
|
|
1522
|
-
metadata: {
|
|
2154
|
+
metadata: {
|
|
2155
|
+
...listingMetadata(resolved), ...(form ? { form } : {}),
|
|
2156
|
+
...(page ? { announcementsPage: page.url } : {}),
|
|
2157
|
+
},
|
|
1523
2158
|
freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
|
|
1524
2159
|
...(choice.length > 0 ? { warnings: choice } : {}),
|
|
1525
2160
|
}, {
|
|
@@ -1527,10 +2162,12 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1527
2162
|
rows: filingRows,
|
|
1528
2163
|
columns: FILING_COLUMNS,
|
|
1529
2164
|
textColumns: FILING_TEXT_COLUMNS,
|
|
1530
|
-
// A listing abroad is looked up by its company: none found means the company files nothing with the SEC
|
|
1531
|
-
|
|
2165
|
+
// A listing abroad is looked up by its company: none found means the company files nothing with the SEC,
|
|
2166
|
+
// and the page its own exchange publishes its announcements on is where to read them.
|
|
2167
|
+
empty: [form ? `No ${form} filings found for ${listingTitle(resolved)}.`
|
|
1532
2168
|
: identity ? noSecReportsNotice(resolved)
|
|
1533
|
-
: `No SEC filings found for ${listingTitle(resolved)}.`,
|
|
2169
|
+
: `No SEC filings found for ${listingTitle(resolved)}.`, pageLine].filter(Boolean).join("\n"),
|
|
2170
|
+
...(pageLine ? { footnote: pageLine, exportNotes: [pageLine] } : {}),
|
|
1534
2171
|
});
|
|
1535
2172
|
} catch (error) {
|
|
1536
2173
|
if (!(error instanceof SecRegistrantMismatchError)) throw error;
|
|
@@ -1541,13 +2178,59 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1541
2178
|
const resolved = identity ?? listingIdentity(listing, await quotePromise);
|
|
1542
2179
|
ctx.printResult({
|
|
1543
2180
|
data: [] as SecFilingItem[],
|
|
1544
|
-
metadata: { ...listingMetadata(resolved), secRegistrant: error.registrantName },
|
|
2181
|
+
metadata: { ...listingMetadata(resolved), secRegistrant: error.registrantName, ...(page ? { announcementsPage: page.url } : {}) },
|
|
1545
2182
|
warnings: ctx.cliOptions.format === "text" ? undefined : [message],
|
|
1546
|
-
}, { rows: filingRows, columns: FILING_COLUMNS, textColumns: FILING_TEXT_COLUMNS, empty: message });
|
|
2183
|
+
}, { rows: filingRows, columns: FILING_COLUMNS, textColumns: FILING_TEXT_COLUMNS, empty: [message, pageLine].filter(Boolean).join("\n") });
|
|
1547
2184
|
}
|
|
1548
2185
|
});
|
|
1549
2186
|
}
|
|
1550
2187
|
|
|
2188
|
+
/**
|
|
2189
|
+
* `filings` for a company on ASX that files nothing with the SEC: its latest
|
|
2190
|
+
* ASX announcements from Gloom Cloud, newest first, under the line saying it
|
|
2191
|
+
* files no SEC reports, with its ASX announcements page. A failed read still
|
|
2192
|
+
* ends on that page, with the reason.
|
|
2193
|
+
*/
|
|
2194
|
+
async function printExchangeAnnouncements(
|
|
2195
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
2196
|
+
input: { resolved: ListingIdentity; count: number; choice: string[]; page: ExchangeAnnouncementsPage },
|
|
2197
|
+
): Promise<void> {
|
|
2198
|
+
const { resolved, count, choice, page } = input;
|
|
2199
|
+
const notice = noSecReportsNotice(resolved);
|
|
2200
|
+
const pageLine = `${page.name}: ${page.url}`;
|
|
2201
|
+
let payload: ExchangeAnnouncementsPayload | null = null;
|
|
2202
|
+
let failure: string | null = null;
|
|
2203
|
+
try {
|
|
2204
|
+
payload = await apiClient.getCloudExchangeAnnouncements({ symbol: resolved.symbol, exchange: resolved.exchange, limit: Math.min(count, 50) });
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
failure = `ASX announcements unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
2207
|
+
}
|
|
2208
|
+
const rows = payload ? announcementRows(payload) : [];
|
|
2209
|
+
const warnings = [
|
|
2210
|
+
...choice,
|
|
2211
|
+
...payload?.stale ? ["ASX could not be reached; these are the announcements Gloom Cloud read last."] : [],
|
|
2212
|
+
...failure ? [failure] : [],
|
|
2213
|
+
];
|
|
2214
|
+
ctx.printResult({
|
|
2215
|
+
data: rows,
|
|
2216
|
+
metadata: {
|
|
2217
|
+
...listingMetadata(resolved), secFilings: 0, announcementsPage: payload?.page ?? page.url,
|
|
2218
|
+
...(payload ? { announcementsTotal: payload.total, announcementsRetrievedAt: payload.retrievedAt } : {}),
|
|
2219
|
+
},
|
|
2220
|
+
freshness: rowsFreshness(rows, { ...ASX_ANNOUNCEMENTS, observedKey: "date" }),
|
|
2221
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
2222
|
+
}, {
|
|
2223
|
+
heading: listingHeading(resolved),
|
|
2224
|
+
...(rows.length > 0 ? { summary: () => `${notice} Its latest ASX announcements:` } : {}),
|
|
2225
|
+
rows: () => rows,
|
|
2226
|
+
columns: ANNOUNCEMENT_COLUMNS,
|
|
2227
|
+
empty: `${failure ? notice : `${notice} ASX lists no announcements for it.`}\n${pageLine}`,
|
|
2228
|
+
footnote: pageLine,
|
|
2229
|
+
exportNotes: [notice, pageLine],
|
|
2230
|
+
exportIdentity: { symbolOrSeries: `${resolved.symbol}:${resolved.exchange}` },
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
|
|
1551
2234
|
type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
|
|
1552
2235
|
|
|
1553
2236
|
/** The company's own Form 4 filings as `fn INS` reads them: whether it has any, and its summary of them. */
|
|
@@ -1620,7 +2303,7 @@ function insiderSummary(
|
|
|
1620
2303
|
const transactions = form4?.status === "available"
|
|
1621
2304
|
? [
|
|
1622
2305
|
...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}` : ""}:
|
|
2306
|
+
`${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}` : ""}: gloom fn INS ${listing.key}`,
|
|
1624
2307
|
].join("\n")
|
|
1625
2308
|
: form4?.status === "no-sec-reports" ? [noSecReportsNotice(identity), form4.home].filter(Boolean).join(" ")
|
|
1626
2309
|
: form4 ? insiderEmptyNotice(form4.status === "foreign-private-issuer", form4.home) : "";
|
|
@@ -1651,7 +2334,8 @@ async function printBeneficialOwners(
|
|
|
1651
2334
|
const sharesOutstanding = classSharesToday(financials);
|
|
1652
2335
|
const rows = buildBeneficialReportRows(payload, { history, holders, sharesOutstanding });
|
|
1653
2336
|
const notices = beneficialCoverageNotices(payload.coverage);
|
|
1654
|
-
const
|
|
2337
|
+
const sharesOutstandingAsOf = classSharesAsOf(financials);
|
|
2338
|
+
const legend = beneficialReportLegend(rows, sharesOutstanding, sharesOutstandingAsOf);
|
|
1655
2339
|
ctx.printResult({
|
|
1656
2340
|
data: rows,
|
|
1657
2341
|
...(notices.length ? { warnings: notices } : {}),
|
|
@@ -1665,6 +2349,7 @@ async function printBeneficialOwners(
|
|
|
1665
2349
|
coverage: payload.coverage,
|
|
1666
2350
|
complete: beneficialListComplete(payload),
|
|
1667
2351
|
sharesOutstanding,
|
|
2352
|
+
...(sharesOutstandingAsOf ? { sharesOutstandingAsOf } : {}),
|
|
1668
2353
|
},
|
|
1669
2354
|
}, {
|
|
1670
2355
|
heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
|
|
@@ -1687,6 +2372,9 @@ async function printBeneficialOwners(
|
|
|
1687
2372
|
});
|
|
1688
2373
|
}
|
|
1689
2374
|
|
|
2375
|
+
/** Holder rows `holders` and `13f` print without `--limit`, as fn HDS does. */
|
|
2376
|
+
const HOLDERS_DEFAULT_ROWS = 25;
|
|
2377
|
+
|
|
1690
2378
|
async function runHolders(
|
|
1691
2379
|
rawArgs: string[],
|
|
1692
2380
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
@@ -1705,8 +2393,15 @@ async function runHolders(
|
|
|
1705
2393
|
const listing = await requireCliListing(raw, exchangeOption, market, ctx);
|
|
1706
2394
|
if (form !== "13f") return printBeneficialOwners(listing, market, form, history, ctx);
|
|
1707
2395
|
const insiderCommand = commandName === "insider" && ownerTypes != null;
|
|
2396
|
+
const institutional = !ownerTypes?.has("insider");
|
|
1708
2397
|
const venues = loadBareVenues(listing, market);
|
|
1709
|
-
|
|
2398
|
+
// A US line's institutions are every 13F filer, from the SEC holder index as fn 13F lists them,
|
|
2399
|
+
// as many as the limit asks (HDS's 25 by default); elsewhere, or when the index cannot be read, the source's few.
|
|
2400
|
+
const indexTicker = institutional ? indexHolderTicker(listing.request.symbol, listing.request.exchange) : null;
|
|
2401
|
+
// --tail counts back from the end of the rows loaded, so it loads at least as many as it asks for.
|
|
2402
|
+
const tail = ctx.cliOptions.tail;
|
|
2403
|
+
const indexLimit = Math.min(ctx.cliOptions.limit ?? Math.max(tail ?? 0, HOLDERS_DEFAULT_ROWS), HOLDER_ROWS_MAX);
|
|
2404
|
+
const [source, quote, form4, index] = await Promise.all([
|
|
1710
2405
|
loadForListing(
|
|
1711
2406
|
listing, market, ctx,
|
|
1712
2407
|
() => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
|
|
@@ -1715,7 +2410,12 @@ async function runHolders(
|
|
|
1715
2410
|
),
|
|
1716
2411
|
loadListingQuote(market.dataProvider, listing),
|
|
1717
2412
|
insiderCommand ? loadForm4Report(market, listing) : null,
|
|
2413
|
+
indexTicker ? loadIndexedHolders(indexTicker, indexLimit).catch(logFailure("cli.commands-market", "Read the 13F holder index failed", null)) : null,
|
|
1718
2414
|
]);
|
|
2415
|
+
const indexed = !!index?.rows.length && indexHolderTicker(listing.request.symbol, listing.request.exchange, source) != null;
|
|
2416
|
+
const data = indexed
|
|
2417
|
+
? withIndexedHolders(source, indexTicker!, { ...index!, rows: index!.rows.slice(0, indexLimit) }, indexedHolderUnits(source, quote?.price))
|
|
2418
|
+
: source;
|
|
1719
2419
|
const identity = listingIdentity(listing, quote);
|
|
1720
2420
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
1721
2421
|
// A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
|
|
@@ -1727,7 +2427,6 @@ async function runHolders(
|
|
|
1727
2427
|
});
|
|
1728
2428
|
const shownRows = windowRows(rows, ctx.cliOptions).rows;
|
|
1729
2429
|
const shown = shownRows.length;
|
|
1730
|
-
const institutional = !ownerTypes?.has("insider");
|
|
1731
2430
|
const total = institutional ? data.summary?.institutionsCount ?? null : null;
|
|
1732
2431
|
const reportDate = sharedReportDate(rows);
|
|
1733
2432
|
const valueBasis = rows.length > 0 ? holderValueBasis(reportDate, data.valueBasis, valueCurrency) : null;
|
|
@@ -1755,7 +2454,13 @@ async function runHolders(
|
|
|
1755
2454
|
: rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
|
|
1756
2455
|
// Named as fn HDS takes it, so the closing notes point to the same listing.
|
|
1757
2456
|
const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
|
|
1758
|
-
const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
|
|
2457
|
+
const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total, indexed });
|
|
2458
|
+
// The index's smallest filers are pages past what was loaded: --tail is the end of the loaded rows, not of every filer.
|
|
2459
|
+
const tailNote = indexed && tail != null && total != null && rows.length < total
|
|
2460
|
+
? `--tail ${tail} is the last ${shown} of the largest ${rows.length} loaded, not the smallest of all ${total.toLocaleString("en-US")} holders.`
|
|
2461
|
+
: null;
|
|
2462
|
+
const partialNote = indexed ? indexedHoldersPartialNote(index, indexLimit) : null;
|
|
2463
|
+
const warnings = [...choice, ...(partialNote ? [partialNote] : []), ...(tailNote ? [tailNote] : [])];
|
|
1759
2464
|
ctx.printResult({
|
|
1760
2465
|
data: rows,
|
|
1761
2466
|
...(freshness ? { freshness } : {}),
|
|
@@ -1780,7 +2485,7 @@ async function runHolders(
|
|
|
1780
2485
|
form4: form4 ? { summary: form4.summary, lines: form4.lines, filings: form4.filings, newestFiled: form4.newestFiled, notCovered: form4.home } : null,
|
|
1781
2486
|
} : {}),
|
|
1782
2487
|
},
|
|
1783
|
-
...(
|
|
2488
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
1784
2489
|
}, {
|
|
1785
2490
|
heading: `${listingHeading(identity)}${cliStyles.muted(facts.map((fact) => ` · ${fact}`).join(""))}`,
|
|
1786
2491
|
columns,
|
|
@@ -1835,6 +2540,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1835
2540
|
]);
|
|
1836
2541
|
const identity = listingIdentity(listing, quote);
|
|
1837
2542
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
2543
|
+
const { upside, reference } = analystUpside(data, quote);
|
|
1838
2544
|
ctx.printResult({
|
|
1839
2545
|
data,
|
|
1840
2546
|
metadata: {
|
|
@@ -1842,6 +2548,8 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1842
2548
|
recommendationRating: data.recommendationRating,
|
|
1843
2549
|
priceTarget: data.priceTarget,
|
|
1844
2550
|
recommendations: data.recommendations,
|
|
2551
|
+
upside: upside ?? null,
|
|
2552
|
+
upsideReference: analystReferenceMetadata(reference),
|
|
1845
2553
|
},
|
|
1846
2554
|
freshness: rowsFreshness(analystRows(data), {
|
|
1847
2555
|
...REPORTED_DATA, basis: "analyst ratings", observedKey: "date", oldest: null,
|
|
@@ -1850,7 +2558,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1850
2558
|
}, {
|
|
1851
2559
|
heading: listingHeading(identity),
|
|
1852
2560
|
rows: analystRows,
|
|
1853
|
-
summary: analystSummary,
|
|
2561
|
+
summary: (research) => analystSummary(research, quote),
|
|
1854
2562
|
columns: [
|
|
1855
2563
|
{ key: "date", header: "Date" },
|
|
1856
2564
|
{ key: "firm", header: "Firm" },
|
|
@@ -1872,13 +2580,18 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
|
|
|
1872
2580
|
await withMarketData(ctx, async (market) => {
|
|
1873
2581
|
const listing = await requireCliListing(raw, exchangeOption, market, ctx);
|
|
1874
2582
|
const venues = loadBareVenues(listing, market);
|
|
1875
|
-
|
|
2583
|
+
// The earnings rows are resolved against the calendar `earnings` reads, so the next report is the same in both.
|
|
2584
|
+
const calendar = market.dataProvider.getEarningsCalendar?.([listing.key])
|
|
2585
|
+
.catch(logFailure("cli.commands-market", "Read the earnings calendar for events failed", [] as EarningsEvent[]));
|
|
2586
|
+
const [recorded, quote, events] = await Promise.all([
|
|
1876
2587
|
loadForListing(
|
|
1877
2588
|
listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange),
|
|
1878
2589
|
undefined, { command: "events", noun: "corporate events" },
|
|
1879
2590
|
),
|
|
1880
2591
|
loadListingQuote(market.dataProvider, listing),
|
|
2592
|
+
calendar,
|
|
1881
2593
|
]);
|
|
2594
|
+
const data = withResolvedEarnings(recorded, calendarEventOf(events, listing.key), newYorkDay());
|
|
1882
2595
|
const identity = listingIdentity(listing, quote);
|
|
1883
2596
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
1884
2597
|
ctx.printResult({
|
|
@@ -1909,14 +2622,14 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1909
2622
|
// With --leaps, --expiration is a window of one expiry.
|
|
1910
2623
|
const expirationGiven = expiration != null || rawArgs.some((arg) => arg === "--expiration" || arg.startsWith("--expiration="));
|
|
1911
2624
|
const criteria = parseLeapsCriteria({ ...leapsOptions.raw, ...expirationGiven ? { "--expiration": expiration ?? "" } : {} });
|
|
1912
|
-
if ("error" in criteria) ctx.fail(criteria.error, `Usage:
|
|
2625
|
+
if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloom ${LEAPS_USAGE}`);
|
|
1913
2626
|
const entries = normalizeSymbols(args);
|
|
1914
|
-
if (entries.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage:
|
|
2627
|
+
if (entries.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloom ${LEAPS_USAGE}`);
|
|
1915
2628
|
await withMarketData(ctx, async (market) => runLeapsScreen(await readSymbols(entries, market.dataProvider), exchangeOption, criteria, market, ctx));
|
|
1916
2629
|
return;
|
|
1917
2630
|
}
|
|
1918
2631
|
if (leapsOptions.given.length > 0) {
|
|
1919
|
-
ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage:
|
|
2632
|
+
ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage: gloom ${LEAPS_USAGE}`);
|
|
1920
2633
|
}
|
|
1921
2634
|
const raw = requireOneArg(args, OPTIONS_USAGE, "symbol", ctx);
|
|
1922
2635
|
let expirationDate: number | undefined;
|
|
@@ -1925,7 +2638,7 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1925
2638
|
if (parsed == null) {
|
|
1926
2639
|
ctx.fail(
|
|
1927
2640
|
expiration == null ? "--expiration needs a date." : `Invalid --expiration "${expiration}".`,
|
|
1928
|
-
`Use ${OPTION_EXPIRATION_FORMAT}
|
|
2641
|
+
`Use ${OPTION_EXPIRATION_FORMAT}.${expiration == null ? "" : expirationHint(expiration)}\nUsage: gloom ${OPTIONS_USAGE}`,
|
|
1929
2642
|
);
|
|
1930
2643
|
}
|
|
1931
2644
|
expirationDate = parsed;
|
|
@@ -1938,30 +2651,58 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1938
2651
|
const quotePromise = loadListingQuote(market.dataProvider, listing);
|
|
1939
2652
|
const fate = listingFateLoader(listing, market, quotePromise);
|
|
1940
2653
|
const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
|
|
1941
|
-
const
|
|
1942
|
-
|
|
2654
|
+
const now = Date.now();
|
|
2655
|
+
// Expiries close on the listing's clock: a coin's options (Deribit) settle at 08:00 UTC on any day.
|
|
2656
|
+
const expiryVenue = optionExpiryVenue(symbol, exchange);
|
|
2657
|
+
const readChain = async (expiry: number | undefined) => {
|
|
2658
|
+
const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expiry, undefined])
|
|
1943
2659
|
.load({ force: refresh });
|
|
1944
2660
|
return {
|
|
1945
2661
|
result: cached,
|
|
1946
|
-
chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange,
|
|
2662
|
+
chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expiry, {
|
|
1947
2663
|
cacheMode: refresh ? "refresh" : "default",
|
|
1948
2664
|
}),
|
|
1949
2665
|
};
|
|
2666
|
+
};
|
|
2667
|
+
// An expiry whose last session has closed has no chain to show: it says so, and names the nearest live one.
|
|
2668
|
+
if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now, expiryVenue)) {
|
|
2669
|
+
const listed = await readChain(undefined).then(({ chain }) => chain.expirationDates,
|
|
2670
|
+
logFailure("cli-market", "Read available option expirations failed", []));
|
|
2671
|
+
ctx.fail(`expiry ${expiryIsoDate(expirationDate)} for ${listing.key} `
|
|
2672
|
+
+ expiredExpiryText(expirationDate, listed, now, { exchange: expiryVenue, flag: "expiration" }),
|
|
2673
|
+
`Run gloom options ${listing.key} to list the live expiries.`);
|
|
2674
|
+
}
|
|
2675
|
+
const { result, chain: read } = await loadForListing(listing, market, ctx, async () => {
|
|
2676
|
+
const first = await readChain(expirationDate);
|
|
2677
|
+
// A source can still default to the expiry that lapsed at Friday's close; the nearest live one is shown instead.
|
|
2678
|
+
const live = expirationDate == null ? liveDefaultExpiry(first.chain, now, expiryVenue) : undefined;
|
|
2679
|
+
if (live === null) return { ...first, chain: { ...first.chain, calls: [], puts: [] } };
|
|
2680
|
+
if (live == null) return first;
|
|
2681
|
+
// The default's list stays the catalogue: a dated chain can list fewer expiries.
|
|
2682
|
+
const shown = await readChain(live);
|
|
2683
|
+
return { ...shown, chain: { ...shown.chain,
|
|
2684
|
+
expirationDates: [...new Set([...first.chain.expirationDates, ...shown.chain.expirationDates])].sort((a, b) => a - b) } };
|
|
1950
2685
|
}, undefined, { command: "options", noun: "options chain", fate }).catch((error: unknown) => {
|
|
1951
2686
|
if (!isOptionsUnavailableError(error)) throw error;
|
|
2687
|
+
// A coin other than BTC and ETH: say which coins have options, and the bitcoin-linked listings.
|
|
2688
|
+
if (isCoinWithoutOptions(listing.key)) return ctx.fail(optionsUnavailableTitle(listing.key), coinOptionsAlternativesText("options"));
|
|
1952
2689
|
// A future or commodity with no chain of its own: name the listed fund whose options stand in.
|
|
1953
2690
|
const alternative = listedOptionsAlternative(listing.key);
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2691
|
+
// A name read as a fund or an index asks about the company's stock it means, whose options are listed.
|
|
2692
|
+
const meant = meantQuestion(listing);
|
|
2693
|
+
return ctx.fail(meant ? `${optionsUnavailableTitle(listing.key)} ${meant}` : optionsUnavailableTitle(listing.key), alternative
|
|
2694
|
+
? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}), gloom options ${alternative.symbol}.`
|
|
2695
|
+
+ ` Strategy payoffs: gloom fn OSA ${alternative.symbol}`
|
|
2696
|
+
: "Strategy payoffs on a listed underlying: gloom fn OSA <ticker>");
|
|
1958
2697
|
});
|
|
2698
|
+
const loaded = withLiveExpiryList(read, now, expiryVenue);
|
|
1959
2699
|
const inputs = await inputsPromise;
|
|
1960
2700
|
if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
|
|
1961
2701
|
// An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
|
|
1962
2702
|
const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
|
|
1963
2703
|
.getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
|
|
1964
|
-
.then((fallback) => fallback.expirationDates,
|
|
2704
|
+
.then((fallback) => liveOptionExpiries(fallback.expirationDates, now, expiryVenue),
|
|
2705
|
+
logFailure("cli-market", "Read available option expirations failed", []));
|
|
1965
2706
|
const miss = missingExpiryMessage(listing.key, expirationDate, listed);
|
|
1966
2707
|
ctx.fail(miss.message, miss.details);
|
|
1967
2708
|
}
|
|
@@ -1979,7 +2720,10 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1979
2720
|
: `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
|
|
1980
2721
|
+ (chain.asOf ? ` (last trade ${chain.asOf})` : "");
|
|
1981
2722
|
const sessionWarning = refreshWarning ? null : priorSessionChainWarning(chain, identity.exchange, Date.now());
|
|
1982
|
-
|
|
2723
|
+
// A venue that marks its contracts (Deribit) states IV and greeks without a quote.
|
|
2724
|
+
const marked = isMarkedChain(chain);
|
|
2725
|
+
const coin = chainCoinCurrency(chain);
|
|
2726
|
+
const modelWarning = !marked && inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
|
|
1983
2727
|
? `IV and delta need a current ${listing.key} quote; those columns are blank`
|
|
1984
2728
|
: null;
|
|
1985
2729
|
const listed = chain.expirationDates.length > 0 || chain.calls.length > 0 || chain.puts.length > 0;
|
|
@@ -2006,25 +2750,28 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2006
2750
|
// `iv` is annualised volatility and `delta` the option's change per 1.00 move in the underlying, both decimals.
|
|
2007
2751
|
units: { iv: "annualized decimal", delta: "decimal per 1.00 move in the underlying" },
|
|
2008
2752
|
// What each contract's `iv` and `impliedVolatility` are, so neither is read as the other.
|
|
2009
|
-
fields: OPTION_VOLATILITY_FIELDS,
|
|
2753
|
+
fields: marked ? MARKED_OPTION_FIELDS : OPTION_VOLATILITY_FIELDS,
|
|
2010
2754
|
// What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
|
|
2011
|
-
model:
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2755
|
+
model: marked
|
|
2756
|
+
? { venue: chain.venue ?? null, marks: "venue", rate: chain.rate ?? 0, basis: "the expiry's forward", coin }
|
|
2757
|
+
: {
|
|
2758
|
+
spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
|
|
2759
|
+
valuationTime: modelSpot ? new Date(modelSpot.valuationTime).toISOString() : null,
|
|
2760
|
+
dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE,
|
|
2761
|
+
},
|
|
2016
2762
|
openInterest: { asOf: basisFields.openInterestAsOf, listedAsOf: basis.openInterest?.listedDate ?? null,
|
|
2017
2763
|
compared: basis.openInterest?.compared ?? 0, matched: basis.openInterest?.matched ?? 0 },
|
|
2018
2764
|
},
|
|
2019
2765
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
2020
2766
|
freshness,
|
|
2021
2767
|
}, {
|
|
2022
|
-
|
|
2768
|
+
// Options a venue other than the US exchanges lists (Deribit's) are headed by that venue, not the coin's board.
|
|
2769
|
+
heading: chain.venue ? `${cliStyles.bold(identity.name || identity.symbol)}${cliStyles.muted(` · ${chain.venue}`)}` : listingHeading(identity),
|
|
2023
2770
|
rows: (data: OptionsChain) => optionRows(data).map((row) => ({ ...row, ...basisFields })),
|
|
2024
2771
|
// Choosing an expiry needs the list; once one is chosen the table is that expiry.
|
|
2025
2772
|
summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
|
|
2026
2773
|
empty: `No option contracts for ${listingTitle(identity)}.`,
|
|
2027
|
-
...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis) } : {}),
|
|
2774
|
+
...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis, chain) } : {}),
|
|
2028
2775
|
exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: currency, interval: OPTIONS_EXPORT_INTERVAL },
|
|
2029
2776
|
// Exports state each decimal's unit in its header and end every row with the spot and dates it is as of.
|
|
2030
2777
|
columns: [
|
|
@@ -2043,8 +2790,34 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2043
2790
|
{ key: "spotBasis", header: "Spot basis" },
|
|
2044
2791
|
{ key: "spotAsOf", header: "Spot as of" },
|
|
2045
2792
|
{ key: "openInterestAsOf", header: "OI as of" },
|
|
2793
|
+
// A venue's marks and its coin prices (Deribit's BTC and ETH options).
|
|
2794
|
+
...(marked ? [
|
|
2795
|
+
{ key: "mark", header: "Mark (USD)" }, { key: "gamma", header: "Gamma (per 1.00)" },
|
|
2796
|
+
{ key: "vega", header: "Vega (USD per vol point)" }, { key: "theta", header: "Theta (USD per day)" },
|
|
2797
|
+
{ key: "forward", header: "Forward" }, { key: "expiresAt", header: "Settles" },
|
|
2798
|
+
] : []),
|
|
2799
|
+
...(coin ? [
|
|
2800
|
+
{ key: "coinBid", header: `Bid (${coin})` }, { key: "coinAsk", header: `Ask (${coin})` },
|
|
2801
|
+
{ key: "coinMark", header: `Mark (${coin})` }, { key: "coinLast", header: `Last (${coin})` },
|
|
2802
|
+
] : []),
|
|
2046
2803
|
],
|
|
2047
|
-
|
|
2804
|
+
// A coin's options are quoted in the coin: its bid, ask and mark, then the mark in dollars and the venue's greeks.
|
|
2805
|
+
textColumns: coin ? [
|
|
2806
|
+
{ key: "side", header: "Side" },
|
|
2807
|
+
{ key: "contract", header: "Contract", shrink: false },
|
|
2808
|
+
{ key: "strike", header: "Strike", align: "right" },
|
|
2809
|
+
{ key: "coinBid", header: `Bid ${coin}`, align: "right", format: formatCoinPremiumCell },
|
|
2810
|
+
{ key: "coinAsk", header: `Ask ${coin}`, align: "right", format: formatCoinPremiumCell },
|
|
2811
|
+
{ key: "coinMark", header: `Mark ${coin}`, align: "right", format: formatCoinPremiumCell },
|
|
2812
|
+
{ key: "mark", header: "Mark $", align: "right", format: formatOptionDollarCell },
|
|
2813
|
+
{ key: "iv", header: "IV", align: "right", format: formatOptionIvCell },
|
|
2814
|
+
{ key: "delta", header: "Delta", align: "right", format: formatOptionDeltaCell },
|
|
2815
|
+
{ key: "gamma", header: "Gamma", align: "right", format: formatOptionGammaCell, optional: true, dropPriority: 3 },
|
|
2816
|
+
{ key: "vega", header: "Vega $", align: "right", format: formatOptionDollarCell, optional: true, dropPriority: 2 },
|
|
2817
|
+
{ key: "theta", header: "Theta $", align: "right", format: formatOptionDollarCell, optional: true, dropPriority: 2 },
|
|
2818
|
+
{ key: "volume", header: "Vol", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
|
|
2819
|
+
{ key: "openInterest", header: "OI", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
|
|
2820
|
+
] : [
|
|
2048
2821
|
{ key: "side", header: "Side" },
|
|
2049
2822
|
{ key: "contract", header: "Contract", shrink: false },
|
|
2050
2823
|
// The contract symbol already carries the date, so this goes first when the table is narrow.
|
|
@@ -2162,15 +2935,35 @@ async function runFxOnDate(
|
|
|
2162
2935
|
async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
2163
2936
|
const args = [...rawArgs];
|
|
2164
2937
|
const dateOption = takeOption(args, "--date");
|
|
2165
|
-
|
|
2938
|
+
const forwardOption = takeOption(args, "--forward");
|
|
2939
|
+
if (args.length === 0) ctx.fail(`Usage: gloom ${FX_USAGE}`);
|
|
2940
|
+
// A forward is implied from today's spot and the latest short rates, which are all Gloom Cloud serves.
|
|
2941
|
+
if (dateOption != null && forwardOption != null) {
|
|
2942
|
+
ctx.fail(
|
|
2943
|
+
"fx --forward does not take --date: the forward is implied from today's spot and the latest short rates, and past short rates are not served.",
|
|
2944
|
+
`For that day's close: gloom fx ${args.join(" ")} --date ${dateOption}. For today's forward, drop --date.`,
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2166
2947
|
const date = dateOption == null ? null : readFxDate(dateOption, ctx);
|
|
2948
|
+
const tenor = forwardOption == null ? null : parseForwardTenor(forwardOption);
|
|
2949
|
+
if (tenor && "error" in tenor) ctx.fail(tenor.error, `Usage: gloom ${FX_FORWARD_USAGE}`);
|
|
2167
2950
|
await withMarketData(ctx, async (market) => {
|
|
2168
2951
|
const parsed = parseFxArgs(args, market.config.baseCurrency);
|
|
2169
|
-
if ("error" in parsed) return ctx.fail(parsed.error, `Usage:
|
|
2952
|
+
if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloom ${forwardOption == null ? FX_USAGE : FX_FORWARD_USAGE}`);
|
|
2170
2953
|
if (date) return runFxOnDate(parsed, date, args, market, ctx);
|
|
2171
2954
|
const { request, amount, toUsdByDefault } = parsed;
|
|
2172
2955
|
const { currency, baseCurrency } = request;
|
|
2173
2956
|
const pair = `${currency}/${baseCurrency}`;
|
|
2957
|
+
if (tenor) {
|
|
2958
|
+
if (amount != null) ctx.fail("--forward prices a pair, not an amount.", `Usage: gloom ${FX_FORWARD_USAGE}`);
|
|
2959
|
+
if (currency === baseCurrency) ctx.fail(`${pair} has no forward: both sides are ${currency}.`, `Usage: gloom ${FX_FORWARD_USAGE}`);
|
|
2960
|
+
}
|
|
2961
|
+
// The short rates are read alongside spot. A failed read is held as a value, not left to reject
|
|
2962
|
+
// unobserved while spot loads, and is raised where the forward needs the rates.
|
|
2963
|
+
const shortRates = tenor && !("error" in tenor) ? apiClient.getCloudShortRates().then(
|
|
2964
|
+
(payload) => ({ payload }),
|
|
2965
|
+
(error: unknown) => ({ error: unavailableOnServer(error, "Forwards are not available from Gloom Cloud yet: it serves no short rates.") }),
|
|
2966
|
+
) : null;
|
|
2174
2967
|
const reasons = new Map<string, string>();
|
|
2175
2968
|
const load = (code: string) => market.dataProvider.getCachedQuery("getExchangeRate", [code])
|
|
2176
2969
|
.load({ force: ctx.cliOptions.refresh }).catch((error) => {
|
|
@@ -2199,13 +2992,68 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
|
|
|
2199
2992
|
const converted = amount == null ? null : amount * rate;
|
|
2200
2993
|
const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate, ...(amount == null ? {} : { amount, converted }) };
|
|
2201
2994
|
// The closing line dates the rate, and says when it is stale, as `fn FXC` does.
|
|
2202
|
-
const
|
|
2995
|
+
const loaded = new Map(codes.flatMap((code, index) => {
|
|
2203
2996
|
const leg = legs[index];
|
|
2204
2997
|
return leg ? [[code, {
|
|
2205
2998
|
rate: leg.value,
|
|
2206
|
-
timing: {
|
|
2999
|
+
timing: {
|
|
3000
|
+
asOf: leg.asOf, staleAt: leg.staleAt, error: leg.refreshError, delayMinutes: leg.delayMinutes,
|
|
3001
|
+
intradayBar: leg.intradayBar, staleReason: leg.staleReason,
|
|
3002
|
+
},
|
|
2207
3003
|
}] as const] : [];
|
|
2208
|
-
}))
|
|
3004
|
+
}));
|
|
3005
|
+
const freshness = fxPairFreshness(loaded);
|
|
3006
|
+
// A rate that is its pair's bar says which bar, and a stale one why it stands.
|
|
3007
|
+
const rateNotes = fxRateNotes(new Map([...loaded].map(([code, leg]) => [code, leg.timing])));
|
|
3008
|
+
if (tenor && shortRates && !("error" in tenor)) {
|
|
3009
|
+
const read = await shortRates;
|
|
3010
|
+
if ("error" in read) throw read.error;
|
|
3011
|
+
const { rates } = read.payload;
|
|
3012
|
+
// BASE/QUOTE: the forward needs the rate of each; a missing one is named, never filled in.
|
|
3013
|
+
const base = shortRateFor(currency, rates);
|
|
3014
|
+
const quote = shortRateFor(baseCurrency, rates);
|
|
3015
|
+
const missing = [base, quote].flatMap((lookup) => "error" in lookup ? [lookup.error] : []);
|
|
3016
|
+
if ("error" in base || "error" in quote) {
|
|
3017
|
+
return ctx.fail(`No ${tenor.label} forward for ${pair}. ${missing.join(" ")}`, "The forward is implied from both currencies' short rates by covered interest parity.");
|
|
3018
|
+
}
|
|
3019
|
+
const implied = impliedForward(rate, tenor.days, base.rate, quote.rate);
|
|
3020
|
+
const forwardDate = new Date(Math.floor(Date.now() / 86_400_000) * 86_400_000 + tenor.days * 86_400_000).toISOString().slice(0, 10);
|
|
3021
|
+
const rateFields = (short: typeof base.rate) => ({
|
|
3022
|
+
currency: short.currency, name: short.name, tenor: short.tenor, value: short.value, asOf: short.asOf,
|
|
3023
|
+
dayCountBasis: short.dayCountBasis, publisher: short.publisher, sourceUrl: short.sourceUrl, stale: short.stale,
|
|
3024
|
+
});
|
|
3025
|
+
const staleRates = [base.rate, quote.rate].filter((short) => short.stale)
|
|
3026
|
+
.map((short) => `${short.name} (${short.currency}) is the last copy read, dated ${short.asOf ?? "undated"}; its source could not be read since.`);
|
|
3027
|
+
const forwardRow = {
|
|
3028
|
+
pair, tenor: tenor.label, days: tenor.days, forwardDate, spot: rate, spotAsOf: asOf, forward: implied.forward,
|
|
3029
|
+
points: implied.points, pointSize: implied.pointSize, premiumPercent: implied.premiumPercent, annualizedPercent: implied.annualizedPercent,
|
|
3030
|
+
baseRate: rateFields(base.rate), quoteRate: rateFields(quote.rate), method: "covered interest parity", label: IMPLIED_FORWARD_LABEL,
|
|
3031
|
+
};
|
|
3032
|
+
const growth = (short: typeof base.rate) => `(1 + ${formatRatePercent(short.value)} × ${tenor.days}/${short.dayCountBasis})`;
|
|
3033
|
+
// A currency typed as its name (yen) says which code it was read as, as the spot answer does.
|
|
3034
|
+
const forwardWarnings = [...request.readAs ?? [], ...rateNotes, ...staleRates];
|
|
3035
|
+
return ctx.printResult({ data: [forwardRow], freshness, ...(forwardWarnings.length > 0 ? { warnings: forwardWarnings } : {}) }, {
|
|
3036
|
+
text: () => [
|
|
3037
|
+
`${pair} ${tenor.label} forward ${formatFxRate(implied.forward)}, ${IMPLIED_FORWARD_LABEL}`,
|
|
3038
|
+
`Spot ${formatFxRate(rate)} · forward points ${formatForwardPoints(implied.points)} (pips of ${implied.pointSize}) · ${implied.premiumPercent >= 0 ? "premium" : "discount"} ${formatSignedPercent(implied.premiumPercent)} over ${tenor.days} days to ${forwardDate}, ${formatSignedPercent(implied.annualizedPercent, 2)} a year (×360/${tenor.days})`,
|
|
3039
|
+
`${currency} rate: ${describeShortRate(base.rate)}`,
|
|
3040
|
+
`${baseCurrency} rate: ${describeShortRate(quote.rate)}`,
|
|
3041
|
+
`Covered interest parity: ${formatFxRate(rate)} × ${growth(quote.rate)} / ${growth(base.rate)}; each rate earns simple interest over the ${tenor.days} days, standing in for a ${tenor.label} term rate.`,
|
|
3042
|
+
].join("\n"),
|
|
3043
|
+
exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: `forward ${tenor.label}` },
|
|
3044
|
+
columns: [
|
|
3045
|
+
{ key: "pair", header: "Pair" },
|
|
3046
|
+
{ key: "tenor", header: "Tenor" },
|
|
3047
|
+
{ key: "days", header: "Days", align: "right" },
|
|
3048
|
+
{ key: "forwardDate", header: "Forward date" },
|
|
3049
|
+
{ key: "spot", header: "Spot", align: "right" },
|
|
3050
|
+
{ key: "forward", header: "Forward", align: "right" },
|
|
3051
|
+
{ key: "points", header: "Points", align: "right" },
|
|
3052
|
+
{ key: "premiumPercent", header: "Premium %", align: "right" },
|
|
3053
|
+
{ key: "annualizedPercent", header: "Annualized %", align: "right" },
|
|
3054
|
+
],
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
2209
3057
|
// An amount with one currency names no target; the answer says it went to USD.
|
|
2210
3058
|
const warnings = [
|
|
2211
3059
|
// A currency typed as its name (yen) says which code it was read as.
|
|
@@ -2213,10 +3061,21 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
|
|
|
2213
3061
|
...toUsdByDefault && amount != null
|
|
2214
3062
|
? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to>.`]
|
|
2215
3063
|
: [],
|
|
3064
|
+
...rateNotes,
|
|
2216
3065
|
];
|
|
2217
|
-
|
|
3066
|
+
// The peso's dollar rate is the official one; the market rates Buenos Aires prices imply follow it, each with its inputs.
|
|
3067
|
+
const peso = isPesoDollarPair(currency, baseCurrency)
|
|
3068
|
+
? await loadPesoDollarRates((targets) => market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh }))
|
|
3069
|
+
: null;
|
|
3070
|
+
if (peso && peso.missing.length > 0) warnings.push(`No price for ${peso.missing.join(", ")}, so the rates from ${peso.missing.length === 1 ? "it are" : "them are"} left out.`);
|
|
3071
|
+
const rateText = peso ? `${describeFxRate(request, rate)} · ${peso.spotKind} rate` : describeFxRate(request, rate);
|
|
3072
|
+
ctx.printResult({ data: [peso ? { ...row, ...pesoDollarRatesJson(peso) } : row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
|
|
2218
3073
|
// The conversion, then the rate it used; the source line under it dates that rate.
|
|
2219
|
-
text: () =>
|
|
3074
|
+
text: () => [
|
|
3075
|
+
...amount == null ? [] : [describeFxConversion(request, amount, rate)],
|
|
3076
|
+
rateText,
|
|
3077
|
+
...peso ? pesoDollarRateLines(peso) : [],
|
|
3078
|
+
].join("\n"),
|
|
2220
3079
|
// The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
|
|
2221
3080
|
exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "spot" },
|
|
2222
3081
|
columns: [
|
|
@@ -2240,7 +3099,7 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2240
3099
|
const args = [...rawArgs];
|
|
2241
3100
|
const exchangeOption = takeOption(args, "--exchange");
|
|
2242
3101
|
const entries = normalizeSymbols(args);
|
|
2243
|
-
if (entries.length === 0) ctx.fail("Usage:
|
|
3102
|
+
if (entries.length === 0) ctx.fail("Usage: gloom earnings <symbol...>");
|
|
2244
3103
|
await withCliServices(ctx, async (services) => {
|
|
2245
3104
|
const symbols = await readSymbols(entries, services.dataProvider);
|
|
2246
3105
|
const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
|
|
@@ -2250,9 +3109,9 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2250
3109
|
// Each bare symbol's venues cost a search, so a long list (a portfolio check, not a lookup) names none.
|
|
2251
3110
|
const venues = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
|
|
2252
3111
|
: listings.map((listing) => [listing, loadBareVenues(listing, services)] as const));
|
|
2253
|
-
let
|
|
3112
|
+
let calendar: EarningsEvent[];
|
|
2254
3113
|
try {
|
|
2255
|
-
|
|
3114
|
+
calendar = await loadForListing(
|
|
2256
3115
|
listings, services, ctx,
|
|
2257
3116
|
() => services.dataProvider.getEarningsCalendar(keys),
|
|
2258
3117
|
(found) => found.length === 0,
|
|
@@ -2262,6 +3121,15 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2262
3121
|
if (failure) ctx.fail(failure);
|
|
2263
3122
|
throw error;
|
|
2264
3123
|
}
|
|
3124
|
+
// Each date is resolved as events, EE, ERN and OSA resolve it: one that has come is read against the
|
|
3125
|
+
// company's quarters on record, for its actual and the next report on record (shared/earnings-calendar).
|
|
3126
|
+
const today = newYorkDay();
|
|
3127
|
+
const resolved = await resolveListingEvents(services.dataProvider, listings.map((listing) => ({
|
|
3128
|
+
key: listing.key, symbol: listing.request.symbol, exchange: listing.request.exchange,
|
|
3129
|
+
})), calendar, today);
|
|
3130
|
+
const events: ResolvedEarningsEvent[] = calendar
|
|
3131
|
+
.map((event) => resolved.get(event.symbol.toUpperCase()) ?? { ...event, dateStatus: calendarStatus(event, today) })
|
|
3132
|
+
.sort((left, right) => left.earningsDate.getTime() - right.earningsDate.getTime());
|
|
2265
3133
|
const eventOf = (listing: CliListing) => events.find((event) => event.symbol.toUpperCase() === listing.key.toUpperCase());
|
|
2266
3134
|
const [abroadNotes, choiceNotes] = await Promise.all([
|
|
2267
3135
|
// A listing abroad with no date of its own says so, with its US line's date when that line is the same company.
|
|
@@ -2281,7 +3149,8 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2281
3149
|
];
|
|
2282
3150
|
ctx.printResult({
|
|
2283
3151
|
data: events,
|
|
2284
|
-
|
|
3152
|
+
// Dated by when it was read: a row's date is when the report is, not how old the calendar is.
|
|
3153
|
+
freshness: rowsFreshness([], { status: "not-a-feed", basis: "calendar", oldest: null }),
|
|
2285
3154
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
2286
3155
|
}, {
|
|
2287
3156
|
rows: earningsRows,
|
|
@@ -2307,7 +3176,9 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2307
3176
|
group: CLI_COMMAND_GROUPS.research,
|
|
2308
3177
|
usage: ["quote <symbol...>", "quote --file <path>"],
|
|
2309
3178
|
options: [EXCHANGE_OPTION, LIST_FILE_OPTION],
|
|
2310
|
-
|
|
3179
|
+
sections: [QUOTE_CSV_SECTION],
|
|
3180
|
+
examples: ["quote AAPL MSFT NVDA", "quote SAN:EPA BHP:ASX", "quote BTC-USD EURUSD=X", "quote binance:BZUSDT", "quote AAPL --json", "quote --file holdings.csv",
|
|
3181
|
+
"quote --file holdings.csv --csv > quotes.csv"],
|
|
2311
3182
|
},
|
|
2312
3183
|
execute: (args, ctx) => runQuote(args, ctx, "quote"),
|
|
2313
3184
|
},
|
|
@@ -2318,6 +3189,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2318
3189
|
group: CLI_COMMAND_GROUPS.research,
|
|
2319
3190
|
usage: ["compare <symbol...>", "compare --file <path>"],
|
|
2320
3191
|
options: [EXCHANGE_OPTION, LIST_FILE_OPTION],
|
|
3192
|
+
sections: [QUOTE_CSV_SECTION],
|
|
2321
3193
|
examples: ["compare KO PEP", "compare SAN:EPA SAN:NYSE", "compare SPY QQQ IWM --csv", "compare - < peers.txt"],
|
|
2322
3194
|
},
|
|
2323
3195
|
execute: (args, ctx) => runQuote(args, ctx, "compare"),
|
|
@@ -2327,26 +3199,45 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2327
3199
|
description: "Fetch open, high, low, close, and volume over a range",
|
|
2328
3200
|
help: {
|
|
2329
3201
|
group: CLI_COMMAND_GROUPS.research,
|
|
2330
|
-
usage: [HISTORY_USAGE],
|
|
3202
|
+
usage: [HISTORY_USAGE, "history --file <path> [--range <range> | --from <year|date> [--to <year|date>]]"],
|
|
2331
3203
|
options: [
|
|
2332
3204
|
{ flags: "--range <range>", description: `${HISTORY_RANGES.join(", ")} (default 1Y); ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}` },
|
|
3205
|
+
{ flags: "--from <year|date>, --start <year|date>", description: `${PRICE_WINDOW_FROM_HELP}. Up to five years come in daily bars, up to twenty weekly, longer monthly` },
|
|
3206
|
+
{ flags: "--to <year|date>, --end <year|date>", description: PRICE_WINDOW_TO_HELP },
|
|
3207
|
+
{ flags: "--file <path>", description: "Read the symbols from a text or CSV file, one per line (- reads standard input), and write every symbol's bars in one table" },
|
|
2333
3208
|
EXCHANGE_OPTION,
|
|
2334
3209
|
],
|
|
2335
3210
|
sections: [{
|
|
2336
3211
|
title: "Bars",
|
|
2337
3212
|
lines: [
|
|
2338
3213
|
`Each range comes in one bar size: ${historyIntervalsByRange()}. The line under the heading, and the --json metadata, give the currency, the bar size actually served, the first and last bar and the bar count. A warning says when the bars are coarser, or start later, than the range asked for, and for a coin, which trades every day, names each day it has no bar for.`,
|
|
2339
|
-
"A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning.
|
|
3214
|
+
"A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. --json rows carry currency, unit, interval and flag.",
|
|
2340
3215
|
"A break in the series flags the bar after it, which keeps its prices: gap after more than three months with no bar (weeks for weekly, daily and intraday bars), jump when the close is over 100 times the one before or under a hundredth of it. A warning names each break, and the heading says the date the series runs unbroken from.",
|
|
3216
|
+
"The heading's last line is the max drawdown of the bars shown: the largest fall from a close to a later close, with the peak and the trough, their dates, and the date a close got back to the peak, if one did (maxDrawdown in --json).",
|
|
3217
|
+
],
|
|
3218
|
+
}, {
|
|
3219
|
+
title: "CSV",
|
|
3220
|
+
lines: [
|
|
3221
|
+
"Every --csv has one header, whatever the symbol, range or market state: Date, Open, High, Low, Close, Volume, Flag, Partial, Filled, Unsettled, Volume unit, Volume note, Venue volume, Volume venue, status, then symbol_or_series, currency_or_unit, interval, source and as_of. Prices are plain numbers in the currency currency_or_unit names. Partial, Filled and Unsettled are true or false, and status says why a row has no prices.",
|
|
3222
|
+
],
|
|
3223
|
+
}, {
|
|
3224
|
+
title: "Several symbols",
|
|
3225
|
+
lines: [
|
|
3226
|
+
`history AAPL MSFT 000660.KS, or --file with one symbol a line (- reads standard input), writes every symbol's bars in one long table, ${HISTORY_LIST_CONCURRENCY} symbols loading at once. Each row names its symbol, its currency and the as-of of that symbol's bars in symbol_or_series, currency_or_unit and as_of.`,
|
|
3227
|
+
"A symbol with no history is one row with its reason in status, and a warning names it; the command fails only when no symbol has any. --tail 5 keeps each symbol's newest five bars.",
|
|
2341
3228
|
],
|
|
2342
3229
|
}, {
|
|
2343
3230
|
title: "Long history",
|
|
2344
3231
|
lines: [
|
|
2345
|
-
"ZAR has daily FRED data from 1980:
|
|
2346
|
-
"NGN, SAR and ILS have no FRED series. Their daily bars cover the last five years (
|
|
3232
|
+
"ZAR has daily FRED data from 1980: gloom fred DEXSFUS --start 1980 --sort asc --csv",
|
|
3233
|
+
"NGN, SAR and ILS have no FRED series. Their daily bars cover the last five years (gloom fn G USDNGN=X --range-preset 5Y --chart-resolution 1d --csv); before that there are monthly bars only (gloom history USDNGN=X --range ALL).",
|
|
2347
3234
|
],
|
|
2348
3235
|
}],
|
|
2349
|
-
examples: [
|
|
3236
|
+
examples: [
|
|
3237
|
+
"history AAPL", "history BHP:ASX --range 5Y", "history JPM --from 2007 --to 2009", "history ZAR=X --range ALL", "history ZAR=X --tail 5",
|
|
3238
|
+
"history AAPL --range 5Y --csv > aapl.csv", "history AAPL MSFT 000660.KS --range 1M --csv",
|
|
3239
|
+
"history --file holdings.txt --start 2024-01-01 --end 2024-12-31 --csv > bars.csv",
|
|
3240
|
+
],
|
|
2350
3241
|
},
|
|
2351
3242
|
unknownOptionHint: historyOptionHint,
|
|
2352
3243
|
execute: runHistory,
|
|
@@ -2422,7 +3313,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2422
3313
|
},
|
|
2423
3314
|
execute: async (args, ctx) => {
|
|
2424
3315
|
const query = args.join(" ");
|
|
2425
|
-
if (!query) ctx.fail("Usage:
|
|
3316
|
+
if (!query) ctx.fail("Usage: gloom provider-search <query>");
|
|
2426
3317
|
await withMarketData(ctx, async (market) => {
|
|
2427
3318
|
const results = await market.dataProvider.search(query);
|
|
2428
3319
|
ctx.printResult({ data: results.slice(0, ctx.cliOptions.limit ?? results.length) }, {
|
|
@@ -2522,6 +3413,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2522
3413
|
title: "Output",
|
|
2523
3414
|
lines: [
|
|
2524
3415
|
"Holders run largest value first, as in fn HDS. --csv and --json carry the HDS report's columns: values in the currency the header names, shares, and the quarter's change and the stake in percent (0.59 is 0.59%).",
|
|
3416
|
+
"A US listing lists every 13F filer, as fn 13F does: the largest 25, or as many as --limit asks up to 200; --tail counts back from the end of those, not from the smallest filer. A listing abroad has only the source's largest few.",
|
|
2525
3417
|
],
|
|
2526
3418
|
}],
|
|
2527
3419
|
examples: ["holders AAPL", "holders SAN:EPA", "holders SAN --csv", "holders CAR --form all", "holders CAR --form 13g --history --json"],
|
|
@@ -2548,7 +3440,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2548
3440
|
options: [EXCHANGE_OPTION],
|
|
2549
3441
|
sections: [{
|
|
2550
3442
|
title: "13F filings",
|
|
2551
|
-
lines: ["For each fund's reported position and its change over the quarter, run
|
|
3443
|
+
lines: ["For each fund's reported position and its change over the quarter, run gloom fn 13F <symbol>."],
|
|
2552
3444
|
}],
|
|
2553
3445
|
examples: ["13f AAPL", "13f BP:NYSE"],
|
|
2554
3446
|
},
|
|
@@ -2556,7 +3448,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2556
3448
|
},
|
|
2557
3449
|
{
|
|
2558
3450
|
name: "filings",
|
|
2559
|
-
description: "Fetch recent SEC filings",
|
|
3451
|
+
description: "Fetch recent SEC filings; a listing abroad gets its exchange's announcements page, and ASX's list",
|
|
2560
3452
|
help: {
|
|
2561
3453
|
group: CLI_COMMAND_GROUPS.companyData,
|
|
2562
3454
|
usage: ["filings <symbol> [--count <n>] [--form <form>]"],
|
|
@@ -2565,7 +3457,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2565
3457
|
{ flags: "--form <form>", description: "Only this form and its amendments, such as 10-K or 13D" },
|
|
2566
3458
|
EXCHANGE_OPTION,
|
|
2567
3459
|
],
|
|
2568
|
-
examples: ["filings AAPL", "filings BHP:ASX", "filings AAPL --count 40 --json", "filings CAR --form 13G"],
|
|
3460
|
+
examples: ["filings AAPL", "filings BHP:ASX", "filings NST.AX", "filings AAPL --count 40 --json", "filings CAR --form 13G"],
|
|
2569
3461
|
},
|
|
2570
3462
|
execute: runFilings,
|
|
2571
3463
|
},
|
|
@@ -2595,9 +3487,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2595
3487
|
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",
|
|
2596
3488
|
help: {
|
|
2597
3489
|
group: CLI_COMMAND_GROUPS.markets,
|
|
2598
|
-
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>"],
|
|
3490
|
+
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>", FX_FORWARD_USAGE],
|
|
2599
3491
|
options: [
|
|
2600
3492
|
{ 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` },
|
|
3493
|
+
{ flags: "--forward <tenor>", description: "The forward implied by both currencies' short rates (covered interest parity), not a dealer quote: 30d, 2w, 3m, up to 1y; today's only, so not with --date" },
|
|
2601
3494
|
],
|
|
2602
3495
|
sections: [{
|
|
2603
3496
|
title: "Direction",
|
|
@@ -2616,8 +3509,14 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2616
3509
|
lines: [
|
|
2617
3510
|
"--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
3511
|
],
|
|
3512
|
+
}, {
|
|
3513
|
+
title: "Forwards",
|
|
3514
|
+
lines: [
|
|
3515
|
+
"--forward <tenor> gives the forward implied by covered interest parity: spot × (1 + quote rate × days/basis) / (1 + base rate × days/basis), with the forward points (pips of 0.0001, 0.01 against JPY) and the premium over spot for the period and a year. It is implied by rates, not a dealer quote.",
|
|
3516
|
+
"The rates are SOFR (USD), €STR (EUR), SONIA (GBP) and TIIE 28d (MXN), each with its day and publisher. A pair whose rate is missing says which one. Tenors count calendar days from today: 30d, 2w, 3m, 1y. The forward is today's: it does not take --date, as only the latest short rates are served.",
|
|
3517
|
+
],
|
|
2619
3518
|
}],
|
|
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"],
|
|
3519
|
+
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", "fx USD/MXN --forward 30d", "fx EUR/USD --forward 3m"],
|
|
2621
3520
|
},
|
|
2622
3521
|
execute: runFx,
|
|
2623
3522
|
},
|