gloomberb 0.18.2 → 0.18.4
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 +106 -5
- 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 +2 -2
- 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 +99 -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 +24 -5
- package/src/api-client/market-discovery.ts +14 -0
- package/src/api-client/members.ts +6 -0
- package/src/api-client/mna.ts +6 -0
- package/src/api-client/paths.ts +18 -1
- package/src/api-client/request.ts +26 -7
- 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/telemetry.ts +17 -0
- package/src/api-client/themes.ts +7 -1
- package/src/api-client/types.ts +90 -2
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/pane-runtime/plugin-bindings.ts +9 -2
- 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/runtime/update.ts +28 -8
- package/src/app/star-prompt/status-line.tsx +2 -2
- package/src/architecture/core-builtin-allowlist.json +5 -2
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- 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 +4 -2
- package/src/cli/commands/keybindings.ts +2 -2
- package/src/cli/commands/market.ts +1189 -296
- package/src/cli/commands/news-hints.ts +2 -2
- package/src/cli/commands/news-search.ts +59 -6
- package/src/cli/commands/options-leaps.ts +41 -26
- package/src/cli/commands/overview.ts +121 -48
- package/src/cli/commands/plugins.ts +9 -9
- package/src/cli/commands/remote.ts +13 -10
- package/src/cli/commands/search.ts +64 -44
- package/src/cli/commands/system.ts +27 -19
- package/src/cli/commands/themes.ts +1 -1
- package/src/cli/commands/ticker.ts +272 -70
- 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 +20 -2
- package/src/cli/exit-codes.ts +1 -1
- package/src/cli/export-source.ts +10 -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 +64 -20
- package/src/cli/help.ts +20 -14
- package/src/cli/history-export.ts +126 -0
- package/src/cli/history-rows.ts +196 -43
- package/src/cli/index-roots.ts +5 -45
- package/src/cli/index.ts +32 -18
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/known-gaps.ts +118 -25
- package/src/cli/listing-arg.ts +315 -43
- package/src/cli/name-entries.ts +73 -22
- 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 +77 -20
- package/src/cli/pane-functions/catalog.ts +40 -23
- package/src/cli/pane-functions/discovery.ts +1 -1
- package/src/cli/pane-functions/dom.ts +12 -8
- package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
- package/src/cli/pane-functions/freshness.ts +174 -23
- package/src/cli/pane-functions/function-help.ts +66 -27
- package/src/cli/pane-functions/headless.ts +48 -16
- package/src/cli/pane-functions/index.ts +82 -21
- 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 +5 -5
- package/src/cli/pane-functions/resolver.ts +65 -14
- 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 +38 -3
- package/src/cli/report-tables.ts +190 -27
- package/src/cli/restore-plugins.ts +29 -14
- package/src/cli/result.ts +122 -25
- 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 +226 -60
- 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 +387 -21
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +9 -3
- package/src/components/chart/composite/legend.tsx +75 -3
- package/src/components/chart/composite/levels.ts +5 -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-surface.tsx +316 -11
- package/src/components/chart/composite/price-axis-labels.tsx +27 -3
- package/src/components/chart/composite/rasterizer.ts +44 -19
- package/src/components/chart/composite/scene.ts +16 -4
- 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 +19 -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/routes/root/command-items.ts +32 -6
- 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/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/data-table/view.tsx +23 -3
- 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 +13 -6
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +234 -60
- 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/model.ts +11 -5
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/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-wizard.tsx +1 -0
- package/src/components/onboarding/portfolio-step/broker-setup-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 +5 -1
- 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/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/core/state/app/layout.ts +4 -4
- 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 +84 -0
- package/src/docs/snapshot.json +65 -32
- package/src/i18n/es.ts +51 -12
- package/src/i18n/ja.ts +51 -12
- package/src/i18n/ko.ts +51 -12
- package/src/i18n/zh-cn.ts +51 -12
- package/src/i18n/zh-tw.ts +51 -12
- 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/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 +49 -7
- package/src/market-data/market/freshness.ts +34 -5
- package/src/market-data/market/period-return.ts +163 -0
- 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/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 +5 -1
- 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 +4 -2
- 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/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 +30 -18
- package/src/plugins/builtin/alerts/calendar-events.ts +378 -0
- package/src/plugins/builtin/alerts/cli.ts +485 -76
- package/src/plugins/builtin/alerts/command.ts +5 -10
- 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 +108 -5
- package/src/plugins/builtin/alerts/fired-log.ts +78 -0
- package/src/plugins/builtin/alerts/format.ts +2 -1
- package/src/plugins/builtin/alerts/headless.ts +100 -39
- package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
- package/src/plugins/builtin/alerts/history-rows.ts +89 -0
- package/src/plugins/builtin/alerts/index.tsx +45 -18
- package/src/plugins/builtin/alerts/iv-level.ts +365 -0
- package/src/plugins/builtin/alerts/levels.ts +3 -3
- package/src/plugins/builtin/alerts/pane.tsx +97 -29
- 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 +77 -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 +13 -10
- package/src/plugins/builtin/alerts/targets.ts +12 -3
- 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 +107 -43
- package/src/plugins/builtin/attention/pane.tsx +22 -16
- 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 +12 -9
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/backtest/headless.ts +3 -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 +21 -12
- package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
- package/src/plugins/builtin/broker-manager/model.ts +12 -2
- 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 +45 -13
- package/src/plugins/builtin/broker-trading/dock.tsx +24 -4
- package/src/plugins/builtin/broker-trading/headless.ts +2 -1
- package/src/plugins/builtin/broker-trading/index.tsx +7 -1
- package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +75 -7
- 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 +115 -26
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
- package/src/plugins/builtin/broker-trading/ticket.tsx +122 -36
- 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 +34 -18
- package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/client.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 +49 -4
- package/src/plugins/builtin/catalysts/pane.tsx +9 -11
- 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 +1 -0
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +2 -1
- package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +2 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +4 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +28 -26
- package/src/plugins/builtin/chart-composer/headless.ts +253 -35
- 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 +128 -28
- 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 +15 -21
- 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 +5 -6
- 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/index.tsx +1 -1
- package/src/plugins/builtin/chat/pane-links/open.ts +1 -1
- package/src/plugins/builtin/chat/people.ts +1 -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/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 +14 -8
- package/src/plugins/builtin/company-attention/app-rank.tsx +11 -10
- 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 +89 -31
- 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 +5 -4
- 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 +7 -4
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/headless.ts +28 -6
- package/src/plugins/builtin/cot/model.ts +12 -0
- package/src/plugins/builtin/cot/pane.tsx +7 -5
- 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/credit-documents/pane.tsx +8 -7
- 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/debt-maturities/pane.tsx +4 -3
- 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 +19 -12
- 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 +132 -20
- package/src/plugins/builtin/econ/detail-view.tsx +48 -10
- 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 +112 -25
- package/src/plugins/builtin/econ/print-watch.ts +2 -1
- package/src/plugins/builtin/econ/types.ts +32 -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 +13 -2
- 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 +103 -0
- package/src/plugins/builtin/econ-statistics/view.ts +16 -1
- package/src/plugins/builtin/equity-screener/client.ts +21 -3
- package/src/plugins/builtin/equity-screener/headless.ts +77 -5
- package/src/plugins/builtin/equity-screener/live.ts +1 -1
- package/src/plugins/builtin/equity-screener/model.ts +34 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +148 -106
- 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/headless.ts +51 -6
- package/src/plugins/builtin/exposure/index.tsx +28 -2
- package/src/plugins/builtin/exposure/model.ts +7 -0
- package/src/plugins/builtin/exposure/pane.tsx +16 -10
- 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/model.ts +10 -9
- package/src/plugins/builtin/futures/returns.ts +8 -36
- package/src/plugins/builtin/futures/table.tsx +1 -1
- 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 +15 -8
- package/src/plugins/builtin/fx-matrix/headless.ts +32 -14
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -11
- package/src/plugins/builtin/fx-matrix/live-legs.ts +22 -0
- package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/headless.ts +9 -4
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/help/index.tsx +13 -3
- 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 +15 -2
- package/src/plugins/builtin/holders/format.ts +28 -17
- package/src/plugins/builtin/holders/headless.ts +48 -8
- 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/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/holders/treemap.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +20 -7
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +3 -3
- package/src/plugins/builtin/insider/signals.tsx +1 -1
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- 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/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/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 +14 -8
- 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 +46 -10
- 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 +36 -21
- 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 +33 -11
- 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/headless.ts +6 -5
- package/src/plugins/builtin/members/pane.tsx +11 -4
- 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 +50 -15
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/openfda/pane.tsx +1 -1
- package/src/plugins/builtin/options/carry.ts +2 -2
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/index.tsx +1 -1
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +51 -23
- package/src/plugins/builtin/options-calculator/headless.ts +46 -7
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +142 -29
- 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 +49 -21
- 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 +22 -10
- package/src/plugins/builtin/perps/equity-row.tsx +11 -3
- package/src/plugins/builtin/perps/freshness.ts +8 -5
- 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 +161 -0
- package/src/plugins/builtin/perps/markets-section.tsx +154 -0
- package/src/plugins/builtin/perps/pane.tsx +36 -17
- 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 +145 -60
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +78 -27
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
- package/src/plugins/builtin/portfolio-list/cli/render.ts +49 -20
- 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 +6 -3
- 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/live-accounts.ts +16 -9
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
- package/src/plugins/builtin/power/client.ts +6 -3
- 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 +44 -32
- package/src/plugins/builtin/power/prices.tsx +10 -3
- 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 +76 -9
- package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +10 -5
- package/src/plugins/builtin/risk-factors/index.tsx +2 -2
- package/src/plugins/builtin/scanner/lock-note.ts +1 -1
- package/src/plugins/builtin/seasonality/client.ts +2 -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 +6 -4
- package/src/plugins/builtin/sectors/sector-model.ts +11 -15
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- 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/pro-lock-note.ts +3 -3
- package/src/plugins/builtin/shared/report-freshness.ts +3 -0
- package/src/plugins/builtin/shared/research-trading.tsx +23 -0
- package/src/plugins/builtin/shared/series-cache.ts +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -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/social-mentions/client.ts +2 -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 +8 -5
- package/src/plugins/builtin/themes/model.ts +14 -4
- package/src/plugins/builtin/themes/pane.tsx +24 -5
- 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 +8 -3
- package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
- package/src/plugins/builtin/vol-surface/client.ts +28 -7
- package/src/plugins/builtin/vol-surface/headless.ts +43 -15
- package/src/plugins/builtin/vol-surface/model.ts +34 -12
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/plugins/builtin/volatility/model.ts +5 -10
- 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 +2 -1
- 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 +10 -8
- 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 +14 -0
- package/src/public/time-series.ts +11 -8
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/config-host.ts +3 -0
- package/src/renderers/browser/main.tsx +23 -3
- 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/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- 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/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +115 -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/core-capabilities.ts +9 -1
- 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/update.ts +210 -30
- package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
- package/src/renderers/electrobun/bun/index.ts +11 -1
- 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/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/config-host.ts +3 -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 +9 -7
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -2
- 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 +8 -1
- 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 +39 -16
- package/src/sources/gloomberb-cloud/news.ts +39 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +14 -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/financial-routes.ts +21 -3
- package/src/sources/provider-router/financials.ts +15 -2
- package/src/sources/provider-router/history.ts +30 -8
- package/src/sources/provider-router/index.ts +16 -0
- package/src/sources/provider-router/primary.ts +1 -1
- package/src/state/app/bootstrap.ts +23 -0
- package/src/state/hooks/inline-tickers.ts +30 -5
- package/src/sync/controller.ts +1 -1
- package/src/sync/core-contributors.ts +46 -14
- package/src/team/collections-sync.ts +2 -1
- package/src/team/collections.ts +178 -17
- package/src/telemetry/crash-reports.ts +21 -4
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/test-support/builtin-panes.ts +1 -1
- package/src/theme/colors.ts +3 -30
- package/src/theme/ui-tokens.ts +37 -1
- package/src/tickers/company-name-listing.ts +215 -36
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/index-roots.ts +47 -0
- package/src/tickers/instrument-label.ts +5 -1
- package/src/tickers/listing-fate.ts +30 -0
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/perpetual-listing.ts +5 -2
- package/src/tickers/search/home-listing.ts +75 -0
- package/src/tickers/search/index.ts +76 -27
- package/src/tickers/search/ranking.ts +54 -4
- 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 +307 -0
- package/src/time-series/chart-bounds.ts +349 -0
- package/src/time-series/chart-seed.ts +142 -0
- package/src/time-series/economic.ts +5 -0
- package/src/time-series/fundamentals.ts +18 -3
- package/src/time-series/history-acquisition.ts +499 -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/range.ts +9 -1
- package/src/time-series/resolution.ts +174 -16
- package/src/time-series/resolve-context.ts +157 -0
- package/src/time-series/resolve.ts +126 -1577
- package/src/time-series/series-name.ts +38 -0
- package/src/time-series/series-presentation.ts +208 -0
- package/src/time-series/spec.ts +11 -6
- package/src/time-series/studies.ts +109 -12
- package/src/time-series/tail-reconcile.ts +121 -0
- package/src/time-series/types.ts +29 -2
- package/src/time-series/use-chart-resolution.ts +4 -5
- package/src/types/broker.ts +10 -0
- package/src/types/config.ts +14 -0
- package/src/types/data-provider.ts +20 -0
- package/src/types/exchange-rate.ts +6 -0
- package/src/types/financials.ts +104 -7
- package/src/types/headless.ts +83 -6
- package/src/types/instrument.ts +3 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/types/plugin.ts +65 -8
- 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 +175 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +97 -28
- 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-pair.ts +26 -0
- 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 +83 -4
- 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 +83 -0
- package/src/utils/option-expiry.ts +139 -7
- package/src/utils/price-earnings.ts +11 -5
- package/src/utils/price-history.ts +18 -8
- package/src/utils/price-window.ts +131 -0
- package/src/utils/release-date.ts +47 -0
- package/src/utils/sec.ts +71 -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
|
@@ -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,21 +47,26 @@ 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";
|
|
55
|
+
import { isQuoteStaleForCurrentSession, noPrintsTodayText } from "../../market-data/quotes/freshness";
|
|
54
56
|
import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
|
|
55
57
|
import {
|
|
56
|
-
cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions,
|
|
57
|
-
renderFundamentalsReport, writesCurrencyCodes,
|
|
58
|
+
atReportTtmRate, cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions,
|
|
59
|
+
loadReportBondFacts, renderFundamentalsReport, writesCurrencyCodes,
|
|
58
60
|
} from "./ticker";
|
|
61
|
+
import { createBaseConverter } from "../base-converter";
|
|
59
62
|
import { loadStatementFilings } from "../statement-filings";
|
|
60
63
|
import { loadBankFigures } from "../bank-section";
|
|
61
64
|
import { latestFinancialPeriod } from "../../utils/latest-financial-period";
|
|
62
65
|
import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
|
|
63
66
|
import { currencyCodeLabel } from "../../utils/currency-units";
|
|
64
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
futuresContractLines, futuresPricePer, futuresRollLines, isFuturesInstrument, loadUsdPerMmbtu, usdPerMmbtuText, type UsdPerMmbtu,
|
|
69
|
+
} from "../../market-data/futures-contract";
|
|
65
70
|
import {
|
|
66
71
|
chainHasExpiry,
|
|
67
72
|
formatExpiryList,
|
|
@@ -80,9 +85,14 @@ import {
|
|
|
80
85
|
valueChainAtModelSpot,
|
|
81
86
|
type OptionsChainBasis,
|
|
82
87
|
} from "./options-chain";
|
|
83
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
expirationHint, expiredExpiryText, expiryIsoDate, isLiveOptionExpiry, liveDefaultExpiry, liveOptionExpiries, missingExpiryMessage,
|
|
90
|
+
OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, withLiveExpiryList,
|
|
91
|
+
} from "../../utils/option-expiry";
|
|
84
92
|
import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
|
|
85
93
|
import { apiClient } from "../../api-client";
|
|
94
|
+
import type { ExchangeAnnouncementsPayload } from "../../api-client/exchange-announcements";
|
|
95
|
+
import { exchangeAnnouncementsPage, listsExchangeAnnouncements, type ExchangeAnnouncementsPage } from "../../utils/exchange-announcements";
|
|
86
96
|
import { dailyFxLeg, fxCloseOnDate, type FxCloseReading } from "../../market-data/fx-closes";
|
|
87
97
|
import { declaredDividends } from "../../utils/declared-dividends";
|
|
88
98
|
import {
|
|
@@ -91,12 +101,17 @@ import {
|
|
|
91
101
|
import type { CliResultColumn } from "../result";
|
|
92
102
|
import type { PriceHistoryResult } from "../../types/price-history";
|
|
93
103
|
import {
|
|
104
|
+
exportNumber,
|
|
94
105
|
exportRowsTable,
|
|
95
106
|
reportFooterLines,
|
|
96
107
|
selectReportTables,
|
|
97
108
|
type CliReportTables,
|
|
98
109
|
} from "../report-tables";
|
|
110
|
+
import { historyCoverageNotice } from "../../sources/history-coverage";
|
|
111
|
+
import { maxDrawdown, maxDrawdownText } from "../../utils/max-drawdown";
|
|
112
|
+
import { isPastPriceWindow, loadPriceWindowHistory, PRICE_WINDOW_FROM_HELP, PRICE_WINDOW_TO_HELP, priceWindowText, readPriceWindow, type PriceWindow } from "../../utils/price-window";
|
|
99
113
|
import {
|
|
114
|
+
BAR_INTERVAL_LABELS,
|
|
100
115
|
closedPeriodBarDay,
|
|
101
116
|
formingPeriodBar,
|
|
102
117
|
historyBarStatusNote,
|
|
@@ -109,6 +124,7 @@ import {
|
|
|
109
124
|
historyAllRangeYears,
|
|
110
125
|
historyIntervalsByRange,
|
|
111
126
|
historyMissingVolumeNote,
|
|
127
|
+
historyVenueVolumeNote,
|
|
112
128
|
historyNotes,
|
|
113
129
|
historyReportsVolume,
|
|
114
130
|
historyPriceDecimals,
|
|
@@ -116,20 +132,54 @@ import {
|
|
|
116
132
|
historyUnit,
|
|
117
133
|
historyVolume,
|
|
118
134
|
historyVolumeText,
|
|
135
|
+
type HistoryRow,
|
|
136
|
+
type HistoryUnit,
|
|
137
|
+
type HistoryVolume,
|
|
119
138
|
} from "../history-rows";
|
|
139
|
+
import {
|
|
140
|
+
HISTORY_EXPORT_COLUMNS,
|
|
141
|
+
historyExportStatus,
|
|
142
|
+
historyFailureRow,
|
|
143
|
+
oneLineFailure,
|
|
144
|
+
symbolListNotes,
|
|
145
|
+
type HistoryListRow,
|
|
146
|
+
} from "../history-export";
|
|
147
|
+
import { isCliFailure } from "../errors";
|
|
148
|
+
import { mapWithConcurrency } from "../../utils/concurrency";
|
|
120
149
|
import { MISSING_VOLUME_TEXT } from "../../utils/price-history-integrity";
|
|
121
150
|
import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
|
|
122
151
|
import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures-board";
|
|
123
152
|
import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
|
|
124
153
|
import { newsEmptyMessage } from "./news-hints";
|
|
125
|
-
import {
|
|
154
|
+
import {
|
|
155
|
+
commodityNewsRoute,
|
|
156
|
+
commonWordCandidate,
|
|
157
|
+
METALS_NEWS_TOPIC,
|
|
158
|
+
metalCodeNewsRoute,
|
|
159
|
+
type MetalNewsRoute,
|
|
160
|
+
metalNewsHeading,
|
|
161
|
+
metalNewsRoute,
|
|
162
|
+
newsFallbackText,
|
|
163
|
+
newsSearchHeading,
|
|
164
|
+
newsSearchText,
|
|
165
|
+
newsSearchTextProblem,
|
|
166
|
+
noMetalStoriesMessage,
|
|
167
|
+
noStoriesMessage,
|
|
168
|
+
quotedSearchCommand,
|
|
169
|
+
searchNewsStories,
|
|
170
|
+
wordMeansMore,
|
|
171
|
+
} from "./news-search";
|
|
126
172
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
127
173
|
import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
|
|
128
174
|
import { isFiniteNumber } from "../../utils/guards";
|
|
129
175
|
import { exportedFundamentals } from "../../utils/price-earnings";
|
|
130
176
|
import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
|
|
131
|
-
import {
|
|
132
|
-
import {
|
|
177
|
+
import { fxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
|
|
178
|
+
import { quoteBarNotes } from "../../market-data/quotes/intraday-bar";
|
|
179
|
+
import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
|
|
180
|
+
import { isPesoDollarPair, loadPesoDollarRates, pesoDollarRateLines, pesoDollarRatesJson } from "../peso-dollar-rates";
|
|
181
|
+
import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
|
|
182
|
+
import { viewMarketCapitalization } from "../../utils/market-capitalization";
|
|
133
183
|
import {
|
|
134
184
|
barHistoryFreshness,
|
|
135
185
|
barResolutionFromDates,
|
|
@@ -147,6 +197,14 @@ import {
|
|
|
147
197
|
quarterLabel,
|
|
148
198
|
showsEpsBasis,
|
|
149
199
|
} from "../../plugins/builtin/shared/earnings-report";
|
|
200
|
+
import {
|
|
201
|
+
calendarEventOf,
|
|
202
|
+
calendarStatus,
|
|
203
|
+
newYorkDay,
|
|
204
|
+
resolveListingEvents,
|
|
205
|
+
withResolvedEarnings,
|
|
206
|
+
type ResolvedEarningsEvent,
|
|
207
|
+
} from "../../plugins/builtin/shared/earnings-calendar";
|
|
150
208
|
import {
|
|
151
209
|
EXCHANGE_OPTION,
|
|
152
210
|
listingHeading,
|
|
@@ -155,12 +213,17 @@ import {
|
|
|
155
213
|
listingVenues,
|
|
156
214
|
listingChoiceNotes,
|
|
157
215
|
listingReadNotes,
|
|
216
|
+
failUnchosenListing,
|
|
158
217
|
loadBareVenues,
|
|
159
218
|
loadForListing,
|
|
160
219
|
loadListingQuote,
|
|
220
|
+
loadListingQuoteAnswer,
|
|
161
221
|
listingFateLoader,
|
|
222
|
+
meantQuestion,
|
|
162
223
|
movedSymbolLine,
|
|
224
|
+
ListingArgError,
|
|
163
225
|
requireCliListing,
|
|
226
|
+
resolveCliListing,
|
|
164
227
|
type CliListing,
|
|
165
228
|
type ListingIdentity,
|
|
166
229
|
} from "../listing-arg";
|
|
@@ -173,6 +236,8 @@ import { CBOE_YIELD_INDEX_CLOSE } from "../../plugins/builtin/shared/treasury-cu
|
|
|
173
236
|
import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
|
|
174
237
|
import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
|
|
175
238
|
import { providerMissReason } from "../../sources/provider-errors";
|
|
239
|
+
import { isNetworkFailure } from "../../utils/network-failure";
|
|
240
|
+
import { withSavedCopy, type ReportFreshness } from "../pane-functions/freshness";
|
|
176
241
|
import { isNotATickerMessage } from "../not-a-ticker";
|
|
177
242
|
import { primeBoardCoins } from "../board-coins";
|
|
178
243
|
import { suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
|
|
@@ -180,11 +245,12 @@ import { quotedPhrase, splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-
|
|
|
180
245
|
import { isCompanyNameInput } from "../../tickers/company-name-listing";
|
|
181
246
|
import { readNameEntries, readPhraseArgument } from "../name-entries";
|
|
182
247
|
import { readSymbolList, SymbolListError } from "../symbol-list";
|
|
248
|
+
import { notASymbolError } from "../symbol-cells";
|
|
183
249
|
import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
|
|
184
250
|
import { knownGapMessage, subMinuteDataMessage } from "../known-gaps";
|
|
185
251
|
import { phraseSearchReport } from "./search";
|
|
186
252
|
import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
|
|
187
|
-
import { isCryptoPairSymbol } from "../../utils/crypto-pair";
|
|
253
|
+
import { isCryptoPairSymbol, isRoundTheClockCoin } from "../../utils/crypto-pair";
|
|
188
254
|
import { isUsListingExchange } from "../../utils/exchanges";
|
|
189
255
|
import {
|
|
190
256
|
describeFormerIssuer, describeTickerChange, movedAwayChange, startTickerChangeLookup, tickerChangeLine, type FormerIssuer, type TickerChange,
|
|
@@ -193,7 +259,21 @@ import { noSecReportsNotice, nonUsSecListingVenue, secFilingItemCodes } from "..
|
|
|
193
259
|
import type { MarketContext } from "../types";
|
|
194
260
|
import { windowRows } from "../row-window";
|
|
195
261
|
import { answerQuote, errorMessage, notUsTickerSentence } from "../quote-answer";
|
|
196
|
-
import { crossRate, describeFxConversion, describeFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
|
|
262
|
+
import { crossRate, describeFxConversion, describeFxRate, formatFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
|
|
263
|
+
import {
|
|
264
|
+
describeShortRate,
|
|
265
|
+
formatForwardPoints,
|
|
266
|
+
formatRatePercent,
|
|
267
|
+
formatSignedPercent,
|
|
268
|
+
IMPLIED_FORWARD_LABEL,
|
|
269
|
+
impliedForward,
|
|
270
|
+
parseForwardTenor,
|
|
271
|
+
shortRateFor,
|
|
272
|
+
} from "../fx-forward";
|
|
273
|
+
import { unavailableOnServer } from "../../plugins/builtin/shared/cloud-resource";
|
|
274
|
+
|
|
275
|
+
/** `fx`'s forward form, for its usage line. */
|
|
276
|
+
const FX_FORWARD_USAGE = "fx <base>/<quote> --forward <tenor>";
|
|
197
277
|
import {
|
|
198
278
|
holderListFacts,
|
|
199
279
|
holderListPeriod,
|
|
@@ -215,11 +295,20 @@ import {
|
|
|
215
295
|
} from "../../plugins/builtin/holders/report-rows";
|
|
216
296
|
import { fetchBeneficialOwners } from "../../plugins/builtin/holders/beneficial-client";
|
|
217
297
|
import { centralBankEventsMessage } from "../central-bank-events";
|
|
218
|
-
import { classSharesToday } from "../../plugins/builtin/holders/client";
|
|
298
|
+
import { classSharesAsOf, classSharesToday } from "../../plugins/builtin/holders/client";
|
|
299
|
+
import {
|
|
300
|
+
HOLDER_ROWS_MAX,
|
|
301
|
+
indexedHoldersPartialNote,
|
|
302
|
+
indexedHolderUnits,
|
|
303
|
+
indexHolderTicker,
|
|
304
|
+
loadIndexedHolders,
|
|
305
|
+
withIndexedHolders,
|
|
306
|
+
} from "../../plugins/builtin/holders/index-holders";
|
|
219
307
|
import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
|
|
220
308
|
import { currentReportItemsLabel } from "../../utils/sec-filings";
|
|
221
|
-
import { loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
222
|
-
import { insiderEmptyNotice } from "../../plugins/builtin/insider/issuer";
|
|
309
|
+
import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
310
|
+
import { insiderEmptyNotice, loadHomeInsiderReportsNote } from "../../plugins/builtin/insider/issuer";
|
|
311
|
+
import { buildInsiderSummary } from "../../plugins/builtin/insider/model";
|
|
223
312
|
import {
|
|
224
313
|
BENEFICIAL_REPORT_COLUMNS,
|
|
225
314
|
beneficialCoverageNotices,
|
|
@@ -248,6 +337,11 @@ type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
|
|
|
248
337
|
futuresContract?: FuturesContractTermsPayload;
|
|
249
338
|
/** The quote of the root's front contract, for a futures row that prices another one (NGF27, NG2). */
|
|
250
339
|
futuresFront?: Quote;
|
|
340
|
+
/**
|
|
341
|
+
* A per-MWh gas price (Dutch TTF) in dollars per MMBtu, the unit Henry Hub
|
|
342
|
+
* trades in, converted at the day's rate: one unit of the quote's currency in dollars.
|
|
343
|
+
*/
|
|
344
|
+
usdPerMmbtu?: UsdPerMmbtu;
|
|
251
345
|
/** A perpetual row's market, funding per eight hours and open interest. */
|
|
252
346
|
perpetualContract?: PerpetualContractPayload;
|
|
253
347
|
/** The symbol changed: its issuer moved to another symbol, or its holder had another before. */
|
|
@@ -290,18 +384,19 @@ function uniqueEntries(entries: readonly string[]): string[] {
|
|
|
290
384
|
|
|
291
385
|
/**
|
|
292
386
|
* The symbols to resolve: each entry with a space read as a company's name
|
|
293
|
-
* when search finds one, else as its words
|
|
294
|
-
* with the repeats that leaves dropped. A name
|
|
387
|
+
* when search finds one, else as its words when they are symbols
|
|
388
|
+
* (`readNameEntries`), with the repeats that leaves dropped. A name stays whole.
|
|
295
389
|
*/
|
|
296
390
|
async function readSymbols(symbols: readonly string[], dataProvider: Parameters<typeof readNameEntries>[1]): Promise<string[]> {
|
|
297
391
|
return uniqueEntries(await readNameEntries(symbols, dataProvider));
|
|
298
392
|
}
|
|
299
393
|
|
|
300
394
|
/**
|
|
301
|
-
* The symbols of `--file <path>`
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* pipes in nothing never
|
|
395
|
+
* The symbols typed, of `--file <path>` and of standard input (`-`), read by
|
|
396
|
+
* the reader `watchlist add` uses, and its notes on what it left out (a
|
|
397
|
+
* stray separator, the prices beside the symbols, in one line). Standard
|
|
398
|
+
* input is read only when asked for, so a script that pipes in nothing never
|
|
399
|
+
* waits on it.
|
|
305
400
|
*/
|
|
306
401
|
async function readListedSymbols(
|
|
307
402
|
args: readonly string[],
|
|
@@ -314,7 +409,11 @@ async function readListedSymbols(
|
|
|
314
409
|
const list = await readSymbolList({
|
|
315
410
|
args, file, stdinPiped: false, readEntries: (entries) => readNameEntries(entries, market.dataProvider),
|
|
316
411
|
});
|
|
317
|
-
|
|
412
|
+
// Only numbers, or nothing: the message says what was left out (`Ignored 1.899, a number, not a symbol.`).
|
|
413
|
+
if (list.symbols.length === 0) {
|
|
414
|
+
const where = file && file !== "-" ? file : args.includes("-") ? "standard input" : "what was given";
|
|
415
|
+
ctx.fail([`No symbols in ${where}.`, ...list.notes].join(" "), usage);
|
|
416
|
+
}
|
|
318
417
|
return { symbols: uniqueEntries(list.symbols), notes: list.notes };
|
|
319
418
|
} catch (error) {
|
|
320
419
|
if (error instanceof SymbolListError) ctx.fail(error.message, usage);
|
|
@@ -322,6 +421,15 @@ async function readListedSymbols(
|
|
|
322
421
|
}
|
|
323
422
|
}
|
|
324
423
|
|
|
424
|
+
/** What `quote --csv` and `compare --csv` write, for `help quote` and `help compare`. */
|
|
425
|
+
const QUOTE_CSV_SECTION = {
|
|
426
|
+
title: "CSV",
|
|
427
|
+
lines: [
|
|
428
|
+
"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.",
|
|
429
|
+
"A row with no figure says why in its status column, never NaN; a symbol with no quote is also named on standard error.",
|
|
430
|
+
],
|
|
431
|
+
};
|
|
432
|
+
|
|
325
433
|
/** `--file` of quote and compare, as `watchlist add` takes it. */
|
|
326
434
|
const LIST_FILE_OPTION = {
|
|
327
435
|
flags: "--file <path>",
|
|
@@ -333,8 +441,8 @@ function listingMetadata(identity: ListingIdentity) {
|
|
|
333
441
|
return { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name };
|
|
334
442
|
}
|
|
335
443
|
|
|
336
|
-
// Fitted to a narrow width,
|
|
337
|
-
// What a user types or reads the price by stays whole.
|
|
444
|
+
// Fitted to a narrow width, quote's Updated, Feed, Cur, Chg Basis and session prints move to labelled lines under
|
|
445
|
+
// their row, in that order, before Name wraps: quote never drops a column. What a user types or reads the price by stays whole.
|
|
338
446
|
const QUOTE_LEAD_COLUMNS = [
|
|
339
447
|
{ key: "symbol", header: "Symbol", shrink: false },
|
|
340
448
|
{ key: "name", header: "Name" },
|
|
@@ -342,18 +450,24 @@ const QUOTE_LEAD_COLUMNS = [
|
|
|
342
450
|
key: "price",
|
|
343
451
|
header: "Last",
|
|
344
452
|
align: "right" as const,
|
|
345
|
-
|
|
346
|
-
|
|
453
|
+
// An export writes the number; the row's Cur and currency_or_unit name what it is in.
|
|
454
|
+
value: (row: QuoteRow) => exportFigure(row.rawPrice),
|
|
455
|
+
format: (_value: unknown, row: QuoteRow) => (
|
|
456
|
+
row.error && !row.price
|
|
347
457
|
// A typo is not an outage: say what is wrong with the symbol itself.
|
|
348
458
|
? isNotATickerMessage(row.error) ? cliStyles.warning("not a ticker") : cliStyles.danger("unavailable")
|
|
349
|
-
: yieldLevelCell(
|
|
459
|
+
: yieldLevelCell(row.price, row)
|
|
350
460
|
),
|
|
351
461
|
},
|
|
352
462
|
{
|
|
353
463
|
key: "changePercent",
|
|
354
464
|
header: "Chg%",
|
|
355
465
|
align: "right" as const,
|
|
356
|
-
format: (value: unknown, row: QuoteRow) =>
|
|
466
|
+
format: (value: unknown, row: QuoteRow) => {
|
|
467
|
+
const move = row.yieldIndex ? yieldChangeCell(row) : formatChangePercentCell(value);
|
|
468
|
+
// Keep a rolling move's short label with the value when narrow tables drop Chg Basis.
|
|
469
|
+
return move && row.changeBasis === "24h" ? `${move} ${cliStyles.muted("24h")}` : move;
|
|
470
|
+
},
|
|
357
471
|
},
|
|
358
472
|
// Text drops a column no row fills, so a table without an extended print stays as narrow as before.
|
|
359
473
|
extendedColumn("PRE"),
|
|
@@ -372,16 +486,21 @@ type QuoteRow = ReturnType<typeof quoteRows>[number];
|
|
|
372
486
|
* its existing columns, so a column read by position stays where it was.
|
|
373
487
|
*/
|
|
374
488
|
const CHANGE_BASIS_COLUMN = {
|
|
375
|
-
key: "changeBasis", header: "Chg Basis", shrink: false,
|
|
489
|
+
key: "changeBasis", header: "Chg Basis", shrink: false, stackPriority: 2,
|
|
490
|
+
format: (value: unknown) => value === "24h" ? "" : cliStyles.muted(String(value ?? "")),
|
|
376
491
|
};
|
|
377
492
|
const CHANGE_BASIS_EXPORT_COLUMNS = [
|
|
378
493
|
{ key: "changeBasis", header: "Chg Basis" },
|
|
379
494
|
{ key: "changeBasisNote", header: "Chg Basis Note" },
|
|
380
495
|
];
|
|
381
496
|
|
|
382
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* The table's columns: Chg Basis follows Chg%, and the note is a line under
|
|
499
|
+
* the table. The figures an export adds (an extended print's move, the day's
|
|
500
|
+
* low and high) are already in the table's own cells.
|
|
501
|
+
*/
|
|
383
502
|
function quoteTextColumns<Column extends { key: string }>(columns: Column[]): Array<Column | typeof CHANGE_BASIS_COLUMN> {
|
|
384
|
-
const exported = new Set(CHANGE_BASIS_EXPORT_COLUMNS.map((column) => column.key));
|
|
503
|
+
const exported = new Set([...CHANGE_BASIS_EXPORT_COLUMNS, ...EXTENDED_CHANGE_EXPORT_COLUMNS, ...DAY_RANGE_EXPORT_COLUMNS].map((column) => column.key));
|
|
385
504
|
const kept = columns.filter((column) => !exported.has(column.key));
|
|
386
505
|
const at = kept.findIndex((column) => column.key === "changePercent") + 1;
|
|
387
506
|
return [...kept.slice(0, at), CHANGE_BASIS_COLUMN, ...kept.slice(at)];
|
|
@@ -398,28 +517,70 @@ function yieldChangeCell(row: QuoteRow): string {
|
|
|
398
517
|
return row.changeBasisPoints == null ? "" : colorBySign(formatBasisPoints(row.changeBasisPoints / 100, 1), row.changeBasisPoints);
|
|
399
518
|
}
|
|
400
519
|
|
|
520
|
+
/** A figure as an export writes it: the number without float noise, nothing for none. */
|
|
521
|
+
function exportFigure(value: number | null | undefined): number | null {
|
|
522
|
+
return value == null ? null : Number.isFinite(value) ? exportNumber(value) : value;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const EXTENDED_SESSIONS = ["PRE", "POST", "OVERNIGHT"] as const satisfies readonly ExtendedSession[];
|
|
526
|
+
|
|
527
|
+
function extendedKey(session: ExtendedSession): string {
|
|
528
|
+
return session === "PRE" ? "preMarket" : session === "POST" ? "afterHours" : "overnight";
|
|
529
|
+
}
|
|
530
|
+
|
|
401
531
|
/**
|
|
402
532
|
* The pre-market, after-hours or overnight print and its move from the
|
|
403
533
|
* regular close, in its own column; "mid" when it is a bid and ask midpoint.
|
|
534
|
+
* An export writes the price alone, its move going to `<session> Chg%`
|
|
535
|
+
* (EXTENDED_CHANGE_EXPORT_COLUMNS) and a midpoint to a closing note.
|
|
404
536
|
*/
|
|
405
537
|
function extendedColumn(session: ExtendedSession) {
|
|
406
538
|
const price = (row: QuoteRow) => row.extendedMark ? `${row.extendedPrice} mid` : row.extendedPrice;
|
|
407
539
|
return {
|
|
408
|
-
key: session
|
|
540
|
+
key: extendedKey(session),
|
|
409
541
|
header: EXTENDED_SESSION_LABELS[session],
|
|
410
542
|
align: "right" as const,
|
|
411
|
-
value: (row: QuoteRow) => row.extendedSession === session
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
543
|
+
value: (row: QuoteRow) => row.extendedSession === session ? exportFigure(row.rawExtendedPrice) : null,
|
|
544
|
+
format: (_value: unknown, row: QuoteRow) => (
|
|
545
|
+
row.extendedSession === session && row.extendedPrice
|
|
546
|
+
? [price(row), formatChangePercentCell(row.extendedChangePercent)].join(" ").trim()
|
|
547
|
+
: ""
|
|
416
548
|
),
|
|
417
549
|
};
|
|
418
550
|
}
|
|
419
551
|
|
|
420
|
-
/**
|
|
552
|
+
/**
|
|
553
|
+
* Each extended print's move from the regular close, as a number: the
|
|
554
|
+
* columns an export adds after those it had, so a column read by position
|
|
555
|
+
* stays where it was. The table keeps the move beside the price.
|
|
556
|
+
*/
|
|
557
|
+
const EXTENDED_CHANGE_EXPORT_COLUMNS = EXTENDED_SESSIONS.map((session) => ({
|
|
558
|
+
key: `${extendedKey(session)}ChangePercent`,
|
|
559
|
+
header: `${EXTENDED_SESSION_LABELS[session]} Chg%`,
|
|
560
|
+
value: (row: QuoteRow) => row.extendedSession === session ? row.extendedChangePercent : null,
|
|
561
|
+
}));
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Why an exported quote row's figures are empty, for its status column: the
|
|
565
|
+
* symbol's failure as the warning says it, or the figure it has no value for.
|
|
566
|
+
*/
|
|
567
|
+
function quoteExportStatus(row: QuoteRow): string {
|
|
568
|
+
if (row.rawPrice == null || !Number.isFinite(row.rawPrice)) return row.error || "Last: no price";
|
|
569
|
+
if (row.changePercent == null || !Number.isFinite(row.changePercent)) return "Chg%: no previous close";
|
|
570
|
+
return "";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** The closing note of an export with an extended price that is a bid and ask midpoint: the cell holds the bare number. */
|
|
574
|
+
function extendedMidNotes(data: readonly QuoteCliRecord[]): string[] {
|
|
575
|
+
return data.flatMap((row) => {
|
|
576
|
+
const extended = getExtendedSessionDisplay(row.quote);
|
|
577
|
+
return extended?.mark ? [`${row.target.symbol} ${EXTENDED_SESSION_LABELS[extended.session]} is the bid and ask midpoint; nothing has traded in that session`] : [];
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** A percent to two decimals, as the table prints it; none for a move that is no number (a zero close). */
|
|
421
582
|
function roundedPercent(value: number | undefined): number | null {
|
|
422
|
-
return value == null ? null : Number(value.toFixed(2));
|
|
583
|
+
return value == null || !Number.isFinite(value) ? null : Number(value.toFixed(2));
|
|
423
584
|
}
|
|
424
585
|
|
|
425
586
|
/** The quote's currency code, US cents as US¢; an export keeps the code (USX). */
|
|
@@ -427,35 +588,54 @@ function currencyCell(value: unknown): string {
|
|
|
427
588
|
return currencyCodeLabel(typeof value === "string" ? value : "");
|
|
428
589
|
}
|
|
429
590
|
|
|
591
|
+
/** The session prints of a narrow quote table move under their row after its other columns, before the name. */
|
|
592
|
+
const EXTENDED_KEYS = new Set(["preMarket", "afterHours", "overnight"]);
|
|
593
|
+
|
|
430
594
|
function quoteColumns() {
|
|
431
595
|
return [
|
|
432
|
-
...QUOTE_LEAD_COLUMNS,
|
|
433
|
-
{ key: "currency", header: "Cur", shrink: false,
|
|
596
|
+
...QUOTE_LEAD_COLUMNS.map((column) => EXTENDED_KEYS.has(column.key) ? { ...column, stackPriority: 1 } : column),
|
|
597
|
+
{ key: "currency", header: "Cur", shrink: false, stackPriority: 3, format: currencyCell },
|
|
434
598
|
// Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
|
|
435
599
|
// row says so here, so the closing line's "N of M stale" always points at rows the table marks.
|
|
436
600
|
{
|
|
437
601
|
key: "feed",
|
|
438
602
|
header: "Feed",
|
|
439
603
|
shrink: false,
|
|
440
|
-
|
|
441
|
-
dropPriority: 2,
|
|
604
|
+
stackPriority: 4,
|
|
442
605
|
format: (value: unknown) => value === "stale" ? cliStyles.warning(value) : String(value ?? ""),
|
|
443
606
|
},
|
|
444
|
-
|
|
445
|
-
{ key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
|
|
607
|
+
{ key: "updatedAt", header: "Updated", shrink: false, stackPriority: 5 },
|
|
446
608
|
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
609
|
+
...EXTENDED_CHANGE_EXPORT_COLUMNS,
|
|
447
610
|
];
|
|
448
611
|
}
|
|
449
612
|
|
|
613
|
+
/** The day's low and high as numbers, after the columns an export already had. */
|
|
614
|
+
const DAY_RANGE_EXPORT_COLUMNS = [
|
|
615
|
+
{ key: "dayLow", header: "Day Low", value: (row: QuoteRow) => exportFigure(row.rawDayLow) },
|
|
616
|
+
{ key: "dayHigh", header: "Day High", value: (row: QuoteRow) => exportFigure(row.rawDayHigh) },
|
|
617
|
+
];
|
|
618
|
+
|
|
450
619
|
function compareColumns() {
|
|
451
620
|
return [
|
|
452
621
|
...QUOTE_LEAD_COLUMNS,
|
|
453
|
-
{
|
|
622
|
+
{
|
|
623
|
+
key: "previousClose", header: "Prev Close", align: "right" as const,
|
|
624
|
+
value: (row: QuoteRow) => exportFigure(row.rawPreviousClose),
|
|
625
|
+
format: (_value: unknown, row: QuoteRow) => yieldLevelCell(row.previousClose, row),
|
|
626
|
+
},
|
|
454
627
|
// A narrow terminal drops these before it cuts the names short.
|
|
455
|
-
|
|
628
|
+
// An export writes the range as plain numbers (12.5-13.1), and each end as a number in Day Low and Day High.
|
|
629
|
+
{
|
|
630
|
+
key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1,
|
|
631
|
+
value: (row: QuoteRow) => row.rawDayLow != null && row.rawDayHigh != null ? `${exportFigure(row.rawDayLow)}-${exportFigure(row.rawDayHigh)}` : "",
|
|
632
|
+
format: (_value: unknown, row: QuoteRow) => row.dayRange,
|
|
633
|
+
},
|
|
456
634
|
{ key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
|
|
457
635
|
{ key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3, format: currencyCell },
|
|
458
636
|
...CHANGE_BASIS_EXPORT_COLUMNS,
|
|
637
|
+
...EXTENDED_CHANGE_EXPORT_COLUMNS,
|
|
638
|
+
...DAY_RANGE_EXPORT_COLUMNS,
|
|
459
639
|
];
|
|
460
640
|
}
|
|
461
641
|
|
|
@@ -509,12 +689,18 @@ function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { prev
|
|
|
509
689
|
return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
|
|
510
690
|
}
|
|
511
691
|
|
|
692
|
+
/** An explicit missing comparison wins over any leftover numeric move in a CLI quote. */
|
|
693
|
+
function quoteSessionFields(quote: Quote | null | undefined) {
|
|
694
|
+
const fields = getQuoteSessionFields(quote);
|
|
695
|
+
return quote?.changeBasis === "none" ? { ...fields, change: null, changePercent: null } : fields;
|
|
696
|
+
}
|
|
697
|
+
|
|
512
698
|
function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
513
699
|
return results.map((result) => {
|
|
514
700
|
const quote = result.quote;
|
|
515
701
|
// Last and Chg% are the regular session, as `ticker` and the quote monitor read it; a
|
|
516
702
|
// pre-market or after-hours print is its own column, measured from that session's close.
|
|
517
|
-
const display =
|
|
703
|
+
const display = quoteSessionFields(quote);
|
|
518
704
|
const extended = getExtendedSessionDisplay(quote);
|
|
519
705
|
const yieldIndex = isYieldIndexSymbol(result.target.symbol);
|
|
520
706
|
// An index level is in points, not in the currency its members trade in.
|
|
@@ -526,25 +712,26 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
526
712
|
minimumFractionDigits: indexPoints ? 2 : currencyPriceMinDigits(quote?.currency),
|
|
527
713
|
...nearParCoin(quote) ? { fixedFractionDigits: STABLECOIN_PRICE_DIGITS } : {},
|
|
528
714
|
};
|
|
529
|
-
const price = (value: number | undefined) => (
|
|
715
|
+
const price = (value: number | null | undefined) => (
|
|
530
716
|
quote && value != null
|
|
531
717
|
? indexPoints ? formatMarketPrice(value, options) : formatMarketPriceWithCurrency(value, quote.currency, options)
|
|
532
718
|
: ""
|
|
533
719
|
);
|
|
534
720
|
// A price in cents says what it is per, as the contract terms word it: 314.95¢/lb.
|
|
535
|
-
const per = display
|
|
721
|
+
const per = display.price != null ? futuresPricePer(result.futuresContract, quote?.currency) : "";
|
|
722
|
+
const previousClose = display.price != null && display.change != null ? display.price - display.change : quote?.previousClose;
|
|
536
723
|
return {
|
|
537
724
|
symbol: result.target.symbol,
|
|
538
725
|
name: quote?.name || result.listing?.name || "",
|
|
539
|
-
price: `${price(display
|
|
540
|
-
rawPrice: display
|
|
726
|
+
price: `${price(display.price)}${per}`,
|
|
727
|
+
rawPrice: display.price,
|
|
541
728
|
priceBasis: quote?.priceBasis ?? null,
|
|
542
729
|
instrumentType: quote?.instrumentType ?? null,
|
|
543
|
-
change: display
|
|
544
|
-
changePercent: roundedPercent(display
|
|
730
|
+
change: display.change,
|
|
731
|
+
changePercent: roundedPercent(display.changePercent ?? undefined),
|
|
545
732
|
// A Cboe yield index is read as a yield in percent and moves in basis points.
|
|
546
733
|
yieldIndex,
|
|
547
|
-
changeBasisPoints: yieldIndex && display
|
|
734
|
+
changeBasisPoints: yieldIndex && display.change != null ? toBasisPoints(display.change, 1) : null,
|
|
548
735
|
extendedSession: extended?.session ?? null,
|
|
549
736
|
extendedPrice: price(extended?.price),
|
|
550
737
|
rawExtendedPrice: extended?.price ?? null,
|
|
@@ -555,7 +742,10 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
|
|
|
555
742
|
session: quoteSessionStateLabel(quote, now) ?? "",
|
|
556
743
|
sessionLabel: quoteSessionLabel(quote, now) ?? "",
|
|
557
744
|
// The close the shown move is measured from: the one before the session Last is.
|
|
558
|
-
previousClose: price(
|
|
745
|
+
previousClose: price(previousClose),
|
|
746
|
+
rawPreviousClose: previousClose ?? null,
|
|
747
|
+
rawDayLow: quote?.low != null && quote.high != null ? quote.low : null,
|
|
748
|
+
rawDayHigh: quote?.low != null && quote.high != null ? quote.high : null,
|
|
559
749
|
dayRange: quote?.low != null && quote.high != null
|
|
560
750
|
? indexPoints ? `${price(quote.low)}-${price(quote.high)}` : formatPriceRange(quote.low, quote.high, quote.currency, options, "-")
|
|
561
751
|
: "",
|
|
@@ -624,15 +814,17 @@ function filingsWithItemLabels(filings: SecFilingItem[]) {
|
|
|
624
814
|
});
|
|
625
815
|
}
|
|
626
816
|
|
|
627
|
-
|
|
817
|
+
/** The summary over the ratings: the upside from Last, and the price and time it is measured from, as `fn ANR` gives them. */
|
|
818
|
+
function analystSummary(data: AnalystResearchData, quote: Quote | null): string {
|
|
628
819
|
const target = data.priceTarget;
|
|
629
820
|
const currency = analystTargetCurrency(data);
|
|
630
|
-
const upside =
|
|
821
|
+
const { upside, reference } = analystUpside(data, quote);
|
|
631
822
|
const analysts = recommendationTotal(data);
|
|
632
823
|
const mix = formatRecommendationMix(data);
|
|
633
824
|
const entries: Array<[string, string]> = [
|
|
634
825
|
["Average Target", formatAnalystPrice(target?.average, currency)],
|
|
635
826
|
["Upside", upside == null ? "-" : formatPercent(upside)],
|
|
827
|
+
["Upside vs", upside == null ? "-" : formatAnalystReference(reference, currency)],
|
|
636
828
|
["Low / Median / High", target && [target.low, target.median, target.high].some((value) => value != null)
|
|
637
829
|
? [target.low, target.median, target.high].map((value) => formatAnalystPrice(value, currency)).join(" / ")
|
|
638
830
|
: "-"],
|
|
@@ -733,13 +925,13 @@ function formatEpsCell(value: unknown): string {
|
|
|
733
925
|
return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "";
|
|
734
926
|
}
|
|
735
927
|
|
|
736
|
-
function earningsRows(events:
|
|
928
|
+
function earningsRows(events: ResolvedEarningsEvent[]) {
|
|
737
929
|
return events.map((event) => ({
|
|
738
930
|
symbol: event.symbol,
|
|
739
931
|
name: event.name,
|
|
740
932
|
date: isoDate(event.earningsDate).slice(0, 10),
|
|
741
|
-
//
|
|
742
|
-
status: earningsDateLabel(event),
|
|
933
|
+
// Out, announced by the company, or projected from its past reports.
|
|
934
|
+
status: earningsDateLabel(event.dateStatus),
|
|
743
935
|
timing: event.timing,
|
|
744
936
|
epsEstimate: event.epsEstimate,
|
|
745
937
|
epsActual: event.epsActual,
|
|
@@ -755,17 +947,17 @@ const MAX_CHOICE_LOOKUPS = 10;
|
|
|
755
947
|
* The line `quote` prints beside a symbol that changed, as `history` and
|
|
756
948
|
* `ticker` word it: a company that moved, with nothing new listed there,
|
|
757
949
|
* names the quote of its new symbol ("HFC became DINO on 2022-03-15:
|
|
758
|
-
* `
|
|
950
|
+
* `gloom quote DINO`"); its old line's last price stays in the row.
|
|
759
951
|
*/
|
|
760
952
|
function quoteChangeLine(change: TickerChange | null | undefined, formerIssuer?: FormerIssuer | null): string | null {
|
|
761
953
|
const moved = movedAwayChange(change);
|
|
762
954
|
// One line per symbol: a company that left it, else the company that merged away from it (GOGL).
|
|
763
|
-
return (moved ? movedSymbolLine(moved, `
|
|
955
|
+
return (moved ? movedSymbolLine(moved, `gloom quote ${moved.tradesAs}`) : tickerChangeLine(change)) ?? formerIssuer?.note ?? null;
|
|
764
956
|
}
|
|
765
957
|
|
|
766
958
|
async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], commandName: "quote" | "compare") {
|
|
767
959
|
const args = [...rawArgs];
|
|
768
|
-
const usage = `Usage:
|
|
960
|
+
const usage = `Usage: gloom ${commandName} <symbol...> [--file <path>]`;
|
|
769
961
|
const exchange = takeOption(args, "--exchange") ?? "";
|
|
770
962
|
// A list from a file or standard input (`-`), read as `watchlist add` reads one.
|
|
771
963
|
const file = args.some((arg) => arg === "--file" || arg.startsWith("--file=")) ? takeOption(args, "--file") ?? "" : undefined;
|
|
@@ -778,16 +970,33 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
778
970
|
await withMarketData(ctx, async (market) => {
|
|
779
971
|
const read = phrase ? await readPhraseArgument(phrase, market.dataProvider) : null;
|
|
780
972
|
if (read?.kind === "phrase") return answerPhrase(read.phrase, market, ctx);
|
|
781
|
-
//
|
|
782
|
-
const list =
|
|
973
|
+
// Typed or from a file, one reader names what it left out: a stray separator, the prices beside the symbols.
|
|
974
|
+
const list = read ? null : await readListedSymbols(args, file, market, ctx, usage);
|
|
783
975
|
const symbols = read?.kind === "name" ? [read.name]
|
|
784
976
|
: read?.kind === "symbols" ? uniqueEntries(read.symbols)
|
|
785
|
-
: list
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
977
|
+
: list!.symbols;
|
|
978
|
+
// What cannot be a symbol (N/A, $$$, a price alone) is never looked up; one line names it.
|
|
979
|
+
const refused = symbols.flatMap((symbol) => notASymbolError(symbol) ?? []);
|
|
980
|
+
if (refused.length === symbols.length) {
|
|
981
|
+
ctx.fail(refused.length === 1 ? refused[0]!.message : `Nothing to quote: ${refusedList(refused)} are not symbols.`, usage);
|
|
982
|
+
}
|
|
983
|
+
// With several symbols, --exchange is for the ones that name no exchange of their own, and one that names no
|
|
984
|
+
// listing (EQT:NYSE:US) is named in a line while the others are quoted.
|
|
985
|
+
const resolvedListings = await Promise.all(symbols.filter((symbol) => !notASymbolError(symbol)).map(async (symbol) => {
|
|
986
|
+
if (symbols.length === 1) return { symbol, listing: await requireCliListing(symbol, exchange, market, ctx) };
|
|
987
|
+
try {
|
|
988
|
+
return { symbol, listing: await resolveCliListing(symbol, exchange, market, { ownExchangeWins: true }) };
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (error instanceof ListingArgError) return { symbol, error };
|
|
991
|
+
throw error;
|
|
992
|
+
}
|
|
993
|
+
}));
|
|
994
|
+
const unread = resolvedListings.flatMap((entry) => "error" in entry && entry.error ? [{ symbol: entry.symbol, error: entry.error }] : []);
|
|
995
|
+
const listings = resolvedListings.flatMap((entry) => "listing" in entry && entry.listing ? [entry.listing] : []);
|
|
996
|
+
if (listings.length === 0) {
|
|
997
|
+
const [first] = unread;
|
|
998
|
+
ctx.fail(first!.error.message, [...unread.slice(1).map(unreadLine), ...refused.length > 0 ? [refusedLine(refused)] : []].join("\n") || first!.error.details);
|
|
999
|
+
}
|
|
791
1000
|
const targets: QuoteSubscriptionTarget[] = listings.map((listing) => ({ symbol: listing.request.symbol, exchange: listing.request.exchange }));
|
|
792
1001
|
// A bare symbol's venues load beside its quote, to name the company it was read as when several list it.
|
|
793
1002
|
// Each costs a search, so a long list (a portfolio check, not a lookup) names none.
|
|
@@ -797,7 +1006,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
797
1006
|
const changes = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
|
|
798
1007
|
: listings.map((listing) => [listing, startTickerChangeLookup(market.dataProvider, listing.symbol, listing.exchange)] as const));
|
|
799
1008
|
// The service's quote of the moment, not each form's local copy from whenever it was last asked for:
|
|
800
|
-
// BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote
|
|
1009
|
+
// BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote. A local copy answers only offline.
|
|
801
1010
|
const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true });
|
|
802
1011
|
const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
|
|
803
1012
|
const asked = results.map(listingOf);
|
|
@@ -818,7 +1027,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
818
1027
|
const tickerChanges = symbolChanges.map(({ change }) => change);
|
|
819
1028
|
const formerIssuers = symbolChanges.map(({ formerIssuer }) => formerIssuer);
|
|
820
1029
|
const failed = answers[0]?.failure;
|
|
821
|
-
if (
|
|
1030
|
+
if (symbols.length === 1 && failed) {
|
|
822
1031
|
ctx.fail(failed.message, [quoteChangeLine(tickerChanges[0], formerIssuers[0]), failed.details].filter(Boolean).join("\n") || undefined);
|
|
823
1032
|
}
|
|
824
1033
|
// A futures quote adds its dated settlement and contract terms, as `ticker` prints them; `compare` stays a table.
|
|
@@ -827,6 +1036,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
827
1036
|
: null));
|
|
828
1037
|
// A contract that is not its root's front (NGF27, NG2) prints the front's own price beside it, read in one batch.
|
|
829
1038
|
const fronts = await loadFuturesFrontQuotes(contracts, market.dataProvider);
|
|
1039
|
+
// A per-MWh gas price (TTF) adds its dollars per MMBtu at the day's rate, to read beside Henry Hub.
|
|
1040
|
+
const gasInUsd = await Promise.all(answers.map(({ result }, index) => loadUsdPerMmbtu(
|
|
1041
|
+
getRegularSessionDisplay(result.quote)?.price, contracts[index], result.quote?.currency, (currency) => market.dataProvider.getExchangeRate(currency))));
|
|
830
1042
|
// A perpetual adds its funding per eight hours and open interest, the figures PERP lists beside its mark.
|
|
831
1043
|
const perpetualContracts = await Promise.all(answers.map(({ result }) => commandName === "quote" && result.quote
|
|
832
1044
|
? cliPerpetualContract(result.quote)
|
|
@@ -841,12 +1053,13 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
841
1053
|
target: result.target,
|
|
842
1054
|
listing: listingMetadata(listingIdentity(listing, result.quote)),
|
|
843
1055
|
// The figures the table shows, beside the quote as the source sent it.
|
|
844
|
-
...
|
|
1056
|
+
...quoteSessionFields(result.quote),
|
|
845
1057
|
// What `change` is measured from, with the label and holiday note the table shows.
|
|
846
1058
|
changeBasis: changeBasisJson(quoteChangeBasis(result.quote, { terms: contracts[index] }), now),
|
|
847
1059
|
quote: withPreviousCloseDate(result.quote),
|
|
848
1060
|
...contracts[index] ? { futuresContract: contracts[index] } : {},
|
|
849
1061
|
...fronts[index] ? { futuresFront: fronts[index] } : {},
|
|
1062
|
+
...gasInUsd[index] ? { usdPerMmbtu: gasInUsd[index] } : {},
|
|
850
1063
|
...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
|
|
851
1064
|
...usSuggestions[index] ? { usSuggestion: usSuggestions[index] } : {},
|
|
852
1065
|
error: failure?.oneLine ?? errorMessage(result.error),
|
|
@@ -856,21 +1069,34 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
856
1069
|
}));
|
|
857
1070
|
const notes = [
|
|
858
1071
|
...list?.notes ?? [],
|
|
1072
|
+
...refused.length > 0 ? [refusedLine(refused)] : [],
|
|
1073
|
+
...unread.map(unreadLine),
|
|
859
1074
|
// FB now lists another security; the company that was FB trades as META. A symbol a
|
|
860
1075
|
// company renamed into (META) is what was typed: its change is in the row, not here.
|
|
861
1076
|
// GOGL was Golden Ocean, which merged into CMB.Tech; a fund lists there now.
|
|
862
1077
|
...tickerChanges.flatMap((change, index) => quoteChangeLine(change, formerIssuers[index]) ?? []),
|
|
863
1078
|
// Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
|
|
864
1079
|
...answers.flatMap((answer) => answer.notes),
|
|
865
|
-
// A row that stopped trading long ago reads as a quote that is merely stale
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1080
|
+
// A row that stopped trading long ago reads as a quote that is merely stale, and one with
|
|
1081
|
+
// nothing printed in a session the calendar has open (a feed that stalled, a typhoon day) says since when.
|
|
1082
|
+
...data.flatMap((row) => {
|
|
1083
|
+
if (!row.quote) return [];
|
|
1084
|
+
const label = `${row.target.symbol}${row.listing.name ? ` (${row.listing.name})` : ""}`;
|
|
1085
|
+
const ended = noLongerTradingNote(label, row.quote, now);
|
|
1086
|
+
const quiet = ended ? null : noPrintsTodayText(row.quote, now);
|
|
1087
|
+
return [ended ?? (quiet ? `${label}: ${quiet}` : []), quoteBarNotes(label, row.quote, now)].flat();
|
|
1088
|
+
}),
|
|
1089
|
+
// A receipt and its home line each say the ratio, as `ticker` does.
|
|
1090
|
+
...data.flatMap((row) => {
|
|
1091
|
+
const pair = listingDepositaryPair({ quote: row.quote });
|
|
1092
|
+
return pair ? [`${row.target.symbol}: ${depositaryPairText(pair, isReceiptSide({ quote: row.quote }))}.`] : [];
|
|
1093
|
+
}),
|
|
1094
|
+
// A cell shows only "unavailable", and an exported row only its status, so stderr names each failure; the JSON rows carry each reason.
|
|
1095
|
+
...ctx.cliOptions.format !== "json" ? quoteNotes(data, { exchange }) : [],
|
|
871
1096
|
];
|
|
872
1097
|
const columns = commandName === "compare" ? compareColumns() : quoteColumns();
|
|
873
1098
|
const basisNotes = changeBasisNotes(data, now);
|
|
1099
|
+
const midNotes = extendedMidNotes(data);
|
|
874
1100
|
if (ctx.cliOptions.format !== "json") await primeBoardCoins(notes);
|
|
875
1101
|
// Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
|
|
876
1102
|
const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
|
|
@@ -879,7 +1105,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
879
1105
|
warnings: notes.length > 0 ? notes : undefined,
|
|
880
1106
|
// A Cboe yield index stops at its 15:00 ET close, before the Treasury's curve is taken: the line says so, so its
|
|
881
1107
|
// close does not read as a contradiction of DGS10 or GC for the same day.
|
|
882
|
-
|
|
1108
|
+
// Dated by the quotes as served, which say whether one is the saved copy.
|
|
1109
|
+
freshness: withSavedCopy(
|
|
1110
|
+
quotesFreshness(data.map((row) => row.quote), yieldsOnly ? { schedule: `Cboe close ${CBOE_YIELD_INDEX_CLOSE}` } : undefined),
|
|
1111
|
+
answers.map(({ result }) => result.quote),
|
|
1112
|
+
),
|
|
883
1113
|
}, {
|
|
884
1114
|
rows: quoteRows,
|
|
885
1115
|
// An index row's Cur is blank, its level having no currency; the export names what it is in instead.
|
|
@@ -887,6 +1117,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
887
1117
|
symbolOrSeries: row.symbol,
|
|
888
1118
|
currencyOrUnit: row.currency || (row.yieldIndex ? "pct" : isIndexLevel(row.instrumentType) ? INDEX_POINTS_EXPORT_UNIT : null),
|
|
889
1119
|
}),
|
|
1120
|
+
// A row with no price or move says why in its status column, as the warnings do.
|
|
1121
|
+
exportStatus: quoteExportStatus,
|
|
1122
|
+
...(midNotes.length > 0 ? { exportNotes: midNotes } : {}),
|
|
890
1123
|
details: quoteDetailsText,
|
|
891
1124
|
...(basisNotes.length ? { footnote: basisNotes.join("\n") } : {}),
|
|
892
1125
|
columns,
|
|
@@ -894,11 +1127,27 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
894
1127
|
? columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column)
|
|
895
1128
|
: columns),
|
|
896
1129
|
});
|
|
897
|
-
// The table
|
|
1130
|
+
// The table and the warnings name each symbol it found nothing for; with none found, the exit status says so too.
|
|
898
1131
|
if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
|
|
899
1132
|
});
|
|
900
1133
|
}
|
|
901
1134
|
|
|
1135
|
+
/** The entries refused before any lookup, named as written: `$$$, N/A`, split by semicolons when one has a comma. */
|
|
1136
|
+
function refusedList(refused: ReadonlyArray<{ input: string }>): string {
|
|
1137
|
+
const inputs = refused.map((error) => error.input.trim());
|
|
1138
|
+
return inputs.join(inputs.some((input) => input.includes(",")) ? "; " : ", ");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** The one line a quote prints for what it did not look up: `Not a symbol, not quoted: $$$, N/A.` */
|
|
1142
|
+
function refusedLine(refused: ReadonlyArray<{ input: string }>): string {
|
|
1143
|
+
return `Not ${refused.length === 1 ? "a symbol" : "symbols"}, not quoted: ${refusedList(refused)}.`;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** A symbol of a list that names no listing: `Not quoted: EQT:NYSE:US (Unknown exchange US for EQT:NYSE).` */
|
|
1147
|
+
function unreadLine({ symbol, error }: { symbol: string; error: ListingArgError }): string {
|
|
1148
|
+
return `Not quoted: ${symbol} (${error.message.replace(/\.$/, "")}).`;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
902
1151
|
/**
|
|
903
1152
|
* `quote "10 year yield"`: a phrase that is no ticker and no company's name
|
|
904
1153
|
* is answered with what search finds for it and the catalog, never quoted
|
|
@@ -908,7 +1157,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
908
1157
|
async function answerPhrase(phrase: string, market: MarketContext, ctx: Parameters<CliCommandDef["execute"]>[1]): Promise<void> {
|
|
909
1158
|
const said = `"${phrase}" is not a ticker or a company's name.`;
|
|
910
1159
|
if (ctx.cliOptions.format !== "text") {
|
|
911
|
-
ctx.fail(said, [knownGapMessage(phrase), `Search for it:
|
|
1160
|
+
ctx.fail(said, [knownGapMessage(phrase), `Search for it: gloom search "${phrase}"`, `Functions and data: gloom catalog "${phrase}"`]
|
|
912
1161
|
.filter(Boolean).join("\n"));
|
|
913
1162
|
}
|
|
914
1163
|
process.exitCode = CLI_EXIT_CODES.error;
|
|
@@ -963,12 +1212,15 @@ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
|
|
|
963
1212
|
}
|
|
964
1213
|
const terms = row.futuresContract;
|
|
965
1214
|
if (!terms || !row.quote) return [];
|
|
966
|
-
const settle: [string, string] = ["Settle",
|
|
1215
|
+
const settle: [string, string] = ["Settle", quoteSettleText(row.quote, terms)];
|
|
967
1216
|
// The move Chg% gives, in the quote's money and with what it is measured from: "vs Fri settle".
|
|
968
1217
|
const session = getRegularSessionDisplay(row.quote);
|
|
969
1218
|
const change: Array<[string, string]> = session?.change != null && Number.isFinite(session.change)
|
|
970
1219
|
? [["Change", quoteChangeText(row.quote, session, withCurrencyMinorDigits(quoteFormatOptions(row.quote, "FUTURE"), row.quote.currency), terms)]]
|
|
971
1220
|
: [];
|
|
1221
|
+
// European gas beside Henry Hub: "≈ $30.832/MMBtu at EUR/USD 1.1650", a conversion that says so.
|
|
1222
|
+
const inUsd = row.usdPerMmbtu;
|
|
1223
|
+
if (inUsd) change.push(["In USD", usdPerMmbtuText(inUsd)]);
|
|
972
1224
|
const metrics = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
|
|
973
1225
|
const rolls = futuresRollLines(terms, row.futuresFront).map((line): [string, string] => [line.label, line.value]);
|
|
974
1226
|
const at = futuresContractLines(terms, row.quote.symbol || row.target.symbol)[0]?.key === "contract" ? 1 : 0;
|
|
@@ -989,17 +1241,21 @@ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeB
|
|
|
989
1241
|
return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
|
|
990
1242
|
}
|
|
991
1243
|
|
|
992
|
-
/**
|
|
1244
|
+
/**
|
|
1245
|
+
* A crypto pair that will not load points at the crypto board. Structured
|
|
1246
|
+
* formats keep the error as thrown, and so does no connection at all, which
|
|
1247
|
+
* the board cannot get past either.
|
|
1248
|
+
*/
|
|
993
1249
|
function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
|
|
994
|
-
if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
|
|
1250
|
+
if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol) || isNetworkFailure(error)) throw error;
|
|
995
1251
|
return ctx.fail(errorMessage(error) ?? `No history available for ${symbol}`, CRYPTO_BOARD_HINT);
|
|
996
1252
|
}
|
|
997
1253
|
|
|
998
|
-
const HISTORY_USAGE = "history <symbol
|
|
1254
|
+
const HISTORY_USAGE = "history <symbol...> [--range <range> | --from <year|date> [--to <year|date>]]";
|
|
999
1255
|
const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
|
|
1000
1256
|
const HISTORY_YTD_RANGE = "YTD starts at the prior year's last close, the one year-to-date returns are measured from";
|
|
1001
|
-
/** Options people reach for to ask for dates, which history answers with
|
|
1002
|
-
const DATE_WINDOW_OPTIONS = new Set(["--
|
|
1257
|
+
/** Options people reach for to ask for dates, which history answers with --from and --to (or --start and --end). */
|
|
1258
|
+
const DATE_WINDOW_OPTIONS = new Set(["--since", "--until", "--date", "--start-date", "--end-date"]);
|
|
1003
1259
|
/** Options people reach for to pick a bar size, which history derives from the range. */
|
|
1004
1260
|
const BAR_SIZE_OPTIONS = new Set(["--interval", "--resolution", "--bar", "--bars", "--bar-size", "--granularity", "--timeframe", "--frequency", "--freq", "--tick", "--ticks"]);
|
|
1005
1261
|
|
|
@@ -1029,16 +1285,26 @@ function financialsSymbolArg(args: readonly string[]): string | undefined {
|
|
|
1029
1285
|
function financialsOptionHint(flag: string, args: readonly string[]): string | null {
|
|
1030
1286
|
if (!QUARTERLY_OPTIONS.has(flag)) return null;
|
|
1031
1287
|
const symbol = financialsSymbolArg(args) ?? "<symbol>";
|
|
1032
|
-
return `financials lists fiscal years. For quarters:
|
|
1033
|
-
+ `or
|
|
1288
|
+
return `financials lists fiscal years. For quarters: gloom fn GF ${symbol} --period quarterly --periods 40 (one metric at a time), `
|
|
1289
|
+
+ `or gloom fn FA ${symbol} --period quarterly (the statements).`;
|
|
1034
1290
|
}
|
|
1035
1291
|
|
|
1036
1292
|
function historyOptionHint(flag: string, args: readonly string[]): string | null {
|
|
1037
|
-
if (DATE_WINDOW_OPTIONS.has(flag))
|
|
1293
|
+
if (DATE_WINDOW_OPTIONS.has(flag)) {
|
|
1294
|
+
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(", ")}.`;
|
|
1295
|
+
}
|
|
1038
1296
|
if (BAR_SIZE_OPTIONS.has(flag)) return `history serves each --range in one bar size. ${subMinuteDataMessage(historySymbolArg(args))}`;
|
|
1039
1297
|
return null;
|
|
1040
1298
|
}
|
|
1041
1299
|
|
|
1300
|
+
/** "Asked from 1990-01-01, data starts 1995-01-03 (first available bar)" when the bars start over a week into the window. */
|
|
1301
|
+
function windowHistoryNotes(window: PriceWindow, rows: readonly { date: string }[]): string[] {
|
|
1302
|
+
const first = rows[0]?.date.slice(0, 10);
|
|
1303
|
+
if (!first) return [];
|
|
1304
|
+
return Date.parse(`${first}T00:00:00Z`) - Date.parse(`${window.from}T00:00:00Z`) > 7 * 86_400_000
|
|
1305
|
+
? [`Asked from ${window.from}, data starts ${first} (first available bar)`] : [];
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1042
1308
|
/** "Asked YTD, data starts 2026-03-05 (first available bar)" when no bar reaches the prior year's last close. */
|
|
1043
1309
|
function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
|
|
1044
1310
|
const first = rows[0]?.date.slice(0, 10);
|
|
@@ -1046,116 +1312,246 @@ function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
|
|
|
1046
1312
|
return first && last && first > `${Number(last.slice(0, 4)) - 1}-12-31` ? [`Asked YTD, data starts ${first} (first available bar)`] : [];
|
|
1047
1313
|
}
|
|
1048
1314
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1315
|
+
/** What `history` was asked for: a range, or a window of dates in its place. */
|
|
1316
|
+
interface HistoryAsk {
|
|
1317
|
+
range: HistoryRange;
|
|
1318
|
+
window: PriceWindow | null;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** One listing's bars as `history` prints them, with what its heading, warnings and closing line say. */
|
|
1322
|
+
interface ListingHistory {
|
|
1323
|
+
listing: CliListing;
|
|
1324
|
+
identity: ListingIdentity;
|
|
1325
|
+
unit: HistoryUnit;
|
|
1326
|
+
data: HistoryRow[];
|
|
1327
|
+
resolution: PriceHistoryResult["resolution"];
|
|
1328
|
+
interval: HistoryRow["interval"];
|
|
1329
|
+
decimals: number;
|
|
1330
|
+
/** Intraday bars, which print in UTC as times. */
|
|
1331
|
+
intraday: boolean;
|
|
1332
|
+
volume: HistoryVolume | null;
|
|
1333
|
+
freshness: ReportFreshness | undefined;
|
|
1334
|
+
notes: string[];
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** The command a listing that cannot be served is pointed back at: `history`, `history --range 5Y`. */
|
|
1338
|
+
function historyCommand({ range, window }: HistoryAsk): string {
|
|
1339
|
+
// A window that runs to today retries to the day it is run on, not the one it was first run on.
|
|
1340
|
+
return window ? `history --from ${window.from}${isPastPriceWindow(window) ? ` --to ${window.to}` : ""}` : range === "1Y" ? "history" : `history --range ${range}`;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* One listing's bars, dated and checked as `history` prints them. Without
|
|
1345
|
+
* `lookups` it does not look up the other venues of a bare symbol or whether
|
|
1346
|
+
* its company moved, which a long list skips, as `quote` does.
|
|
1347
|
+
*/
|
|
1348
|
+
async function loadListingHistory(
|
|
1349
|
+
listing: CliListing,
|
|
1350
|
+
market: MarketContext,
|
|
1351
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1352
|
+
ask: HistoryAsk,
|
|
1353
|
+
{ lookups }: { lookups: boolean },
|
|
1354
|
+
): Promise<ListingHistory> {
|
|
1355
|
+
const { range, window } = ask;
|
|
1052
1356
|
// YTD reads the year's daily bars and starts at the last one on or before 31 Dec.
|
|
1053
1357
|
const loadRange: TimeRange = range === "YTD" ? "1Y" : range;
|
|
1358
|
+
const venues = lookups ? loadBareVenues(listing, market) : null;
|
|
1359
|
+
const { symbol, exchange } = listing.request;
|
|
1360
|
+
const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
|
|
1361
|
+
const load = () => window ? loadPriceWindowHistory(market.dataProvider, symbol, exchange, window, context)
|
|
1362
|
+
: market.dataProvider.getPriceHistoryWithMetadata
|
|
1363
|
+
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
|
|
1364
|
+
: market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
|
|
1365
|
+
const quoteAnswer = loadListingQuoteAnswer(market.dataProvider, listing);
|
|
1366
|
+
const quoted = quoteAnswer.then((answer) => answer.quote);
|
|
1367
|
+
const fate = lookups ? listingFateLoader(listing, market, quoted) : null;
|
|
1368
|
+
const request = { command: historyCommand(ask), noun: "history", ...(fate ? { fate } : {}) };
|
|
1369
|
+
const [loaded, quote] = await Promise.all([
|
|
1370
|
+
loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, request)
|
|
1371
|
+
.catch((error) => failHistory(error, listing.key, ctx)),
|
|
1372
|
+
quoted,
|
|
1373
|
+
]);
|
|
1374
|
+
// A bare symbol several companies list is never served from the one the data service picked, in a range or a window:
|
|
1375
|
+
// it asks for an exchange as `quote` does, and in a list that is the symbol's row alone.
|
|
1376
|
+
await failUnchosenListing(listing, market, ctx, request, (await quoteAnswer).error);
|
|
1377
|
+
const { resolution } = loaded;
|
|
1378
|
+
// While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
|
|
1379
|
+
// A window that ended before today has no session in progress.
|
|
1380
|
+
const points = window && isPastPriceWindow(window) ? loaded.points
|
|
1381
|
+
: withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
|
|
1382
|
+
const fromQuote = points !== loaded.points;
|
|
1383
|
+
const identity = listingIdentity(listing, quote);
|
|
1384
|
+
// Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
|
|
1385
|
+
const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(logFailure("cli.commands-market", "Read quote metadata for history failed", null));
|
|
1386
|
+
const unit = historyUnit(listing.key, listed ?? quote);
|
|
1387
|
+
const traded = {
|
|
1388
|
+
listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
|
|
1389
|
+
};
|
|
1390
|
+
// A coin's volume is traded value in its quote currency: said in every row and on the status line.
|
|
1391
|
+
const volume = historyVolume(unit, traded);
|
|
1392
|
+
const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
|
|
1393
|
+
// Bars stop arriving on the listing's own trading days, not New York's.
|
|
1394
|
+
const venue = quote?.listingExchangeName || exchange || undefined;
|
|
1395
|
+
const served = range === "YTD" && !window ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval, venue))) : rows;
|
|
1396
|
+
// October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
|
|
1397
|
+
const forming = formingPeriodBar(served, venue);
|
|
1398
|
+
// A complete one is dated by the day its last session closed, the day its close is from.
|
|
1399
|
+
const closedDay = forming ? null : closedPeriodBarDay(served, venue);
|
|
1400
|
+
const data = forming?.rows ?? served;
|
|
1401
|
+
const interval = data[0]?.interval ?? null;
|
|
1402
|
+
// Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
|
|
1403
|
+
const intraday = data.some((row) => row.date.length > 10);
|
|
1404
|
+
// A bar history: dated by its last bar, stale once bars of its size stop arriving.
|
|
1405
|
+
// The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
|
|
1406
|
+
const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
|
|
1407
|
+
// The source that served the bars, a broker or another source when Gloom Cloud could not.
|
|
1408
|
+
const source = loaded.sourceName ? { source: loaded.sourceName } : {};
|
|
1409
|
+
// A window that closed before today is the history asked for: no later bar makes it stale.
|
|
1410
|
+
const freshness = withSavedCopy(window && isPastPriceWindow(window) ? rowsFreshness(data, {
|
|
1411
|
+
...source,
|
|
1412
|
+
status: "not-a-feed", basis: `${interval ? BAR_INTERVAL_LABELS[interval] : "price"} bars, ${priceWindowText(window)}`, observedKey: "date", oldest: null,
|
|
1413
|
+
}) : rowsFreshness(data, {
|
|
1414
|
+
...source,
|
|
1415
|
+
...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
|
|
1416
|
+
observedKey: "date",
|
|
1417
|
+
oldest: null,
|
|
1418
|
+
cadenceExchange: venue,
|
|
1419
|
+
...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
|
|
1420
|
+
...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
|
|
1421
|
+
...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
|
|
1422
|
+
// The rows are drawn from the bars served, which say whether they are the saved copy.
|
|
1423
|
+
}), loaded);
|
|
1424
|
+
const gaps = historyGapNote(data, traded);
|
|
1425
|
+
const coverage = window ? historyCoverageNotice(loaded.coverageStart, Date.parse(`${window.from}T00:00:00Z`), loaded.coverageNote)
|
|
1426
|
+
: range === "YTD" ? null : historyCoverageWarning(range, loaded);
|
|
1427
|
+
const notes = [
|
|
1428
|
+
// 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.
|
|
1429
|
+
...data.length === 0 ? [emptyHistoryNote(listing.key, ask)] : [],
|
|
1430
|
+
// FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
|
|
1431
|
+
...fate ? listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }) : [],
|
|
1432
|
+
...lookups ? await listingChoiceNotes(listing, market, quote, venues) : [],
|
|
1433
|
+
// ALL says where the source's bars start (BTC-USD's in 2014-09), unless the coverage line does.
|
|
1434
|
+
...window ? [...windowHistoryNotes(window, data), coverage ?? []]
|
|
1435
|
+
: range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date, Date.now(), coverage != null), coverage ?? []],
|
|
1436
|
+
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1437
|
+
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1438
|
+
historyVenueVolumeNote(data) ?? [],
|
|
1439
|
+
historyBarStatusNote(forming ? served : data) ?? [],
|
|
1440
|
+
].flat();
|
|
1441
|
+
return {
|
|
1442
|
+
listing, identity, unit, data, resolution, interval, intraday, volume, freshness, notes,
|
|
1443
|
+
decimals: historyPriceDecimals(data, listing.saved?.metadata.assetCategory),
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** What a listing with no bar for what was asked says, in a warning and in a list's status column. */
|
|
1448
|
+
function emptyHistoryNote(symbol: string, { range, window }: HistoryAsk): string {
|
|
1449
|
+
return window ? `No bars for ${symbol} from ${priceWindowText(window)}.` : `No bars for ${symbol} in ${range}.`;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** A bar's prices to the decimals its history prints them to. */
|
|
1453
|
+
function historyPrice(decimals: number) {
|
|
1454
|
+
return (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** The columns a history table prints: Time in UTC for intraday bars, prices to `price`'s decimals. */
|
|
1458
|
+
function historyTextColumns(intraday: boolean, price: (value: unknown, row: Record<string, unknown>) => string): CliResultColumn[] {
|
|
1459
|
+
return [
|
|
1460
|
+
intraday
|
|
1461
|
+
? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
|
|
1462
|
+
: { key: "date", header: "Date" },
|
|
1463
|
+
{ key: "open", header: "Open", align: "right", format: price },
|
|
1464
|
+
{ key: "high", header: "High", align: "right", format: price },
|
|
1465
|
+
{ key: "low", header: "Low", align: "right", format: price },
|
|
1466
|
+
{ key: "close", header: "Close", align: "right", format: price },
|
|
1467
|
+
{ key: "volume", header: "Volume", align: "right", format: formatCountCell },
|
|
1468
|
+
];
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* The volume a table prints: a bar with one venue's count in place of the
|
|
1473
|
+
* market's shows it beside the venue, never as its volume, and a bar without
|
|
1474
|
+
* volume in a history that reports it prints as missing, never as 0 or blank.
|
|
1475
|
+
*/
|
|
1476
|
+
function historyVolumeCell(missingVolume: (row: Record<string, unknown>) => string) {
|
|
1477
|
+
return (value: unknown, row: Record<string, unknown>) => formatCountCell(value)
|
|
1478
|
+
|| (typeof row.venueVolume === "number"
|
|
1479
|
+
? cliStyles.muted(`${formatCountCell(row.venueVolume)} ${typeof row.volumeVenue === "string" ? row.volumeVenue : ""}`.trim())
|
|
1480
|
+
: missingVolume(row));
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/** Text marks a bar still forming, rebuilt from hourly bars or closing before its settlement beside its flag; exports keep them as columns. */
|
|
1484
|
+
const HISTORY_FLAG_TEXT_COLUMN: CliResultColumn = {
|
|
1485
|
+
key: "flag",
|
|
1486
|
+
header: "Flag",
|
|
1487
|
+
format: (value, row) => [
|
|
1488
|
+
typeof value === "string" ? cliStyles.warning(value) : "",
|
|
1489
|
+
row.partial ? cliStyles.muted("partial") : "",
|
|
1490
|
+
row.filled ? cliStyles.muted("filled") : "",
|
|
1491
|
+
row.unsettled ? cliStyles.muted("unsettled") : "",
|
|
1492
|
+
typeof row.status === "string" ? cliStyles.warning(row.status) : "",
|
|
1493
|
+
].filter(Boolean).join(" "),
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
/** The dates `--from` and `--to` (or `--start` and `--end`) name, each given once. */
|
|
1497
|
+
function historyWindowOption(args: string[], rangeOption: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): PriceWindow | null {
|
|
1498
|
+
const bound = (flag: string, alias: string) => {
|
|
1499
|
+
const value = takeOption(args, flag);
|
|
1500
|
+
const aliased = takeOption(args, alias);
|
|
1501
|
+
if (value != null && aliased != null) ctx.fail(`${alias} is ${flag}; give one of them.`, `Usage: gloom ${HISTORY_USAGE}`);
|
|
1502
|
+
return { value: value ?? aliased, flag: aliased != null ? alias : flag };
|
|
1503
|
+
};
|
|
1504
|
+
const typedFrom = bound("--from", "--start");
|
|
1505
|
+
const typedTo = bound("--to", "--end");
|
|
1506
|
+
// Every message names the options as typed: an --end alone asks for a --start, not a --from.
|
|
1507
|
+
const from = { ...typedFrom, flag: typedFrom.value == null && typedTo.flag === "--end" ? "--start" : typedFrom.flag };
|
|
1508
|
+
const to = { ...typedTo, flag: typedTo.value == null && from.flag === "--start" ? "--end" : typedTo.flag };
|
|
1509
|
+
let window: PriceWindow | null;
|
|
1510
|
+
try {
|
|
1511
|
+
window = readPriceWindow(from.value, to.value, new Date(), undefined, { from: from.flag, to: to.flag });
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
return ctx.fail(errorMessage(error) ?? `Invalid ${from.flag} or ${to.flag}.`, `Usage: gloom ${HISTORY_USAGE}`);
|
|
1514
|
+
}
|
|
1515
|
+
if (window && rangeOption != null) ctx.fail(`${from.flag} replaces --range; give one of them.`, `Usage: gloom ${HISTORY_USAGE}`);
|
|
1516
|
+
return window;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** How many symbols of a list load at once: enough that a few hundred take a minute or two, not one call after another. */
|
|
1520
|
+
const HISTORY_LIST_CONCURRENCY = 8;
|
|
1521
|
+
|
|
1522
|
+
async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1523
|
+
const args = [...rawArgs];
|
|
1524
|
+
const rangeOption = takeOption(args, "--range");
|
|
1525
|
+
// --from 2007 --to 2009: those years' bars, daily up to five years, in place of a range ending now.
|
|
1526
|
+
const window = historyWindowOption(args, rangeOption, ctx);
|
|
1527
|
+
const ask: HistoryAsk = { range: parseRange(rangeOption, ctx), window };
|
|
1054
1528
|
const requestedExchange = takeOption(args, "--exchange");
|
|
1529
|
+
// A list from a file or standard input (`-`), or several symbols typed (AAPL MSFT, AAPL,MSFT), is one long table.
|
|
1530
|
+
// One argument with spaces stays one, as before: it may be a company's name.
|
|
1531
|
+
const file = args.some((arg) => arg === "--file" || arg.startsWith("--file=")) ? takeOption(args, "--file") ?? "" : undefined;
|
|
1532
|
+
if (file != null || args.includes("-") || uniqueEntries(args).length > 1 || args.some((arg) => /[,;]/.test(arg))) {
|
|
1533
|
+
return runHistoryList(args, file, requestedExchange, ask, ctx);
|
|
1534
|
+
}
|
|
1055
1535
|
const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
|
|
1056
1536
|
await withMarketData(ctx, async (market) => {
|
|
1057
1537
|
const listing = await requireCliListing(raw, requestedExchange, market, ctx);
|
|
1058
|
-
const
|
|
1059
|
-
const {
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
|
|
1063
|
-
: market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
|
|
1064
|
-
const quoted = loadListingQuote(market.dataProvider, listing);
|
|
1065
|
-
const fate = listingFateLoader(listing, market, quoted);
|
|
1066
|
-
const [loaded, quote] = await Promise.all([
|
|
1067
|
-
loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
|
|
1068
|
-
command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history", fate,
|
|
1069
|
-
})
|
|
1070
|
-
.catch((error) => failHistory(error, listing.key, ctx)),
|
|
1071
|
-
quoted,
|
|
1072
|
-
]);
|
|
1073
|
-
const { resolution } = loaded;
|
|
1074
|
-
// While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
|
|
1075
|
-
const points = withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
|
|
1076
|
-
const fromQuote = points !== loaded.points;
|
|
1077
|
-
const identity = listingIdentity(listing, quote);
|
|
1078
|
-
// Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
|
|
1079
|
-
const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(logFailure("cli.commands-market", "Read quote metadata for history failed", null));
|
|
1080
|
-
const unit = historyUnit(listing.key, listed ?? quote);
|
|
1081
|
-
const traded = {
|
|
1082
|
-
listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
|
|
1083
|
-
};
|
|
1084
|
-
// A coin's volume is traded value in its quote currency: said in every row and on the status line.
|
|
1085
|
-
const volume = historyVolume(unit, traded);
|
|
1086
|
-
const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
|
|
1087
|
-
const served = range === "YTD" ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval))) : rows;
|
|
1088
|
-
// Bars stop arriving on the listing's own trading days, not New York's.
|
|
1089
|
-
const venue = quote?.listingExchangeName || exchange || undefined;
|
|
1090
|
-
// October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
|
|
1091
|
-
const forming = formingPeriodBar(served, venue);
|
|
1092
|
-
// A complete one is dated by the day its last session closed, the day its close is from.
|
|
1093
|
-
const closedDay = forming ? null : closedPeriodBarDay(served, venue);
|
|
1094
|
-
const data = forming?.rows ?? served;
|
|
1095
|
-
const interval = data[0]?.interval ?? null;
|
|
1096
|
-
const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
|
|
1097
|
-
const price = (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
|
|
1098
|
-
// Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
|
|
1099
|
-
const intraday = data.some((row) => row.date.length > 10);
|
|
1100
|
-
// A bar history: dated by its last bar, stale once bars of its size stop arriving.
|
|
1101
|
-
// The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
|
|
1102
|
-
const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
|
|
1103
|
-
const freshness = rowsFreshness(data, {
|
|
1104
|
-
...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
|
|
1105
|
-
observedKey: "date",
|
|
1106
|
-
oldest: null,
|
|
1107
|
-
cadenceExchange: venue,
|
|
1108
|
-
...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
|
|
1109
|
-
...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
|
|
1110
|
-
...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
|
|
1111
|
-
});
|
|
1112
|
-
const gaps = historyGapNote(data, traded);
|
|
1113
|
-
const notes = [
|
|
1114
|
-
// FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
|
|
1115
|
-
...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
|
|
1116
|
-
...await listingChoiceNotes(listing, market, quote, venues),
|
|
1117
|
-
...range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? []],
|
|
1118
|
-
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1119
|
-
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1120
|
-
historyBarStatusNote(forming ? served : data) ?? [],
|
|
1121
|
-
].flat();
|
|
1122
|
-
const priceColumns: CliResultColumn[] = [
|
|
1123
|
-
intraday
|
|
1124
|
-
? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
|
|
1125
|
-
: { key: "date", header: "Date" },
|
|
1126
|
-
{ key: "open", header: "Open", align: "right", format: price },
|
|
1127
|
-
{ key: "high", header: "High", align: "right", format: price },
|
|
1128
|
-
{ key: "low", header: "Low", align: "right", format: price },
|
|
1129
|
-
{ key: "close", header: "Close", align: "right", format: price },
|
|
1130
|
-
{ key: "volume", header: "Volume", align: "right", format: formatCountCell },
|
|
1131
|
-
];
|
|
1132
|
-
// A bar without volume in a history that reports it prints as missing, never as 0 or blank beside the others.
|
|
1538
|
+
const history = await loadListingHistory(listing, market, ctx, ask, { lookups: true });
|
|
1539
|
+
const { identity, unit, data, interval, intraday, volume, freshness, notes } = history;
|
|
1540
|
+
const price = historyPrice(history.decimals);
|
|
1541
|
+
// A bar without volume in a history that reports it prints as missing.
|
|
1133
1542
|
const missingVolume = historyReportsVolume(data) ? MISSING_VOLUME_TEXT : "";
|
|
1134
|
-
const
|
|
1135
|
-
: { ...column, format: (
|
|
1136
|
-
//
|
|
1137
|
-
const
|
|
1138
|
-
key: "flag", header: "Flag", format: (value) => typeof value === "string" ? cliStyles.warning(value) : "",
|
|
1139
|
-
};
|
|
1140
|
-
// Text also marks a bar still forming, rebuilt from hourly bars or closing before its settlement; exports keep them as fields.
|
|
1141
|
-
const flagTextColumn: CliResultColumn = {
|
|
1142
|
-
...flagColumn,
|
|
1143
|
-
format: (value, row) => [
|
|
1144
|
-
typeof value === "string" ? cliStyles.warning(value) : "",
|
|
1145
|
-
row.partial ? cliStyles.muted("partial") : "",
|
|
1146
|
-
row.filled ? cliStyles.muted("filled") : "",
|
|
1147
|
-
row.unsettled ? cliStyles.muted("unsettled") : "",
|
|
1148
|
-
].filter(Boolean).join(" "),
|
|
1149
|
-
};
|
|
1150
|
-
const marked = data.some((row) => row.partial || row.filled);
|
|
1151
|
-
const unsettled = data.some((row) => row.unsettled);
|
|
1543
|
+
const textColumns = historyTextColumns(intraday, price).map((column) => column.key !== "volume" ? column
|
|
1544
|
+
: { ...column, format: historyVolumeCell(() => missingVolume) });
|
|
1545
|
+
// The largest fall from a close to a later one in the bars shown, said under the heading.
|
|
1546
|
+
const drawdown = maxDrawdown(data);
|
|
1152
1547
|
ctx.printResult({
|
|
1153
1548
|
data,
|
|
1154
1549
|
metadata: {
|
|
1155
1550
|
...listingMetadata(identity),
|
|
1156
|
-
range,
|
|
1157
|
-
resolution,
|
|
1158
|
-
requestedRange: range,
|
|
1551
|
+
range: window ? null : ask.range,
|
|
1552
|
+
resolution: history.resolution,
|
|
1553
|
+
requestedRange: window ? null : ask.range,
|
|
1554
|
+
...(window ? { requestedWindow: window } : {}),
|
|
1159
1555
|
currency: unit.currency,
|
|
1160
1556
|
unit: unit.unit,
|
|
1161
1557
|
interval,
|
|
@@ -1164,28 +1560,144 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1164
1560
|
// The first bar after the last gap or jump; the first bar when there is none.
|
|
1165
1561
|
unbrokenFrom: historyUnbrokenFrom(data) ?? data[0]?.date ?? null,
|
|
1166
1562
|
bars: data.length,
|
|
1563
|
+
maxDrawdown: drawdown,
|
|
1167
1564
|
asOf: freshness?.asOf ?? null,
|
|
1168
1565
|
...(volume ? { volumeUnit: volume.unit, volumePeriod: interval === "1d" ? "UTC day" : interval } : {}),
|
|
1169
1566
|
},
|
|
1170
1567
|
...(notes.length > 0 ? { warnings: notes } : {}),
|
|
1171
1568
|
freshness,
|
|
1172
1569
|
}, {
|
|
1173
|
-
heading:
|
|
1570
|
+
heading: [listingHeading(identity), cliStyles.muted(historyFacts(unit, data).join(" · ")),
|
|
1571
|
+
...drawdown && interval ? [cliStyles.muted(maxDrawdownText(drawdown, { basis: `${BAR_INTERVAL_LABELS[interval]} closes`, price,
|
|
1572
|
+
date: (text) => intraday ? formatUtcTime(text) : text.slice(0, 10) }))] : []].join("\n"),
|
|
1174
1573
|
dateKey: "date",
|
|
1175
|
-
textColumns: [...
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
...marked ? [{ key: "partial", header: "Partial" }, { key: "filled", header: "Filled" }] : [],
|
|
1180
|
-
...unsettled ? [{ key: "unsettled", header: "Unsettled" }] : [],
|
|
1181
|
-
...volume ? [{ key: "volumeUnit", header: "Volume unit" }, { key: "volumeNote", header: "Volume note" }] : [],
|
|
1182
|
-
],
|
|
1574
|
+
textColumns: [...textColumns, HISTORY_FLAG_TEXT_COLUMN],
|
|
1575
|
+
// One header whatever the market is doing, for one symbol as for a list (HISTORY_EXPORT_COLUMNS).
|
|
1576
|
+
columns: HISTORY_EXPORT_COLUMNS,
|
|
1577
|
+
exportStatus: historyExportStatus,
|
|
1183
1578
|
// 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.
|
|
1184
|
-
exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
|
|
1579
|
+
exportIdentity: { symbolOrSeries: history.listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
|
|
1185
1580
|
});
|
|
1186
1581
|
});
|
|
1187
1582
|
}
|
|
1188
1583
|
|
|
1584
|
+
/** A symbol of a list: its bars, or why it has none. */
|
|
1585
|
+
type ListedHistory = { symbol: string; history: ListingHistory } | { symbol: string; error: string };
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* `history --file holdings.txt` (or several symbols, or `-` for standard
|
|
1589
|
+
* input): every symbol's bars in one long table, `HISTORY_LIST_CONCURRENCY`
|
|
1590
|
+
* symbols loading at once. A symbol that serves nothing is one row with its
|
|
1591
|
+
* reason in `status`, and a warning; the command fails only when none served.
|
|
1592
|
+
*/
|
|
1593
|
+
async function runHistoryList(
|
|
1594
|
+
args: string[],
|
|
1595
|
+
file: string | undefined,
|
|
1596
|
+
exchange: string | undefined,
|
|
1597
|
+
ask: HistoryAsk,
|
|
1598
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1599
|
+
) {
|
|
1600
|
+
const usage = `Usage: gloom ${HISTORY_USAGE}`;
|
|
1601
|
+
await withMarketData(ctx, async (market) => {
|
|
1602
|
+
const list = await readListedSymbols(args, file, market, ctx, usage);
|
|
1603
|
+
const refused = list.symbols.flatMap((symbol) => notASymbolError(symbol) ?? []);
|
|
1604
|
+
const symbols = list.symbols.filter((symbol) => !notASymbolError(symbol));
|
|
1605
|
+
if (symbols.length === 0) ctx.fail(refused.length === 1 ? refused[0]!.message : `No history to read: ${refusedList(refused)} are not symbols.`, usage);
|
|
1606
|
+
// A long list is a batch, not a lookup: it skips the per-symbol venue and moved-company lookups, as `quote` does.
|
|
1607
|
+
const lookups = symbols.length <= MAX_CHOICE_LOOKUPS;
|
|
1608
|
+
const listed = await mapWithConcurrency(symbols, HISTORY_LIST_CONCURRENCY, async (symbol): Promise<ListedHistory> => {
|
|
1609
|
+
try {
|
|
1610
|
+
const listing = await resolveCliListing(symbol, exchange, market, { ownExchangeWins: true });
|
|
1611
|
+
return { symbol, history: await loadListingHistory(listing, market, ctx, ask, { lookups }) };
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
const message = errorMessage(error) ?? `No history for ${symbol}`;
|
|
1614
|
+
return { symbol, error: isCliFailure(error) ? oneLineFailure(message, error.details) : message };
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
const served = listed.flatMap((entry) => "history" in entry && entry.history.data.length > 0 ? [entry.history] : []);
|
|
1618
|
+
// A symbol with no bars, whether its request failed or came back empty, is one row with the reason.
|
|
1619
|
+
const rows: HistoryListRow[] = listed.flatMap((entry) => "history" in entry
|
|
1620
|
+
? entry.history.data.length > 0
|
|
1621
|
+
? entry.history.data.map((row) => ({ symbol: entry.history.listing.key, ...row }))
|
|
1622
|
+
: [historyFailureRow(entry.history.listing.key, emptyHistoryNote(entry.history.listing.key, ask))]
|
|
1623
|
+
: [historyFailureRow(entry.symbol, entry.error)]);
|
|
1624
|
+
const bySymbol = new Map(served.map((history) => [history.listing.key, history] as const));
|
|
1625
|
+
const notes = [
|
|
1626
|
+
...list.notes,
|
|
1627
|
+
...refused.length > 0 ? [`Not ${refused.length === 1 ? "a symbol" : "symbols"}, no history read: ${refusedList(refused)}.`] : [],
|
|
1628
|
+
...symbolListNotes(listed.map((entry) => ({
|
|
1629
|
+
symbol: "history" in entry ? entry.history.listing.key : entry.symbol,
|
|
1630
|
+
notes: "history" in entry ? entry.history.notes : [entry.error],
|
|
1631
|
+
}))),
|
|
1632
|
+
];
|
|
1633
|
+
// The closing lines date the list by its newest bar; each row's as_of is its own symbol's.
|
|
1634
|
+
const lastBars = served.flatMap((history) => history.data.at(-1) ?? []);
|
|
1635
|
+
const pastWindow = ask.window && isPastPriceWindow(ask.window);
|
|
1636
|
+
const listFreshness = served.length === 0 ? undefined : rowsFreshness(lastBars, pastWindow && ask.window ? {
|
|
1637
|
+
status: "not-a-feed", basis: `price bars, ${priceWindowText(ask.window)}`, observedKey: "date", oldest: null,
|
|
1638
|
+
} : {
|
|
1639
|
+
...barHistoryFreshness(served.some((history) => history.intraday) ? null : served[0]!.interval ?? "1d"),
|
|
1640
|
+
observedKey: "date",
|
|
1641
|
+
oldest: null,
|
|
1642
|
+
});
|
|
1643
|
+
// A saved copy is marked on the bars a symbol loaded, not on the rows drawn from them (loadListingHistory
|
|
1644
|
+
// reads it there): the list ends "couldn't refresh; saved <time>" for the oldest copy among its symbols.
|
|
1645
|
+
const savedAt = served.flatMap((history) => history.freshness?.savedAt ?? []).sort()[0];
|
|
1646
|
+
const freshness = listFreshness && savedAt && !listFreshness.savedAt ? { ...listFreshness, savedAt } : listFreshness;
|
|
1647
|
+
const decimals = (row: Record<string, unknown>) => bySymbol.get(String(row.symbol))?.decimals ?? 2;
|
|
1648
|
+
const intraday = served.some((history) => history.intraday);
|
|
1649
|
+
const textColumns = historyTextColumns(intraday, (value, row) => historyPrice(decimals(row))(value)).map((column) => column.key !== "volume" ? column
|
|
1650
|
+
: { ...column, format: historyVolumeCell((row) => {
|
|
1651
|
+
const history = bySymbol.get(String(row.symbol));
|
|
1652
|
+
return history && row.date && historyReportsVolume(history.data) ? MISSING_VOLUME_TEXT : "";
|
|
1653
|
+
}) });
|
|
1654
|
+
ctx.printResult({
|
|
1655
|
+
data: rows,
|
|
1656
|
+
metadata: {
|
|
1657
|
+
range: ask.window ? null : ask.range,
|
|
1658
|
+
requestedRange: ask.window ? null : ask.range,
|
|
1659
|
+
...(ask.window ? { requestedWindow: ask.window } : {}),
|
|
1660
|
+
// Each symbol as a single `history` describes it, or the reason it has no bars.
|
|
1661
|
+
symbols: listed.map((entry) => "history" in entry ? {
|
|
1662
|
+
...listingMetadata(entry.history.identity),
|
|
1663
|
+
currency: entry.history.unit.currency,
|
|
1664
|
+
unit: entry.history.unit.unit,
|
|
1665
|
+
interval: entry.history.interval,
|
|
1666
|
+
firstDate: entry.history.data[0]?.date ?? null,
|
|
1667
|
+
lastDate: entry.history.data.at(-1)?.date ?? null,
|
|
1668
|
+
bars: entry.history.data.length,
|
|
1669
|
+
asOf: entry.history.freshness?.asOf ?? null,
|
|
1670
|
+
} : { symbol: entry.symbol, error: entry.error }),
|
|
1671
|
+
},
|
|
1672
|
+
...(notes.length > 0 ? { warnings: notes } : {}),
|
|
1673
|
+
...(served.length === 0 ? { failure: { code: "cli_error", message: `No history for ${symbols.join(", ")}.` } } : {}),
|
|
1674
|
+
freshness,
|
|
1675
|
+
// With no bars to date, an export still ends its rows with what each is of, under the same header.
|
|
1676
|
+
...(freshness ? {} : { exportFreshness: rowsFreshness([], { status: "not-a-feed", basis: "price bars" }) }),
|
|
1677
|
+
}, {
|
|
1678
|
+
seriesKey: "symbol",
|
|
1679
|
+
dateKey: "date",
|
|
1680
|
+
textColumns: [{ key: "symbol", header: "Symbol", shrink: false }, ...textColumns,
|
|
1681
|
+
{ key: "currency", header: "Cur", shrink: false, format: currencyCell }, HISTORY_FLAG_TEXT_COLUMN],
|
|
1682
|
+
columns: HISTORY_EXPORT_COLUMNS,
|
|
1683
|
+
exportStatus: historyExportStatus,
|
|
1684
|
+
// Every row names its symbol, the currency its prices are in, its bar size and the as-of of its own symbol's bars.
|
|
1685
|
+
exportIdentity: (row) => {
|
|
1686
|
+
const history = bySymbol.get(String(row.symbol));
|
|
1687
|
+
return {
|
|
1688
|
+
symbolOrSeries: String(row.symbol),
|
|
1689
|
+
currencyOrUnit: history ? history.unit.currency ?? history.unit.unit : null,
|
|
1690
|
+
interval: history?.interval ?? null,
|
|
1691
|
+
// A symbol with no bars has no as-of of its own.
|
|
1692
|
+
asOf: history ? history.freshness?.asOf ?? undefined : null,
|
|
1693
|
+
};
|
|
1694
|
+
},
|
|
1695
|
+
});
|
|
1696
|
+
// The table and the warnings name each symbol with no bars; with none served, the exit status says so too.
|
|
1697
|
+
if (served.length === 0) process.exitCode = CLI_EXIT_CODES.error;
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1189
1701
|
type FinancialsView = "statements" | "fundamentals" | "valuation";
|
|
1190
1702
|
|
|
1191
1703
|
const STATEMENT_COLUMNS: CliResultColumn<ReturnType<typeof financialStatementRows>[number]>[] = [
|
|
@@ -1233,9 +1745,12 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1233
1745
|
await withMarketData(ctx, async (market) => {
|
|
1234
1746
|
const listing = await requireCliListing(raw, exchangeOption, market, ctx);
|
|
1235
1747
|
const venues = loadBareVenues(listing, market);
|
|
1236
|
-
const
|
|
1748
|
+
const loaded = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
|
|
1237
1749
|
listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
|
|
1238
1750
|
), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
|
|
1751
|
+
// A converted TTM block at the rate `fx` reports, as `ticker` shows it; only the US dollar legs are read.
|
|
1752
|
+
const financials = view === "statements" ? loaded
|
|
1753
|
+
: await atReportTtmRate(loaded, createBaseConverter(market.dataProvider, "USD"));
|
|
1239
1754
|
// A trailing yield is measured from the cash history as DVD measures it, a forward yield dated ahead names the
|
|
1240
1755
|
// declared distribution it annualizes (not in JSON), and one whose dividend stopped says so; read beside the listing notes.
|
|
1241
1756
|
const distributions = view === "statements" ? null : loadDividendDistributions(
|
|
@@ -1258,7 +1773,8 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1258
1773
|
const freshness = view === "valuation" ? valuationFreshness(financials) : fundamentalsFreshness(financials);
|
|
1259
1774
|
// Market cap, the multiples and the yield at the regular session price, as the text gives them; the quote stays as sent.
|
|
1260
1775
|
const priced = atRegularSessionPrice(financials);
|
|
1261
|
-
|
|
1776
|
+
// One market cap per view: the share count times the price where the provider's sits more than 2% from it.
|
|
1777
|
+
const capitalization = viewMarketCapitalization(priced.quote, priced.fundamentals, regularSessionValuationPrice(priced.quote));
|
|
1262
1778
|
// The filings behind the latest statements, and for a bank its deposits, uninsured share and CRE.
|
|
1263
1779
|
const [statementFilings, bank] = view === "fundamentals"
|
|
1264
1780
|
? await Promise.all([
|
|
@@ -1306,8 +1822,13 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1306
1822
|
const freshness = rowsFreshness(rows, {
|
|
1307
1823
|
...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
|
|
1308
1824
|
});
|
|
1309
|
-
// The rows are the annual years served; older ones are not, and the closing lines say so
|
|
1310
|
-
|
|
1825
|
+
// The rows are the annual years served; older ones are not, and the closing lines say so, after how old
|
|
1826
|
+
// the latest year is when it ended over 15 months ago.
|
|
1827
|
+
const stale = stalePeriodNote(rows.map((row) => ({ date: row.date, lead: row.revenue != null })));
|
|
1828
|
+
// Years filled from the listing's exchange, for the revenue and net income columns.
|
|
1829
|
+
const filled = filledPeriodsNote(financials.annualStatements.filter((statement) => rows.some((row) => row.date === statement.date)),
|
|
1830
|
+
new Set(["totalRevenue", "netIncome"]));
|
|
1831
|
+
const historyNotes = [...stale ? [stale] : [], ...filled ? [filled] : [], ...statementHistoryNotes("annual", rows.map((row) => row.date), listing.key)];
|
|
1311
1832
|
// NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
|
|
1312
1833
|
const tables = tabular
|
|
1313
1834
|
? selectTables({
|
|
@@ -1347,9 +1868,9 @@ const NEWS_COLUMNS = [
|
|
|
1347
1868
|
header: "Published",
|
|
1348
1869
|
format: (value: unknown) => typeof value === "string" ? formatUtcTime(value) : "",
|
|
1349
1870
|
},
|
|
1350
|
-
{ key: "source", header: "Source"
|
|
1871
|
+
{ key: "source", header: "Source" },
|
|
1351
1872
|
{ key: "title", header: "Title" },
|
|
1352
|
-
{ key: "tickers", header: "Tickers"
|
|
1873
|
+
{ key: "tickers", header: "Tickers" },
|
|
1353
1874
|
{ key: "url", header: "URL", optional: true },
|
|
1354
1875
|
];
|
|
1355
1876
|
|
|
@@ -1364,7 +1885,14 @@ const NEWS_COLUMNS = [
|
|
|
1364
1885
|
async function printNewsSearch(
|
|
1365
1886
|
text: string,
|
|
1366
1887
|
articles: NewsArticle[],
|
|
1367
|
-
{ reason, empty, commodity = true, failure }: {
|
|
1888
|
+
{ reason, empty, commodity = true, failure, ticker = null }: {
|
|
1889
|
+
reason?: string;
|
|
1890
|
+
empty?: string;
|
|
1891
|
+
commodity?: boolean;
|
|
1892
|
+
failure?: string;
|
|
1893
|
+
/** The listing a coin pair's search stands in for. */
|
|
1894
|
+
ticker?: string | null;
|
|
1895
|
+
},
|
|
1368
1896
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
1369
1897
|
) {
|
|
1370
1898
|
const searched = failure === undefined;
|
|
@@ -1373,7 +1901,7 @@ async function printNewsSearch(
|
|
|
1373
1901
|
ctx.printResult({
|
|
1374
1902
|
data: articles,
|
|
1375
1903
|
...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
|
|
1376
|
-
metadata: { ticker
|
|
1904
|
+
metadata: { ticker, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
|
|
1377
1905
|
freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
|
|
1378
1906
|
}, {
|
|
1379
1907
|
heading: newsSearchHeading(text, reason),
|
|
@@ -1383,6 +1911,50 @@ async function printNewsSearch(
|
|
|
1383
1911
|
});
|
|
1384
1912
|
}
|
|
1385
1913
|
|
|
1914
|
+
/** The ticker a metal's name is in capitals, when one answers to exactly that: GOLD, Gold.com Inc., for "gold". */
|
|
1915
|
+
async function tickerOfWord(word: string, market: MarketContext): Promise<{ symbol: string; name: string | null } | null> {
|
|
1916
|
+
const symbol = word.toUpperCase();
|
|
1917
|
+
const listing = await resolveCliListing(symbol, null, market).catch(() => {
|
|
1918
|
+
// Intentional ignore: a word no listing answers to offers no ticker under the metal's news.
|
|
1919
|
+
return null;
|
|
1920
|
+
});
|
|
1921
|
+
if (!listing || listing.symbol.toUpperCase() !== symbol) return null;
|
|
1922
|
+
const quote = await loadListingQuote(market.dataProvider, listing);
|
|
1923
|
+
const identity = listingIdentity(listing, quote);
|
|
1924
|
+
return quote !== null || identity.name != null ? { symbol: listing.symbol, name: identity.name ?? null } : null;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* A metal's market news: the stories tagged METALS that mention
|
|
1929
|
+
* it, for its future, its board code or its name. Under them, the ticker its
|
|
1930
|
+
* name is in capitals, whose company news `news GOLD` keeps.
|
|
1931
|
+
*/
|
|
1932
|
+
async function printMetalNews(route: MetalNewsRoute, limit: number, ctx: Parameters<CliCommandDef["execute"]>[1], market?: MarketContext) {
|
|
1933
|
+
const search = searchNewsStories(route.metal, limit, [METALS_NEWS_TOPIC]).then((found) => ({ found, failure: undefined }), (error: unknown) => {
|
|
1934
|
+
logFailure("cli.commands-market", "Search metals news failed")(error);
|
|
1935
|
+
return { found: [] as NewsArticle[], failure: errorMessage(error) ?? "no answer" };
|
|
1936
|
+
});
|
|
1937
|
+
const word = route.word;
|
|
1938
|
+
const ticker = word
|
|
1939
|
+
? await (market ? tickerOfWord(word, market) : withMarketData(ctx, (context) => tickerOfWord(word, context)))
|
|
1940
|
+
.catch(logFailure("cli.commands-market", "Read the ticker a metal's name is failed", null))
|
|
1941
|
+
: null;
|
|
1942
|
+
const { found, failure } = await search;
|
|
1943
|
+
const companyNews = ticker ? `gloom news ${ticker.symbol}` : null;
|
|
1944
|
+
ctx.printResult({
|
|
1945
|
+
data: found,
|
|
1946
|
+
...(failure === undefined ? {} : { warnings: [`Searching metals stories for "${route.metal}" failed: ${failure}`] }),
|
|
1947
|
+
metadata: { ticker: null, search: route.metal, topics: [METALS_NEWS_TOPIC], feed: null, ...(companyNews ? { companyNews } : {}) },
|
|
1948
|
+
freshness: rowsFreshness(newsRows(found), cloudNewsFreshness(await cloudRealtimeAccess())),
|
|
1949
|
+
}, {
|
|
1950
|
+
heading: metalNewsHeading(route),
|
|
1951
|
+
rows: newsRows,
|
|
1952
|
+
columns: NEWS_COLUMNS,
|
|
1953
|
+
empty: failure === undefined ? noMetalStoriesMessage(route) : "No stories to show.",
|
|
1954
|
+
...(ticker ? { footnote: `${word} ${cliArrow("→")} ${route.metal}'s market news. For ${ticker.name ?? ticker.symbol} (ticker ${ticker.symbol}), try: ${companyNews}` } : {}),
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1386
1958
|
async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1387
1959
|
const args = [...rawArgs];
|
|
1388
1960
|
const feed = parseNewsFeed(takeOption(args, "--feed"));
|
|
@@ -1391,27 +1963,42 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1391
1963
|
const limit = ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 20;
|
|
1392
1964
|
const text = newsSearchText(args);
|
|
1393
1965
|
if (text != null) {
|
|
1394
|
-
if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage:
|
|
1966
|
+
if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloom news <query>");
|
|
1395
1967
|
const problem = newsSearchTextProblem(text);
|
|
1396
1968
|
if (problem) ctx.fail(problem);
|
|
1397
1969
|
await printNewsSearch(text, await searchNewsStories(text, limit), {}, ctx);
|
|
1398
1970
|
return;
|
|
1399
1971
|
}
|
|
1400
|
-
|
|
1972
|
+
// A metal's future or name is its market's news, not a company's: GOLD in capitals stays Gold.com's.
|
|
1973
|
+
const metal = args.length === 1 && !exchangeOption && !feed ? metalNewsRoute(args[0]!) : null;
|
|
1974
|
+
if (metal) {
|
|
1975
|
+
await printMetalNews(metal, limit, ctx);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloom news <symbol> --exchange <code>");
|
|
1401
1979
|
await withMarketData(ctx, async (market) => {
|
|
1402
1980
|
const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
|
|
1403
|
-
const
|
|
1981
|
+
const quoteLoad = listing ? loadListingQuote(market.dataProvider, listing) : Promise.resolve(null);
|
|
1982
|
+
// A coin pair's feed searches for the coin when no story is filed under the pair, by the name its quote gives.
|
|
1983
|
+
const coin = !!listing && !feed && isRoundTheClockCoin(listing.request.symbol, listing.request.exchange);
|
|
1984
|
+
const loadNews = async () => market.dataProvider.getNews({
|
|
1404
1985
|
feed: feed ?? (listing ? "ticker" : "latest"),
|
|
1405
1986
|
// Still set for news plugins that read the deprecated scope.
|
|
1406
1987
|
scope: listing ? "ticker" : "global",
|
|
1407
1988
|
ticker: listing?.request.symbol,
|
|
1408
1989
|
exchange: listing?.request.exchange || undefined,
|
|
1409
1990
|
limit,
|
|
1991
|
+
...(coin ? { listingName: (await quoteLoad)?.name || listing!.saved?.metadata.name || undefined } : {}),
|
|
1410
1992
|
});
|
|
1411
1993
|
const [articles, quote] = await Promise.all([
|
|
1412
1994
|
listing ? loadForListing(listing, market, ctx, loadNews, (found) => found.length === 0) : loadNews(),
|
|
1413
|
-
|
|
1995
|
+
quoteLoad,
|
|
1414
1996
|
]);
|
|
1997
|
+
const searchedFor = listing ? articles[0]?.searchedFor : undefined;
|
|
1998
|
+
if (listing && searchedFor) {
|
|
1999
|
+
await printNewsSearch(searchedFor, articles, { reason: `no stories filed under ${listing.symbol}`, ticker: listing.key }, ctx);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
1415
2002
|
const identity = listing ? listingIdentity(listing, quote) : null;
|
|
1416
2003
|
const resolved = quote !== null || identity?.name != null;
|
|
1417
2004
|
const unfiled = !!listing && !listing.exchange && !feed && articles.length === 0;
|
|
@@ -1429,6 +2016,12 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1429
2016
|
const emptyMessage = (searched: boolean) => listing && identity
|
|
1430
2017
|
? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
|
|
1431
2018
|
: undefined;
|
|
2019
|
+
// A metal's board code no listing answers to (GC, SI) is the metal's market news.
|
|
2020
|
+
const metalCode = !resolved && unfiled && !exchangeOption ? metalCodeNewsRoute(args[0]!) : null;
|
|
2021
|
+
if (metalCode) {
|
|
2022
|
+
await printMetalNews(metalCode, limit, ctx, market);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1432
2025
|
if (word && reason) {
|
|
1433
2026
|
// The lookup that found no ticker already answered; a search that fails leaves that answer.
|
|
1434
2027
|
const search = await searchNewsStories(word, limit).then((found) => ({ found, failure: undefined }), (error: unknown) => {
|
|
@@ -1446,7 +2039,7 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
1446
2039
|
const typed = args[0]?.trim() ?? "";
|
|
1447
2040
|
const wordHint = listing && resolved && !listing.exchange && !feed && !exchangeOption && commonWordCandidate(typed, identity?.name)
|
|
1448
2041
|
&& wordMeansMore(await searchNewsStories(typed, 20).catch(logFailure("cli.commands-market", "Search news stories for a ticker word failed", [])), listing.symbol)
|
|
1449
|
-
? `${typed} → ${identity?.name ?? listing.symbol} (ticker ${listing.symbol}). For stories that mention ${typed.toLowerCase()}, try: ${quotedSearchCommand(typed.toLowerCase())}`
|
|
2042
|
+
? `${typed} ${cliArrow("→")} ${identity?.name ?? listing.symbol} (ticker ${listing.symbol}). For stories that mention ${typed.toLowerCase()}, try: ${quotedSearchCommand(typed.toLowerCase())}`
|
|
1450
2043
|
: null;
|
|
1451
2044
|
ctx.printResult({
|
|
1452
2045
|
data: articles,
|
|
@@ -1489,6 +2082,27 @@ async function registrantUsExchange(market: MarketContext, symbol: string, regis
|
|
|
1489
2082
|
return (venues.find((venue) => isUsListingExchange(venue.exchange)) ?? venues[0])?.exchange ?? null;
|
|
1490
2083
|
}
|
|
1491
2084
|
|
|
2085
|
+
/** ASX announcements as `filings` lists them for a listing that files nothing with the SEC. */
|
|
2086
|
+
const ANNOUNCEMENT_COLUMNS = [
|
|
2087
|
+
{ key: "date", header: "Released", format: (value: unknown) => typeof value === "string" ? `${value.slice(0, 16).replace("T", " ")} UTC` : "" },
|
|
2088
|
+
{ key: "headline", header: "Headline", maxWidth: 60 },
|
|
2089
|
+
{ key: "type", header: "Type", maxWidth: 36 },
|
|
2090
|
+
{ key: "priceSensitive", header: "Price sensitive", format: (value: unknown) => value === true ? "yes" : "" },
|
|
2091
|
+
{ key: "url", header: "URL", optional: true },
|
|
2092
|
+
];
|
|
2093
|
+
|
|
2094
|
+
const ASX_ANNOUNCEMENTS = { source: "ASX company announcements", status: "not-a-feed", basis: "exchange announcements", oldest: null } as const;
|
|
2095
|
+
|
|
2096
|
+
function announcementRows(payload: ExchangeAnnouncementsPayload) {
|
|
2097
|
+
return payload.announcements.map((item) => ({
|
|
2098
|
+
date: item.date,
|
|
2099
|
+
headline: item.headline,
|
|
2100
|
+
type: item.types.join("; "),
|
|
2101
|
+
priceSensitive: item.priceSensitive,
|
|
2102
|
+
url: item.url,
|
|
2103
|
+
}));
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1492
2106
|
async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
1493
2107
|
const args = [...rawArgs];
|
|
1494
2108
|
// Filings come newest first, so --tail asks for the same first n as --limit.
|
|
@@ -1497,12 +2111,16 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1497
2111
|
const form = takeOption(args, "--form")?.trim() || null;
|
|
1498
2112
|
const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
|
|
1499
2113
|
await withMarketData(ctx, async (market) => {
|
|
1500
|
-
|
|
2114
|
+
// NST.AX is NST:ASX, as quote, valuation and the pane functions read it: the SEC is asked by venue.
|
|
2115
|
+
const listing = await requireCliListing(raw, exchangeOption, market, ctx, { venueSuffix: true });
|
|
1501
2116
|
const venues = loadBareVenues(listing, market);
|
|
1502
2117
|
const { symbol, exchange } = listing.request;
|
|
1503
2118
|
// A listing outside the US sends its company to the lookup, which needs its quote first.
|
|
1504
2119
|
const quotePromise = loadListingQuote(market.dataProvider, listing);
|
|
1505
2120
|
const identity = nonUsSecListingVenue(symbol, exchange) ? listingIdentity(listing, await quotePromise) : null;
|
|
2121
|
+
// Where a listing abroad publishes its announcements, whatever the SEC holds for it.
|
|
2122
|
+
const page = identity ? exchangeAnnouncementsPage(identity.symbol, identity.exchange) : null;
|
|
2123
|
+
const pageLine = page ? `${page.name}: ${page.url}` : null;
|
|
1506
2124
|
try {
|
|
1507
2125
|
const context = identity?.name ? { listingName: identity.name } : undefined;
|
|
1508
2126
|
const [filings, quote] = await Promise.all([
|
|
@@ -1516,9 +2134,17 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1516
2134
|
]);
|
|
1517
2135
|
const resolved = identity ?? listingIdentity(listing, quote);
|
|
1518
2136
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
2137
|
+
// A company on ASX that files nothing with the SEC lists its ASX announcements instead.
|
|
2138
|
+
if (identity && filings.length === 0 && !form && listsExchangeAnnouncements(identity.exchange)) {
|
|
2139
|
+
await printExchangeAnnouncements(ctx, { resolved, count, choice, page: page! });
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
1519
2142
|
ctx.printResult({
|
|
1520
2143
|
data: filingsWithItemLabels(filings),
|
|
1521
|
-
metadata: {
|
|
2144
|
+
metadata: {
|
|
2145
|
+
...listingMetadata(resolved), ...(form ? { form } : {}),
|
|
2146
|
+
...(page ? { announcementsPage: page.url } : {}),
|
|
2147
|
+
},
|
|
1522
2148
|
freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
|
|
1523
2149
|
...(choice.length > 0 ? { warnings: choice } : {}),
|
|
1524
2150
|
}, {
|
|
@@ -1526,10 +2152,12 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1526
2152
|
rows: filingRows,
|
|
1527
2153
|
columns: FILING_COLUMNS,
|
|
1528
2154
|
textColumns: FILING_TEXT_COLUMNS,
|
|
1529
|
-
// A listing abroad is looked up by its company: none found means the company files nothing with the SEC
|
|
1530
|
-
|
|
2155
|
+
// A listing abroad is looked up by its company: none found means the company files nothing with the SEC,
|
|
2156
|
+
// and the page its own exchange publishes its announcements on is where to read them.
|
|
2157
|
+
empty: [form ? `No ${form} filings found for ${listingTitle(resolved)}.`
|
|
1531
2158
|
: identity ? noSecReportsNotice(resolved)
|
|
1532
|
-
: `No SEC filings found for ${listingTitle(resolved)}.`,
|
|
2159
|
+
: `No SEC filings found for ${listingTitle(resolved)}.`, pageLine].filter(Boolean).join("\n"),
|
|
2160
|
+
...(pageLine ? { footnote: pageLine, exportNotes: [pageLine] } : {}),
|
|
1533
2161
|
});
|
|
1534
2162
|
} catch (error) {
|
|
1535
2163
|
if (!(error instanceof SecRegistrantMismatchError)) throw error;
|
|
@@ -1540,59 +2168,136 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1540
2168
|
const resolved = identity ?? listingIdentity(listing, await quotePromise);
|
|
1541
2169
|
ctx.printResult({
|
|
1542
2170
|
data: [] as SecFilingItem[],
|
|
1543
|
-
metadata: { ...listingMetadata(resolved), secRegistrant: error.registrantName },
|
|
2171
|
+
metadata: { ...listingMetadata(resolved), secRegistrant: error.registrantName, ...(page ? { announcementsPage: page.url } : {}) },
|
|
1544
2172
|
warnings: ctx.cliOptions.format === "text" ? undefined : [message],
|
|
1545
|
-
}, { rows: filingRows, columns: FILING_COLUMNS, textColumns: FILING_TEXT_COLUMNS, empty: message });
|
|
2173
|
+
}, { rows: filingRows, columns: FILING_COLUMNS, textColumns: FILING_TEXT_COLUMNS, empty: [message, pageLine].filter(Boolean).join("\n") });
|
|
1546
2174
|
}
|
|
1547
2175
|
});
|
|
1548
2176
|
}
|
|
1549
2177
|
|
|
2178
|
+
/**
|
|
2179
|
+
* `filings` for a company on ASX that files nothing with the SEC: its latest
|
|
2180
|
+
* ASX announcements from Gloom Cloud, newest first, under the line saying it
|
|
2181
|
+
* files no SEC reports, with its ASX announcements page. A failed read still
|
|
2182
|
+
* ends on that page, with the reason.
|
|
2183
|
+
*/
|
|
2184
|
+
async function printExchangeAnnouncements(
|
|
2185
|
+
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
2186
|
+
input: { resolved: ListingIdentity; count: number; choice: string[]; page: ExchangeAnnouncementsPage },
|
|
2187
|
+
): Promise<void> {
|
|
2188
|
+
const { resolved, count, choice, page } = input;
|
|
2189
|
+
const notice = noSecReportsNotice(resolved);
|
|
2190
|
+
const pageLine = `${page.name}: ${page.url}`;
|
|
2191
|
+
let payload: ExchangeAnnouncementsPayload | null = null;
|
|
2192
|
+
let failure: string | null = null;
|
|
2193
|
+
try {
|
|
2194
|
+
payload = await apiClient.getCloudExchangeAnnouncements({ symbol: resolved.symbol, exchange: resolved.exchange, limit: Math.min(count, 50) });
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
failure = `ASX announcements unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
2197
|
+
}
|
|
2198
|
+
const rows = payload ? announcementRows(payload) : [];
|
|
2199
|
+
const warnings = [
|
|
2200
|
+
...choice,
|
|
2201
|
+
...payload?.stale ? ["ASX could not be reached; these are the announcements Gloom Cloud read last."] : [],
|
|
2202
|
+
...failure ? [failure] : [],
|
|
2203
|
+
];
|
|
2204
|
+
ctx.printResult({
|
|
2205
|
+
data: rows,
|
|
2206
|
+
metadata: {
|
|
2207
|
+
...listingMetadata(resolved), secFilings: 0, announcementsPage: payload?.page ?? page.url,
|
|
2208
|
+
...(payload ? { announcementsTotal: payload.total, announcementsRetrievedAt: payload.retrievedAt } : {}),
|
|
2209
|
+
},
|
|
2210
|
+
freshness: rowsFreshness(rows, { ...ASX_ANNOUNCEMENTS, observedKey: "date" }),
|
|
2211
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
2212
|
+
}, {
|
|
2213
|
+
heading: listingHeading(resolved),
|
|
2214
|
+
...(rows.length > 0 ? { summary: () => `${notice} Its latest ASX announcements:` } : {}),
|
|
2215
|
+
rows: () => rows,
|
|
2216
|
+
columns: ANNOUNCEMENT_COLUMNS,
|
|
2217
|
+
empty: `${failure ? notice : `${notice} ASX lists no announcements for it.`}\n${pageLine}`,
|
|
2218
|
+
footnote: pageLine,
|
|
2219
|
+
exportNotes: [notice, pageLine],
|
|
2220
|
+
exportIdentity: { symbolOrSeries: `${resolved.symbol}:${resolved.exchange}` },
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
1550
2224
|
type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
|
|
1551
2225
|
|
|
2226
|
+
/** The company's own Form 4 filings as `fn INS` reads them: whether it has any, and its summary of them. */
|
|
2227
|
+
interface Form4Report {
|
|
2228
|
+
status: Form4Insiders;
|
|
2229
|
+
/** `fn INS`'s line of the last 90 days' open-market buys and sales in the filings read; null when it has none. */
|
|
2230
|
+
summary: string | null;
|
|
2231
|
+
/** The Form 4 lines in the filings read, and how many filings that was. */
|
|
2232
|
+
lines: number;
|
|
2233
|
+
filings: number;
|
|
2234
|
+
/** The newest filing's date (YYYY-MM-DD), which dates the report. */
|
|
2235
|
+
newestFiled: string | null;
|
|
2236
|
+
/** "Canadian insider reports (SEDI) not covered." beside the reason a company from abroad has none. */
|
|
2237
|
+
home: string | null;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
1552
2240
|
/**
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
* nothing for files no SEC
|
|
2241
|
+
* The company's Form 4 filings, read as `fn INS` reads its first page (its
|
|
2242
|
+
* latest 20 of its own) and summed up as it sums them; null when they could
|
|
2243
|
+
* not be read. A listing abroad the SEC lists nothing for files no SEC
|
|
2244
|
+
* reports at all, and a company from abroad with none names its home
|
|
2245
|
+
* market's insider reports as not covered either.
|
|
1556
2246
|
*/
|
|
1557
|
-
async function
|
|
2247
|
+
async function loadForm4Report(market: MarketContext, listing: CliListing): Promise<Form4Report | null> {
|
|
2248
|
+
const { symbol, exchange } = listing.request;
|
|
1558
2249
|
try {
|
|
1559
|
-
const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider,
|
|
1560
|
-
limit:
|
|
1561
|
-
exchange
|
|
2250
|
+
const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider, symbol, {
|
|
2251
|
+
limit: INSIDER_REPORT_FILINGS,
|
|
2252
|
+
exchange,
|
|
1562
2253
|
});
|
|
1563
|
-
if (parsed.length > 0)
|
|
1564
|
-
|
|
1565
|
-
|
|
2254
|
+
if (parsed.length > 0) {
|
|
2255
|
+
const filed = parsed.map(({ filing }) => new Date(filing.filingDate).getTime()).filter(Number.isFinite);
|
|
2256
|
+
return {
|
|
2257
|
+
status: "available",
|
|
2258
|
+
summary: buildInsiderSummary(parsed, Date.now(), parsed),
|
|
2259
|
+
lines: parsed.filter(({ transaction }) => transaction != null).length,
|
|
2260
|
+
filings: new Set(parsed.map(({ filing }) => filing.accessionNumber)).size,
|
|
2261
|
+
newestFiled: filed.length > 0 ? new Date(Math.max(...filed)).toISOString().slice(0, 10) : null,
|
|
2262
|
+
home: null,
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
const status: Form4Insiders = noFilings && nonUsSecListingVenue(symbol, exchange) ? "no-sec-reports"
|
|
2266
|
+
: foreignPrivateIssuer ? "foreign-private-issuer" : "none";
|
|
2267
|
+
const home = await loadHomeInsiderReportsNote(market.dataProvider, listing.key, exchange, foreignPrivateIssuer);
|
|
2268
|
+
return { status, summary: null, lines: 0, filings: 0, newestFiled: null, home };
|
|
1566
2269
|
} catch (error) {
|
|
1567
|
-
logFailure("cli.commands-market", "Read insider
|
|
2270
|
+
logFailure("cli.commands-market", "Read insider filings failed")(error);
|
|
1568
2271
|
return null;
|
|
1569
2272
|
}
|
|
1570
2273
|
}
|
|
1571
2274
|
|
|
1572
2275
|
/**
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
* Form 4
|
|
1576
|
-
*
|
|
2276
|
+
* The insider lines under `insider`'s holders. A company's insiders file
|
|
2277
|
+
* Form 4, which 13F holder lists rarely name, so the report leads with
|
|
2278
|
+
* `fn INS`'s summary of the Form 4 filings and points to the transactions;
|
|
2279
|
+
* a company with none says why, in the words `fn INS` uses. The insider share
|
|
2280
|
+
* of the company follows when the holder data reports it.
|
|
1577
2281
|
*/
|
|
1578
2282
|
function insiderSummary(
|
|
1579
2283
|
data: HolderData,
|
|
1580
|
-
ownerTypes: Set<string>,
|
|
1581
2284
|
listing: Pick<CliListing, "key">,
|
|
1582
2285
|
identity: ListingIdentity,
|
|
1583
|
-
form4:
|
|
2286
|
+
form4: Form4Report | null,
|
|
1584
2287
|
): string {
|
|
1585
2288
|
const held = data.summary?.insidersPercentHeld;
|
|
1586
2289
|
const share = held == null || !Number.isFinite(held)
|
|
1587
2290
|
? ""
|
|
1588
2291
|
: `Insiders hold ${formatFractionPercentCell(held)} of shares outstanding.`;
|
|
1589
|
-
const listed = data.holders.some((holder) => ownerTypes.has(holder.ownerType));
|
|
1590
2292
|
// The listing as the command read it (CAR:ASX), not the source's spelling of it (CAR.AX), which fn does not take.
|
|
1591
|
-
const transactions = form4 === "available"
|
|
1592
|
-
?
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
2293
|
+
const transactions = form4?.status === "available"
|
|
2294
|
+
? [
|
|
2295
|
+
...form4.summary ? [form4.summary] : [],
|
|
2296
|
+
`${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}`,
|
|
2297
|
+
].join("\n")
|
|
2298
|
+
: form4?.status === "no-sec-reports" ? [noSecReportsNotice(identity), form4.home].filter(Boolean).join(" ")
|
|
2299
|
+
: form4 ? insiderEmptyNotice(form4.status === "foreign-private-issuer", form4.home) : "";
|
|
2300
|
+
return [transactions, share].filter(Boolean).join("\n");
|
|
1596
2301
|
}
|
|
1597
2302
|
|
|
1598
2303
|
/** `holders --form 13d|13g|all`: the 13D/13G beneficial owners, joined by name to the 13F holders. */
|
|
@@ -1619,7 +2324,8 @@ async function printBeneficialOwners(
|
|
|
1619
2324
|
const sharesOutstanding = classSharesToday(financials);
|
|
1620
2325
|
const rows = buildBeneficialReportRows(payload, { history, holders, sharesOutstanding });
|
|
1621
2326
|
const notices = beneficialCoverageNotices(payload.coverage);
|
|
1622
|
-
const
|
|
2327
|
+
const sharesOutstandingAsOf = classSharesAsOf(financials);
|
|
2328
|
+
const legend = beneficialReportLegend(rows, sharesOutstanding, sharesOutstandingAsOf);
|
|
1623
2329
|
ctx.printResult({
|
|
1624
2330
|
data: rows,
|
|
1625
2331
|
...(notices.length ? { warnings: notices } : {}),
|
|
@@ -1633,6 +2339,7 @@ async function printBeneficialOwners(
|
|
|
1633
2339
|
coverage: payload.coverage,
|
|
1634
2340
|
complete: beneficialListComplete(payload),
|
|
1635
2341
|
sharesOutstanding,
|
|
2342
|
+
...(sharesOutstandingAsOf ? { sharesOutstandingAsOf } : {}),
|
|
1636
2343
|
},
|
|
1637
2344
|
}, {
|
|
1638
2345
|
heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
|
|
@@ -1655,6 +2362,9 @@ async function printBeneficialOwners(
|
|
|
1655
2362
|
});
|
|
1656
2363
|
}
|
|
1657
2364
|
|
|
2365
|
+
/** Holder rows `holders` and `13f` print without `--limit`, as fn HDS does. */
|
|
2366
|
+
const HOLDERS_DEFAULT_ROWS = 25;
|
|
2367
|
+
|
|
1658
2368
|
async function runHolders(
|
|
1659
2369
|
rawArgs: string[],
|
|
1660
2370
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
@@ -1673,8 +2383,15 @@ async function runHolders(
|
|
|
1673
2383
|
const listing = await requireCliListing(raw, exchangeOption, market, ctx);
|
|
1674
2384
|
if (form !== "13f") return printBeneficialOwners(listing, market, form, history, ctx);
|
|
1675
2385
|
const insiderCommand = commandName === "insider" && ownerTypes != null;
|
|
2386
|
+
const institutional = !ownerTypes?.has("insider");
|
|
1676
2387
|
const venues = loadBareVenues(listing, market);
|
|
1677
|
-
|
|
2388
|
+
// A US line's institutions are every 13F filer, from the SEC holder index as fn 13F lists them,
|
|
2389
|
+
// as many as the limit asks (HDS's 25 by default); elsewhere, or when the index cannot be read, the source's few.
|
|
2390
|
+
const indexTicker = institutional ? indexHolderTicker(listing.request.symbol, listing.request.exchange) : null;
|
|
2391
|
+
// --tail counts back from the end of the rows loaded, so it loads at least as many as it asks for.
|
|
2392
|
+
const tail = ctx.cliOptions.tail;
|
|
2393
|
+
const indexLimit = Math.min(ctx.cliOptions.limit ?? Math.max(tail ?? 0, HOLDERS_DEFAULT_ROWS), HOLDER_ROWS_MAX);
|
|
2394
|
+
const [source, quote, form4, index] = await Promise.all([
|
|
1678
2395
|
loadForListing(
|
|
1679
2396
|
listing, market, ctx,
|
|
1680
2397
|
() => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
|
|
@@ -1682,8 +2399,13 @@ async function runHolders(
|
|
|
1682
2399
|
{ command: commandName, noun: "holder data" },
|
|
1683
2400
|
),
|
|
1684
2401
|
loadListingQuote(market.dataProvider, listing),
|
|
1685
|
-
insiderCommand ?
|
|
2402
|
+
insiderCommand ? loadForm4Report(market, listing) : null,
|
|
2403
|
+
indexTicker ? loadIndexedHolders(indexTicker, indexLimit).catch(logFailure("cli.commands-market", "Read the 13F holder index failed", null)) : null,
|
|
1686
2404
|
]);
|
|
2405
|
+
const indexed = !!index?.rows.length && indexHolderTicker(listing.request.symbol, listing.request.exchange, source) != null;
|
|
2406
|
+
const data = indexed
|
|
2407
|
+
? withIndexedHolders(source, indexTicker!, { ...index!, rows: index!.rows.slice(0, indexLimit) }, indexedHolderUnits(source, quote?.price))
|
|
2408
|
+
: source;
|
|
1687
2409
|
const identity = listingIdentity(listing, quote);
|
|
1688
2410
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
1689
2411
|
// A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
|
|
@@ -1695,7 +2417,6 @@ async function runHolders(
|
|
|
1695
2417
|
});
|
|
1696
2418
|
const shownRows = windowRows(rows, ctx.cliOptions).rows;
|
|
1697
2419
|
const shown = shownRows.length;
|
|
1698
|
-
const institutional = !ownerTypes?.has("insider");
|
|
1699
2420
|
const total = institutional ? data.summary?.institutionsCount ?? null : null;
|
|
1700
2421
|
const reportDate = sharedReportDate(rows);
|
|
1701
2422
|
const valueBasis = rows.length > 0 ? holderValueBasis(reportDate, data.valueBasis, valueCurrency) : null;
|
|
@@ -1704,7 +2425,9 @@ async function runHolders(
|
|
|
1704
2425
|
const markedRows = rows.some((row) => holderShareBasisMarker(row));
|
|
1705
2426
|
// The heading names the listing; its unit, date and how much of the list follows on the same line.
|
|
1706
2427
|
// One "as of" only when every row shown is of that date; each row carries its own.
|
|
1707
|
-
|
|
2428
|
+
// An insider list the 13F filings name nobody in is not "none reported": the Form 4 summary under it says what insiders did.
|
|
2429
|
+
const form4Only = insiderCommand && rows.length === 0;
|
|
2430
|
+
const facts = form4Only ? [] : holderListFacts({
|
|
1708
2431
|
currency: valueCurrency,
|
|
1709
2432
|
listingCurrency: data.currency,
|
|
1710
2433
|
period: holderListPeriod(shownRows, data.asOf),
|
|
@@ -1715,11 +2438,19 @@ async function runHolders(
|
|
|
1715
2438
|
});
|
|
1716
2439
|
const unvaluedNote = unvaluedHoldersNote(shownRows);
|
|
1717
2440
|
const columns = holderReportColumns(shownRows, valueCurrency);
|
|
1718
|
-
// Dated as HDS dates it: by the list's as-of.
|
|
1719
|
-
const freshness =
|
|
2441
|
+
// Dated as HDS dates it: by the list's as-of; an insider report of Form 4 filings alone as INS dates it, by the newest.
|
|
2442
|
+
const freshness = form4Only
|
|
2443
|
+
? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" })
|
|
2444
|
+
: rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
|
|
1720
2445
|
// Named as fn HDS takes it, so the closing notes point to the same listing.
|
|
1721
2446
|
const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
|
|
1722
|
-
const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
|
|
2447
|
+
const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total, indexed });
|
|
2448
|
+
// The index's smallest filers are pages past what was loaded: --tail is the end of the loaded rows, not of every filer.
|
|
2449
|
+
const tailNote = indexed && tail != null && total != null && rows.length < total
|
|
2450
|
+
? `--tail ${tail} is the last ${shown} of the largest ${rows.length} loaded, not the smallest of all ${total.toLocaleString("en-US")} holders.`
|
|
2451
|
+
: null;
|
|
2452
|
+
const partialNote = indexed ? indexedHoldersPartialNote(index, indexLimit) : null;
|
|
2453
|
+
const warnings = [...choice, ...(partialNote ? [partialNote] : []), ...(tailNote ? [tailNote] : [])];
|
|
1723
2454
|
ctx.printResult({
|
|
1724
2455
|
data: rows,
|
|
1725
2456
|
...(freshness ? { freshness } : {}),
|
|
@@ -1738,9 +2469,13 @@ async function runHolders(
|
|
|
1738
2469
|
truncated: shown < (total ?? rows.length),
|
|
1739
2470
|
valueBasis,
|
|
1740
2471
|
positionsBasis,
|
|
1741
|
-
...(insiderCommand ? {
|
|
2472
|
+
...(insiderCommand ? {
|
|
2473
|
+
form4Insiders: form4?.status ?? null,
|
|
2474
|
+
// INS's summary of the latest Form 4 filings, as `fn INS` gives it in its metadata.
|
|
2475
|
+
form4: form4 ? { summary: form4.summary, lines: form4.lines, filings: form4.filings, newestFiled: form4.newestFiled, notCovered: form4.home } : null,
|
|
2476
|
+
} : {}),
|
|
1742
2477
|
},
|
|
1743
|
-
...(
|
|
2478
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
1744
2479
|
}, {
|
|
1745
2480
|
heading: `${listingHeading(identity)}${cliStyles.muted(facts.map((fact) => ` · ${fact}`).join(""))}`,
|
|
1746
2481
|
columns,
|
|
@@ -1772,7 +2507,7 @@ async function runHolders(
|
|
|
1772
2507
|
exportIdentity: { symbolOrSeries: listing.key },
|
|
1773
2508
|
summary: () => [
|
|
1774
2509
|
[valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
|
|
1775
|
-
insiderCommand ? insiderSummary(data,
|
|
2510
|
+
insiderCommand ? insiderSummary(data, listing, identity, form4) : "",
|
|
1776
2511
|
].filter(Boolean).join("\n"),
|
|
1777
2512
|
empty: `No holders reported for ${listingTitle(identity)}.`,
|
|
1778
2513
|
});
|
|
@@ -1795,6 +2530,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1795
2530
|
]);
|
|
1796
2531
|
const identity = listingIdentity(listing, quote);
|
|
1797
2532
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
2533
|
+
const { upside, reference } = analystUpside(data, quote);
|
|
1798
2534
|
ctx.printResult({
|
|
1799
2535
|
data,
|
|
1800
2536
|
metadata: {
|
|
@@ -1802,6 +2538,8 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1802
2538
|
recommendationRating: data.recommendationRating,
|
|
1803
2539
|
priceTarget: data.priceTarget,
|
|
1804
2540
|
recommendations: data.recommendations,
|
|
2541
|
+
upside: upside ?? null,
|
|
2542
|
+
upsideReference: analystReferenceMetadata(reference),
|
|
1805
2543
|
},
|
|
1806
2544
|
freshness: rowsFreshness(analystRows(data), {
|
|
1807
2545
|
...REPORTED_DATA, basis: "analyst ratings", observedKey: "date", oldest: null,
|
|
@@ -1810,7 +2548,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1810
2548
|
}, {
|
|
1811
2549
|
heading: listingHeading(identity),
|
|
1812
2550
|
rows: analystRows,
|
|
1813
|
-
summary: analystSummary,
|
|
2551
|
+
summary: (research) => analystSummary(research, quote),
|
|
1814
2552
|
columns: [
|
|
1815
2553
|
{ key: "date", header: "Date" },
|
|
1816
2554
|
{ key: "firm", header: "Firm" },
|
|
@@ -1832,13 +2570,18 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
|
|
|
1832
2570
|
await withMarketData(ctx, async (market) => {
|
|
1833
2571
|
const listing = await requireCliListing(raw, exchangeOption, market, ctx);
|
|
1834
2572
|
const venues = loadBareVenues(listing, market);
|
|
1835
|
-
|
|
2573
|
+
// The earnings rows are resolved against the calendar `earnings` reads, so the next report is the same in both.
|
|
2574
|
+
const calendar = market.dataProvider.getEarningsCalendar?.([listing.key])
|
|
2575
|
+
.catch(logFailure("cli.commands-market", "Read the earnings calendar for events failed", [] as EarningsEvent[]));
|
|
2576
|
+
const [recorded, quote, events] = await Promise.all([
|
|
1836
2577
|
loadForListing(
|
|
1837
2578
|
listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange),
|
|
1838
2579
|
undefined, { command: "events", noun: "corporate events" },
|
|
1839
2580
|
),
|
|
1840
2581
|
loadListingQuote(market.dataProvider, listing),
|
|
2582
|
+
calendar,
|
|
1841
2583
|
]);
|
|
2584
|
+
const data = withResolvedEarnings(recorded, calendarEventOf(events, listing.key), newYorkDay());
|
|
1842
2585
|
const identity = listingIdentity(listing, quote);
|
|
1843
2586
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
1844
2587
|
ctx.printResult({
|
|
@@ -1869,14 +2612,14 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1869
2612
|
// With --leaps, --expiration is a window of one expiry.
|
|
1870
2613
|
const expirationGiven = expiration != null || rawArgs.some((arg) => arg === "--expiration" || arg.startsWith("--expiration="));
|
|
1871
2614
|
const criteria = parseLeapsCriteria({ ...leapsOptions.raw, ...expirationGiven ? { "--expiration": expiration ?? "" } : {} });
|
|
1872
|
-
if ("error" in criteria) ctx.fail(criteria.error, `Usage:
|
|
2615
|
+
if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloom ${LEAPS_USAGE}`);
|
|
1873
2616
|
const entries = normalizeSymbols(args);
|
|
1874
|
-
if (entries.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage:
|
|
2617
|
+
if (entries.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloom ${LEAPS_USAGE}`);
|
|
1875
2618
|
await withMarketData(ctx, async (market) => runLeapsScreen(await readSymbols(entries, market.dataProvider), exchangeOption, criteria, market, ctx));
|
|
1876
2619
|
return;
|
|
1877
2620
|
}
|
|
1878
2621
|
if (leapsOptions.given.length > 0) {
|
|
1879
|
-
ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage:
|
|
2622
|
+
ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage: gloom ${LEAPS_USAGE}`);
|
|
1880
2623
|
}
|
|
1881
2624
|
const raw = requireOneArg(args, OPTIONS_USAGE, "symbol", ctx);
|
|
1882
2625
|
let expirationDate: number | undefined;
|
|
@@ -1885,7 +2628,7 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1885
2628
|
if (parsed == null) {
|
|
1886
2629
|
ctx.fail(
|
|
1887
2630
|
expiration == null ? "--expiration needs a date." : `Invalid --expiration "${expiration}".`,
|
|
1888
|
-
`Use ${OPTION_EXPIRATION_FORMAT}
|
|
2631
|
+
`Use ${OPTION_EXPIRATION_FORMAT}.${expiration == null ? "" : expirationHint(expiration)}\nUsage: gloom ${OPTIONS_USAGE}`,
|
|
1889
2632
|
);
|
|
1890
2633
|
}
|
|
1891
2634
|
expirationDate = parsed;
|
|
@@ -1898,30 +2641,54 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1898
2641
|
const quotePromise = loadListingQuote(market.dataProvider, listing);
|
|
1899
2642
|
const fate = listingFateLoader(listing, market, quotePromise);
|
|
1900
2643
|
const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
|
|
1901
|
-
const
|
|
1902
|
-
|
|
2644
|
+
const now = Date.now();
|
|
2645
|
+
const readChain = async (expiry: number | undefined) => {
|
|
2646
|
+
const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expiry, undefined])
|
|
1903
2647
|
.load({ force: refresh });
|
|
1904
2648
|
return {
|
|
1905
2649
|
result: cached,
|
|
1906
|
-
chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange,
|
|
2650
|
+
chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expiry, {
|
|
1907
2651
|
cacheMode: refresh ? "refresh" : "default",
|
|
1908
2652
|
}),
|
|
1909
2653
|
};
|
|
2654
|
+
};
|
|
2655
|
+
// An expiry whose last session has closed has no chain to show: it says so, and names the nearest live one.
|
|
2656
|
+
if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now, exchange)) {
|
|
2657
|
+
const listed = await readChain(undefined).then(({ chain }) => chain.expirationDates,
|
|
2658
|
+
logFailure("cli-market", "Read available option expirations failed", []));
|
|
2659
|
+
ctx.fail(`expiry ${expiryIsoDate(expirationDate)} for ${listing.key} `
|
|
2660
|
+
+ expiredExpiryText(expirationDate, listed, now, { exchange, flag: "expiration" }),
|
|
2661
|
+
`Run gloom options ${listing.key} to list the live expiries.`);
|
|
2662
|
+
}
|
|
2663
|
+
const { result, chain: read } = await loadForListing(listing, market, ctx, async () => {
|
|
2664
|
+
const first = await readChain(expirationDate);
|
|
2665
|
+
// A source can still default to the expiry that lapsed at Friday's close; the nearest live one is shown instead.
|
|
2666
|
+
const live = expirationDate == null ? liveDefaultExpiry(first.chain, now, exchange) : undefined;
|
|
2667
|
+
if (live === null) return { ...first, chain: { ...first.chain, calls: [], puts: [] } };
|
|
2668
|
+
if (live == null) return first;
|
|
2669
|
+
// The default's list stays the catalogue: a dated chain can list fewer expiries.
|
|
2670
|
+
const shown = await readChain(live);
|
|
2671
|
+
return { ...shown, chain: { ...shown.chain,
|
|
2672
|
+
expirationDates: [...new Set([...first.chain.expirationDates, ...shown.chain.expirationDates])].sort((a, b) => a - b) } };
|
|
1910
2673
|
}, undefined, { command: "options", noun: "options chain", fate }).catch((error: unknown) => {
|
|
1911
2674
|
if (!isOptionsUnavailableError(error)) throw error;
|
|
1912
2675
|
// A future or commodity with no chain of its own: name the listed fund whose options stand in.
|
|
1913
2676
|
const alternative = listedOptionsAlternative(listing.key);
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2677
|
+
// A name read as a fund or an index asks about the company's stock it means, whose options are listed.
|
|
2678
|
+
const meant = meantQuestion(listing);
|
|
2679
|
+
return ctx.fail(meant ? `${optionsUnavailableTitle(listing.key)} ${meant}` : optionsUnavailableTitle(listing.key), alternative
|
|
2680
|
+
? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}), gloom options ${alternative.symbol}.`
|
|
2681
|
+
+ ` Strategy payoffs: gloom fn OSA ${alternative.symbol}`
|
|
2682
|
+
: "Strategy payoffs on a listed underlying: gloom fn OSA <ticker>");
|
|
1918
2683
|
});
|
|
2684
|
+
const loaded = withLiveExpiryList(read, now, exchange);
|
|
1919
2685
|
const inputs = await inputsPromise;
|
|
1920
2686
|
if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
|
|
1921
2687
|
// An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
|
|
1922
2688
|
const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
|
|
1923
2689
|
.getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
|
|
1924
|
-
.then((fallback) => fallback.expirationDates,
|
|
2690
|
+
.then((fallback) => liveOptionExpiries(fallback.expirationDates, now, exchange),
|
|
2691
|
+
logFailure("cli-market", "Read available option expirations failed", []));
|
|
1925
2692
|
const miss = missingExpiryMessage(listing.key, expirationDate, listed);
|
|
1926
2693
|
ctx.fail(miss.message, miss.details);
|
|
1927
2694
|
}
|
|
@@ -2122,15 +2889,35 @@ async function runFxOnDate(
|
|
|
2122
2889
|
async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
2123
2890
|
const args = [...rawArgs];
|
|
2124
2891
|
const dateOption = takeOption(args, "--date");
|
|
2125
|
-
|
|
2892
|
+
const forwardOption = takeOption(args, "--forward");
|
|
2893
|
+
if (args.length === 0) ctx.fail(`Usage: gloom ${FX_USAGE}`);
|
|
2894
|
+
// A forward is implied from today's spot and the latest short rates, which are all Gloom Cloud serves.
|
|
2895
|
+
if (dateOption != null && forwardOption != null) {
|
|
2896
|
+
ctx.fail(
|
|
2897
|
+
"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.",
|
|
2898
|
+
`For that day's close: gloom fx ${args.join(" ")} --date ${dateOption}. For today's forward, drop --date.`,
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2126
2901
|
const date = dateOption == null ? null : readFxDate(dateOption, ctx);
|
|
2902
|
+
const tenor = forwardOption == null ? null : parseForwardTenor(forwardOption);
|
|
2903
|
+
if (tenor && "error" in tenor) ctx.fail(tenor.error, `Usage: gloom ${FX_FORWARD_USAGE}`);
|
|
2127
2904
|
await withMarketData(ctx, async (market) => {
|
|
2128
2905
|
const parsed = parseFxArgs(args, market.config.baseCurrency);
|
|
2129
|
-
if ("error" in parsed) return ctx.fail(parsed.error, `Usage:
|
|
2906
|
+
if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloom ${forwardOption == null ? FX_USAGE : FX_FORWARD_USAGE}`);
|
|
2130
2907
|
if (date) return runFxOnDate(parsed, date, args, market, ctx);
|
|
2131
2908
|
const { request, amount, toUsdByDefault } = parsed;
|
|
2132
2909
|
const { currency, baseCurrency } = request;
|
|
2133
2910
|
const pair = `${currency}/${baseCurrency}`;
|
|
2911
|
+
if (tenor) {
|
|
2912
|
+
if (amount != null) ctx.fail("--forward prices a pair, not an amount.", `Usage: gloom ${FX_FORWARD_USAGE}`);
|
|
2913
|
+
if (currency === baseCurrency) ctx.fail(`${pair} has no forward: both sides are ${currency}.`, `Usage: gloom ${FX_FORWARD_USAGE}`);
|
|
2914
|
+
}
|
|
2915
|
+
// The short rates are read alongside spot. A failed read is held as a value, not left to reject
|
|
2916
|
+
// unobserved while spot loads, and is raised where the forward needs the rates.
|
|
2917
|
+
const shortRates = tenor && !("error" in tenor) ? apiClient.getCloudShortRates().then(
|
|
2918
|
+
(payload) => ({ payload }),
|
|
2919
|
+
(error: unknown) => ({ error: unavailableOnServer(error, "Forwards are not available from Gloom Cloud yet: it serves no short rates.") }),
|
|
2920
|
+
) : null;
|
|
2134
2921
|
const reasons = new Map<string, string>();
|
|
2135
2922
|
const load = (code: string) => market.dataProvider.getCachedQuery("getExchangeRate", [code])
|
|
2136
2923
|
.load({ force: ctx.cliOptions.refresh }).catch((error) => {
|
|
@@ -2159,13 +2946,68 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
|
|
|
2159
2946
|
const converted = amount == null ? null : amount * rate;
|
|
2160
2947
|
const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate, ...(amount == null ? {} : { amount, converted }) };
|
|
2161
2948
|
// The closing line dates the rate, and says when it is stale, as `fn FXC` does.
|
|
2162
|
-
const
|
|
2949
|
+
const loaded = new Map(codes.flatMap((code, index) => {
|
|
2163
2950
|
const leg = legs[index];
|
|
2164
2951
|
return leg ? [[code, {
|
|
2165
2952
|
rate: leg.value,
|
|
2166
|
-
timing: {
|
|
2953
|
+
timing: {
|
|
2954
|
+
asOf: leg.asOf, staleAt: leg.staleAt, error: leg.refreshError, delayMinutes: leg.delayMinutes,
|
|
2955
|
+
intradayBar: leg.intradayBar, staleReason: leg.staleReason,
|
|
2956
|
+
},
|
|
2167
2957
|
}] as const] : [];
|
|
2168
|
-
}))
|
|
2958
|
+
}));
|
|
2959
|
+
const freshness = fxPairFreshness(loaded);
|
|
2960
|
+
// A rate that is its pair's bar says which bar, and a stale one why it stands.
|
|
2961
|
+
const rateNotes = fxRateNotes(new Map([...loaded].map(([code, leg]) => [code, leg.timing])));
|
|
2962
|
+
if (tenor && shortRates && !("error" in tenor)) {
|
|
2963
|
+
const read = await shortRates;
|
|
2964
|
+
if ("error" in read) throw read.error;
|
|
2965
|
+
const { rates } = read.payload;
|
|
2966
|
+
// BASE/QUOTE: the forward needs the rate of each; a missing one is named, never filled in.
|
|
2967
|
+
const base = shortRateFor(currency, rates);
|
|
2968
|
+
const quote = shortRateFor(baseCurrency, rates);
|
|
2969
|
+
const missing = [base, quote].flatMap((lookup) => "error" in lookup ? [lookup.error] : []);
|
|
2970
|
+
if ("error" in base || "error" in quote) {
|
|
2971
|
+
return ctx.fail(`No ${tenor.label} forward for ${pair}. ${missing.join(" ")}`, "The forward is implied from both currencies' short rates by covered interest parity.");
|
|
2972
|
+
}
|
|
2973
|
+
const implied = impliedForward(rate, tenor.days, base.rate, quote.rate);
|
|
2974
|
+
const forwardDate = new Date(Math.floor(Date.now() / 86_400_000) * 86_400_000 + tenor.days * 86_400_000).toISOString().slice(0, 10);
|
|
2975
|
+
const rateFields = (short: typeof base.rate) => ({
|
|
2976
|
+
currency: short.currency, name: short.name, tenor: short.tenor, value: short.value, asOf: short.asOf,
|
|
2977
|
+
dayCountBasis: short.dayCountBasis, publisher: short.publisher, sourceUrl: short.sourceUrl, stale: short.stale,
|
|
2978
|
+
});
|
|
2979
|
+
const staleRates = [base.rate, quote.rate].filter((short) => short.stale)
|
|
2980
|
+
.map((short) => `${short.name} (${short.currency}) is the last copy read, dated ${short.asOf ?? "undated"}; its source could not be read since.`);
|
|
2981
|
+
const forwardRow = {
|
|
2982
|
+
pair, tenor: tenor.label, days: tenor.days, forwardDate, spot: rate, spotAsOf: asOf, forward: implied.forward,
|
|
2983
|
+
points: implied.points, pointSize: implied.pointSize, premiumPercent: implied.premiumPercent, annualizedPercent: implied.annualizedPercent,
|
|
2984
|
+
baseRate: rateFields(base.rate), quoteRate: rateFields(quote.rate), method: "covered interest parity", label: IMPLIED_FORWARD_LABEL,
|
|
2985
|
+
};
|
|
2986
|
+
const growth = (short: typeof base.rate) => `(1 + ${formatRatePercent(short.value)} × ${tenor.days}/${short.dayCountBasis})`;
|
|
2987
|
+
// A currency typed as its name (yen) says which code it was read as, as the spot answer does.
|
|
2988
|
+
const forwardWarnings = [...request.readAs ?? [], ...rateNotes, ...staleRates];
|
|
2989
|
+
return ctx.printResult({ data: [forwardRow], freshness, ...(forwardWarnings.length > 0 ? { warnings: forwardWarnings } : {}) }, {
|
|
2990
|
+
text: () => [
|
|
2991
|
+
`${pair} ${tenor.label} forward ${formatFxRate(implied.forward)}, ${IMPLIED_FORWARD_LABEL}`,
|
|
2992
|
+
`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})`,
|
|
2993
|
+
`${currency} rate: ${describeShortRate(base.rate)}`,
|
|
2994
|
+
`${baseCurrency} rate: ${describeShortRate(quote.rate)}`,
|
|
2995
|
+
`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.`,
|
|
2996
|
+
].join("\n"),
|
|
2997
|
+
exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: `forward ${tenor.label}` },
|
|
2998
|
+
columns: [
|
|
2999
|
+
{ key: "pair", header: "Pair" },
|
|
3000
|
+
{ key: "tenor", header: "Tenor" },
|
|
3001
|
+
{ key: "days", header: "Days", align: "right" },
|
|
3002
|
+
{ key: "forwardDate", header: "Forward date" },
|
|
3003
|
+
{ key: "spot", header: "Spot", align: "right" },
|
|
3004
|
+
{ key: "forward", header: "Forward", align: "right" },
|
|
3005
|
+
{ key: "points", header: "Points", align: "right" },
|
|
3006
|
+
{ key: "premiumPercent", header: "Premium %", align: "right" },
|
|
3007
|
+
{ key: "annualizedPercent", header: "Annualized %", align: "right" },
|
|
3008
|
+
],
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
2169
3011
|
// An amount with one currency names no target; the answer says it went to USD.
|
|
2170
3012
|
const warnings = [
|
|
2171
3013
|
// A currency typed as its name (yen) says which code it was read as.
|
|
@@ -2173,10 +3015,21 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
|
|
|
2173
3015
|
...toUsdByDefault && amount != null
|
|
2174
3016
|
? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to>.`]
|
|
2175
3017
|
: [],
|
|
3018
|
+
...rateNotes,
|
|
2176
3019
|
];
|
|
2177
|
-
|
|
3020
|
+
// The peso's dollar rate is the official one; the market rates Buenos Aires prices imply follow it, each with its inputs.
|
|
3021
|
+
const peso = isPesoDollarPair(currency, baseCurrency)
|
|
3022
|
+
? await loadPesoDollarRates((targets) => market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh }))
|
|
3023
|
+
: null;
|
|
3024
|
+
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.`);
|
|
3025
|
+
const rateText = peso ? `${describeFxRate(request, rate)} · ${peso.spotKind} rate` : describeFxRate(request, rate);
|
|
3026
|
+
ctx.printResult({ data: [peso ? { ...row, ...pesoDollarRatesJson(peso) } : row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
|
|
2178
3027
|
// The conversion, then the rate it used; the source line under it dates that rate.
|
|
2179
|
-
text: () =>
|
|
3028
|
+
text: () => [
|
|
3029
|
+
...amount == null ? [] : [describeFxConversion(request, amount, rate)],
|
|
3030
|
+
rateText,
|
|
3031
|
+
...peso ? pesoDollarRateLines(peso) : [],
|
|
3032
|
+
].join("\n"),
|
|
2180
3033
|
// The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
|
|
2181
3034
|
exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "spot" },
|
|
2182
3035
|
columns: [
|
|
@@ -2200,7 +3053,7 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2200
3053
|
const args = [...rawArgs];
|
|
2201
3054
|
const exchangeOption = takeOption(args, "--exchange");
|
|
2202
3055
|
const entries = normalizeSymbols(args);
|
|
2203
|
-
if (entries.length === 0) ctx.fail("Usage:
|
|
3056
|
+
if (entries.length === 0) ctx.fail("Usage: gloom earnings <symbol...>");
|
|
2204
3057
|
await withCliServices(ctx, async (services) => {
|
|
2205
3058
|
const symbols = await readSymbols(entries, services.dataProvider);
|
|
2206
3059
|
const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
|
|
@@ -2210,9 +3063,9 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2210
3063
|
// Each bare symbol's venues cost a search, so a long list (a portfolio check, not a lookup) names none.
|
|
2211
3064
|
const venues = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
|
|
2212
3065
|
: listings.map((listing) => [listing, loadBareVenues(listing, services)] as const));
|
|
2213
|
-
let
|
|
3066
|
+
let calendar: EarningsEvent[];
|
|
2214
3067
|
try {
|
|
2215
|
-
|
|
3068
|
+
calendar = await loadForListing(
|
|
2216
3069
|
listings, services, ctx,
|
|
2217
3070
|
() => services.dataProvider.getEarningsCalendar(keys),
|
|
2218
3071
|
(found) => found.length === 0,
|
|
@@ -2222,6 +3075,15 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2222
3075
|
if (failure) ctx.fail(failure);
|
|
2223
3076
|
throw error;
|
|
2224
3077
|
}
|
|
3078
|
+
// Each date is resolved as events, EE, ERN and OSA resolve it: one that has come is read against the
|
|
3079
|
+
// company's quarters on record, for its actual and the next report on record (shared/earnings-calendar).
|
|
3080
|
+
const today = newYorkDay();
|
|
3081
|
+
const resolved = await resolveListingEvents(services.dataProvider, listings.map((listing) => ({
|
|
3082
|
+
key: listing.key, symbol: listing.request.symbol, exchange: listing.request.exchange,
|
|
3083
|
+
})), calendar, today);
|
|
3084
|
+
const events: ResolvedEarningsEvent[] = calendar
|
|
3085
|
+
.map((event) => resolved.get(event.symbol.toUpperCase()) ?? { ...event, dateStatus: calendarStatus(event, today) })
|
|
3086
|
+
.sort((left, right) => left.earningsDate.getTime() - right.earningsDate.getTime());
|
|
2225
3087
|
const eventOf = (listing: CliListing) => events.find((event) => event.symbol.toUpperCase() === listing.key.toUpperCase());
|
|
2226
3088
|
const [abroadNotes, choiceNotes] = await Promise.all([
|
|
2227
3089
|
// A listing abroad with no date of its own says so, with its US line's date when that line is the same company.
|
|
@@ -2241,7 +3103,8 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
2241
3103
|
];
|
|
2242
3104
|
ctx.printResult({
|
|
2243
3105
|
data: events,
|
|
2244
|
-
|
|
3106
|
+
// Dated by when it was read: a row's date is when the report is, not how old the calendar is.
|
|
3107
|
+
freshness: rowsFreshness([], { status: "not-a-feed", basis: "calendar", oldest: null }),
|
|
2245
3108
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
2246
3109
|
}, {
|
|
2247
3110
|
rows: earningsRows,
|
|
@@ -2267,7 +3130,9 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2267
3130
|
group: CLI_COMMAND_GROUPS.research,
|
|
2268
3131
|
usage: ["quote <symbol...>", "quote --file <path>"],
|
|
2269
3132
|
options: [EXCHANGE_OPTION, LIST_FILE_OPTION],
|
|
2270
|
-
|
|
3133
|
+
sections: [QUOTE_CSV_SECTION],
|
|
3134
|
+
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",
|
|
3135
|
+
"quote --file holdings.csv --csv > quotes.csv"],
|
|
2271
3136
|
},
|
|
2272
3137
|
execute: (args, ctx) => runQuote(args, ctx, "quote"),
|
|
2273
3138
|
},
|
|
@@ -2278,6 +3143,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2278
3143
|
group: CLI_COMMAND_GROUPS.research,
|
|
2279
3144
|
usage: ["compare <symbol...>", "compare --file <path>"],
|
|
2280
3145
|
options: [EXCHANGE_OPTION, LIST_FILE_OPTION],
|
|
3146
|
+
sections: [QUOTE_CSV_SECTION],
|
|
2281
3147
|
examples: ["compare KO PEP", "compare SAN:EPA SAN:NYSE", "compare SPY QQQ IWM --csv", "compare - < peers.txt"],
|
|
2282
3148
|
},
|
|
2283
3149
|
execute: (args, ctx) => runQuote(args, ctx, "compare"),
|
|
@@ -2287,26 +3153,45 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2287
3153
|
description: "Fetch open, high, low, close, and volume over a range",
|
|
2288
3154
|
help: {
|
|
2289
3155
|
group: CLI_COMMAND_GROUPS.research,
|
|
2290
|
-
usage: [HISTORY_USAGE],
|
|
3156
|
+
usage: [HISTORY_USAGE, "history --file <path> [--range <range> | --from <year|date> [--to <year|date>]]"],
|
|
2291
3157
|
options: [
|
|
2292
3158
|
{ flags: "--range <range>", description: `${HISTORY_RANGES.join(", ")} (default 1Y); ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}` },
|
|
3159
|
+
{ 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` },
|
|
3160
|
+
{ flags: "--to <year|date>, --end <year|date>", description: PRICE_WINDOW_TO_HELP },
|
|
3161
|
+
{ 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" },
|
|
2293
3162
|
EXCHANGE_OPTION,
|
|
2294
3163
|
],
|
|
2295
3164
|
sections: [{
|
|
2296
3165
|
title: "Bars",
|
|
2297
3166
|
lines: [
|
|
2298
3167
|
`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.`,
|
|
2299
|
-
"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.
|
|
3168
|
+
"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.",
|
|
2300
3169
|
"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.",
|
|
3170
|
+
"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).",
|
|
3171
|
+
],
|
|
3172
|
+
}, {
|
|
3173
|
+
title: "CSV",
|
|
3174
|
+
lines: [
|
|
3175
|
+
"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.",
|
|
3176
|
+
],
|
|
3177
|
+
}, {
|
|
3178
|
+
title: "Several symbols",
|
|
3179
|
+
lines: [
|
|
3180
|
+
`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.`,
|
|
3181
|
+
"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.",
|
|
2301
3182
|
],
|
|
2302
3183
|
}, {
|
|
2303
3184
|
title: "Long history",
|
|
2304
3185
|
lines: [
|
|
2305
|
-
"ZAR has daily FRED data from 1980:
|
|
2306
|
-
"NGN, SAR and ILS have no FRED series. Their daily bars cover the last five years (
|
|
3186
|
+
"ZAR has daily FRED data from 1980: gloom fred DEXSFUS --start 1980 --sort asc --csv",
|
|
3187
|
+
"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).",
|
|
2307
3188
|
],
|
|
2308
3189
|
}],
|
|
2309
|
-
examples: [
|
|
3190
|
+
examples: [
|
|
3191
|
+
"history AAPL", "history BHP:ASX --range 5Y", "history JPM --from 2007 --to 2009", "history ZAR=X --range ALL", "history ZAR=X --tail 5",
|
|
3192
|
+
"history AAPL --range 5Y --csv > aapl.csv", "history AAPL MSFT 000660.KS --range 1M --csv",
|
|
3193
|
+
"history --file holdings.txt --start 2024-01-01 --end 2024-12-31 --csv > bars.csv",
|
|
3194
|
+
],
|
|
2310
3195
|
},
|
|
2311
3196
|
unknownOptionHint: historyOptionHint,
|
|
2312
3197
|
execute: runHistory,
|
|
@@ -2382,7 +3267,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2382
3267
|
},
|
|
2383
3268
|
execute: async (args, ctx) => {
|
|
2384
3269
|
const query = args.join(" ");
|
|
2385
|
-
if (!query) ctx.fail("Usage:
|
|
3270
|
+
if (!query) ctx.fail("Usage: gloom provider-search <query>");
|
|
2386
3271
|
await withMarketData(ctx, async (market) => {
|
|
2387
3272
|
const results = await market.dataProvider.search(query);
|
|
2388
3273
|
ctx.printResult({ data: results.slice(0, ctx.cliOptions.limit ?? results.length) }, {
|
|
@@ -2482,6 +3367,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2482
3367
|
title: "Output",
|
|
2483
3368
|
lines: [
|
|
2484
3369
|
"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%).",
|
|
3370
|
+
"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.",
|
|
2485
3371
|
],
|
|
2486
3372
|
}],
|
|
2487
3373
|
examples: ["holders AAPL", "holders SAN:EPA", "holders SAN --csv", "holders CAR --form all", "holders CAR --form 13g --history --json"],
|
|
@@ -2508,7 +3394,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2508
3394
|
options: [EXCHANGE_OPTION],
|
|
2509
3395
|
sections: [{
|
|
2510
3396
|
title: "13F filings",
|
|
2511
|
-
lines: ["For each fund's reported position and its change over the quarter, run
|
|
3397
|
+
lines: ["For each fund's reported position and its change over the quarter, run gloom fn 13F <symbol>."],
|
|
2512
3398
|
}],
|
|
2513
3399
|
examples: ["13f AAPL", "13f BP:NYSE"],
|
|
2514
3400
|
},
|
|
@@ -2516,7 +3402,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2516
3402
|
},
|
|
2517
3403
|
{
|
|
2518
3404
|
name: "filings",
|
|
2519
|
-
description: "Fetch recent SEC filings",
|
|
3405
|
+
description: "Fetch recent SEC filings; a listing abroad gets its exchange's announcements page, and ASX's list",
|
|
2520
3406
|
help: {
|
|
2521
3407
|
group: CLI_COMMAND_GROUPS.companyData,
|
|
2522
3408
|
usage: ["filings <symbol> [--count <n>] [--form <form>]"],
|
|
@@ -2525,7 +3411,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2525
3411
|
{ flags: "--form <form>", description: "Only this form and its amendments, such as 10-K or 13D" },
|
|
2526
3412
|
EXCHANGE_OPTION,
|
|
2527
3413
|
],
|
|
2528
|
-
examples: ["filings AAPL", "filings BHP:ASX", "filings AAPL --count 40 --json", "filings CAR --form 13G"],
|
|
3414
|
+
examples: ["filings AAPL", "filings BHP:ASX", "filings NST.AX", "filings AAPL --count 40 --json", "filings CAR --form 13G"],
|
|
2529
3415
|
},
|
|
2530
3416
|
execute: runFilings,
|
|
2531
3417
|
},
|
|
@@ -2555,9 +3441,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2555
3441
|
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",
|
|
2556
3442
|
help: {
|
|
2557
3443
|
group: CLI_COMMAND_GROUPS.markets,
|
|
2558
|
-
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>"],
|
|
3444
|
+
usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>", FX_FORWARD_USAGE],
|
|
2559
3445
|
options: [
|
|
2560
3446
|
{ 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` },
|
|
3447
|
+
{ 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" },
|
|
2561
3448
|
],
|
|
2562
3449
|
sections: [{
|
|
2563
3450
|
title: "Direction",
|
|
@@ -2576,8 +3463,14 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
2576
3463
|
lines: [
|
|
2577
3464
|
"--date gives the rate at that day's daily FX close instead of the latest: fx USD/CAD --date 2026-09-02, or with an amount, fx 1250 USD/CAD --date 2026-09-02. Each currency's USD pair is read at its close of that day and crossed through USD, as the latest rate is. A weekend or market holiday has no close: the answer then gives the trading day before it and says so. The source line dates the close; --json adds date (the day asked for) and asOf (the close used).",
|
|
2578
3465
|
],
|
|
3466
|
+
}, {
|
|
3467
|
+
title: "Forwards",
|
|
3468
|
+
lines: [
|
|
3469
|
+
"--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.",
|
|
3470
|
+
"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.",
|
|
3471
|
+
],
|
|
2579
3472
|
}],
|
|
2580
|
-
examples: ["fx EUR", "fx NGN", "fx yen", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx USD/CAD --date 2026-09-02", "fx JPY --json"],
|
|
3473
|
+
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"],
|
|
2581
3474
|
},
|
|
2582
3475
|
execute: runFx,
|
|
2583
3476
|
},
|