@gloom-sh/terminal 0.18.5 → 0.19.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +163 -8
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +8 -21
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +17 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +34 -12
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +5 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +10 -10
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +22 -4
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +48 -5
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +11 -2
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/version.ts +2 -2
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatPriceEarnings, NOT_MEANINGFUL } from "../../../utils/price-earnings";
|
|
2
|
-
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { TextAttributes } from "../../../ui";
|
|
4
4
|
import {
|
|
5
5
|
DataTableView,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type PaneFooterSegment,
|
|
12
12
|
} from "../../../components";
|
|
13
13
|
import type { PaneProps } from "../../../types/plugin";
|
|
14
|
-
import { useAppSelector, usePaneInstance } from "../../../state/app/context";
|
|
14
|
+
import { useAppSelector, usePaneInstance, usePaneTitle, useUpdatePaneSettings } from "../../../state/app/context";
|
|
15
15
|
import { getSharedMarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
|
|
16
16
|
import { buildQuoteKey } from "../../../market-data/selectors";
|
|
17
17
|
import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
|
|
@@ -25,7 +25,10 @@ import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../
|
|
|
25
25
|
import { formatCompact, formatLevelPercent, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
|
|
26
26
|
import { parseDisplayDate } from "../../../utils/datetime-format";
|
|
27
27
|
import { convertMarketCapitalization } from "../../../utils/market-capitalization";
|
|
28
|
-
import { usePluginTickerActions } from "../../runtime";
|
|
28
|
+
import { useAssetData, usePluginTickerActions } from "../../runtime";
|
|
29
|
+
import { apiClient } from "../../../api-client";
|
|
30
|
+
import { errorMessage } from "../../../utils/errors";
|
|
31
|
+
import { formatTickerListInput } from "../../../tickers/list";
|
|
29
32
|
import { handleRefreshKey, useClampSelectedIndex } from "../../../components/data-table/table-pane";
|
|
30
33
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
31
34
|
import { useBoundTicker as useSymbolBinding } from "../shared/ticker-request";
|
|
@@ -35,6 +38,10 @@ import { quoteFreshnessFields } from "../shared/report-freshness";
|
|
|
35
38
|
import { RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE, formatRelativePrice, relativeValuationValues, withLiveQuote } from "./relative-valuation-model";
|
|
36
39
|
import { conversionNote, conversionPairText, convertMarketCaps, usdRateFromEntry } from "./relative-valuation-currency";
|
|
37
40
|
import { fundamentalsBasisParts, fundamentalsThroughText } from "./relative-valuation-basis";
|
|
41
|
+
import {
|
|
42
|
+
AUTO_PEERS_SETTING, findPeers, PEER_SELECTION_SETTING, PeerPickError, peerSelectionOf, relativeValuationTitle,
|
|
43
|
+
} from "./relative-valuation-peers";
|
|
44
|
+
import { relativeSymbolsFromPane } from "./relative-valuation-settings";
|
|
38
45
|
|
|
39
46
|
type ExtendedColumnId = "preMarket" | "afterHours" | "overnight";
|
|
40
47
|
type RelativeColumnId = "symbol" | "price" | "changePercent" | ExtendedColumnId | "marketCap" | "trailingPE" | "forwardPE" | "evSales" | "fcfYield" | "revenueGrowth" | "operatingMargin";
|
|
@@ -44,14 +51,6 @@ type RelativeRow = ReturnType<typeof relativeValuationValues> & {
|
|
|
44
51
|
error?: string;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
|
-
function relativeSymbolsFromPane(symbol: string | null, paneSettings: Record<string, unknown> | undefined): string[] {
|
|
48
|
-
const settingsSymbols = Array.isArray(paneSettings?.symbols)
|
|
49
|
-
? paneSettings.symbols.filter((value): value is string => typeof value === "string")
|
|
50
|
-
: [];
|
|
51
|
-
if (settingsSymbols.length > 0) return settingsSymbols;
|
|
52
|
-
return symbol ? [symbol] : [];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
54
|
const EXTENDED_COLUMN_IDS: Record<ExtendedSession, ExtendedColumnId> = { PRE: "preMarket", POST: "afterHours", OVERNIGHT: "overnight" };
|
|
56
55
|
|
|
57
56
|
/** The latest quarter's revenue against the same quarter a year earlier; the footer names the quarter. */
|
|
@@ -97,7 +96,7 @@ function extendedMove(row: RelativeRow, columnId: ExtendedColumnId): number | nu
|
|
|
97
96
|
* AFTER-HRS or OVERNIGHT column, named as BI and MOST name it, follows CHG%
|
|
98
97
|
* for each session in `extendedSessions`.
|
|
99
98
|
*/
|
|
100
|
-
function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[]): RelativeColumn[] {
|
|
99
|
+
function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[], marginNamesYear: boolean): RelativeColumn[] {
|
|
101
100
|
const symbolWidth = 8;
|
|
102
101
|
const priceWidth = 10;
|
|
103
102
|
const pctWidth = 8;
|
|
@@ -117,7 +116,8 @@ function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly E
|
|
|
117
116
|
{ id: "fcfYield", label: "FCF%", width: metricWidth, align: "right" },
|
|
118
117
|
// Latest quarter against the year before, and a trailing-twelve-month margin.
|
|
119
118
|
{ id: "revenueGrowth", label: REVENUE_GROWTH_LABEL, width: metricWidth, align: "right" },
|
|
120
|
-
|
|
119
|
+
// Wider while a peer's margin is of its latest fiscal year and names it ("16.83% FY25").
|
|
120
|
+
{ id: "operatingMargin", label: "TTM OP%", width: marginNamesYear ? 12 : metricWidth, align: "right" },
|
|
121
121
|
];
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -150,6 +150,8 @@ function sortRelativeRowIndices(
|
|
|
150
150
|
.map((entry) => entry.index);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const NO_SYMBOLS: string[] = [];
|
|
154
|
+
|
|
153
155
|
/** Live values keep moving; the row order follows them at most this often. */
|
|
154
156
|
const RELATIVE_ORDER_SAMPLE_MS = 5_000;
|
|
155
157
|
|
|
@@ -169,7 +171,37 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
169
171
|
// Keyed on the list itself, so a settings write that leaves the peers alone
|
|
170
172
|
// does not start the snapshot over.
|
|
171
173
|
const symbolList = relativeSymbolsFromPane(symbol, pane?.settings).join(",");
|
|
172
|
-
const
|
|
174
|
+
const listed = useMemo(() => (symbolList ? symbolList.split(",") : []), [symbolList]);
|
|
175
|
+
const settings = pane?.settings;
|
|
176
|
+
// A pane opened on one ticker compares it with the peers `fn RV <ticker> --peers` picks, by the same
|
|
177
|
+
// method, and keeps them in its settings with how they were chosen.
|
|
178
|
+
const peerSelection = useMemo(() => peerSelectionOf(settings?.[PEER_SELECTION_SETTING], listed), [listed, settings]);
|
|
179
|
+
const peerSubject = settings?.[AUTO_PEERS_SETTING] === true && listed.length === 1 && !peerSelection ? listed[0]! : null;
|
|
180
|
+
const dataProvider = useAssetData();
|
|
181
|
+
const updateSettings = useUpdatePaneSettings();
|
|
182
|
+
const [peerAttempt, setPeerAttempt] = useState(0);
|
|
183
|
+
const [peerFailure, setPeerFailure] = useState<{ subject: string; message: string } | null>(null);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!peerSubject || !dataProvider) return;
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
setPeerFailure(null);
|
|
188
|
+
findPeers(peerSubject, { marketData: dataProvider, apiClient, signal: controller.signal }).then((selection) => {
|
|
189
|
+
if (controller.signal.aborted) return;
|
|
190
|
+
const compared = [selection.subject, ...selection.peers];
|
|
191
|
+
updateSettings({ symbols: compared, symbolsText: formatTickerListInput(compared), [PEER_SELECTION_SETTING]: selection });
|
|
192
|
+
}, (failure: unknown) => {
|
|
193
|
+
if (controller.signal.aborted) return;
|
|
194
|
+
setPeerFailure({ subject: peerSubject, message: failure instanceof PeerPickError ? failure.reason : `No peers: ${errorMessage(failure)}` });
|
|
195
|
+
});
|
|
196
|
+
return () => controller.abort();
|
|
197
|
+
}, [dataProvider, peerAttempt, peerSubject, updateSettings]);
|
|
198
|
+
const peerError = peerSubject && peerFailure?.subject === peerSubject ? peerFailure.message : null;
|
|
199
|
+
// The table waits for the peers, so it fills once; without any it shows the ticker alone.
|
|
200
|
+
const pickingPeers = peerSubject != null && peerError == null;
|
|
201
|
+
const symbols = pickingPeers ? NO_SYMBOLS : listed;
|
|
202
|
+
// The title says how the peers were chosen, or lists the tickers; a title the user gave the pane stays.
|
|
203
|
+
const ownsTitle = /^RV(?:\s|$)/.test(pane?.title ?? "");
|
|
204
|
+
usePaneTitle(ownsTitle && listed.length > 0 ? relativeValuationTitle(listed, peerSelection) : null);
|
|
173
205
|
const { navigateTicker } = usePluginTickerActions();
|
|
174
206
|
const loadPeers = useCallback(async (forceRefresh: boolean): Promise<PeerSnapshot[]> => {
|
|
175
207
|
const coordinator = getSharedMarketDataCoordinator();
|
|
@@ -188,8 +220,14 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
188
220
|
// Fundamentals come from one batched snapshot; prices stream on top of it.
|
|
189
221
|
// A changed peer list keeps the last snapshot up until its own answer.
|
|
190
222
|
const peerSnapshot = useAsyncResource(symbols.length > 0 ? loadPeers : null, { keepPreviousData: true });
|
|
191
|
-
const { data: peers,
|
|
192
|
-
const
|
|
223
|
+
const { data: peers, updatedAt, reload: reloadSnapshot } = peerSnapshot;
|
|
224
|
+
const loading = peerSnapshot.loading || pickingPeers;
|
|
225
|
+
const error = symbols.length > 0 ? peerSnapshot.error : pickingPeers ? null : "No tickers selected";
|
|
226
|
+
// Refreshing asks for the peers again when picking them failed.
|
|
227
|
+
const reload = useCallback(() => {
|
|
228
|
+
if (peerError) setPeerAttempt((attempt) => attempt + 1);
|
|
229
|
+
return reloadSnapshot();
|
|
230
|
+
}, [peerError, reloadSnapshot]);
|
|
193
231
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
194
232
|
const [sortPreference, setSortPreference] = useState<RelativeSortPreference>(DEFAULT_RELATIVE_SORT);
|
|
195
233
|
const appBaseCurrency = useAppSelector((state) => state.config.baseCurrency);
|
|
@@ -217,9 +255,10 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
217
255
|
const fxRates = useFxRatesMap([baseCurrency, ...rows.map((row) => row.marketCapCurrency)]);
|
|
218
256
|
// A pre-market or after-hours print gets its own column when a peer has one.
|
|
219
257
|
const extendedSessionsKey = extendedSessionsOf(rows).join(",");
|
|
258
|
+
const marginNamesYear = rows.some((row) => row.operatingMarginPeriod != null);
|
|
220
259
|
const columns = useMemo(
|
|
221
|
-
() => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
|
|
222
|
-
[baseCurrency, extendedSessionsKey],
|
|
260
|
+
() => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], marginNamesYear),
|
|
261
|
+
[baseCurrency, extendedSessionsKey, marginNamesYear],
|
|
223
262
|
);
|
|
224
263
|
|
|
225
264
|
const comparableRows = useMemo(() => rows.map((row) => ({
|
|
@@ -272,7 +311,7 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
272
311
|
|
|
273
312
|
const staleSymbols = rows.filter((row) => row.quoteStale).map((row) => row.symbol);
|
|
274
313
|
const rowErrors = rows.filter((row) => row.error).map((row) => `${row.symbol}: ${row.error}`);
|
|
275
|
-
const status = [error, ...rowErrors, staleSymbols.length ? `Stale quotes: ${staleSymbols.join(", ")}` : null,
|
|
314
|
+
const status = [error, peerError, ...rowErrors, staleSymbols.length ? `Stale quotes: ${staleSymbols.join(", ")}` : null,
|
|
276
315
|
missingFx ? "Market-cap FX unavailable" : null].filter(Boolean).join(" · ") || null;
|
|
277
316
|
|
|
278
317
|
usePaneNoticeFooter({
|
|
@@ -319,7 +358,7 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
319
358
|
case "revenueGrowth":
|
|
320
359
|
return { text: formatPercent(row.revenueGrowth ?? undefined), color: shownPriceColor((row.revenueGrowth ?? 0) * 100, 2) };
|
|
321
360
|
case "operatingMargin":
|
|
322
|
-
return { text: formatLevelPercent(row.operatingMargin ?? undefined)
|
|
361
|
+
return { text: `${formatLevelPercent(row.operatingMargin ?? undefined)}${row.operatingMargin != null && row.operatingMarginPeriod ? ` ${row.operatingMarginPeriod}` : ""}`, color: colors.text };
|
|
323
362
|
}
|
|
324
363
|
}, []);
|
|
325
364
|
|
|
@@ -3,21 +3,58 @@ import type { HeadlessPaneContext } from "../../../types/headless";
|
|
|
3
3
|
import type { TickerFinancials } from "../../../types/financials";
|
|
4
4
|
import { isUsListingExchange, parsePublicTickerKey } from "../../../utils/exchanges";
|
|
5
5
|
import { errorMessage } from "../../../utils/errors";
|
|
6
|
+
import { isRecord } from "../../../utils/guards";
|
|
7
|
+
import { formatTickerListInput } from "../../../tickers/list";
|
|
6
8
|
import { selectMarketCapitalization } from "../../../utils/market-capitalization";
|
|
9
|
+
import { notATickerSymbol } from "../../../sources/provider-errors";
|
|
7
10
|
import { fetchScreen } from "../equity-screener/client";
|
|
11
|
+
import { loadHeadlessFinancials } from "../shared/headless-market-data";
|
|
8
12
|
import { loadFxLegs } from "./relative-valuation-currency";
|
|
9
13
|
|
|
14
|
+
/** The pane setting holding the peers chosen for its subject (`PeerSelection`), which the report reads back too. */
|
|
15
|
+
export const PEER_SELECTION_SETTING = "peerSelection";
|
|
16
|
+
/** The pane setting `--peers` sets, and a pane opened on one ticker: pick that ticker's peers. */
|
|
17
|
+
export const AUTO_PEERS_SETTING = "autoPeers";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What picking peers reads through: the subject's financials and FX rates
|
|
21
|
+
* from the market data client, the covered equities from the screener. The
|
|
22
|
+
* report passes its own context, the pane the app's clients, so both pick
|
|
23
|
+
* the same peers.
|
|
24
|
+
*/
|
|
25
|
+
export type PeerPickContext = Pick<HeadlessPaneContext, "signal" | "marketData" | "apiClient" | "resolveInstrument">;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* No peers could be picked. The message is the report's, which names
|
|
29
|
+
* `--peers` and the command that lists peers instead; `reason` is the pane's,
|
|
30
|
+
* which says only why.
|
|
31
|
+
*/
|
|
32
|
+
export class PeerPickError extends Error {
|
|
33
|
+
constructor(readonly reason: string, reportReason: string, symbol: string) {
|
|
34
|
+
super(`${reportReason} List them yourself: gloom fn RV ${symbol} <tickers>.`);
|
|
35
|
+
this.name = "PeerPickError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
/** How many peers `--peers` adds at most, and the fewest it settles for before it falls back. */
|
|
11
40
|
const MOST_PEERS = 6;
|
|
12
41
|
const FEWEST_PEERS = 4;
|
|
42
|
+
/**
|
|
43
|
+
* A peer of comparable size is at most this many times smaller than the
|
|
44
|
+
* subject: an airline keeps its smaller airlines, a $5T company is not shown
|
|
45
|
+
* beside $2M ones. A larger peer always counts; a small company is compared
|
|
46
|
+
* with its industry's leaders.
|
|
47
|
+
*/
|
|
48
|
+
const COMPARABLE_CAP_FACTOR = 100;
|
|
13
49
|
|
|
14
50
|
/**
|
|
15
51
|
* How the peers were picked: the subject's own industry, the industry its
|
|
16
|
-
* business belongs to when its profile files it under another,
|
|
52
|
+
* business belongs to when its profile files it under another, its
|
|
53
|
+
* industry's peers of comparable size and then its sector's, a curated
|
|
17
54
|
* group's core first (the subject is one of that core) and its industry
|
|
18
55
|
* after, or a curated list when too few covered equities can be read.
|
|
19
56
|
*/
|
|
20
|
-
type PeerMethod = "industry" | "related-industry" | "curated-core" | "curated";
|
|
57
|
+
type PeerMethod = "industry" | "related-industry" | "sector" | "curated-core" | "curated";
|
|
21
58
|
|
|
22
59
|
/** The peers `--peers` added to one symbol, and how; the pane and the report both read it. */
|
|
23
60
|
export interface PeerSelection {
|
|
@@ -32,6 +69,10 @@ export interface PeerSelection {
|
|
|
32
69
|
curatedPeers?: string[];
|
|
33
70
|
/** The curated group, as the header and the note name it (`online learning`). */
|
|
34
71
|
curatedGroup?: string;
|
|
72
|
+
/** The sector the rest of the peers come from, when too few in `industry` are of comparable size. */
|
|
73
|
+
sector?: string;
|
|
74
|
+
/** The peers of comparable size taken from `industry`, first in `peers`; the rest came from `sector`. */
|
|
75
|
+
industryPeers?: string[];
|
|
35
76
|
/** How the peers were chosen, read after "Peers chosen automatically: ". */
|
|
36
77
|
basis: string;
|
|
37
78
|
}
|
|
@@ -81,6 +122,8 @@ export interface PeerCandidate {
|
|
|
81
122
|
marketCapUsd: number | null;
|
|
82
123
|
/** It reports revenue figures (growth or a margin): a shell or a treasury vehicle filed in an industry does not. */
|
|
83
124
|
reportsOperations: boolean;
|
|
125
|
+
/** Its sector, as the screener names it. */
|
|
126
|
+
sector?: string | null;
|
|
84
127
|
}
|
|
85
128
|
|
|
86
129
|
/** Where the subject's peers come from: its own industry, or the related one its business belongs to. */
|
|
@@ -157,10 +200,76 @@ function listText(items: readonly string[]): string {
|
|
|
157
200
|
return items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
158
201
|
}
|
|
159
202
|
|
|
160
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* The covered equities the screener returned for a subject's group: its
|
|
205
|
+
* industry's, its curated core's, and its sector's of comparable size when
|
|
206
|
+
* the industry has too few.
|
|
207
|
+
*/
|
|
161
208
|
export interface PeerCandidates {
|
|
162
209
|
industry: readonly PeerCandidate[];
|
|
163
210
|
core: readonly PeerCandidate[];
|
|
211
|
+
sector?: { name: string; candidates: readonly PeerCandidate[] } | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** At least a hundredth of the subject's market cap. */
|
|
215
|
+
function comparableCap(subjectCap: number | null, cap: number | null): boolean {
|
|
216
|
+
return subjectCap != null && subjectCap > 0 && cap != null && cap > 0 && cap * COMPARABLE_CAP_FACTOR >= subjectCap;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function comparablePeers(
|
|
220
|
+
subject: Pick<PeerSubject, "symbol" | "marketCapUsd">,
|
|
221
|
+
candidates: readonly PeerCandidate[],
|
|
222
|
+
options?: { limit?: number; exclude?: readonly string[] },
|
|
223
|
+
): string[] {
|
|
224
|
+
return rankPeerCandidates(subject, candidates.filter((candidate) => comparableCap(subject.marketCapUsd, candidate.marketCapUsd)), options);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A subject of known size whose own industry (no curated group) has fewer
|
|
229
|
+
* than four covered equities of comparable size: a megacap among micro-caps,
|
|
230
|
+
* or a small industry. Its sector is asked for the rest.
|
|
231
|
+
*/
|
|
232
|
+
function needsSectorPeers(subject: PeerSubject, group: PeerGroup, industry: readonly PeerCandidate[]): boolean {
|
|
233
|
+
return !group.related && subject.marketCapUsd != null && subject.marketCapUsd > 0
|
|
234
|
+
&& comparablePeers(subject, industry).length < FEWEST_PEERS;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The sector to widen to, as the screener files the subject, else as it files
|
|
239
|
+
* most of the subject's industry, else as the subject's profile names it.
|
|
240
|
+
*/
|
|
241
|
+
function peerSectorOf(subject: Pick<PeerSubject, "symbol">, industry: readonly PeerCandidate[], profileSector?: string | null): string | null {
|
|
242
|
+
const own = industry.find((candidate) => bareSymbol(candidate.symbol) === bareSymbol(subject.symbol))?.sector?.trim();
|
|
243
|
+
if (own) return own;
|
|
244
|
+
const counts = new Map<string, number>();
|
|
245
|
+
for (const candidate of industry) {
|
|
246
|
+
const sector = candidate.sector?.trim();
|
|
247
|
+
if (sector) counts.set(sector, (counts.get(sector) ?? 0) + 1);
|
|
248
|
+
}
|
|
249
|
+
return [...counts].sort((left, right) => right[1] - left[1])[0]?.[0] ?? (profileSector?.trim() || null);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The industry's peers of comparable size first, then the sector's closest in
|
|
254
|
+
* market cap, also of comparable size, to six. Null when industry and sector
|
|
255
|
+
* together still have fewer than four: the industry's closest stand, as they
|
|
256
|
+
* would without a sector.
|
|
257
|
+
*/
|
|
258
|
+
function sectorSelection(subject: PeerSubject, group: PeerGroup, sector: PeerCandidates["sector"], industryPeers: string[]): PeerSelection | null {
|
|
259
|
+
if (!sector) return null;
|
|
260
|
+
const fill = comparablePeers(subject, sector.candidates, { limit: MOST_PEERS - industryPeers.length, exclude: industryPeers });
|
|
261
|
+
const peers = [...industryPeers, ...fill];
|
|
262
|
+
if (fill.length === 0 || peers.length < FEWEST_PEERS) return null;
|
|
263
|
+
const count = (names: readonly string[]) => (names.length === 1 ? "the covered equity" : `the ${names.length} covered equities`);
|
|
264
|
+
const fromSector = `${count(fill)} in the ${sector.name} sector closest to ${subject.symbol} in market cap`;
|
|
265
|
+
return {
|
|
266
|
+
subject: subject.symbol, peers, method: "sector", industry: group.industry, subjectIndustry: subject.industry,
|
|
267
|
+
sector: sector.name, industryPeers,
|
|
268
|
+
basis: industryPeers.length === 0
|
|
269
|
+
? `${fromSector}, as no other covered equity in ${group.industry} has a hundredth of its market cap`
|
|
270
|
+
: `${listText(industryPeers)}, ${count(industryPeers)} in ${group.industry} of at least a hundredth of ${subject.symbol}'s market cap; `
|
|
271
|
+
+ `${listText(fill)}, ${fromSector}`,
|
|
272
|
+
};
|
|
164
273
|
}
|
|
165
274
|
|
|
166
275
|
/**
|
|
@@ -168,7 +277,10 @@ export interface PeerCandidates {
|
|
|
168
277
|
* curated core is compared with the rest of that core first, closest in
|
|
169
278
|
* market cap, and the industry's closest fill the table to six. Any other
|
|
170
279
|
* subject takes the closest of its own industry, or of the related industry
|
|
171
|
-
* its business belongs to with that core among the candidates.
|
|
280
|
+
* its business belongs to with that core among the candidates. Its own
|
|
281
|
+
* industry's are the ones of at least a hundredth of its market cap; with
|
|
282
|
+
* fewer than four of those and the sector read, they come first and the
|
|
283
|
+
* sector's closest of comparable size fill the table. Fewer than
|
|
172
284
|
* four, or no candidates at all (`candidates` null: the screener did not
|
|
173
285
|
* answer), falls back to the related industry's curated core when it has
|
|
174
286
|
* one; otherwise what was found, or null when nothing was.
|
|
@@ -177,10 +289,15 @@ export function selectPeers(subject: PeerSubject, group: PeerGroup, candidates:
|
|
|
177
289
|
const symbol = subject.symbol;
|
|
178
290
|
const related = group.related;
|
|
179
291
|
const curatedCore = group.inCore && related ? (candidates ? rankPeerCandidates(subject, candidates.core) : []) : [];
|
|
292
|
+
// Its own industry's peers of comparable size; too few, and the sector's fill in when it was read.
|
|
293
|
+
const comparable = candidates && !related ? comparablePeers(subject, candidates.industry) : [];
|
|
294
|
+
const widened = candidates && !related && comparable.length < FEWEST_PEERS ? sectorSelection(subject, group, candidates.sector, comparable) : null;
|
|
295
|
+
if (widened) return widened;
|
|
180
296
|
const fill = candidates
|
|
181
297
|
? group.inCore
|
|
182
298
|
? rankPeerCandidates(subject, candidates.industry, { limit: MOST_PEERS - curatedCore.length, exclude: curatedCore })
|
|
183
|
-
:
|
|
299
|
+
: comparable.length >= FEWEST_PEERS ? comparable
|
|
300
|
+
: rankPeerCandidates(subject, [...candidates.industry, ...related ? candidates.core : []])
|
|
184
301
|
: [];
|
|
185
302
|
const peers = [...curatedCore, ...fill];
|
|
186
303
|
if (peers.length < FEWEST_PEERS && related) {
|
|
@@ -225,6 +342,10 @@ export function peerMethodTitle(selection: PeerSelection, { automatic = false }:
|
|
|
225
342
|
return `${peers} by industry: ${selection.industry}`;
|
|
226
343
|
case "related-industry":
|
|
227
344
|
return `${peers} by related industry: ${selection.industry}`;
|
|
345
|
+
case "sector":
|
|
346
|
+
return selection.industryPeers?.length
|
|
347
|
+
? `${peers} by industry: ${selection.industry}, then sector: ${selection.sector}`
|
|
348
|
+
: `${peers} by sector: ${selection.sector}`;
|
|
228
349
|
case "curated-core":
|
|
229
350
|
return selection.peers.length > (selection.curatedPeers?.length ?? 0)
|
|
230
351
|
? `${peers}: ${selection.curatedGroup} curated core, then ${selection.industry}`
|
|
@@ -234,6 +355,17 @@ export function peerMethodTitle(selection: PeerSelection, { automatic = false }:
|
|
|
234
355
|
}
|
|
235
356
|
}
|
|
236
357
|
|
|
358
|
+
/**
|
|
359
|
+
* An RV pane's title: the subject and how its peers were chosen
|
|
360
|
+
* (`RV AAPL + auto peers by industry: Consumer Electronics`), as a capture
|
|
361
|
+
* of `--peers` is titled, or the tickers as listed.
|
|
362
|
+
*/
|
|
363
|
+
export function relativeValuationTitle(symbols: readonly string[], selection: PeerSelection | null): string {
|
|
364
|
+
return selection
|
|
365
|
+
? `RV ${selection.subject} + ${peerMethodTitle(selection, { automatic: true })}`
|
|
366
|
+
: `RV ${formatTickerListInput([...symbols])}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
237
369
|
function screenSymbol(row: ScreenRow): string {
|
|
238
370
|
return isUsListingExchange(row.exchange) ? row.symbol : `${row.symbol}:${row.exchange}`;
|
|
239
371
|
}
|
|
@@ -245,16 +377,36 @@ function candidateOf(row: ScreenRow): PeerCandidate {
|
|
|
245
377
|
marketCapUsd: metric("marketCap"),
|
|
246
378
|
reportsOperations: metric("revenueGrowthPercent") != null || metric("grossMarginPercent") != null
|
|
247
379
|
|| metric("operatingMarginPercent") != null,
|
|
380
|
+
sector: row.sector,
|
|
248
381
|
};
|
|
249
382
|
}
|
|
250
383
|
|
|
251
|
-
async function screenRows(
|
|
252
|
-
|
|
253
|
-
|
|
384
|
+
async function screenRows(
|
|
385
|
+
criteria: ScreenDefinition["criteria"],
|
|
386
|
+
ctx: PeerPickContext,
|
|
387
|
+
{ direction = "desc", limit }: { direction?: "asc" | "desc"; limit?: number } = {},
|
|
388
|
+
): Promise<ScreenRow[]> {
|
|
389
|
+
const definition: ScreenDefinition = { version: 1, criteria, sort: { field: "marketCap", direction }, currency: "USD" };
|
|
390
|
+
return (await fetchScreen(definition, null, ctx.signal, ctx.apiClient, limit)).rows;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The sector's covered equities of comparable size nearest the subject's cap:
|
|
395
|
+
* the next larger and the next smaller, asked apart so a crowded sector
|
|
396
|
+
* cannot crowd out the closest.
|
|
397
|
+
*/
|
|
398
|
+
async function sectorCandidates(sector: string, capUsd: number, ctx: PeerPickContext): Promise<PeerCandidate[]> {
|
|
399
|
+
const inSector: ScreenDefinition["criteria"][number] = { field: "sector", op: "in", value: [sector] };
|
|
400
|
+
const [larger, smaller] = await Promise.all([
|
|
401
|
+
screenRows([inSector, { field: "marketCap", op: "gte", value: Math.floor(capUsd) }], ctx, { direction: "asc", limit: 25 }),
|
|
402
|
+
screenRows([inSector, { field: "marketCap", op: "between", value: [Math.floor(capUsd / COMPARABLE_CAP_FACTOR), Math.ceil(capUsd)] }],
|
|
403
|
+
ctx, { direction: "desc", limit: 25 }),
|
|
404
|
+
]);
|
|
405
|
+
return [...larger, ...smaller].map(candidateOf);
|
|
254
406
|
}
|
|
255
407
|
|
|
256
408
|
/** The subject's market cap in US dollars, through the app's FX rates when it is quoted in another currency. */
|
|
257
|
-
async function subjectCapUsd(financials: TickerFinancials | null, ctx:
|
|
409
|
+
async function subjectCapUsd(financials: TickerFinancials | null, ctx: PeerPickContext): Promise<number | null> {
|
|
258
410
|
const cap = selectMarketCapitalization(financials?.quote, financials?.fundamentals);
|
|
259
411
|
if (!cap) return null;
|
|
260
412
|
if (cap.currency === "USD") return cap.value;
|
|
@@ -266,27 +418,29 @@ async function subjectCapUsd(financials: TickerFinancials | null, ctx: HeadlessP
|
|
|
266
418
|
/**
|
|
267
419
|
* Picks the peers of `symbol` for `--peers`: its group from its symbol or
|
|
268
420
|
* profile, the group's covered equities from the screener (its industry, and
|
|
269
|
-
* a related industry's curated core
|
|
270
|
-
*
|
|
271
|
-
*
|
|
421
|
+
* a related industry's curated core, or its sector's of comparable size when
|
|
422
|
+
* the industry has too few), ranked as `selectPeers` says. Throws a
|
|
423
|
+
* `PeerPickError` when nothing places the subject, or nothing can be found.
|
|
272
424
|
*/
|
|
273
|
-
export async function pickPeers(symbol: string, financials: TickerFinancials | null, ctx:
|
|
425
|
+
export async function pickPeers(symbol: string, financials: TickerFinancials | null, ctx: PeerPickContext): Promise<PeerSelection> {
|
|
274
426
|
const subject: PeerSubject = {
|
|
275
427
|
symbol,
|
|
276
428
|
industry: financials?.profile?.industry?.trim() || null,
|
|
277
429
|
description: financials?.profile?.description ?? null,
|
|
278
430
|
marketCapUsd: null,
|
|
279
431
|
};
|
|
280
|
-
const listYourOwn = `List them yourself: gloom fn RV ${symbol} <tickers>.`;
|
|
281
432
|
const group = peerGroupOf(subject);
|
|
282
|
-
if (!group)
|
|
433
|
+
if (!group) {
|
|
434
|
+
throw new PeerPickError(`No peers: ${symbol}'s profile names no industry.`,
|
|
435
|
+
`${symbol}'s profile names no industry, so --peers cannot pick its peers.`, symbol);
|
|
436
|
+
}
|
|
283
437
|
let candidates: PeerCandidates | null = null;
|
|
284
438
|
let failure: string | null = null;
|
|
285
439
|
try {
|
|
286
440
|
const [capUsd, industryRows, coreRows] = await Promise.all([
|
|
287
441
|
subjectCapUsd(financials, ctx).catch(() => null),
|
|
288
|
-
screenRows({ field: "industry", op: "in", value: [group.industry] }, ctx),
|
|
289
|
-
group.related ? screenRows({ field: "symbol", op: "in", value: [...group.related.core] }, ctx) : Promise.resolve([]),
|
|
442
|
+
screenRows([{ field: "industry", op: "in", value: [group.industry] }], ctx),
|
|
443
|
+
group.related ? screenRows([{ field: "symbol", op: "in", value: [...group.related.core] }], ctx) : Promise.resolve([]),
|
|
290
444
|
]);
|
|
291
445
|
subject.marketCapUsd = capUsd;
|
|
292
446
|
candidates = { industry: industryRows.map(candidateOf), core: coreRows.map(candidateOf) };
|
|
@@ -294,9 +448,52 @@ export async function pickPeers(symbol: string, financials: TickerFinancials | n
|
|
|
294
448
|
ctx.signal.throwIfAborted();
|
|
295
449
|
failure = errorMessage(error);
|
|
296
450
|
}
|
|
451
|
+
// A megacap among micro-caps: the sector's companies of comparable size. Without them the industry's stand.
|
|
452
|
+
const sectorName = candidates && needsSectorPeers(subject, group, candidates.industry)
|
|
453
|
+
? peerSectorOf(subject, candidates.industry, financials?.profile?.sector) : null;
|
|
454
|
+
if (candidates && sectorName) {
|
|
455
|
+
const sector = await sectorCandidates(sectorName, subject.marketCapUsd!, ctx).catch(() => {
|
|
456
|
+
ctx.signal.throwIfAborted();
|
|
457
|
+
return null;
|
|
458
|
+
});
|
|
459
|
+
if (sector) candidates = { ...candidates, sector: { name: sectorName, candidates: sector } };
|
|
460
|
+
}
|
|
297
461
|
const selection = selectPeers(subject, group, candidates);
|
|
298
462
|
if (selection) return selection;
|
|
299
|
-
throw
|
|
300
|
-
?
|
|
301
|
-
|
|
463
|
+
throw failure
|
|
464
|
+
? new PeerPickError(`No peers: the covered equities in ${group.industry} could not be read (${failure}).`,
|
|
465
|
+
`--peers could not read the covered equities in ${group.industry}: ${failure}`, symbol)
|
|
466
|
+
: new PeerPickError(`No peers: no other covered equity is in ${group.industry}.`,
|
|
467
|
+
`No other covered equity is in ${group.industry}, so --peers found no peers for ${symbol}.`, symbol);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The peers of one symbol, as `fn RV <symbol> --peers` and the RV pane opened
|
|
472
|
+
* on one ticker both pick them: its financials for its industry, profile and
|
|
473
|
+
* market cap, then `pickPeers`. A word that is no ticker fails as such; any
|
|
474
|
+
* other failure to read the financials leaves the peers to what the symbol
|
|
475
|
+
* alone places (a curated core's member).
|
|
476
|
+
*/
|
|
477
|
+
export async function findPeers(symbol: string, ctx: PeerPickContext): Promise<PeerSelection> {
|
|
478
|
+
const financials = await loadHeadlessFinancials(ctx, symbol).catch((error: unknown) => {
|
|
479
|
+
ctx.signal.throwIfAborted();
|
|
480
|
+
// A word that is no ticker is said as `fn RV duolingo lrn` says it, which the CLI follows with the search to run.
|
|
481
|
+
if (notATickerSymbol(errorMessage(error))) throw error;
|
|
482
|
+
return null;
|
|
483
|
+
});
|
|
484
|
+
return pickPeers(symbol, financials, ctx);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The peers chosen for a list, as the expansion or the pane left them in its
|
|
489
|
+
* settings: null unless `symbols` is still the subject followed by those
|
|
490
|
+
* peers, in order.
|
|
491
|
+
*/
|
|
492
|
+
export function peerSelectionOf(value: unknown, symbols: readonly string[]): PeerSelection | null {
|
|
493
|
+
if (!isRecord(value) || typeof value.subject !== "string" || !Array.isArray(value.peers)
|
|
494
|
+
|| typeof value.method !== "string" || typeof value.basis !== "string") return null;
|
|
495
|
+
const selection = value as unknown as PeerSelection;
|
|
496
|
+
const expected = [selection.subject, ...selection.peers];
|
|
497
|
+
return expected.length === symbols.length && expected.every((symbol, index) => symbol.toUpperCase() === symbols[index]?.toUpperCase())
|
|
498
|
+
? selection : null;
|
|
302
499
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PaneSettingsContext, PaneSettingsDef } from "../../../types/plugin";
|
|
2
|
+
import { formatTickerListInput, parseTickerListInput } from "../../../tickers/list";
|
|
3
|
+
import { AUTO_PEERS_SETTING, PEER_SELECTION_SETTING, peerSelectionOf } from "./relative-valuation-peers";
|
|
4
|
+
|
|
5
|
+
/** The most tickers RV compares, as it reads its argument. */
|
|
6
|
+
export const MAX_RELATIVE_VALUATION_TICKERS = 12;
|
|
7
|
+
|
|
8
|
+
/** The tickers an RV pane compares: the list in its settings, else the ticker it is bound to. */
|
|
9
|
+
export function relativeSymbolsFromPane(symbol: string | null, paneSettings: Record<string, unknown> | undefined): string[] {
|
|
10
|
+
const settingsSymbols = Array.isArray(paneSettings?.symbols)
|
|
11
|
+
? paneSettings.symbols.filter((value): value is string => typeof value === "string")
|
|
12
|
+
: [];
|
|
13
|
+
if (settingsSymbols.length > 0) return settingsSymbols;
|
|
14
|
+
return symbol ? [symbol] : [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The pane's Tickers setting: the list it compares, with how its peers were
|
|
19
|
+
* chosen while they are the ones picked. A list typed there is compared as
|
|
20
|
+
* typed, one ticker alone included.
|
|
21
|
+
*/
|
|
22
|
+
export function buildRelativeValuationSettingsDef(context: PaneSettingsContext): PaneSettingsDef {
|
|
23
|
+
const symbols = relativeSymbolsFromPane(null, context.settings);
|
|
24
|
+
const selection = peerSelectionOf(context.settings[PEER_SELECTION_SETTING], symbols);
|
|
25
|
+
return {
|
|
26
|
+
title: "Relative Valuation Settings",
|
|
27
|
+
values: { symbolsText: formatTickerListInput(symbols) },
|
|
28
|
+
fields: [{
|
|
29
|
+
key: "symbolsText",
|
|
30
|
+
label: "Tickers",
|
|
31
|
+
description: selection
|
|
32
|
+
? `Peers chosen automatically: ${selection.basis}. Edit the list to compare your own.`
|
|
33
|
+
: `Enter up to ${MAX_RELATIVE_VALUATION_TICKERS} tickers separated by commas.`,
|
|
34
|
+
type: "text",
|
|
35
|
+
placeholder: "AAPL, MSFT, GOOGL",
|
|
36
|
+
}],
|
|
37
|
+
applyValue: (settings, field, value) => {
|
|
38
|
+
if (field.key !== "symbolsText") return { ...settings, [field.key]: value };
|
|
39
|
+
const next = parseTickerListInput(typeof value === "string" ? value : "", MAX_RELATIVE_VALUATION_TICKERS);
|
|
40
|
+
const { [PEER_SELECTION_SETTING]: _picked, ...rest } = settings;
|
|
41
|
+
return { ...rest, symbols: next, symbolsText: formatTickerListInput(next), [AUTO_PEERS_SETTING]: false };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -4,7 +4,8 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
|
4
4
|
import { formatCompactCurrency } from "../../../utils/format";
|
|
5
5
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
6
6
|
import { loadReverseDcfInputs } from "./client";
|
|
7
|
-
import { describeFcfMargin, DISCOUNT_RATES, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS,
|
|
7
|
+
import { describeFcfMargin, DISCOUNT_RATES, pastGrowthBasis, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS, ttmBaseNote,
|
|
8
|
+
type ImpliedGrowth } from "./model";
|
|
8
9
|
|
|
9
10
|
const percent = (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--";
|
|
10
11
|
/** An assumption as it is set: 9%, 2.5%. */
|
|
@@ -35,6 +36,7 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
35
36
|
const quote = inputs.financials?.quote;
|
|
36
37
|
// The enterprise value is measured at the regular session price, so the report is dated by it: after the close, the close.
|
|
37
38
|
const valuedAt = regularSessionValuationTime(quote);
|
|
39
|
+
const notes = [model.enterpriseValueNote, ttmBaseNote(model)].filter((note): note is string => !!note);
|
|
38
40
|
return {
|
|
39
41
|
sections: [
|
|
40
42
|
{ title: "Reverse DCF", columns: [{ key: "label", header: "" }, { key: "value", header: "Value" }], rows: [
|
|
@@ -42,7 +44,7 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
42
44
|
: `${typeof growth === "number" ? percent(growth) : growth} at ${rate(discountRate)} discount, ${rate(TERMINAL_GROWTH)} terminal` },
|
|
43
45
|
{ label: "Implied revenue, year 10", value: model.impliedRevenue == null ? "--"
|
|
44
46
|
: `${formatCompactCurrency(model.impliedRevenue, currency)} at ${describeFcfMargin(fcfMargin, fcfMarginBasis)}` },
|
|
45
|
-
{ label: "Past FCF growth", value: model.pastGrowth ? `${percent(model.pastGrowth.rate)}
|
|
47
|
+
{ label: "Past FCF growth", value: model.pastGrowth ? `${percent(model.pastGrowth.rate)} ${pastGrowthBasis(model.pastGrowth)}` : "--" },
|
|
46
48
|
{ label: "FCF yield", value: percent(model.fcfYield) },
|
|
47
49
|
{ label: "Enterprise value", value: `${formatCompactCurrency(model.enterpriseValue ?? undefined, currency)}${model.enterpriseValueNote ? " (provider)" : ""}` },
|
|
48
50
|
{ label: "Free cash flow, TTM", value: formatCompactCurrency(model.freeCashFlow ?? undefined, currency) },
|
|
@@ -60,7 +62,7 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
60
62
|
},
|
|
61
63
|
unavailableSymbols: model.implied ? [] : [instrument.symbol],
|
|
62
64
|
errors: [inputs.error, model.error].filter((value): value is string => !!value),
|
|
63
|
-
...
|
|
65
|
+
...notes.length > 0 ? { notes } : {},
|
|
64
66
|
metadata: { currency: model.currency, discountRate, terminalGrowth: TERMINAL_GROWTH, discountRates: DISCOUNT_RATES, fcfMargin,
|
|
65
67
|
fcfMarginSource: fcfMarginBasis.source, companyFcfMargin: fcfMarginBasis.company, fcfMarginGap: fcfMarginBasis.gap,
|
|
66
68
|
impliedRevenue: model.impliedRevenue, stale: inputs.stale, fetchedAt: inputs.fetchedAt, methodology: "docs/research-data.md#reverse-dcf" },
|