@gloom-sh/terminal 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +140 -7
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +17 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +11 -0
- 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/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/types.ts +70 -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/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 +72 -26
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +61 -12
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +55 -34
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +8 -0
- package/src/cli/glossary.ts +25 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +180 -52
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +86 -3
- package/src/cli/pane-functions/function-help.ts +13 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +49 -14
- 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/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/pane-templates/items.ts +3 -2
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- 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 +4 -1
- 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/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/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 +71 -137
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/market-data/depositary-pair.ts +2 -2
- 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/news/aggregator.ts +113 -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 +11 -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/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +336 -77
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- 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 +24 -6
- 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/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/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- 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/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- 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/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -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 +7 -7
- 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/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/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +36 -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 +1 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- 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/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/headless.ts +32 -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 +14 -0
- 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/news/index.tsx +1 -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 +4 -1
- 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/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- 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 +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
- 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/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 +27 -3
- 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 +2 -1
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- 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/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 +3 -2
- 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/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 +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- 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-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 +2 -2
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- 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/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 +12 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- 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/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -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 +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- 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/index.ts +1 -0
- 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.ts +44 -3
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/terminal-codes.ts +19 -0
- 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/financials.ts +32 -1
- package/src/types/headless.ts +21 -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 +44 -0
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- 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/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -109,7 +109,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
|
|
|
109
109
|
const refreshedCurrent = refreshedIv?.owner === ivLoader && refreshedIv.at > (loadedIv.updatedAt ?? 0) ? refreshedIv.data : null;
|
|
110
110
|
const iv = { ...loadedIv, data: refreshedCurrent ?? loadedIv.data };
|
|
111
111
|
ivDataRef.current = iv.data;
|
|
112
|
-
useLiveSessionRefresh(refreshIv, ATM_IV_REFRESH_MS, showIv && spotAvailable && !!iv.data?.reference && !loadedIv.loading,
|
|
112
|
+
const ivRefresh = useLiveSessionRefresh(refreshIv, ATM_IV_REFRESH_MS, showIv && spotAvailable && !!iv.data?.reference && !loadedIv.loading,
|
|
113
113
|
loadedIv.updatedAt);
|
|
114
114
|
const model = useMemo(() => history.data ? projectRealizedVolatility(history.data.history, {
|
|
115
115
|
symbol: history.data.symbol, estimator, windows, lookbackYears: Number(lookback) === 2 ? 2 : 1,
|
|
@@ -117,7 +117,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
|
|
|
117
117
|
const chartInput = useMemo(() => model ? { history: model.history, rolling: model.series, windows: model.windows,
|
|
118
118
|
priceUnit: priceSeriesUnit(quote?.currency ?? "", quote?.instrumentType), iv: showIv ? iv.data?.reference ?? null : null } : null,
|
|
119
119
|
[model, quote?.currency, quote?.instrumentType, showIv, iv.data]);
|
|
120
|
-
const notices = [identityError, history.error, history.data?.error, iv.error, iv.data?.error,
|
|
120
|
+
const notices = [identityError, history.error, history.data?.error, iv.error, ivRefresh.error, iv.data?.error,
|
|
121
121
|
...(model?.warnings ?? []), ...(showIv ? iv.data?.warnings ?? [] : []),
|
|
122
122
|
...(showIv && symbol && !spotAvailable ? ["Current ATM IV unavailable: underlying quote is missing or stale"] : []),
|
|
123
123
|
].filter((value): value is string => !!value);
|
|
@@ -161,7 +161,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
|
|
|
161
161
|
stale: history.data?.stale ?? false, source: history.data?.source ?? null,
|
|
162
162
|
asOf: model?.asOf?.toISOString() ?? null,
|
|
163
163
|
errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []),
|
|
164
|
-
...(showIv ? [iv.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
|
|
164
|
+
...(showIv ? [iv.error, ivRefresh.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
|
|
165
165
|
].filter((value): value is string => !!value))],
|
|
166
166
|
currentIv: showIv && iv.data?.reference ? { value: iv.data.reference.value * 100,
|
|
167
167
|
date: iv.data.reference.date.toISOString(), label: iv.data.reference.label,
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "../../../api-client/types";
|
|
8
8
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
9
9
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
10
|
+
import { quoteTradingExchange } from "../../../market-data/quotes/metadata";
|
|
10
11
|
import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
|
|
11
12
|
import {
|
|
12
13
|
buildRotation,
|
|
@@ -75,6 +76,10 @@ export function validateRotationHistory(
|
|
|
75
76
|
throw new Error("Invalid daily history.");
|
|
76
77
|
return response.data;
|
|
77
78
|
}
|
|
79
|
+
/** The venue a quote is for, as `quote` names it; empty when it names none. */
|
|
80
|
+
function listingVenue(quote: CloudQuotePayload | undefined): string {
|
|
81
|
+
return quote ? quoteTradingExchange(quote) : "";
|
|
82
|
+
}
|
|
78
83
|
export async function fetchRotation(
|
|
79
84
|
benchmark: RotationInstrument,
|
|
80
85
|
instruments: RotationInstrument[],
|
|
@@ -106,6 +111,16 @@ export async function fetchRotation(
|
|
|
106
111
|
if (isAccessDenied(error)) throw error;
|
|
107
112
|
quoteError = "Listing currencies unavailable.";
|
|
108
113
|
}
|
|
114
|
+
// A benchmark typed without its exchange (ACWI) is the listing its quote is for (ACWI:NASDAQ), as `quote`
|
|
115
|
+
// reads it: that venue's calendar picks the weekly closes.
|
|
116
|
+
const venue = benchmark.exchange
|
|
117
|
+
? ""
|
|
118
|
+
: listingVenue(quotes.get(rotationId(benchmark)));
|
|
119
|
+
const reference = venue ? { ...benchmark, exchange: venue } : benchmark;
|
|
120
|
+
const requested = unique.map((row) => ({
|
|
121
|
+
id: rotationId(row),
|
|
122
|
+
instrument: rotationId(row) === rotationId(benchmark) ? reference : row,
|
|
123
|
+
}));
|
|
109
124
|
const start = new Date(now);
|
|
110
125
|
start.setUTCFullYear(start.getUTCFullYear() - 3);
|
|
111
126
|
start.setUTCDate(1);
|
|
@@ -115,10 +130,9 @@ export async function fetchRotation(
|
|
|
115
130
|
let next = 0;
|
|
116
131
|
// Bound Cloud history requests; shared server caches batch upstream demand.
|
|
117
132
|
await Promise.all(
|
|
118
|
-
Array.from({ length: Math.min(4,
|
|
119
|
-
while (next <
|
|
120
|
-
const instrument =
|
|
121
|
-
id = rotationId(instrument),
|
|
133
|
+
Array.from({ length: Math.min(4, requested.length) }, async () => {
|
|
134
|
+
while (next < requested.length) {
|
|
135
|
+
const { id, instrument } = requested[next++]!,
|
|
122
136
|
currency = quotes.get(id)?.currency ?? null;
|
|
123
137
|
try {
|
|
124
138
|
const response = await client.getCloudHistory(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { fetchRotation } from "./client";
|
|
3
|
-
import { rotationInstruments, sectorRotationInstruments } from "./model";
|
|
3
|
+
import { DEFAULT_ROTATION_BENCHMARK, rotationInstruments, sectorRotationInstruments } from "./model";
|
|
4
4
|
|
|
5
5
|
/** Same rounding as the pane: ratios to 2 decimals, percentiles whole. */
|
|
6
6
|
const fixed = (value: unknown, digits: number) =>
|
|
@@ -21,7 +21,7 @@ export const rotationHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
21
21
|
key: "benchmark",
|
|
22
22
|
type: "string",
|
|
23
23
|
description: "Benchmark listing.",
|
|
24
|
-
defaultValue:
|
|
24
|
+
defaultValue: DEFAULT_ROTATION_BENCHMARK,
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
key: "trail",
|
|
@@ -44,7 +44,7 @@ export const rotationHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
44
44
|
},
|
|
45
45
|
async load(args, ctx) {
|
|
46
46
|
const references = rotationInstruments(
|
|
47
|
-
String(args.options.benchmark ??
|
|
47
|
+
String(args.options.benchmark ?? DEFAULT_ROTATION_BENCHMARK),
|
|
48
48
|
);
|
|
49
49
|
if (references.length !== 1) throw new Error("Choose one benchmark.");
|
|
50
50
|
const instruments = args.symbols.length
|
|
@@ -3,6 +3,7 @@ import type { PluginModule } from "../plugin-module";
|
|
|
3
3
|
import { rotationCache } from "./client";
|
|
4
4
|
import { rotationHeadless } from "./headless";
|
|
5
5
|
import { RelativeRotationPane } from "./pane";
|
|
6
|
+
import { DEFAULT_ROTATION_BENCHMARK } from "./model";
|
|
6
7
|
|
|
7
8
|
export const relativeRotationModule: PluginModule = {
|
|
8
9
|
panes: [
|
|
@@ -41,7 +42,7 @@ export const relativeRotationModule: PluginModule = {
|
|
|
41
42
|
key: "benchmark",
|
|
42
43
|
label: "Benchmark",
|
|
43
44
|
type: "text",
|
|
44
|
-
placeholder:
|
|
45
|
+
placeholder: DEFAULT_ROTATION_BENCHMARK,
|
|
45
46
|
},
|
|
46
47
|
{
|
|
47
48
|
key: "trail",
|
|
@@ -2,6 +2,7 @@ import { publishedWeekClose } from "../analytics/sharpe-cadence";
|
|
|
2
2
|
import { historyStatistics } from "../../../components/chart/curve/model";
|
|
3
3
|
import type { CloudPricePointPayload } from "../../../api-client/types";
|
|
4
4
|
import { getSectorCollection } from "../sectors/sector-data";
|
|
5
|
+
import { rotationQuadrant, rotationSeries, type RotationQuadrantName } from "../alerts/metric-model";
|
|
5
6
|
import {
|
|
6
7
|
canonicalExchange,
|
|
7
8
|
normalizeSymbol,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
const DAY = 86_400_000;
|
|
12
13
|
const WEEK = 7 * DAY;
|
|
13
14
|
export const ROTATION_LIMIT = 24;
|
|
15
|
+
/** RRG's benchmark when none is chosen; RRG alerts default to it too. */
|
|
16
|
+
export const DEFAULT_ROTATION_BENCHMARK = "SPY:NYSEARCA";
|
|
14
17
|
export interface RotationInstrument {
|
|
15
18
|
symbol: string;
|
|
16
19
|
exchange: string;
|
|
@@ -46,8 +49,7 @@ export interface RotationRow {
|
|
|
46
49
|
asOf: string | null;
|
|
47
50
|
strength: number | null;
|
|
48
51
|
momentum: number | null;
|
|
49
|
-
quadrant:
|
|
50
|
-
"Leading" | "Weakening" | "Lagging" | "Improving" | "Neutral" | null;
|
|
52
|
+
quadrant: RotationQuadrantName | null;
|
|
51
53
|
strengthRank: RotationRank;
|
|
52
54
|
momentumRank: RotationRank;
|
|
53
55
|
relativeReturn13w: number | null;
|
|
@@ -149,21 +151,8 @@ function rotationRank(
|
|
|
149
151
|
lastDate: stats.endDate,
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
momentum: number | null,
|
|
155
|
-
): RotationRow["quadrant"] {
|
|
156
|
-
if (strength == null || momentum == null) return null;
|
|
157
|
-
if (Math.abs(strength - 100) < 1e-9 || Math.abs(momentum - 100) < 1e-9)
|
|
158
|
-
return "Neutral";
|
|
159
|
-
return strength > 100
|
|
160
|
-
? momentum > 100
|
|
161
|
-
? "Leading"
|
|
162
|
-
: "Weakening"
|
|
163
|
-
: momentum > 100
|
|
164
|
-
? "Improving"
|
|
165
|
-
: "Lagging";
|
|
166
|
-
}
|
|
154
|
+
/** The weekly strength, momentum and quadrant math is the RRG alert's, shared with Gloom Cloud's delivery. */
|
|
155
|
+
export { rotationQuadrant };
|
|
167
156
|
export function buildRotation(
|
|
168
157
|
benchmark: RotationHistory,
|
|
169
158
|
instruments: RotationHistory[],
|
|
@@ -219,10 +208,12 @@ export function buildRotation(
|
|
|
219
208
|
const benchmarkUsable = !!benchmark.currency && !!asOf;
|
|
220
209
|
const rows = instruments.map((source): RotationRow => {
|
|
221
210
|
const closes = rotationCloses(source.points, through);
|
|
211
|
+
// A benchmark with no usable weeks says so once in `gaps`; its rows are not to blame for it.
|
|
222
212
|
const gaps = [
|
|
223
213
|
source.error,
|
|
224
214
|
source.stale ? "History is stale." : null,
|
|
225
|
-
|
|
215
|
+
benchmarkUsable &&
|
|
216
|
+
(source.currency !== benchmark.currency || !source.currency)
|
|
226
217
|
? "Price currency does not match the benchmark."
|
|
227
218
|
: null,
|
|
228
219
|
].filter((x): x is string => !!x);
|
|
@@ -240,34 +231,16 @@ export function buildRotation(
|
|
|
240
231
|
? ratio
|
|
241
232
|
: null;
|
|
242
233
|
});
|
|
243
|
-
const
|
|
244
|
-
const window = ratios.slice(Math.max(0, index - 12), index + 1);
|
|
245
|
-
if (
|
|
246
|
-
ratio == null ||
|
|
247
|
-
window.length !== 13 ||
|
|
248
|
-
window.some((value) => value == null)
|
|
249
|
-
)
|
|
250
|
-
return null;
|
|
251
|
-
const mean =
|
|
252
|
-
(window as number[]).reduce((sum, value) => sum + value, 0) / 13;
|
|
253
|
-
return (100 * ratio) / mean;
|
|
254
|
-
});
|
|
234
|
+
const series = rotationSeries(ratios);
|
|
255
235
|
const history = weeks.map((week, index): RotationPoint => {
|
|
256
|
-
const strength =
|
|
257
|
-
previous = index >= 4 ? (strengths[index - 4] ?? null) : null;
|
|
258
|
-
const contiguous =
|
|
259
|
-
index >= 4 &&
|
|
260
|
-
strengths.slice(index - 4, index + 1).every((value) => value != null);
|
|
236
|
+
const { strength, momentum } = series[index]!;
|
|
261
237
|
const priorRatio = index >= 13 ? (ratios[index - 13] ?? null) : null,
|
|
262
238
|
ratio = ratios[index] ?? null;
|
|
263
239
|
return {
|
|
264
240
|
date: weekly.get(week)?.date ?? week,
|
|
265
241
|
week,
|
|
266
242
|
strength,
|
|
267
|
-
momentum
|
|
268
|
-
strength != null && previous != null && contiguous
|
|
269
|
-
? (100 * strength) / previous
|
|
270
|
-
: null,
|
|
243
|
+
momentum,
|
|
271
244
|
relativeReturn13w:
|
|
272
245
|
ratio != null &&
|
|
273
246
|
priorRatio != null &&
|
|
@@ -284,7 +257,10 @@ export function buildRotation(
|
|
|
284
257
|
Number.isFinite(latest.strength) &&
|
|
285
258
|
latest.momentum != null &&
|
|
286
259
|
Number.isFinite(latest.momentum);
|
|
287
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
benchmarkUsable &&
|
|
262
|
+
(!latest || latest.strength == null || latest.momentum == null)
|
|
263
|
+
)
|
|
288
264
|
gaps.push(
|
|
289
265
|
"At least 17 contiguous matched weekly closes are required; a missing week breaks the trail.",
|
|
290
266
|
);
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
37
37
|
import { cachedRotation, loadRotation } from "./client";
|
|
38
38
|
import {
|
|
39
|
+
DEFAULT_ROTATION_BENCHMARK,
|
|
39
40
|
ROTATION_LIMIT,
|
|
40
41
|
rotationId,
|
|
41
42
|
rotationTrailWeeks,
|
|
@@ -140,13 +141,13 @@ function RotationDetail({
|
|
|
140
141
|
export function RelativeRotationPane(props: PaneProps) {
|
|
141
142
|
const [scope] = usePaneSettingValue("scope", "sectors");
|
|
142
143
|
const [symbols] = usePaneSettingValue("symbols", "");
|
|
143
|
-
const [benchmarkText] = usePaneSettingValue("benchmark",
|
|
144
|
+
const [benchmarkText] = usePaneSettingValue("benchmark", DEFAULT_ROTATION_BENCHMARK);
|
|
144
145
|
const [trailText] = usePaneSettingValue("trail", "6");
|
|
145
146
|
const { collectionId } = usePaneCollection();
|
|
146
147
|
const tickers = useTickers();
|
|
147
148
|
const context = useMemo(() => {
|
|
148
149
|
try {
|
|
149
|
-
const references = rotationInstruments(benchmarkText ||
|
|
150
|
+
const references = rotationInstruments(benchmarkText || DEFAULT_ROTATION_BENCHMARK);
|
|
150
151
|
if (references.length !== 1)
|
|
151
152
|
throw new Error("Choose one benchmark in pane settings.");
|
|
152
153
|
const instruments =
|
|
@@ -446,6 +446,7 @@ export function CorporateActionsView({
|
|
|
446
446
|
const financialCurrency = tickerFinancials?.financialCurrency;
|
|
447
447
|
const trailingEps = tickerFinancials?.fundamentals?.eps;
|
|
448
448
|
const trailingEpsCurrency = tickerFinancials?.fundamentals?.epsCurrency;
|
|
449
|
+
const trailingEpsSource = tickerFinancials?.fundamentals?.trailingEpsSource;
|
|
449
450
|
const fundamentalsCurrency = tickerFinancials?.fundamentals?.financialCurrency;
|
|
450
451
|
const marketCapCurrency = tickerFinancials?.fundamentals?.marketCapCurrency;
|
|
451
452
|
const quoteCurrency = tickerFinancials?.quote?.currency;
|
|
@@ -453,10 +454,10 @@ export function CorporateActionsView({
|
|
|
453
454
|
() => (quarterlyStatements || financialCurrency || trailingEps != null ? {
|
|
454
455
|
quarterlyStatements: quarterlyStatements ?? [],
|
|
455
456
|
financialCurrency,
|
|
456
|
-
fundamentals: { eps: trailingEps, epsCurrency: trailingEpsCurrency, financialCurrency: fundamentalsCurrency, marketCapCurrency },
|
|
457
|
+
fundamentals: { eps: trailingEps, epsCurrency: trailingEpsCurrency, financialCurrency: fundamentalsCurrency, marketCapCurrency, trailingEpsSource },
|
|
457
458
|
quote: quoteCurrency ? { currency: quoteCurrency } : null,
|
|
458
459
|
} : null),
|
|
459
|
-
[financialCurrency, fundamentalsCurrency, marketCapCurrency, quarterlyStatements, quoteCurrency, trailingEps, trailingEpsCurrency],
|
|
460
|
+
[financialCurrency, fundamentalsCurrency, marketCapCurrency, quarterlyStatements, quoteCurrency, trailingEps, trailingEpsCurrency, trailingEpsSource],
|
|
460
461
|
);
|
|
461
462
|
const actionsLoader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
|
|
462
463
|
if (!dataProvider?.getCorporateActions) throw new Error("Corporate actions source unavailable");
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "../../../utils/format";
|
|
16
16
|
import { computeTTM } from "../ticker-detail/financials/aggregation";
|
|
17
17
|
import { declaredDividends } from "../../../utils/declared-dividends";
|
|
18
|
-
import { fundamentalsEpsCurrency } from "../../../utils/fundamentals";
|
|
18
|
+
import { fundamentalsEpsCurrency, SERVED_TRAILING_EPS } from "../../../utils/fundamentals";
|
|
19
19
|
import {
|
|
20
20
|
earningsDateStatus,
|
|
21
21
|
earningsDetail,
|
|
@@ -175,11 +175,11 @@ function estimateValue(pair: EstimatePair): string {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* What the TTM row reads: the statements for its four quarters and revenue, and the trailing EPS
|
|
179
|
-
*
|
|
178
|
+
* What the TTM row reads: the statements for its four quarters and revenue, and the trailing EPS of the fundamentals,
|
|
179
|
+
* the figure `ticker` prints as EPS (TTM) (the filed quarters' when they give one, see time-series/filed-trailing-eps).
|
|
180
180
|
*/
|
|
181
181
|
export type EventFinancials = Pick<TickerFinancials, "quarterlyStatements" | "financialCurrency"> & {
|
|
182
|
-
fundamentals?: Pick<NonNullable<TickerFinancials["fundamentals"]>, "eps" | "epsCurrency" | "financialCurrency" | "marketCapCurrency"> | null;
|
|
182
|
+
fundamentals?: Pick<NonNullable<TickerFinancials["fundamentals"]>, "eps" | "epsCurrency" | "financialCurrency" | "marketCapCurrency" | "trailingEpsSource"> | null;
|
|
183
183
|
/** The listing's currency, which says whether an unlabelled EPS can be in the statements' (fundamentalsCurrency). */
|
|
184
184
|
quote?: Pick<NonNullable<TickerFinancials["quote"]>, "currency"> | null;
|
|
185
185
|
};
|
|
@@ -204,9 +204,10 @@ function adjustedQuarterSum(
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
* The TTM row's EPS is the GAAP diluted trailing figure
|
|
208
|
-
*
|
|
209
|
-
*
|
|
207
|
+
* The TTM row's EPS is the GAAP diluted trailing figure `ticker` prints as EPS (TTM): the last four filed quarters',
|
|
208
|
+
* or Gloom Cloud's trailing figure, named so, when they give none. Without either, the four statement quarters'
|
|
209
|
+
* diluted EPS summed. The reported rows above it are often adjusted, so they are never added to it: only when all
|
|
210
|
+
* four are adjusted does the detail also give their sum.
|
|
210
211
|
*/
|
|
211
212
|
function ttmRow(
|
|
212
213
|
quarterlyStatements: readonly FinancialStatement[],
|
|
@@ -226,7 +227,8 @@ function ttmRow(
|
|
|
226
227
|
date: latest?.date ?? "",
|
|
227
228
|
status: "TTM",
|
|
228
229
|
period: "4 qtrs",
|
|
229
|
-
detail: [eps == null ? "statement sum" : served ? "GAAP diluted
|
|
230
|
+
detail: [eps == null ? "statement sum" : !served ? "GAAP diluted (statements)"
|
|
231
|
+
: financials?.fundamentals?.trailingEpsSource === "served" ? `GAAP diluted (${SERVED_TRAILING_EPS})` : "GAAP diluted",
|
|
230
232
|
adjusted == null ? null : `adj sum ${formatNumber(adjusted, 2)}`].filter(Boolean).join(", "),
|
|
231
233
|
epsCurrency: served ? fundamentalsEpsCurrency(financials) : ttm.currency ?? financials?.financialCurrency,
|
|
232
234
|
revenueCurrency: ttm.currency ?? financials?.financialCurrency,
|
|
@@ -83,7 +83,7 @@ function projectEarningsEstimatesHeadless(
|
|
|
83
83
|
returned: rows.length,
|
|
84
84
|
truncated: rows.length < matching.length,
|
|
85
85
|
coverageNote: notice?.text,
|
|
86
|
-
epsBasis: `${EPS_BASIS_NOTE} The TTM row's EPS is the GAAP diluted trailing figure ticker shows as EPS (TTM), or the four statement quarters' diluted EPS when
|
|
86
|
+
epsBasis: `${EPS_BASIS_NOTE} The TTM row's EPS is the GAAP diluted trailing figure ticker shows as EPS (TTM): the last four filed quarters', or Gloom Cloud's TTM (named so) when they give none, or the four statement quarters' diluted EPS when neither is there; reported quarters of different bases are never summed, and the row adds an adjusted sum only when all four quarters are adjusted.`,
|
|
87
87
|
dateNote: "Fiscal period dates are not announcement dates. Revenue is attached only to an identified statement period.",
|
|
88
88
|
unmatchedReportedPeriods,
|
|
89
89
|
},
|
|
@@ -96,7 +96,10 @@ const VALUATION_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
96
96
|
...([["fcfYield", "FCF Yield"], ["revenueGrowth", REVENUE_GROWTH_HEADER], ["operatingMargin", "TTM Op Margin"]] as const).map(([key, header]) => ({
|
|
97
97
|
key, header, align: "right" as const,
|
|
98
98
|
format: (value: unknown, row: Record<string, unknown>) => value == null ? gapText(row, key)
|
|
99
|
-
: key === "revenueGrowth" ? formatPercent(Number(value))
|
|
99
|
+
: key === "revenueGrowth" ? formatPercent(Number(value))
|
|
100
|
+
// A margin of the latest fiscal year, where no trailing quarters were served, says which year.
|
|
101
|
+
: key === "operatingMargin" && typeof row.operatingMarginPeriod === "string"
|
|
102
|
+
? `${formatLevelPercent(Number(value))} ${row.operatingMarginPeriod}` : formatLevelPercent(Number(value)),
|
|
100
103
|
})),
|
|
101
104
|
];
|
|
102
105
|
|
|
@@ -226,6 +229,7 @@ export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
226
229
|
...rows.flatMap((row) => row.evSalesUnavailable ? [`${row.symbol}: ${row.evSalesUnavailable}`] : []),
|
|
227
230
|
...rows.flatMap((row) => row.evSalesNote ? [`${row.symbol}: ${row.evSalesNote}`] : []),
|
|
228
231
|
...rows.flatMap((row) => row.marketCapNote ? [`${row.symbol}: ${row.marketCapNote}`] : []),
|
|
232
|
+
...rows.flatMap((row) => row.marketCapWithheld ? [`${row.symbol}: ${row.marketCapWithheld}`] : []),
|
|
229
233
|
],
|
|
230
234
|
multipleBasis: "Comparable P/E fields require a positive finite multiple over positive EPS, and evSales an enterprise value above zero; notMeaningful says why one is null, and reportedMultiples preserves the finite provider P/E values.",
|
|
231
235
|
marketCapBasis: "Market caps retain their own currency and source. Fundamentals retrieval time is not a valuation date; quote timestamps do not date fallback caps.",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { formatCurrency } from "../../../utils/format";
|
|
2
2
|
import { comparablePriceEarnings, notMeaningfulMultiples } from "../../../utils/price-earnings";
|
|
3
|
-
import { marketCapBasisNote, viewMarketCapitalization } from "../../../utils/market-capitalization";
|
|
3
|
+
import { marketCapBasisNote, viewMarketCapitalization, withheldMarketCap } from "../../../utils/market-capitalization";
|
|
4
4
|
import { enterpriseValueNotMeaningful, enterpriseValueReconciliationNote, enterpriseValueWithdrawn, reportedEnterpriseValue } from "../../../utils/fundamentals";
|
|
5
5
|
import { getExtendedSessionDisplay, getQuoteSessionFields } from "../../../market-data/market/status";
|
|
6
6
|
import { atRegularSessionPrice, movePriceDerivedFigures, regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
7
|
+
import { fiscalPeriodLabel, fiscalYearEndMonth } from "../../../utils/fiscal-period";
|
|
7
8
|
import { latestFinancialPeriod, latestReportedPeriod } from "../../../utils/latest-financial-period";
|
|
8
9
|
import type { Quote, TickerFinancials } from "../../../types/financials";
|
|
9
10
|
|
|
@@ -50,6 +51,25 @@ function fcfYieldGap(fundamentals: TickerFinancials["fundamentals"], capCurrency
|
|
|
50
51
|
}
|
|
51
52
|
export const RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE = "Fundamentals stale: retained values may be out of date";
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The operating margin of the latest fiscal year, for a row whose trailing
|
|
56
|
+
* twelve months are not whole (a quarter the source has not carried yet):
|
|
57
|
+
* that year's operating income over its revenue, named for the year ("FY25")
|
|
58
|
+
* so a cell never passes it off as trailing. Null when the latest year lacks
|
|
59
|
+
* either figure; an older year is never stood in for it.
|
|
60
|
+
*/
|
|
61
|
+
function fiscalYearOperatingMargin(annualStatements: TickerFinancials["annualStatements"] | undefined) {
|
|
62
|
+
const latest = latestFinancialPeriod(annualStatements, (row) => row.date);
|
|
63
|
+
if (!latest || !finiteNumber(latest.operatingIncome) || !finiteNumber(latest.totalRevenue) || latest.totalRevenue <= 0) return null;
|
|
64
|
+
const year = /^FY\d{2}(\d{2})$/.exec(fiscalPeriodLabel(latest.date, "annual", fiscalYearEndMonth(annualStatements ?? [])))?.[1];
|
|
65
|
+
return {
|
|
66
|
+
value: latest.operatingIncome / latest.totalRevenue,
|
|
67
|
+
// The fiscal year's own name; the calendar year of its end where the year cannot be named.
|
|
68
|
+
label: `FY${year ?? latest.date.slice(2, 4)}`,
|
|
69
|
+
periodEnd: latest.date.slice(0, 10),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
/**
|
|
54
74
|
* A peer's row. Last, its change and the extended print are the quote's
|
|
55
75
|
* sessions as `quote` reads them (getQuoteSessionFields): after the close,
|
|
@@ -97,14 +117,17 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
|
|
|
97
117
|
&& fundamentals.freeCashFlow != null && Number.isFinite(fundamentals.freeCashFlow) && capitalization.value > 0
|
|
98
118
|
? fundamentals.freeCashFlow / capitalization.value : null;
|
|
99
119
|
const revenueGrowth = fundamentals?.revenueGrowth ?? fundamentals?.lastQuarterGrowth ?? null;
|
|
100
|
-
const
|
|
120
|
+
const trailingOperatingMargin = fundamentals?.operatingMargin ?? null;
|
|
121
|
+
const fiscalYearMargin = trailingOperatingMargin == null ? fiscalYearOperatingMargin(financials?.annualStatements) : null;
|
|
122
|
+
const operatingMargin = trailingOperatingMargin ?? fiscalYearMargin?.value ?? null;
|
|
101
123
|
const trailingPE = trailingNotMeaningful ? null : comparablePriceEarnings(reportedMultiples.trailingPE);
|
|
102
124
|
const forwardPE = forwardNotMeaningful ? null : comparablePriceEarnings(reportedMultiples.forwardPE);
|
|
103
125
|
// Every figure a row lacks says why, so an export's blank cell is never silent: N/M multiples say so themselves.
|
|
104
126
|
const gaps: Partial<Record<RelativeValuationGapKey, string>> = {};
|
|
105
127
|
if (session.price == null) gaps.price = quoteStale ? QUOTE_STALE : NOT_REPORTED;
|
|
106
128
|
if (session.changePercent == null) gaps.changePercent = quoteStale ? QUOTE_STALE : NOT_REPORTED;
|
|
107
|
-
|
|
129
|
+
const withheld = withheldMarketCap(fundamentals);
|
|
130
|
+
if (capitalization == null) gaps.marketCap = withheld?.gap ?? (quoteStale && !fundamentals?.marketCap ? QUOTE_STALE : NOT_REPORTED);
|
|
108
131
|
if (trailingPE == null && !trailingNotMeaningful && reportedMultiples.trailingPE == null) gaps.trailingPE = NOT_REPORTED;
|
|
109
132
|
if (forwardPE == null && !forwardNotMeaningful && reportedMultiples.forwardPE == null) gaps.forwardPE = NOT_REPORTED;
|
|
110
133
|
if (evSales == null && !evSalesNotMeaningful) gaps.evSales = evSalesGap(fundamentals, quote?.currency, evSalesWithdrawn, enterpriseValueGap);
|
|
@@ -136,6 +159,8 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
|
|
|
136
159
|
marketCapProvenance: capitalization?.provenance ?? null,
|
|
137
160
|
/** valuation's note when the cap is the share count times the price: the provider's figure it replaced. */
|
|
138
161
|
marketCapNote: marketCapBasisNote(capitalization) ?? null,
|
|
162
|
+
/** Why Gloom Cloud withheld the cap (a receipt priced as ordinary shares), as `valuation` and EQS say it; null otherwise. */
|
|
163
|
+
marketCapWithheld: withheld?.note ?? null,
|
|
139
164
|
trailingPE,
|
|
140
165
|
forwardPE,
|
|
141
166
|
reportedMultiples,
|
|
@@ -151,6 +176,9 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
|
|
|
151
176
|
fcfYield,
|
|
152
177
|
revenueGrowth,
|
|
153
178
|
operatingMargin,
|
|
179
|
+
/** The fiscal year the operating margin is of ("FY25") when no trailing figure was served; null when it is trailing twelve months. */
|
|
180
|
+
operatingMarginPeriod: fiscalYearMargin?.label ?? null,
|
|
181
|
+
operatingMarginPeriodEnd: fiscalYearMargin?.periodEnd ?? null,
|
|
154
182
|
/** Why each missing figure is missing, in a few words the row draws in its place. */
|
|
155
183
|
gaps,
|
|
156
184
|
};
|
|
@@ -97,7 +97,7 @@ function extendedMove(row: RelativeRow, columnId: ExtendedColumnId): number | nu
|
|
|
97
97
|
* AFTER-HRS or OVERNIGHT column, named as BI and MOST name it, follows CHG%
|
|
98
98
|
* for each session in `extendedSessions`.
|
|
99
99
|
*/
|
|
100
|
-
function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[]): RelativeColumn[] {
|
|
100
|
+
function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[], marginNamesYear: boolean): RelativeColumn[] {
|
|
101
101
|
const symbolWidth = 8;
|
|
102
102
|
const priceWidth = 10;
|
|
103
103
|
const pctWidth = 8;
|
|
@@ -117,7 +117,8 @@ function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly E
|
|
|
117
117
|
{ id: "fcfYield", label: "FCF%", width: metricWidth, align: "right" },
|
|
118
118
|
// Latest quarter against the year before, and a trailing-twelve-month margin.
|
|
119
119
|
{ id: "revenueGrowth", label: REVENUE_GROWTH_LABEL, width: metricWidth, align: "right" },
|
|
120
|
-
|
|
120
|
+
// Wider while a peer's margin is of its latest fiscal year and names it ("16.83% FY25").
|
|
121
|
+
{ id: "operatingMargin", label: "TTM OP%", width: marginNamesYear ? 12 : metricWidth, align: "right" },
|
|
121
122
|
];
|
|
122
123
|
}
|
|
123
124
|
|
|
@@ -217,9 +218,10 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
217
218
|
const fxRates = useFxRatesMap([baseCurrency, ...rows.map((row) => row.marketCapCurrency)]);
|
|
218
219
|
// A pre-market or after-hours print gets its own column when a peer has one.
|
|
219
220
|
const extendedSessionsKey = extendedSessionsOf(rows).join(",");
|
|
221
|
+
const marginNamesYear = rows.some((row) => row.operatingMarginPeriod != null);
|
|
220
222
|
const columns = useMemo(
|
|
221
|
-
() => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
|
|
222
|
-
[baseCurrency, extendedSessionsKey],
|
|
223
|
+
() => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], marginNamesYear),
|
|
224
|
+
[baseCurrency, extendedSessionsKey, marginNamesYear],
|
|
223
225
|
);
|
|
224
226
|
|
|
225
227
|
const comparableRows = useMemo(() => rows.map((row) => ({
|
|
@@ -319,7 +321,7 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
319
321
|
case "revenueGrowth":
|
|
320
322
|
return { text: formatPercent(row.revenueGrowth ?? undefined), color: shownPriceColor((row.revenueGrowth ?? 0) * 100, 2) };
|
|
321
323
|
case "operatingMargin":
|
|
322
|
-
return { text: formatLevelPercent(row.operatingMargin ?? undefined)
|
|
324
|
+
return { text: `${formatLevelPercent(row.operatingMargin ?? undefined)}${row.operatingMargin != null && row.operatingMarginPeriod ? ` ${row.operatingMarginPeriod}` : ""}`, color: colors.text };
|
|
323
325
|
}
|
|
324
326
|
}, []);
|
|
325
327
|
|
|
@@ -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" },
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { unavailableText } from "../../../components/ui/status";
|
|
2
2
|
import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
3
3
|
import type { Fundamentals, TickerFinancials } from "../../../types/financials";
|
|
4
|
+
import { fiscalPeriodLabel, fiscalYearEndMonth } from "../../../utils/fiscal-period";
|
|
5
|
+
import { formatCompactCurrency } from "../../../utils/format";
|
|
4
6
|
import { enterpriseValueCheck, enterpriseValueLabel, enterpriseValueNotMeaningful, enterpriseValueReconciliationNote,
|
|
5
7
|
enterpriseValueWithdrawn, reportedEnterpriseValue } from "../../../utils/fundamentals";
|
|
6
8
|
|
|
@@ -50,8 +52,10 @@ export interface ReverseDcfModel {
|
|
|
50
52
|
fcfMarginBasis: FcfMarginBasis;
|
|
51
53
|
/** Year-ten revenue at `fcfMargin`: trailing free cash flow grown at the implied rate for ten years, over the margin. */
|
|
52
54
|
impliedRevenue: number | null;
|
|
53
|
-
/** Annual FCF growth between the latest annual statement and one up to five years earlier, when both are positive. */
|
|
54
|
-
pastGrowth: { rate: number; years: number } | null;
|
|
55
|
+
/** Annual FCF growth between the latest annual statement and one up to five years earlier, when both are positive. `period` names the two years: FY2021–FY2025. */
|
|
56
|
+
pastGrowth: { rate: number; years: number; period: string } | null;
|
|
57
|
+
/** The latest annual statement's free cash flow, which the past growth ends at, and the year it is of (FY2025). */
|
|
58
|
+
fiscalYearFcf: { value: number; label: string } | null;
|
|
55
59
|
/** Implied growth for each discount rate (rows) at each terminal growth (columns). */
|
|
56
60
|
sensitivity: { discountRate: number; implied: (ImpliedGrowth | null)[] }[];
|
|
57
61
|
error: string | null;
|
|
@@ -82,15 +86,65 @@ export function solveImpliedGrowth(enterpriseValue: number, fcf: number, discoun
|
|
|
82
86
|
return { kind: "rate", value: (low + high) / 2 };
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
/** The latest annual statements with a free cash flow, oldest first, as many as the past growth looks back over. */
|
|
90
|
+
function annualFreeCashFlows(financials: TickerFinancials) {
|
|
91
|
+
return financials.annualStatements
|
|
87
92
|
.filter((row) => typeof row.freeCashFlow === "number" && Number.isFinite(row.freeCashFlow))
|
|
88
93
|
.sort((a, b) => a.date.localeCompare(b.date))
|
|
89
94
|
.slice(-6);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The two years a past growth spans, as issuers name them: `FY2021–FY2025`; period end dates where a year is off the fiscal calendar. */
|
|
98
|
+
function fiscalSpan(financials: TickerFinancials, from: string, to: string): string {
|
|
99
|
+
const month = fiscalYearEndMonth(financials.annualStatements);
|
|
100
|
+
const [start, end] = [from, to].map((date) => fiscalPeriodLabel(date, "annual", month)) as [string, string];
|
|
101
|
+
return start.startsWith("FY") && end.startsWith("FY") ? `${start}–${end}` : `${from.slice(0, 10)} to ${to.slice(0, 10)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pastFcfGrowth(financials: TickerFinancials): ReverseDcfModel["pastGrowth"] {
|
|
105
|
+
const annual = annualFreeCashFlows(financials);
|
|
90
106
|
const first = annual[0], last = annual.at(-1);
|
|
91
107
|
if (!first || !last || first === last || !(first.freeCashFlow! > 0) || !(last.freeCashFlow! > 0)) return null;
|
|
92
108
|
const years = Math.round((Date.parse(last.date) - Date.parse(first.date)) / (365.25 * 86_400_000));
|
|
93
|
-
return years > 0
|
|
109
|
+
return years > 0
|
|
110
|
+
? { rate: (last.freeCashFlow! / first.freeCashFlow!) ** (1 / years) - 1, years, period: fiscalSpan(financials, first.date, last.date) }
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function latestFiscalYearFcf(financials: TickerFinancials): ReverseDcfModel["fiscalYearFcf"] {
|
|
115
|
+
const last = annualFreeCashFlows(financials).at(-1);
|
|
116
|
+
return last ? { value: last.freeCashFlow!, label: fiscalPeriodLabel(last.date, "annual", fiscalYearEndMonth(financials.annualStatements)) } : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** How far above the latest fiscal year's free cash flow the trailing twelve months' must be to say the two bases differ. */
|
|
120
|
+
const TTM_BASE_RATIO = 1.5;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* How a trailing free cash flow more than 1.5 times the latest fiscal year's
|
|
124
|
+
* differs from it (`2.7x` FY2025's `5.3B USD`): the growth priced in starts
|
|
125
|
+
* from the trailing figure, the past growth ends at the year's. Null when they
|
|
126
|
+
* are near each other, the year's is not positive, or the model has no growth
|
|
127
|
+
* to say.
|
|
128
|
+
*/
|
|
129
|
+
export function ttmBaseGap(model: Pick<ReverseDcfModel, "freeCashFlow" | "fiscalYearFcf" | "currency" | "error">): { multiple: string; year: string; fcf: string } | null {
|
|
130
|
+
const { freeCashFlow, fiscalYearFcf } = model;
|
|
131
|
+
if (model.error || freeCashFlow == null || !fiscalYearFcf || !(fiscalYearFcf.value > 0) || !(freeCashFlow > fiscalYearFcf.value * TTM_BASE_RATIO)) return null;
|
|
132
|
+
return {
|
|
133
|
+
multiple: `${(freeCashFlow / fiscalYearFcf.value).toFixed(1)}x`,
|
|
134
|
+
year: fiscalYearFcf.label,
|
|
135
|
+
fcf: formatCompactCurrency(fiscalYearFcf.value, model.currency ?? undefined),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** "The TTM base differs from FY2025: TTM FCF is 2.7x that year's 5.3B USD.", the one line a report adds when `ttmBaseGap` finds a gap. */
|
|
140
|
+
export function ttmBaseNote(model: Parameters<typeof ttmBaseGap>[0]): string | null {
|
|
141
|
+
const gap = ttmBaseGap(model);
|
|
142
|
+
return gap ? `The TTM base differs from ${gap.year}: TTM FCF is ${gap.multiple} that year's ${gap.fcf}.` : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** What the past growth rate is over, after the rate: `a year, FY2021–FY2025`. */
|
|
146
|
+
export function pastGrowthBasis(growth: NonNullable<ReverseDcfModel["pastGrowth"]>): string {
|
|
147
|
+
return `a year, ${growth.period}`;
|
|
94
148
|
}
|
|
95
149
|
|
|
96
150
|
/**
|
|
@@ -145,7 +199,8 @@ export function projectReverseDcf(
|
|
|
145
199
|
const empty: ReverseDcfModel = { symbol: options.symbol, currency, discountRate: options.discountRate, terminalGrowth: TERMINAL_GROWTH,
|
|
146
200
|
enterpriseValue, computedEnterpriseValue: check?.computed ?? null, enterpriseValueNote: check ? enterpriseValueReconciliationNote(fundamentals) ?? null : null,
|
|
147
201
|
freeCashFlow, fcfYield: null, implied: null, fcfMargin, fcfMarginBasis, impliedRevenue: null,
|
|
148
|
-
pastGrowth: financials ? pastFcfGrowth(financials) : null,
|
|
202
|
+
pastGrowth: financials ? pastFcfGrowth(financials) : null, fiscalYearFcf: financials ? latestFiscalYearFcf(financials) : null,
|
|
203
|
+
sensitivity: [], error: null };
|
|
149
204
|
const valueCurrency = fundamentals?.marketCapCurrency;
|
|
150
205
|
// ADRs and cross-listings can report cash flows in one currency and trade in another.
|
|
151
206
|
if (currency && valueCurrency && currency !== valueCurrency) {
|