@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,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) {
|
|
@@ -10,8 +10,8 @@ import type { PaneProps } from "../../../types/plugin";
|
|
|
10
10
|
import { Box } from "../../../ui";
|
|
11
11
|
import { formatCompact, formatCompactCurrency } from "../../../utils/format";
|
|
12
12
|
import { loadReverseDcfInputs } from "./client";
|
|
13
|
-
import { describeFcfMargin, DISCOUNT_RATES, FORECAST_YEARS, formatFcfMargin, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS,
|
|
14
|
-
type ImpliedGrowth, type ReverseDcfModel } from "./model";
|
|
13
|
+
import { describeFcfMargin, DISCOUNT_RATES, FORECAST_YEARS, formatFcfMargin, pastGrowthBasis, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS,
|
|
14
|
+
ttmBaseGap, type ImpliedGrowth, type ReverseDcfModel } from "./model";
|
|
15
15
|
|
|
16
16
|
export const DISCOUNT_OPTIONS = DISCOUNT_RATES.map((rate) => ({ value: String(Math.round(rate * 100)), label: `${Math.round(rate * 100)}%` }));
|
|
17
17
|
/** The margin setting that follows the company's own trailing margin, the pane's default. */
|
|
@@ -91,15 +91,18 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
|
|
|
91
91
|
const currency = model.currency ?? undefined;
|
|
92
92
|
const implied = model.implied?.kind === "rate" ? model.implied.value : null;
|
|
93
93
|
const past = model.pastGrowth;
|
|
94
|
+
const gap = ttmBaseGap(model);
|
|
94
95
|
return [
|
|
95
96
|
{ id: "implied", label: "Priced-in growth", value: impliedText(model.implied),
|
|
96
97
|
tone: implied == null || !past ? undefined : implied > past.rate ? "negative" : "positive",
|
|
97
98
|
detail: `a year for ${FORECAST_YEARS}y, then ${pct(TERMINAL_GROWTH, false)}` },
|
|
98
99
|
{ id: "revenue", label: `Revenue, yr ${FORECAST_YEARS}`, value: formatCompactCurrency(model.impliedRevenue ?? undefined, currency),
|
|
99
100
|
detail: `at ${describeFcfMargin(model.fcfMargin, model.fcfMarginBasis)}` },
|
|
100
|
-
{ id: "past", label: "Past FCF growth", value: past ? pct(past.rate) : "--", detail: past ?
|
|
101
|
+
{ id: "past", label: "Past FCF growth", value: past ? pct(past.rate) : "--", detail: past ? pastGrowthBasis(past) : undefined },
|
|
101
102
|
{ id: "yield", label: "FCF yield", value: pct(model.fcfYield, false) },
|
|
102
|
-
|
|
103
|
+
// A trailing figure far above the fiscal year the past growth ends at says so beside itself.
|
|
104
|
+
{ id: "fcf", label: "FCF", value: formatCompactCurrency(model.freeCashFlow ?? undefined, currency),
|
|
105
|
+
detail: gap ? `TTM base, ${gap.multiple} ${gap.year}'s ${gap.fcf}` : undefined },
|
|
103
106
|
{ id: "ev", label: "EV", value: formatCompactCurrency(model.enterpriseValue ?? undefined, currency),
|
|
104
107
|
// More than 5% from market cap + debt - cash: the provider's, with the computed figure where it fits.
|
|
105
108
|
detail: model.computedEnterpriseValue != null ? `provider vs ${formatCompact(model.computedEnterpriseValue)}` : undefined },
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "../../../api-client";
|
|
6
6
|
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
7
7
|
import { listingAbroad, type IssuerListingParams } from "../../../api-client/paths";
|
|
8
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
8
9
|
import type { HeadlessPaneApiClient, PluginPersistence } from "../../../types/plugin";
|
|
9
10
|
import { secListingKey } from "../../../utils/sec";
|
|
10
11
|
|
|
@@ -78,13 +79,14 @@ function loadCached<T extends object>(
|
|
|
78
79
|
}
|
|
79
80
|
const fallback = store?.getResource<T>(kind, key, { ...options, allowExpired: true });
|
|
80
81
|
if (!fallback) throw error;
|
|
81
|
-
|
|
82
|
+
// A command that shows it ends with when the copy was saved, not with the failure.
|
|
83
|
+
return servedFromSavedCopy({
|
|
82
84
|
...fallback.value,
|
|
83
85
|
fetchedAt: fallback.fetchedAt,
|
|
84
86
|
stale: true,
|
|
85
87
|
refreshError: (error instanceof Error ? error.message : String(error)).trim() || "Request failed",
|
|
86
88
|
errorStatus: error instanceof ApiRequestError ? error.status : undefined,
|
|
87
|
-
};
|
|
89
|
+
}, fallback.fetchedAt);
|
|
88
90
|
}).finally(() => { if (active.get(key) === request) active.delete(key); });
|
|
89
91
|
active.set(key, request);
|
|
90
92
|
return request.promise;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HeadlessBundleSection, HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
|
+
import { savedCopyMark, unansweredRefreshError } from "../../../data/saved-copies";
|
|
2
3
|
import { loadRiskReportWithClient, loadRiskReportsWithClient, type RiskReportsResult } from "./data";
|
|
3
4
|
import { SEC_FILINGS } from "../shared/report-freshness";
|
|
4
5
|
import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
|
|
@@ -39,7 +40,8 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
39
40
|
}
|
|
40
41
|
const report = await loadRiskReportWithClient(context.apiClient, ticker, year, { force, listing });
|
|
41
42
|
const sections: HeadlessBundleSection[] = [
|
|
42
|
-
|
|
43
|
+
// A report served from a saved copy dates the report by when it was saved (the list does in the metadata).
|
|
44
|
+
{ title: "Filing", ...savedCopyMark(report), entries: [
|
|
43
45
|
{ label: "Ticker", value: report.ticker },
|
|
44
46
|
{ label: "Filing year", value: report.reportYear },
|
|
45
47
|
{ label: "Filed", value: report.filedAt },
|
|
@@ -62,12 +64,13 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
62
64
|
{ key: "group", header: "Group" }, { key: "heading", header: "Heading" },
|
|
63
65
|
{ key: "excerpt", header: "Prior source excerpt" }, { key: "note", header: "Analysis" },
|
|
64
66
|
], rows: report.diff.removed.map((risk, index) => ({ ...risk, note: report.notes.removed.find((note) => note.index === index)?.text ?? null })) });
|
|
65
|
-
const errors = [list
|
|
67
|
+
const errors = [unansweredRefreshError(list), unansweredRefreshError(report)].filter((error): error is string => !!error);
|
|
66
68
|
return {
|
|
67
69
|
sections,
|
|
68
70
|
errors: errors.length ? errors : undefined,
|
|
69
71
|
...(list?.stale || report.stale ? { complete: false } : {}),
|
|
70
72
|
metadata: {
|
|
73
|
+
...savedCopyMark(list),
|
|
71
74
|
ticker: report.ticker, requestedYear: selection, reportYear: report.reportYear,
|
|
72
75
|
filedAt: report.filedAt, updatedAt: report.updatedAt, docUrl: report.docUrl,
|
|
73
76
|
reportFetchedAt: report.fetchedAt, reportStale: report.stale,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import { RoadmapPane } from "./pane";
|
|
3
|
+
|
|
4
|
+
export const roadmapModule: PluginModule = {
|
|
5
|
+
panes: [
|
|
6
|
+
{
|
|
7
|
+
id: "roadmap",
|
|
8
|
+
name: "Roadmap",
|
|
9
|
+
icon: "R",
|
|
10
|
+
component: RoadmapPane,
|
|
11
|
+
reportFreshness: { status: "not-a-feed", basis: "curated list" },
|
|
12
|
+
defaultPosition: "right",
|
|
13
|
+
defaultMode: "floating",
|
|
14
|
+
defaultFloatingSize: { width: 100, height: 30 },
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
paneTemplates: [
|
|
19
|
+
{
|
|
20
|
+
id: "roadmap-pane",
|
|
21
|
+
paneId: "roadmap",
|
|
22
|
+
label: "Roadmap",
|
|
23
|
+
description: "What is being built, what is planned and what shipped. Pro members vote on what comes next.",
|
|
24
|
+
keywords: ["roadmap", "planned", "building", "shipped", "upcoming", "features", "vote", "votes", "request"],
|
|
25
|
+
shortcut: { prefix: "ROADMAP", aliases: ["RMAP"] },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RoadmapItem, RoadmapPayload, RoadmapStatus, RoadmapVote } from "../../../api-client/roadmap";
|
|
2
|
+
import type { TableSection } from "../../../components";
|
|
3
|
+
|
|
4
|
+
export const ROADMAP_STATUS_LABELS: Record<RoadmapStatus, string> = {
|
|
5
|
+
building: "Building",
|
|
6
|
+
planned: "Planned",
|
|
7
|
+
shipped: "Shipped",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SECTION_ORDER: readonly RoadmapStatus[] = ["building", "planned", "shipped"];
|
|
11
|
+
|
|
12
|
+
/** Building, then Planned, then Shipped, each in the order the service sent; an empty group is left out. */
|
|
13
|
+
export function roadmapSections(items: readonly RoadmapItem[]): TableSection<RoadmapItem>[] {
|
|
14
|
+
return SECTION_ORDER.map((status) => ({
|
|
15
|
+
label: ROADMAP_STATUS_LABELS[status],
|
|
16
|
+
key: status,
|
|
17
|
+
items: items.filter((item) => item.status === status),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Votes are taken on what is not shipped yet; a shipped item's count is final. */
|
|
22
|
+
export function isVotable(item: RoadmapItem): boolean {
|
|
23
|
+
return item.status !== "shipped";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The items with this session's own votes on top: the count and mark a vote
|
|
28
|
+
* changed, held until the next read of the list replaces them.
|
|
29
|
+
*/
|
|
30
|
+
export function withLocalVotes(
|
|
31
|
+
payload: RoadmapPayload | null,
|
|
32
|
+
local: Readonly<Record<string, RoadmapVote>>,
|
|
33
|
+
): RoadmapItem[] {
|
|
34
|
+
if (!payload) return [];
|
|
35
|
+
return payload.items.map((item) => {
|
|
36
|
+
const vote = local[item.id];
|
|
37
|
+
return vote ? { ...item, votes: vote.votes, voted: vote.voted } : item;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What the row shows while a vote is on its way: the mark flipped and the count moved by one. */
|
|
42
|
+
export function optimisticVote(item: RoadmapItem, voted: boolean): RoadmapVote {
|
|
43
|
+
const step = voted === item.voted ? 0 : voted ? 1 : -1;
|
|
44
|
+
return { id: item.id, votes: Math.max(0, item.votes + step), voted };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The votes cell: the count, with a check before it when this account voted. */
|
|
48
|
+
export function votesText(item: RoadmapItem): string {
|
|
49
|
+
return item.voted ? `✓ ${item.votes}` : String(item.votes);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What one vote that failed is called on its row, and the reason the detail gives. */
|
|
53
|
+
export interface VoteFailure {
|
|
54
|
+
status: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function voteFailure(error: unknown, voted: boolean): VoteFailure {
|
|
59
|
+
const status = typeof error === "object" && error && "status" in error ? (error as { status?: unknown }).status : undefined;
|
|
60
|
+
const action = voted ? "The vote" : "Taking the vote back";
|
|
61
|
+
if (status === 401) return { status: "sign in", reason: `${action} did not go through: sign in again to vote.` };
|
|
62
|
+
if (status === 402 || status === 403) return { status: "Pro only", reason: `${action} did not go through: voting is for verified Pro members.` };
|
|
63
|
+
if (status === 404) return { status: "removed", reason: "This item is no longer on the roadmap." };
|
|
64
|
+
if (status === 409) return { status: "closed", reason: "This item shipped, so its votes are final." };
|
|
65
|
+
const message = error instanceof Error && error.message.trim() ? error.message.trim() : "the request failed";
|
|
66
|
+
return { status: "failed", reason: `${action} did not go through: ${message.replace(/\.$/, "")}.` };
|
|
67
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
|
+
import { apiClient } from "../../../api-client";
|
|
3
|
+
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
|
+
import type { RoadmapItem, RoadmapVote } from "../../../api-client/roadmap";
|
|
5
|
+
import {
|
|
6
|
+
buildSectionedRows,
|
|
7
|
+
DataTableStackView,
|
|
8
|
+
EMPTY_TABLE_CELL,
|
|
9
|
+
isSectionedItemRow,
|
|
10
|
+
renderSectionedRowHeader,
|
|
11
|
+
usePaneStatusFooter,
|
|
12
|
+
useRowNotes,
|
|
13
|
+
type DataTableCell,
|
|
14
|
+
type DataTableColumn,
|
|
15
|
+
type PaneFooterSegment,
|
|
16
|
+
type PaneHint,
|
|
17
|
+
type SectionedRow,
|
|
18
|
+
} from "../../../components";
|
|
19
|
+
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
20
|
+
import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
|
|
21
|
+
import { useAsyncResource, usePluginPaneState } from "../../../public/react";
|
|
22
|
+
import { useThemeColors } from "../../../theme/theme-context";
|
|
23
|
+
import type { PaneProps } from "../../../types/plugin";
|
|
24
|
+
import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
25
|
+
import { formatShortDate } from "../../../utils/datetime-format";
|
|
26
|
+
import {
|
|
27
|
+
isVotable,
|
|
28
|
+
optimisticVote,
|
|
29
|
+
ROADMAP_STATUS_LABELS,
|
|
30
|
+
roadmapSections,
|
|
31
|
+
voteFailure,
|
|
32
|
+
votesText,
|
|
33
|
+
withLocalVotes,
|
|
34
|
+
type VoteFailure,
|
|
35
|
+
} from "./model";
|
|
36
|
+
|
|
37
|
+
/** The roadmap calls the pane makes; tests hand it their own. */
|
|
38
|
+
export type RoadmapClient = Pick<typeof apiClient, "getRoadmap" | "setRoadmapVote">;
|
|
39
|
+
export const RoadmapClientContext = createContext<RoadmapClient | null>(null);
|
|
40
|
+
|
|
41
|
+
type Row = SectionedRow<RoadmapItem>;
|
|
42
|
+
type ColumnId = "title" | "blurb" | "votes" | "shipped";
|
|
43
|
+
type Column = DataTableColumn & { id: ColumnId };
|
|
44
|
+
|
|
45
|
+
const rowKey = (row: Row) => row.key;
|
|
46
|
+
const itemKey = (item: RoadmapItem) => item.id;
|
|
47
|
+
const voteSignature = (item: RoadmapItem) => `${item.votes}:${item.voted}`;
|
|
48
|
+
|
|
49
|
+
/** Said once, quietly, to everyone who cannot vote: no upgrade pitch. */
|
|
50
|
+
const PRO_VOTES_SEGMENT: PaneFooterSegment = { id: "pro-votes", parts: [{ text: "Pro members vote", tone: "muted" }] };
|
|
51
|
+
const NO_SEGMENTS: PaneFooterSegment[] = [];
|
|
52
|
+
|
|
53
|
+
function shippedDate(item: RoadmapItem): string {
|
|
54
|
+
return item.shippedAt ? formatShortDate(item.shippedAt, { fallback: "" }) : "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `votesCell` is what the votes cell says: the count, or a vote's failure on that row. */
|
|
58
|
+
function buildColumns(items: readonly RoadmapItem[], width: number, votesCell: (item: RoadmapItem) => string): Column[] {
|
|
59
|
+
const longest = Math.max(8, ...items.map((item) => item.title.length));
|
|
60
|
+
const narrow = width < 64;
|
|
61
|
+
const columns: Column[] = narrow
|
|
62
|
+
? [{ id: "title", label: "Feature", width: Math.min(longest, 24), flexGrow: 1, align: "left" }]
|
|
63
|
+
: [
|
|
64
|
+
{ id: "title", label: "Feature", width: Math.min(longest, width >= 120 ? 36 : 26), align: "left" },
|
|
65
|
+
{ id: "blurb", label: "Summary", width: 16, flexGrow: 1, align: "left" },
|
|
66
|
+
];
|
|
67
|
+
columns.push({ id: "votes", label: "Votes", width: Math.max(5, ...items.map((item) => votesCell(item).length)), align: "right" });
|
|
68
|
+
if (!narrow && items.some((item) => item.shippedAt)) {
|
|
69
|
+
columns.push({ id: "shipped", label: "Shipped", width: 12, align: "left" });
|
|
70
|
+
}
|
|
71
|
+
return columns;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function RoadmapDetail({ item, failure, width, scrollRef }: {
|
|
75
|
+
item: RoadmapItem;
|
|
76
|
+
failure: VoteFailure | null;
|
|
77
|
+
width: number;
|
|
78
|
+
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
79
|
+
}) {
|
|
80
|
+
const colors = useThemeColors();
|
|
81
|
+
// The stack title names the item; the facts and the blurb follow.
|
|
82
|
+
const facts: DetailFact[] = [
|
|
83
|
+
{ label: "Status", value: ROADMAP_STATUS_LABELS[item.status] },
|
|
84
|
+
{ label: "Votes", value: item.voted ? `${item.votes}, yours included` : String(item.votes) },
|
|
85
|
+
...(item.shippedAt ? [{ label: "Shipped", value: shippedDate(item) }] : []),
|
|
86
|
+
...(failure ? [{ label: "Vote", value: failure.reason, color: colors.negative }] : []),
|
|
87
|
+
];
|
|
88
|
+
return (
|
|
89
|
+
<DetailReader
|
|
90
|
+
ref={scrollRef}
|
|
91
|
+
resetScrollKey={item.id}
|
|
92
|
+
width={width}
|
|
93
|
+
facts={facts}
|
|
94
|
+
body={item.blurb || null}
|
|
95
|
+
bodyFormat="text"
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function RoadmapPane({ focused, width, height }: PaneProps) {
|
|
101
|
+
const colors = useThemeColors();
|
|
102
|
+
const client = useContext(RoadmapClientContext) ?? apiClient;
|
|
103
|
+
const access = usePlanAccess();
|
|
104
|
+
// A sign-in, a sign-out or a new plan asks again: the votes and the right to
|
|
105
|
+
// cast one are the account's. The new loader starts from no list, so the
|
|
106
|
+
// last account's marks and vote hint go at once, not when (or if) the new
|
|
107
|
+
// read lands. A refresh of the same session keeps its list.
|
|
108
|
+
const sessionKey = JSON.stringify([apiClient.getCurrentUser()?.id ?? null, access.emailVerified, access.hasProAccess]);
|
|
109
|
+
const loader = useCallback((_force: boolean) => client.getRoadmap(), [client, sessionKey]);
|
|
110
|
+
const resource = useAsyncResource(loader);
|
|
111
|
+
const sessionRef = useRef(sessionKey);
|
|
112
|
+
sessionRef.current = sessionKey;
|
|
113
|
+
const payload = resource.data;
|
|
114
|
+
const canVote = payload?.canVote === true;
|
|
115
|
+
|
|
116
|
+
// This session's votes, over the list until the next read replaces it.
|
|
117
|
+
const [localVotes, setLocalVotes] = useState<Record<string, RoadmapVote>>({});
|
|
118
|
+
useEffect(() => setLocalVotes({}), [resource.updatedAt]);
|
|
119
|
+
const items = useMemo(() => withLocalVotes(payload, localVotes), [payload, localVotes]);
|
|
120
|
+
const rows = useMemo(() => buildSectionedRows(roadmapSections(items), itemKey), [items]);
|
|
121
|
+
const { noteFor, setNote, clearNotes } = useRowNotes<RoadmapItem, VoteFailure>(items, { key: itemKey, signature: voteSignature });
|
|
122
|
+
// Another account's votes and refusals are not this one's.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
setLocalVotes({});
|
|
125
|
+
clearNotes();
|
|
126
|
+
}, [sessionKey, clearNotes]);
|
|
127
|
+
|
|
128
|
+
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedItemId", null);
|
|
129
|
+
const [openId, setOpenId] = usePluginPaneState<string | null>("openItemId", null);
|
|
130
|
+
const selected = items.find((item) => item.id === selectedId) ?? rows.find(isSectionedItemRow)?.item ?? null;
|
|
131
|
+
const open = openId ? items.find((item) => item.id === openId) ?? null : null;
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
// An item that left the roadmap closes once the list that dropped it is in.
|
|
134
|
+
if (openId && payload && !open) setOpenId(null);
|
|
135
|
+
}, [open, openId, payload]);
|
|
136
|
+
const detailScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
137
|
+
|
|
138
|
+
const inFlight = useRef(new Set<string>());
|
|
139
|
+
const reload = resource.reload;
|
|
140
|
+
const toggleVote = useCallback(async (item: RoadmapItem) => {
|
|
141
|
+
if (!canVote || !isVotable(item) || inFlight.current.has(item.id)) return;
|
|
142
|
+
const voted = !item.voted;
|
|
143
|
+
const session = sessionRef.current;
|
|
144
|
+
inFlight.current.add(item.id);
|
|
145
|
+
setNote(item.id, null);
|
|
146
|
+
setLocalVotes((current) => ({ ...current, [item.id]: optimisticVote(item, voted) }));
|
|
147
|
+
try {
|
|
148
|
+
const answer = await client.setRoadmapVote(item.id, voted);
|
|
149
|
+
// An answer for the account that just left changes nothing on screen.
|
|
150
|
+
if (sessionRef.current !== session) return;
|
|
151
|
+
setLocalVotes((current) => ({ ...current, [item.id]: answer }));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (sessionRef.current !== session) return;
|
|
154
|
+
setLocalVotes((current) => ({ ...current, [item.id]: { id: item.id, votes: item.votes, voted: item.voted } }));
|
|
155
|
+
setNote(item.id, voteFailure(error, voted));
|
|
156
|
+
} finally {
|
|
157
|
+
inFlight.current.delete(item.id);
|
|
158
|
+
}
|
|
159
|
+
}, [canVote, client, setNote]);
|
|
160
|
+
|
|
161
|
+
usePaneRefreshKey(() => void reload(), { focused });
|
|
162
|
+
|
|
163
|
+
const target = open ?? selected;
|
|
164
|
+
const hints = useMemo<PaneHint[]>(() => (
|
|
165
|
+
canVote && target && isVotable(target)
|
|
166
|
+
? [{ id: "vote", key: "v", label: target.voted ? " unvote" : "ote", title: target.voted ? "Take vote back" : "Vote", onPress: () => void toggleVote(target) }]
|
|
167
|
+
: []
|
|
168
|
+
), [canVote, target, toggleVote]);
|
|
169
|
+
usePaneStatusFooter({
|
|
170
|
+
registrationId: "roadmap",
|
|
171
|
+
// A failed refresh keeps the list; the failure goes in the footer.
|
|
172
|
+
loading: resource.loading && !!payload,
|
|
173
|
+
error: payload ? resource.error : null,
|
|
174
|
+
info: !canVote && items.length > 0 ? [PRO_VOTES_SEGMENT] : NO_SEGMENTS,
|
|
175
|
+
hints,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const columns = useMemo(
|
|
179
|
+
() => buildColumns(items, width, (item) => noteFor(item)?.status ?? votesText(item)),
|
|
180
|
+
[items, noteFor, width],
|
|
181
|
+
);
|
|
182
|
+
const renderCell = useCallback((row: Row, column: Column): DataTableCell => {
|
|
183
|
+
if (!isSectionedItemRow(row)) return EMPTY_TABLE_CELL;
|
|
184
|
+
const item = row.item;
|
|
185
|
+
switch (column.id) {
|
|
186
|
+
case "title":
|
|
187
|
+
return { text: item.title, color: colors.textBright };
|
|
188
|
+
case "blurb":
|
|
189
|
+
return { text: item.blurb, color: colors.textDim };
|
|
190
|
+
case "votes": {
|
|
191
|
+
const failure = noteFor(item);
|
|
192
|
+
if (failure) return { text: failure.status, color: colors.negative };
|
|
193
|
+
return {
|
|
194
|
+
text: votesText(item),
|
|
195
|
+
value: item.votes,
|
|
196
|
+
color: item.voted ? colors.positive : isVotable(item) ? colors.text : colors.textDim,
|
|
197
|
+
...(item.voted ? { attributes: TextAttributes.BOLD } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "shipped":
|
|
201
|
+
return { text: shippedDate(item), color: colors.textDim };
|
|
202
|
+
}
|
|
203
|
+
}, [colors, noteFor]);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<DataTableStackView<Row, Column>
|
|
207
|
+
focused={focused}
|
|
208
|
+
detailOpen={!!open}
|
|
209
|
+
onBack={() => setOpenId(null)}
|
|
210
|
+
detailTitle={open?.title}
|
|
211
|
+
detailContent={open ? (
|
|
212
|
+
<RoadmapDetail item={open} failure={noteFor(open)} width={width} scrollRef={detailScrollRef} />
|
|
213
|
+
) : <Box flexGrow={1} />}
|
|
214
|
+
detailScrollRef={detailScrollRef}
|
|
215
|
+
rootWidth={width}
|
|
216
|
+
rootHeight={height}
|
|
217
|
+
// A refresh keeps the list on screen; only a first read waits or fails in the body.
|
|
218
|
+
status={payload ? undefined : {
|
|
219
|
+
loading: resource.loading,
|
|
220
|
+
error: resource.error,
|
|
221
|
+
subject: "Roadmap",
|
|
222
|
+
retry: { onRetry: reload, pending: resource.loading },
|
|
223
|
+
}}
|
|
224
|
+
emptyStateTitle="Nothing on the roadmap yet."
|
|
225
|
+
columns={columns}
|
|
226
|
+
items={rows}
|
|
227
|
+
getItemKey={rowKey}
|
|
228
|
+
isNavigable={isSectionedItemRow}
|
|
229
|
+
renderSectionHeader={renderSectionedRowHeader}
|
|
230
|
+
selection={{
|
|
231
|
+
kind: "id",
|
|
232
|
+
selectedId: selected?.id ?? null,
|
|
233
|
+
getId: rowKey,
|
|
234
|
+
onChange: (id) => setSelectedId(String(id)),
|
|
235
|
+
}}
|
|
236
|
+
onActivate={(row) => { if (isSectionedItemRow(row)) setOpenId(row.item.id); }}
|
|
237
|
+
sortColumnId={null}
|
|
238
|
+
sortDirection="asc"
|
|
239
|
+
renderCell={renderCell}
|
|
240
|
+
selectedTextOverridesCellColor
|
|
241
|
+
showHorizontalScrollbar={false}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -81,6 +81,22 @@ function printRoot(underlying: string): string {
|
|
|
81
81
|
return underlying.toUpperCase().replace(/\d+$/, "");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whose prints the pane shows: one name (`FLOW NVDA`), which takes the place
|
|
86
|
+
* of the universe filter, or the market, narrowed to the user's own tickers
|
|
87
|
+
* when the universe filter says so.
|
|
88
|
+
*/
|
|
89
|
+
export interface FlowScope {
|
|
90
|
+
symbol?: string | null;
|
|
91
|
+
watchlist: ReadonlySet<string>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The listed symbol a `FLOW <ticker>` pane is about, as the pane setting or report argument names it. */
|
|
95
|
+
export function flowScopeSymbol(value: unknown): string | null {
|
|
96
|
+
const symbol = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
97
|
+
return symbol || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
/**
|
|
85
101
|
* The shared feed is published once for everyone, so every user preference is a
|
|
86
102
|
* local predicate over the same events. Never push these upstream.
|
|
@@ -88,13 +104,14 @@ function printRoot(underlying: string): string {
|
|
|
88
104
|
export function filterFlowEvents(
|
|
89
105
|
events: readonly ScannerFlowEvent[] | undefined,
|
|
90
106
|
filters: FlowFilters,
|
|
91
|
-
|
|
107
|
+
scope: FlowScope,
|
|
92
108
|
now = Date.now(),
|
|
93
109
|
): ScannerFlowEvent[] {
|
|
94
110
|
const minPremium = Number(filters.minPremium);
|
|
95
111
|
const minVolOi = filters.volOi === "off" ? null : Number(filters.volOi);
|
|
96
112
|
const maxExpiryDays = filters.expiry === "all" ? null : Number(filters.expiry);
|
|
97
|
-
const roots =
|
|
113
|
+
const roots = scope.symbol ? new Set([flowOptionRoot(scope.symbol)])
|
|
114
|
+
: filters.universe === "watchlist" ? new Set([...scope.watchlist].map(flowOptionRoot)) : null;
|
|
98
115
|
|
|
99
116
|
return (events ?? []).filter((event) => {
|
|
100
117
|
if (!(event.premium >= minPremium)) return false;
|
|
@@ -121,7 +138,15 @@ export function flowEmptyState(
|
|
|
121
138
|
received: number,
|
|
122
139
|
visible: number,
|
|
123
140
|
status: ScannerStatus | undefined,
|
|
141
|
+
symbol: string | null = null,
|
|
124
142
|
): { title: string; hint: string } {
|
|
143
|
+
// One name's prints are mostly recorded ones, and a quiet name has none at all.
|
|
144
|
+
if (symbol) {
|
|
145
|
+
return {
|
|
146
|
+
title: `No ${symbol} prints under these filters.`,
|
|
147
|
+
hint: "Flow is recorded for the most active names, the day's movers and names with a flow alert.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
125
150
|
if (received > visible) {
|
|
126
151
|
return {
|
|
127
152
|
title: `${received - visible} ${received - visible === 1 ? "print" : "prints"} hidden by filters.`,
|
|
@@ -219,22 +244,43 @@ export function mergeFlowRows(
|
|
|
219
244
|
/** The pane's filters as a query for recorded prints, so each page is rows it will show. */
|
|
220
245
|
export function flowHistoryQuery(
|
|
221
246
|
filters: FlowFilters,
|
|
222
|
-
|
|
247
|
+
scope: FlowScope,
|
|
223
248
|
before?: { at: number; id: string },
|
|
224
249
|
limit = FLOW_HISTORY_PAGE,
|
|
225
250
|
): ScannerFlowHistoryQuery {
|
|
226
251
|
return {
|
|
227
252
|
...(before ? { before } : {}),
|
|
228
253
|
limit,
|
|
254
|
+
...flowFilterQuery(filters),
|
|
255
|
+
...(scope.symbol ? { symbols: [scope.symbol.toUpperCase()] }
|
|
256
|
+
: filters.universe === "watchlist" ? { symbols: [...scope.watchlist].map((symbol) => symbol.toUpperCase()).sort() } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function flowFilterQuery(filters: FlowFilters): Omit<ScannerFlowHistoryQuery, "before" | "limit" | "symbols"> {
|
|
261
|
+
return {
|
|
229
262
|
minPremium: Number(filters.minPremium),
|
|
230
263
|
...(filters.side === "calls" ? { right: "C" as const } : filters.side === "puts" ? { right: "P" as const } : {}),
|
|
231
264
|
...(filters.kind === "sweeps" ? { kind: "sweep" as const } : filters.kind === "blocks" ? { kind: "block" as const } : {}),
|
|
232
265
|
...(filters.volOi === "off" ? {} : { minVolOi: Number(filters.volOi) }),
|
|
233
266
|
...(filters.expiry === "all" ? {} : { maxExpiryDays: Number(filters.expiry) }),
|
|
234
|
-
...(filters.universe === "watchlist" ? { symbols: [...watchlist].map((symbol) => symbol.toUpperCase()).sort() } : {}),
|
|
235
267
|
};
|
|
236
268
|
}
|
|
237
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The session summary's query as the Cloud route reads it: the same filters as
|
|
272
|
+
* the recorded prints, with one name as `symbol`, which also asks for its strikes.
|
|
273
|
+
*/
|
|
274
|
+
export function flowSessionSearch(filters: FlowFilters, scope: FlowScope): string {
|
|
275
|
+
const params = new URLSearchParams(flowHistorySearch({
|
|
276
|
+
...flowFilterQuery(filters),
|
|
277
|
+
...(!scope.symbol && filters.universe === "watchlist"
|
|
278
|
+
? { symbols: [...scope.watchlist].map((symbol) => symbol.toUpperCase()).sort() } : {}),
|
|
279
|
+
}));
|
|
280
|
+
if (scope.symbol) params.set("symbol", scope.symbol.toUpperCase());
|
|
281
|
+
return params.toString();
|
|
282
|
+
}
|
|
283
|
+
|
|
238
284
|
function localDay(at: number): string {
|
|
239
285
|
const date = new Date(at);
|
|
240
286
|
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|