@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
|
@@ -2,40 +2,31 @@ import type { FundMember } from "../../../api-client/members";
|
|
|
2
2
|
import type { Portfolio, Watchlist } from "../../../types/ticker";
|
|
3
3
|
import { getSectorCollection } from "../sectors/sector-data";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/** Focused industries, named by the fund whose published holdings define the group. */
|
|
6
|
+
const INDUSTRIES = [
|
|
7
|
+
{ etf: "SOXX", name: "Semiconductors" },
|
|
8
|
+
{ etf: "XPH", name: "Pharmaceuticals" },
|
|
9
|
+
{ etf: "XBI", name: "Biotech" },
|
|
10
|
+
{ etf: "ITA", name: "Aerospace & defense" },
|
|
11
|
+
{ etf: "KRE", name: "Regional banks" },
|
|
12
|
+
{ etf: "IGV", name: "Software" },
|
|
13
|
+
{ etf: "XRT", name: "Retail" },
|
|
14
|
+
{ etf: "XHB", name: "Homebuilders" },
|
|
15
|
+
{ etf: "XOP", name: "Oil & gas exploration" },
|
|
16
|
+
] as const;
|
|
11
17
|
|
|
12
|
-
/**
|
|
13
|
-
const SECTOR_OF_FUND: Readonly<Record<string, readonly string[]>> = {
|
|
14
|
-
XLK: ["Information Technology"],
|
|
15
|
-
XLV: ["Health Care"],
|
|
16
|
-
XLF: ["Financials"],
|
|
17
|
-
XLY: ["Consumer Discretionary"],
|
|
18
|
-
// The holdings file says "Communication"; GICS's own name is the longer one.
|
|
19
|
-
XLC: ["Communication", "Communication Services"],
|
|
20
|
-
XLI: ["Industrials"],
|
|
21
|
-
XLP: ["Consumer Staples"],
|
|
22
|
-
XLE: ["Energy"],
|
|
23
|
-
XLU: ["Utilities"],
|
|
24
|
-
XLRE: ["Real Estate"],
|
|
25
|
-
XLB: ["Materials"],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** Which companies ERN lists: the market's, the user's, one portfolio or watchlist, or one sector fund's. */
|
|
18
|
+
/** Which companies ERN lists: the market, the user's lists, or a fund's holdings. */
|
|
29
19
|
export type EarningsScope =
|
|
30
20
|
| { kind: "all" }
|
|
31
21
|
| { kind: "mine" }
|
|
32
22
|
| { kind: "portfolio" | "watchlist"; id: string; name: string }
|
|
33
|
-
| { kind: "
|
|
23
|
+
| { kind: "fund"; etf: string; name: string };
|
|
34
24
|
|
|
35
25
|
export interface EarningsScopeOption {
|
|
36
26
|
value: string;
|
|
37
27
|
label: string;
|
|
38
28
|
description?: string;
|
|
29
|
+
group?: string;
|
|
39
30
|
scope: EarningsScope;
|
|
40
31
|
}
|
|
41
32
|
|
|
@@ -47,30 +38,28 @@ export const MINE_SCOPE = "mine";
|
|
|
47
38
|
const kindNote = (name: string, kind: "Portfolio" | "Watchlist") =>
|
|
48
39
|
name.toLowerCase().includes(kind.toLowerCase()) ? undefined : kind;
|
|
49
40
|
|
|
50
|
-
/**
|
|
51
|
-
* The choices of ERN's Names filter, in menu order: the market, every held or
|
|
52
|
-
* watched name, each portfolio and watchlist, then SECT's sector funds, each
|
|
53
|
-
* named with its fund ("Technology (XLK)").
|
|
54
|
-
*/
|
|
41
|
+
/** Personal lists, broad sectors and focused industries stay separate in the Names menu. */
|
|
55
42
|
export function earningsScopeOptions(collections: { portfolios: readonly Portfolio[]; watchlists: readonly Watchlist[] }): EarningsScopeOption[] {
|
|
56
43
|
return [
|
|
57
44
|
{ value: ALL_SCOPE, label: "All", scope: { kind: "all" } },
|
|
58
45
|
{ value: MINE_SCOPE, label: "Mine", description: "Portfolios and watchlists", scope: { kind: "mine" } },
|
|
59
46
|
...collections.portfolios.map((portfolio): EarningsScopeOption => ({
|
|
60
|
-
value: `portfolio:${portfolio.id}`, label: portfolio.name, description: kindNote(portfolio.name, "Portfolio"),
|
|
47
|
+
value: `portfolio:${portfolio.id}`, label: portfolio.name, description: kindNote(portfolio.name, "Portfolio"), group: "Your lists",
|
|
61
48
|
scope: { kind: "portfolio", id: portfolio.id, name: portfolio.name },
|
|
62
49
|
})),
|
|
63
50
|
...collections.watchlists.map((watchlist): EarningsScopeOption => ({
|
|
64
|
-
value: `watchlist:${watchlist.id}`, label: watchlist.name, description: kindNote(watchlist.name, "Watchlist"),
|
|
51
|
+
value: `watchlist:${watchlist.id}`, label: watchlist.name, description: kindNote(watchlist.name, "Watchlist"), group: "Your lists",
|
|
65
52
|
scope: { kind: "watchlist", id: watchlist.id, name: watchlist.name },
|
|
66
53
|
})),
|
|
67
|
-
...getSectorCollection("sectors").items.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
54
|
+
...getSectorCollection("sectors").items.map((sector): EarningsScopeOption => ({
|
|
55
|
+
// Keep the saved sector choice when switching to that fund's own holdings.
|
|
56
|
+
value: `sector:${sector.etf}`, label: `${sector.name} (${sector.etf})`, group: "Sectors",
|
|
57
|
+
scope: { kind: "fund", etf: sector.etf, name: sector.name },
|
|
58
|
+
})),
|
|
59
|
+
...INDUSTRIES.map((industry): EarningsScopeOption => ({
|
|
60
|
+
value: `industry:${industry.etf}`, label: `${industry.name} (${industry.etf})`, group: "Industries",
|
|
61
|
+
scope: { kind: "fund", etf: industry.etf, name: industry.name },
|
|
62
|
+
})),
|
|
74
63
|
];
|
|
75
64
|
}
|
|
76
65
|
|
|
@@ -79,11 +68,10 @@ export function readEarningsScope(value: string, options: readonly EarningsScope
|
|
|
79
68
|
return options.find((option) => option.value === value) ?? options[0]!;
|
|
80
69
|
}
|
|
81
70
|
|
|
82
|
-
/** A
|
|
83
|
-
export function
|
|
84
|
-
const symbols = members
|
|
85
|
-
.filter((member) => member.symbol && member.sector && sectors.includes(member.sector))
|
|
71
|
+
/** A fund's companies, largest weight first; cash and other unlisted holdings have no symbol. */
|
|
72
|
+
export function fundSymbols(members: readonly FundMember[]): string[] {
|
|
73
|
+
const symbols = [...members]
|
|
86
74
|
.sort((left, right) => (right.weight ?? 0) - (left.weight ?? 0))
|
|
87
|
-
.
|
|
75
|
+
.flatMap((member) => member.symbol?.trim() ? [member.symbol.trim().toUpperCase()] : []);
|
|
88
76
|
return [...new Set(symbols)];
|
|
89
77
|
}
|
|
@@ -29,7 +29,7 @@ import { usePluginPaneState } from "../../runtime";
|
|
|
29
29
|
import { newYorkToday } from "../earnings/board-model";
|
|
30
30
|
import { ProWall, SignInWall } from "../cloud/auth-actions";
|
|
31
31
|
import { cachedUpcomingCalls, calendarAway, loadUpcomingCalls, statusOf, upcomingCutNotice, type UpcomingCallsResult } from "./data";
|
|
32
|
-
import {
|
|
32
|
+
import { callPeriod } from "./format";
|
|
33
33
|
import {
|
|
34
34
|
calendarDays,
|
|
35
35
|
calendarSections,
|
|
@@ -106,7 +106,7 @@ function renderCalendarCell(row: CalendarRow, column: CalendarColumn, now: numbe
|
|
|
106
106
|
return clock ? { text: clock, color: tone(colors.textDim) } : { text: MISSING, color: colors.textMuted };
|
|
107
107
|
}
|
|
108
108
|
case "period":
|
|
109
|
-
return { text:
|
|
109
|
+
return { text: callPeriod(call), color: tone(colors.textDim) };
|
|
110
110
|
case "away":
|
|
111
111
|
return { text: calendarAway(call, now), color: over ? colors.textMuted : colors.textBright };
|
|
112
112
|
}
|
|
@@ -2,7 +2,7 @@ import type { CloudEarningsCallPayload } from "../../../api-client";
|
|
|
2
2
|
import { formatShortDate } from "../../../utils/datetime-format";
|
|
3
3
|
|
|
4
4
|
/** "FQ4 26" — fiscal period, compact enough for a table column. */
|
|
5
|
-
|
|
5
|
+
function formatPeriod(
|
|
6
6
|
fiscalYear: number | null,
|
|
7
7
|
fiscalQuarter: number | null,
|
|
8
8
|
): string {
|
|
@@ -36,8 +36,37 @@ export function formatTimestamp(seconds: number): string {
|
|
|
36
36
|
return `${minutes}:${String(remainder).padStart(2, "0")}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
type NamedCall = Pick<
|
|
40
|
+
CloudEarningsCallPayload,
|
|
41
|
+
"ticker" | "fiscalYear" | "fiscalQuarter" | "eventKind" | "eventLabel"
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
/** An event that is not an earnings call (a deal call, an investor day), shown by its label and never as a quarter. */
|
|
45
|
+
export function eventLabelOf(call: Omit<NamedCall, "ticker">): string | null {
|
|
46
|
+
return call.eventKind === "other" && call.eventLabel ? call.eventLabel : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fits the period column; the full label is in the detail title.
|
|
50
|
+
const SHORT_EVENT_LABELS: Record<string, string> = {
|
|
51
|
+
"M&A call": "M&A",
|
|
52
|
+
conference: "Conf",
|
|
53
|
+
"investor day": "Inv day",
|
|
54
|
+
"business update": "Update",
|
|
55
|
+
"trial results": "Trial",
|
|
56
|
+
"shareholder call": "Holders",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The period column: "FQ4 26", or a short event label ("M&A", "Inv day"). */
|
|
60
|
+
export function callPeriod(call: Omit<NamedCall, "ticker">): string {
|
|
61
|
+
const label = eventLabelOf(call);
|
|
62
|
+
if (label) return SHORT_EVENT_LABELS[label] ?? label;
|
|
63
|
+
return formatPeriod(call.fiscalYear, call.fiscalQuarter);
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
/** Title for the detail stack: names the call so the body need not repeat it. */
|
|
40
|
-
export function callTitle(call:
|
|
67
|
+
export function callTitle(call: NamedCall): string {
|
|
68
|
+
const label = eventLabelOf(call);
|
|
69
|
+
if (label) return `${call.ticker} ${label}`;
|
|
41
70
|
const period = formatPeriod(call.fiscalYear, call.fiscalQuarter);
|
|
42
71
|
return period === "—" ? call.ticker : `${call.ticker} ${period}`;
|
|
43
72
|
}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type UpcomingCallsResult,
|
|
24
24
|
type UpcomingDays,
|
|
25
25
|
} from "./data";
|
|
26
|
-
import {
|
|
26
|
+
import { callPeriod, eventLabelOf } from "./format";
|
|
27
27
|
import type { MarketDay } from "../earnings/board-model";
|
|
28
28
|
import {
|
|
29
29
|
calendarDays,
|
|
@@ -53,6 +53,7 @@ const CALL_COLUMNS = [
|
|
|
53
53
|
{ key: "callAt", header: "Call date" },
|
|
54
54
|
{ key: "fiscalYear", header: "FY", align: "right" as const },
|
|
55
55
|
{ key: "fiscalQuarter", header: "FQ", align: "right" as const },
|
|
56
|
+
{ key: "event", header: "Event" },
|
|
56
57
|
{ key: "status", header: "Status" },
|
|
57
58
|
{ key: "durationSeconds", header: "Seconds", align: "right" as const },
|
|
58
59
|
{ key: "wordCount", header: "Words", align: "right" as const },
|
|
@@ -124,6 +125,8 @@ function callRow(call: CloudEarningsCallPayload, now: number) {
|
|
|
124
125
|
callAt: call.callAt,
|
|
125
126
|
fiscalYear: call.fiscalYear,
|
|
126
127
|
fiscalQuarter: call.fiscalQuarter,
|
|
128
|
+
// A deal call, a conference, an investor day: not a quarter's earnings call.
|
|
129
|
+
event: eventLabelOf(call),
|
|
127
130
|
status: call.hasTranscript ? "transcribed" : callStatusLabel(call, now) || call.status,
|
|
128
131
|
upcoming,
|
|
129
132
|
in: upcoming ? callCountdown(call, now) : null,
|
|
@@ -219,6 +222,7 @@ function transcriptResult(
|
|
|
219
222
|
company: transcript.companyName,
|
|
220
223
|
fiscalYear: transcript.fiscalYear,
|
|
221
224
|
fiscalQuarter: transcript.fiscalQuarter,
|
|
225
|
+
event: eventLabelOf(transcript),
|
|
222
226
|
callAt: transcript.callAt,
|
|
223
227
|
durationSeconds: transcript.durationSeconds,
|
|
224
228
|
wordCount: transcript.wordCount,
|
|
@@ -259,7 +263,8 @@ function projectCalendar(
|
|
|
259
263
|
timeKnown: callTimeKnown(call),
|
|
260
264
|
local: callLocalClock(call),
|
|
261
265
|
newYork: callNewYorkClock(call),
|
|
262
|
-
period:
|
|
266
|
+
period: callPeriod(call),
|
|
267
|
+
event: eventLabelOf(call),
|
|
263
268
|
fiscalYear: call.fiscalYear,
|
|
264
269
|
fiscalQuarter: call.fiscalQuarter,
|
|
265
270
|
in: calendarAway(call, now),
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
CloudEarningsTranscriptPayload,
|
|
4
4
|
CloudTranscriptTurnPayload,
|
|
5
5
|
} from "../../../api-client";
|
|
6
|
+
import { eventLabelOf } from "./format";
|
|
6
7
|
import { splitParagraphs } from "./prose";
|
|
7
8
|
|
|
8
9
|
export type TranscriptSection =
|
|
@@ -210,7 +211,11 @@ export function findCallForQuarter(
|
|
|
210
211
|
): CloudEarningsCallPayload | null {
|
|
211
212
|
const sorted = sortCallsNewest(calls);
|
|
212
213
|
const token = requestedQuarter.trim().toUpperCase().replace(/[\s_-]+/g, "");
|
|
213
|
-
|
|
214
|
+
// The latest earnings call: a deal call or an investor day held after it is
|
|
215
|
+
// not the quarter's call, unless the company has nothing else.
|
|
216
|
+
if (!token || token === "LATEST") {
|
|
217
|
+
return sorted.find((call) => !eventLabelOf(call)) ?? sorted[0] ?? null;
|
|
218
|
+
}
|
|
214
219
|
|
|
215
220
|
const quarterFirst = token.match(/^(?:F?Q)?([1-4])(?:\/?(\d{2}|\d{4}))?$/);
|
|
216
221
|
if (quarterFirst) {
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
loadTranscript,
|
|
50
50
|
statusOf,
|
|
51
51
|
} from "./data";
|
|
52
|
-
import { callTitle, formatCallDate, formatDuration,
|
|
52
|
+
import { callPeriod, callTitle, eventLabelOf, formatCallDate, formatDuration, formatSentiment } from "./format";
|
|
53
53
|
import { findCallForQuarter, findInLines, MIN_FIND_LENGTH, turnParagraphs } from "./model";
|
|
54
54
|
import { READER_TABS, TranscriptView, type ReaderTab, type TranscriptPlayback } from "./transcript-view";
|
|
55
55
|
import { PLAYBACK_RATES, SEEK_STEP_SECONDS, useCallPlayback } from "./use-call-playback";
|
|
@@ -101,7 +101,7 @@ function renderCell(call: CloudEarningsCallPayload, column: CallColumn, now: num
|
|
|
101
101
|
case "date":
|
|
102
102
|
return { text: formatCallDate(call.callAt), color: colors.textDim };
|
|
103
103
|
case "period":
|
|
104
|
-
return { text:
|
|
104
|
+
return { text: callPeriod(call) };
|
|
105
105
|
case "length":
|
|
106
106
|
return { text: formatDuration(call.durationSeconds), color: colors.textDim };
|
|
107
107
|
case "sentiment": {
|
|
@@ -525,7 +525,8 @@ function CallsListView({
|
|
|
525
525
|
[
|
|
526
526
|
call.ticker,
|
|
527
527
|
call.companyName ?? "",
|
|
528
|
-
|
|
528
|
+
callPeriod(call),
|
|
529
|
+
eventLabelOf(call) ?? "",
|
|
529
530
|
formatCallDate(call.callAt),
|
|
530
531
|
]
|
|
531
532
|
.join(" ")
|
|
@@ -1020,7 +1021,7 @@ function CallsListView({
|
|
|
1020
1021
|
<AudioPlayer
|
|
1021
1022
|
src={playback.audio.url}
|
|
1022
1023
|
sourceKey={selected.id}
|
|
1023
|
-
label={`${callTitle(selected)} call recording`}
|
|
1024
|
+
label={`${callTitle(selected)}${eventLabelOf(selected) ? "" : " call"} recording`}
|
|
1024
1025
|
durationSeconds={playback.audio.durationSeconds}
|
|
1025
1026
|
rates={PLAYBACK_RATES}
|
|
1026
1027
|
rate={playback.rate}
|
|
@@ -29,6 +29,8 @@ export interface EconEvent {
|
|
|
29
29
|
source?: { name: string; url?: string };
|
|
30
30
|
/** The indicator this release is of, the same from one release to the next; `id` is this row's alone. */
|
|
31
31
|
indicatorId?: string | null;
|
|
32
|
+
/** Which feed the forecast was read from (`week-feed`, `release-calendar`) and when that feed was read. */
|
|
33
|
+
consensus?: { source: string; readAt: string } | null;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Whether a row's time is blank because its publisher states none (a bank whose calendar gives the day only). */
|
|
@@ -60,6 +60,12 @@ export interface StatDef {
|
|
|
60
60
|
note: string;
|
|
61
61
|
/** Other words the catalog and the pane's filter find it by: `PMMS` for the mortgage rate. */
|
|
62
62
|
keywords?: readonly string[];
|
|
63
|
+
/**
|
|
64
|
+
* The names a reader gives the indicator, read as an argument ahead of any
|
|
65
|
+
* FRED series id: `PCE` is the price index release, not the series FRED files
|
|
66
|
+
* under that id (personal spending), and `gdp` is real GDP, not its nowcast.
|
|
67
|
+
*/
|
|
68
|
+
aliases?: readonly string[];
|
|
63
69
|
/** Concise measurement identity that must remain beside the values. */
|
|
64
70
|
measurementBasis?: string;
|
|
65
71
|
/**
|
|
@@ -17,7 +17,7 @@ import { formatMoveRank } from "../shared/move-rank";
|
|
|
17
17
|
import type { StatPoint } from "../shared/stat-transform";
|
|
18
18
|
import { formatSigma } from "../shared/trend";
|
|
19
19
|
import { STAT_CATEGORIES } from "./defs";
|
|
20
|
-
import { DEFAULT_STAT_ID,
|
|
20
|
+
import { DEFAULT_STAT_ID, readStatArg, STATS } from "./stats";
|
|
21
21
|
import { selectStatViews, type StatRangeId, type StatViewModel } from "./view";
|
|
22
22
|
|
|
23
23
|
/** How a report reads a statistic: a spread in basis points, everything else as the pane does. */
|
|
@@ -238,7 +238,7 @@ export const econStatisticsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
238
238
|
argument: {
|
|
239
239
|
kind: "free-text",
|
|
240
240
|
placeholder: "statistic",
|
|
241
|
-
description: "Optional statistic id, FRED series id,
|
|
241
|
+
description: "Optional statistic id, name (PCE, GDP), label, FRED series id, or prefix.",
|
|
242
242
|
optional: true,
|
|
243
243
|
},
|
|
244
244
|
options: [{
|
|
@@ -250,7 +250,8 @@ export const econStatisticsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
250
250
|
}],
|
|
251
251
|
describe: (args) => `Economic Statistics | ${String(args.options.range)}`,
|
|
252
252
|
async load(args, ctx) {
|
|
253
|
-
const
|
|
253
|
+
const read = typeof args.argument === "string" ? readStatArg(args.argument) : null;
|
|
254
|
+
const requested = read?.stat ?? null;
|
|
254
255
|
if (args.argument && !requested) {
|
|
255
256
|
throw new Error(
|
|
256
257
|
`Unknown economic statistic "${String(args.argument)}". Use one of: ${STATS.map(({ id }) => id).join(", ")}.`,
|
|
@@ -261,6 +262,7 @@ export const econStatisticsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
261
262
|
loader: createStatSeriesLoader(ctx.apiClient),
|
|
262
263
|
...(requested ? { stats: [requested] } : {}),
|
|
263
264
|
});
|
|
264
|
-
|
|
265
|
+
const report = projectStatsHeadlessBundle(bundle, range, requested?.id ?? DEFAULT_STAT_ID);
|
|
266
|
+
return read?.readAs ? { ...report, warnings: [read.readAs] } : report;
|
|
265
267
|
},
|
|
266
268
|
};
|
|
@@ -68,6 +68,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
68
68
|
}),
|
|
69
69
|
stat({
|
|
70
70
|
id: "pce-yoy",
|
|
71
|
+
aliases: ["pce", "pce inflation", "pce price index"],
|
|
71
72
|
label: "PCE Prices",
|
|
72
73
|
shortLabel: "PCE y/y",
|
|
73
74
|
category: "inflation",
|
|
@@ -179,6 +180,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
179
180
|
// ---- Growth ----------------------------------------------------------
|
|
180
181
|
stat({
|
|
181
182
|
id: "real-gdp",
|
|
183
|
+
aliases: ["gdp", "gdp growth"],
|
|
182
184
|
measurementBasis: "q/q annualized",
|
|
183
185
|
label: "Real GDP",
|
|
184
186
|
shortLabel: "Real GDP q/q",
|
|
@@ -637,15 +639,40 @@ export const STATS: readonly StatDef[] = [
|
|
|
637
639
|
|
|
638
640
|
export const DEFAULT_STAT_ID = STATS[0]!.id;
|
|
639
641
|
|
|
640
|
-
/**
|
|
641
|
-
|
|
642
|
+
/** A statistic an argument names, and what to say when it had to be read. */
|
|
643
|
+
interface StatArgRead {
|
|
644
|
+
stat: StatDef;
|
|
645
|
+
/** "Read "PCE" as PCE Prices (PCE y/y)." when the argument is a name or a prefix, not the statistic's id, label or FRED series; null when it spelled one. */
|
|
646
|
+
readAs: string | null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Reads a shortcut argument like `ECST cpi` as a statistic. What it is called
|
|
651
|
+
* comes before what FRED files it under: a statistic's id, its label, one of
|
|
652
|
+
* its common names (`PCE`, `gdp`), then a FRED series id, then the start of an
|
|
653
|
+
* id or a label. A name or a start says what it read, and a name that is also
|
|
654
|
+
* another statistic's FRED series (`PCE` is personal spending there) points at it.
|
|
655
|
+
*/
|
|
656
|
+
export function readStatArg(arg: string | null | undefined): StatArgRead | null {
|
|
642
657
|
const query = arg?.trim().toLowerCase();
|
|
643
658
|
if (!query) return null;
|
|
644
|
-
|
|
645
|
-
?? STATS.find((entry) => entry.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
659
|
+
const exact = STATS.find((entry) => entry.id === query)
|
|
660
|
+
?? STATS.find((entry) => entry.label.toLowerCase() === query || entry.shortLabel.toLowerCase() === query);
|
|
661
|
+
if (exact) return { stat: exact, readAs: null };
|
|
662
|
+
const named = STATS.find((entry) => entry.aliases?.some((alias) => alias.toLowerCase() === query));
|
|
663
|
+
const series = STATS.find((entry) => entry.seriesId.toLowerCase() === query);
|
|
664
|
+
if (!named && series) return { stat: series, readAs: null };
|
|
665
|
+
const stat = named ?? STATS.find((entry) =>
|
|
666
|
+
entry.id.startsWith(query)
|
|
667
|
+
|| entry.label.toLowerCase().startsWith(query)
|
|
668
|
+
|| entry.shortLabel.toLowerCase().startsWith(query));
|
|
669
|
+
if (!stat) return null;
|
|
670
|
+
const otherSeries = series && series !== stat
|
|
671
|
+
? ` FRED's series ${series.seriesId} is ${series.label.toLowerCase()}: type ${series.id} for it.` : "";
|
|
672
|
+
return { stat, readAs: `Read "${arg!.trim()}" as ${stat.label} (${stat.shortLabel}).${otherSeries}` };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Resolves shortcut arguments like `ECST cpi` onto a statistic. */
|
|
676
|
+
export function resolveStatArg(arg: string | null | undefined): StatDef | null {
|
|
677
|
+
return readStatArg(arg)?.stat ?? null;
|
|
651
678
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ScreenDefinition, ScreenPayload, ScreenRow } from "../../../api-client/equity-screener";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The sector and industry names a screen's `sector` and `industry` criteria
|
|
5
|
+
* match, as the screener's rows carry them (read from the covered universe on
|
|
6
|
+
* 2026-10-28): the eleven sectors and the industries under them. A criterion
|
|
7
|
+
* matches a name exactly, so `Utility` or `utilities` matches nothing.
|
|
8
|
+
*/
|
|
9
|
+
export const SCREEN_SECTORS = [
|
|
10
|
+
"Basic Materials", "Communication Services", "Consumer Cyclical", "Consumer Defensive", "Energy",
|
|
11
|
+
"Financial Services", "Healthcare", "Industrials", "Real Estate", "Technology", "Utilities",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export const SCREEN_INDUSTRIES = [
|
|
15
|
+
"Advertising Agencies", "Aerospace & Defense", "Agricultural Inputs", "Airlines", "Airports & Air Services",
|
|
16
|
+
"Aluminum", "Apparel Manufacturing", "Apparel Retail", "Asset Management", "Auto Manufacturers", "Auto Parts",
|
|
17
|
+
"Auto & Truck Dealerships", "Banks - Diversified", "Banks - Regional", "Beverages - Brewers",
|
|
18
|
+
"Beverages - Non-Alcoholic", "Beverages - Wineries & Distilleries", "Biotechnology", "Broadcasting",
|
|
19
|
+
"Building Materials", "Building Products & Equipment", "Business Equipment & Supplies", "Capital Markets",
|
|
20
|
+
"Chemicals", "Coking Coal", "Communication Equipment", "Computer Hardware", "Confectioners", "Conglomerates",
|
|
21
|
+
"Consulting Services", "Consumer Electronics", "Copper", "Credit Services", "Department Stores",
|
|
22
|
+
"Diagnostics & Research", "Discount Stores", "Drug Manufacturers - General",
|
|
23
|
+
"Drug Manufacturers - Specialty & Generic", "Education & Training Services", "Electrical Equipment & Parts",
|
|
24
|
+
"Electronic Components", "Electronic Gaming & Multimedia", "Electronics & Computer Distribution",
|
|
25
|
+
"Engineering & Construction", "Entertainment", "Farm & Heavy Construction Machinery", "Farm Products",
|
|
26
|
+
"Financial Conglomerates", "Financial Data & Stock Exchanges", "Food Distribution", "Footwear & Accessories",
|
|
27
|
+
"Furnishings, Fixtures & Appliances", "Gambling", "Gold", "Grocery Stores", "Healthcare Plans",
|
|
28
|
+
"Health Information Services", "Home Improvement Retail", "Household & Personal Products",
|
|
29
|
+
"Industrial Distribution", "Information Technology Services", "Infrastructure Operations", "Insurance Brokers",
|
|
30
|
+
"Insurance - Diversified", "Insurance - Life", "Insurance - Property & Casualty", "Insurance - Reinsurance",
|
|
31
|
+
"Insurance - Specialty", "Integrated Freight & Logistics", "Internet Content & Information", "Internet Retail",
|
|
32
|
+
"Leisure", "Lodging", "Lumber & Wood Production", "Luxury Goods", "Marine Shipping", "Medical Care Facilities",
|
|
33
|
+
"Medical Devices", "Medical Distribution", "Medical Instruments & Supplies", "Metal Fabrication",
|
|
34
|
+
"Mortgage Finance", "Oil & Gas Drilling", "Oil & Gas E&P", "Oil & Gas Equipment & Services",
|
|
35
|
+
"Oil & Gas Integrated", "Oil & Gas Midstream", "Oil & Gas Refining & Marketing",
|
|
36
|
+
"Other Industrial Metals & Mining", "Other Precious Metals & Mining", "Packaged Foods",
|
|
37
|
+
"Packaging & Containers", "Paper & Paper Products", "Personal Services", "Pharmaceutical Retailers",
|
|
38
|
+
"Pollution & Treatment Controls", "Publishing", "Railroads", "Real Estate - Development",
|
|
39
|
+
"Real Estate Services", "Recreational Vehicles", "REIT - Diversified", "REIT - Healthcare Facilities",
|
|
40
|
+
"REIT - Hotel & Motel", "REIT - Industrial", "REIT - Mortgage", "REIT - Office", "REIT - Residential",
|
|
41
|
+
"REIT - Retail", "REIT - Specialty", "Rental & Leasing Services", "Residential Construction",
|
|
42
|
+
"Resorts & Casinos", "Restaurants", "Scientific & Technical Instruments", "Security & Protection Services",
|
|
43
|
+
"Semiconductor Equipment & Materials", "Semiconductors", "Shell Companies", "Silver",
|
|
44
|
+
"Software - Application", "Software - Infrastructure", "Solar", "Specialty Business Services",
|
|
45
|
+
"Specialty Chemicals", "Specialty Industrial Machinery", "Specialty Retail", "Staffing & Employment Services",
|
|
46
|
+
"Steel", "Telecom Services", "Textile Manufacturing", "Thermal Coal", "Tobacco", "Tools & Accessories",
|
|
47
|
+
"Travel Services", "Trucking", "Uranium", "Utilities - Diversified", "Utilities - Independent Power Producers",
|
|
48
|
+
"Utilities - Regulated Electric", "Utilities - Regulated Gas", "Utilities - Regulated Water",
|
|
49
|
+
"Utilities - Renewable", "Waste Management",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
/** GICS sector names, which these sectors are not, as the sector each means. */
|
|
53
|
+
const GICS_SECTORS: Record<string, (typeof SCREEN_SECTORS)[number]> = {
|
|
54
|
+
informationtechnology: "Technology",
|
|
55
|
+
financials: "Financial Services",
|
|
56
|
+
healthcare: "Healthcare",
|
|
57
|
+
consumerstaples: "Consumer Defensive",
|
|
58
|
+
consumerdiscretionary: "Consumer Cyclical",
|
|
59
|
+
materials: "Basic Materials",
|
|
60
|
+
telecommunicationservices: "Communication Services",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The category fields whose names a screen can be checked against. A symbol has no list to check. */
|
|
64
|
+
type NamedField = "sector" | "industry" | "exchange" | "currency";
|
|
65
|
+
const NAMED_FIELDS: readonly NamedField[] = ["sector", "industry", "exchange", "currency"];
|
|
66
|
+
|
|
67
|
+
/** A criterion's value that names nothing the screen covers, with the names it was likely meant as. */
|
|
68
|
+
interface UnmatchedCategoryValue {
|
|
69
|
+
field: NamedField;
|
|
70
|
+
value: string;
|
|
71
|
+
closest: string[];
|
|
72
|
+
/** The value is a name of the other of sector and industry: `Solar` given as a sector. */
|
|
73
|
+
otherField?: "sector" | "industry";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Names compared without case, spaces or punctuation: `Oil and Gas E&P` beside `oil & gas e&p`. */
|
|
77
|
+
const fold = (name: string) => name.toLowerCase().replace(/\band\b/g, "&").replace(/[^a-z0-9&]+/g, "");
|
|
78
|
+
|
|
79
|
+
function editDistance(left: string, right: string): number {
|
|
80
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
81
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
82
|
+
const current = [row];
|
|
83
|
+
for (let column = 1; column <= right.length; column += 1) {
|
|
84
|
+
const cost = left[row - 1] === right[column - 1] ? 0 : 1;
|
|
85
|
+
current[column] = Math.min(previous[column]! + 1, current[column - 1]! + 1, previous[column - 1]! + cost);
|
|
86
|
+
}
|
|
87
|
+
previous = current;
|
|
88
|
+
}
|
|
89
|
+
return previous[right.length]!;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** How far a typed name is from a candidate, 0 the same and 1 nothing alike: against the whole name or one of its parts. */
|
|
93
|
+
function nameDistance(typed: string, candidate: string): number {
|
|
94
|
+
const needle = fold(typed);
|
|
95
|
+
if (!needle) return 1;
|
|
96
|
+
const parts = [candidate, ...candidate.split(/\s+-\s+|\s+/)].map(fold).filter(Boolean);
|
|
97
|
+
return Math.min(...parts.map((part) => {
|
|
98
|
+
if (part === needle) return 0;
|
|
99
|
+
// "Bank" in "Banks - Regional", "Regulated" in "Utilities - Regulated Gas".
|
|
100
|
+
if (needle.length >= 4 && part.startsWith(needle)) return 0.2;
|
|
101
|
+
return editDistance(needle, part) / Math.max(needle.length, part.length);
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Names further than this from what was typed are not offered. */
|
|
106
|
+
const CLOSEST_DISTANCE = 0.4;
|
|
107
|
+
const CLOSEST_LIMIT = 5;
|
|
108
|
+
|
|
109
|
+
/** The names a typed one was likely meant as, closest first. */
|
|
110
|
+
function closestCategoryNames(typed: string, names: Iterable<string>, limit = CLOSEST_LIMIT): string[] {
|
|
111
|
+
const scored = [...new Set(names)].map((name) => ({ name, distance: nameDistance(typed, name) }));
|
|
112
|
+
// The name typed in another case or spacing is the one meant, whatever else is near it.
|
|
113
|
+
const same = scored.filter(({ name }) => fold(name) === fold(typed));
|
|
114
|
+
return (same.length > 0 ? same : scored)
|
|
115
|
+
.filter(({ distance }) => distance <= CLOSEST_DISTANCE)
|
|
116
|
+
.sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name))
|
|
117
|
+
.slice(0, limit)
|
|
118
|
+
.map(({ name }) => name);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The names each category field matches: the sector and industry lists above
|
|
123
|
+
* with those the rows carry, and the exchanges and currencies the covered
|
|
124
|
+
* universe reports. A field with no list (an older server's universe) is left
|
|
125
|
+
* out, so nothing is said of its values.
|
|
126
|
+
*/
|
|
127
|
+
function knownNames(payload: Pick<ScreenPayload, "universe">, rows: readonly Pick<ScreenRow, NamedField>[]): Record<NamedField, Set<string> | null> {
|
|
128
|
+
const seen = (field: NamedField) => rows.flatMap((row) => row[field] ? [row[field]!] : []);
|
|
129
|
+
const served = (list: readonly string[] | undefined, field: NamedField) =>
|
|
130
|
+
list?.length ? new Set([...list, ...seen(field)]) : null;
|
|
131
|
+
return {
|
|
132
|
+
sector: new Set([...SCREEN_SECTORS, ...payload.universe.sectors ?? [], ...seen("sector")]),
|
|
133
|
+
industry: new Set([...SCREEN_INDUSTRIES, ...seen("industry")]),
|
|
134
|
+
exchange: served(payload.universe.exchanges, "exchange"),
|
|
135
|
+
currency: served(payload.universe.currencies, "currency"),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The values of a screen's category criteria that name nothing the screen
|
|
141
|
+
* covers (`Utility` for the Utilities sector), so the screen reads why it
|
|
142
|
+
* matched nothing, or less than it should, rather than "No data".
|
|
143
|
+
*/
|
|
144
|
+
export function unmatchedCategoryValues(
|
|
145
|
+
definition: ScreenDefinition,
|
|
146
|
+
payload: Pick<ScreenPayload, "universe">,
|
|
147
|
+
rows: readonly Pick<ScreenRow, NamedField>[] = [],
|
|
148
|
+
): UnmatchedCategoryValue[] {
|
|
149
|
+
const known = knownNames(payload, rows);
|
|
150
|
+
const unmatched: UnmatchedCategoryValue[] = [];
|
|
151
|
+
for (const criterion of definition.criteria) {
|
|
152
|
+
if (criterion.op !== "in" || !(NAMED_FIELDS as readonly string[]).includes(criterion.field)) continue;
|
|
153
|
+
const field = criterion.field as NamedField;
|
|
154
|
+
const names = known[field];
|
|
155
|
+
if (!names) continue;
|
|
156
|
+
for (const value of criterion.value) {
|
|
157
|
+
if (names.has(value)) continue;
|
|
158
|
+
const other = field === "sector" ? "industry" : field === "industry" ? "sector" : null;
|
|
159
|
+
const otherNames = other ? known[other] : null;
|
|
160
|
+
const otherField = other && otherNames && [...otherNames].some((name) => fold(name) === fold(value)) ? other : undefined;
|
|
161
|
+
const gics = field === "sector" ? GICS_SECTORS[fold(value)] : undefined;
|
|
162
|
+
const closest = gics ? [gics] : closestCategoryNames(value, names);
|
|
163
|
+
unmatched.push({ field, value, closest, ...(otherField ? { otherField } : {}) });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return unmatched;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const quoted = (value: string) => JSON.stringify(value);
|
|
170
|
+
|
|
171
|
+
/** What a screen says of a value that names nothing: `No sector is named "Utility". Closest: Utilities.` */
|
|
172
|
+
export function unmatchedCategoryText(entry: UnmatchedCategoryValue): string {
|
|
173
|
+
const head = `No ${entry.field} is named ${quoted(entry.value)}`;
|
|
174
|
+
if (entry.otherField) return `${head}; it is ${entry.otherField === "industry" ? "an industry" : "a sector"} name, so use "field": ${quoted(entry.otherField)}.`;
|
|
175
|
+
return entry.closest.length > 0 ? `${head}. Closest: ${entry.closest.join(", ")}.` : `${head}.`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Where every sector and industry name is listed, said once after the values that name nothing. */
|
|
179
|
+
export const CATEGORY_NAMES_POINTER = "gloom catalog EQS lists every sector and industry.";
|
|
@@ -21,7 +21,7 @@ const savedPath = (id: string) => `saved/${encodeURIComponent(id)}`;
|
|
|
21
21
|
export const screenerApi = {
|
|
22
22
|
query: (query: ScreenQuery, signal?: AbortSignal, client: ScreenerClient = apiClient) =>
|
|
23
23
|
client.equityScreener<ScreenPayload>("query", json("POST", query, { signal })),
|
|
24
|
-
fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis,volatility,dividends", { signal }),
|
|
24
|
+
fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis,volatility,dividends,technical,option-volatility,ownership", { signal }),
|
|
25
25
|
export: (definition: ScreenDefinition, snapshotId: string) =>
|
|
26
26
|
apiClient.equityScreener<ScreenExportResponse>("export", json("POST", { ...definition, snapshotId }, { headers: { Accept: "application/json" } })),
|
|
27
27
|
saved: async (signal?: AbortSignal, client: ScreenerClient = apiClient) =>
|
|
@@ -40,10 +40,12 @@ export async function fetchScreen(
|
|
|
40
40
|
cursor: string | null = null,
|
|
41
41
|
signal?: AbortSignal,
|
|
42
42
|
client: ScreenerClient = apiClient,
|
|
43
|
+
/** Rows a page holds, at most the server's 200. */
|
|
44
|
+
limit = 100,
|
|
43
45
|
): Promise<ScreenPayload> {
|
|
44
46
|
const payload = validateScreenPayload(
|
|
45
47
|
await screenerApi.query(
|
|
46
|
-
{ ...parseScreenDefinition(definition), limit
|
|
48
|
+
{ ...parseScreenDefinition(definition), limit, cursor },
|
|
47
49
|
signal,
|
|
48
50
|
client,
|
|
49
51
|
),
|