gloomberb 0.16.3 → 0.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/api-client/askg.ts +43 -0
- package/src/api-client/auth.ts +15 -0
- package/src/api-client/beneficial-owners.ts +4 -0
- package/src/api-client/chat.ts +19 -0
- package/src/api-client/company-kpis.ts +14 -2
- package/src/api-client/data.ts +34 -16
- package/src/api-client/index.ts +12 -0
- package/src/api-client/normalizers.ts +27 -0
- package/src/api-client/paths.ts +88 -23
- package/src/api-client/request.ts +2 -1
- package/src/api-client/research-activity.ts +31 -0
- package/src/api-client/socket.ts +39 -8
- package/src/api-client/types.ts +57 -1
- package/src/api-client/upgrade-experiment.ts +90 -0
- package/src/app/global-shortcuts.ts +11 -1
- package/src/app/keybindings/actions.ts +10 -0
- package/src/app/pane-runtime/index.tsx +8 -0
- package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +65 -25
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
- package/src/app/runtime/desktop-deeplink.ts +5 -0
- package/src/app/star-prompt/model.ts +98 -0
- package/src/app/star-prompt/runtime.ts +41 -0
- package/src/app/star-prompt/status-line.tsx +190 -0
- package/src/app.tsx +13 -7
- package/src/architecture/core-builtin-allowlist.json +70 -0
- package/src/brokers/signed-in/adapter.ts +33 -2
- package/src/brokers/signed-in/client.ts +10 -0
- package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
- package/src/brokers/signed-in/sign-in.ts +62 -5
- package/src/capabilities/registry.ts +8 -1
- package/src/capabilities/types.ts +2 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +359 -99
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +164 -26
- package/src/cli/commands/search.ts +29 -8
- package/src/cli/commands/system.ts +99 -19
- package/src/cli/commands/themes.ts +64 -0
- package/src/cli/commands/ticker.ts +62 -30
- package/src/cli/desktop-pane-shot.ts +97 -16
- package/src/cli/errors.ts +25 -2
- package/src/cli/freshness.ts +7 -4
- package/src/cli/fx-pair.ts +57 -0
- package/src/cli/help.ts +14 -1
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +9 -6
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +50 -6
- package/src/cli/pane-functions/capabilities.ts +8 -2
- package/src/cli/pane-functions/catalog.ts +13 -2
- package/src/cli/pane-functions/data.ts +36 -5
- package/src/cli/pane-functions/dom.ts +21 -13
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +67 -22
- package/src/cli/pane-functions/headless.ts +5 -4
- package/src/cli/pane-functions/index.ts +3 -0
- package/src/cli/pane-functions/market-session.ts +133 -0
- package/src/cli/pane-functions/options.ts +15 -4
- package/src/cli/pane-functions/report-notices.ts +63 -0
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +4 -6
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +105 -23
- package/src/cli/registry.ts +2 -1
- package/src/cli/result.ts +50 -13
- package/src/cli/row-window.ts +76 -0
- package/src/cli/scroll-truncation.ts +34 -0
- package/src/components/chart/composite/composite-chart.tsx +43 -4
- package/src/components/chart/composite/legend.tsx +39 -6
- package/src/components/chart/composite/rasterizer.ts +64 -4
- package/src/components/chart/composite/series-cues.ts +71 -0
- package/src/components/chart/composite/text-renderer.ts +16 -8
- package/src/components/chart/composite/types.ts +3 -0
- package/src/components/command-bar/commands/direct/index.ts +9 -1
- package/src/components/command-bar/commands/plugin/items.ts +3 -1
- package/src/components/command-bar/commands/registry.ts +7 -0
- package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/routes/root/command-items.ts +2 -0
- package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
- package/src/components/command-bar/routes/root/results.ts +40 -10
- package/src/components/command-bar/routes/root/runtime.ts +8 -0
- package/src/components/command-bar/routes/root/search-providers.ts +97 -14
- package/src/components/command-bar/surface/index.tsx +18 -4
- package/src/components/command-bar/theme-picker.tsx +48 -13
- package/src/components/command-bar/workflow/ops.ts +5 -3
- package/src/components/data-table/view.tsx +28 -9
- package/src/components/layout/floating-pane.tsx +6 -2
- package/src/components/layout/market-summary.ts +23 -9
- package/src/components/layout/pane/content.tsx +46 -29
- package/src/components/layout/pane/cycle-order.ts +2 -11
- package/src/components/layout/pane/header.tsx +2 -1
- package/src/components/layout/pane/title.ts +1 -1
- package/src/components/layout/presentation.ts +22 -0
- package/src/components/layout/shell/active-drag.ts +1 -1
- package/src/components/layout/shell/drag/index.ts +4 -0
- package/src/components/layout/shell/drag/live.ts +76 -5
- package/src/components/layout/shell/drag/runtime.ts +8 -8
- package/src/components/layout/shell/index.tsx +47 -6
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +51 -10
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/pane/new-layout.ts +116 -0
- package/src/components/layout/shell/shortcuts.ts +5 -2
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-text.tsx +9 -5
- package/src/components/metric-treemap/heat-surface.tsx +27 -8
- package/src/components/metric-treemap/heat-tiles.ts +48 -17
- package/src/components/onboarding/onboarding-frame.tsx +33 -10
- package/src/components/onboarding/onboarding-wizard.tsx +49 -40
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
- package/src/components/onboarding/pro-step-layout.ts +86 -0
- package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
- package/src/components/ui/button.tsx +10 -0
- package/src/components/ui/data-table/index.tsx +38 -0
- package/src/components/ui/data-table/types.ts +8 -1
- package/src/components/ui/data-table/visible-range.ts +2 -0
- package/src/components/ui/icon.tsx +11 -2
- package/src/components/ui/index.ts +1 -1
- package/src/core/connection-health.ts +1 -1
- package/src/core/state/app/layout-reducer.ts +89 -3
- package/src/core/state/app/state.ts +8 -5
- package/src/core/state/app/types.ts +13 -2
- package/src/data/config/layout.ts +55 -21
- package/src/data/config/store/fold-panes.ts +149 -0
- package/src/data/config/store/migrations.ts +7 -0
- package/src/data/config/store/node.ts +27 -6
- package/src/data/config/store/normalize.ts +37 -3
- package/src/i18n/es.ts +14 -1
- package/src/i18n/ja.ts +14 -1
- package/src/i18n/ko.ts +14 -1
- package/src/i18n/zh-cn.ts +14 -1
- package/src/i18n/zh-tw.ts +14 -1
- package/src/layout/desks.ts +1 -2
- package/src/layout/pane-layout-move.ts +264 -0
- package/src/layout/pane-manager/hidden-panes.ts +160 -0
- package/src/layout/pane-manager/index.ts +1 -0
- package/src/market-data/coordinator/entries.ts +7 -1
- package/src/market-data/coordinator/quotes.ts +2 -2
- package/src/market-data/market/freshness.ts +118 -18
- package/src/market-data/market/status.ts +40 -0
- package/src/market-data/options-alternatives.ts +69 -0
- package/src/market-data/quotes/contributions.ts +19 -2
- package/src/market-data/quotes/metadata.ts +16 -0
- package/src/market-data/quotes/screener-live-quotes.ts +56 -5
- package/src/plugins/builtin/account-management/keyboard.ts +3 -0
- package/src/plugins/builtin/account-management/model.ts +1 -0
- package/src/plugins/builtin/account-management/navigation.ts +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +31 -4
- package/src/plugins/builtin/alerts/quotes.ts +3 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
- package/src/plugins/builtin/analytics/risk-model.ts +37 -8
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/broker-manager/model.ts +8 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
- package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
- package/src/plugins/builtin/broker-manager/table.ts +5 -0
- package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +34 -36
- package/src/plugins/builtin/chat/command-bar.ts +138 -0
- package/src/plugins/builtin/chat/composer-commands.ts +21 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
- package/src/plugins/builtin/chat/content/footer.tsx +5 -3
- package/src/plugins/builtin/chat/content/index.tsx +21 -1
- package/src/plugins/builtin/chat/content/mentions.ts +2 -1
- package/src/plugins/builtin/chat/content/user-map.ts +3 -1
- package/src/plugins/builtin/chat/controller/index.ts +48 -0
- package/src/plugins/builtin/chat/conversations.ts +256 -0
- package/src/plugins/builtin/chat/discord-command.ts +91 -0
- package/src/plugins/builtin/chat/edit-window.ts +12 -6
- package/src/plugins/builtin/chat/ghost-user.ts +14 -0
- package/src/plugins/builtin/chat/index.tsx +40 -2
- package/src/plugins/builtin/chat/layout.ts +2 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/message/header.tsx +24 -15
- package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/pane-requests.ts +60 -0
- package/src/plugins/builtin/chat/pane-state.ts +32 -2
- package/src/plugins/builtin/chat/pane.tsx +8 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
- package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
- package/src/plugins/builtin/cloud/askg/model.ts +129 -70
- package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
- package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
- package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
- package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
- package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
- package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
- package/src/plugins/builtin/cloud/plugin.tsx +36 -1
- package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
- package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
- package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
- package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
- package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
- package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
- package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
- package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
- package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
- package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
- package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
- package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
- package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
- package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
- package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
- package/src/plugins/builtin/comment-letters/client.ts +1 -3
- package/src/plugins/builtin/company-kpis/client.ts +23 -8
- package/src/plugins/builtin/company-kpis/headless.ts +3 -1
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
- package/src/plugins/builtin/composite-plugins.ts +107 -64
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
- package/src/plugins/builtin/credit-boards/index.tsx +21 -21
- package/src/plugins/builtin/credit-boards/model.ts +0 -1
- package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
- package/src/plugins/builtin/debt-maturities/client.ts +37 -9
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/data.ts +28 -7
- package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
- package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +42 -17
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/index.tsx +68 -22
- package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +17 -0
- package/src/plugins/builtin/executives/data.ts +17 -6
- package/src/plugins/builtin/executives/pane.tsx +16 -7
- package/src/plugins/builtin/filing-events/pane.tsx +22 -5
- package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
- package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
- package/src/plugins/builtin/gpu/headless.ts +9 -3
- package/src/plugins/builtin/gpu/model.ts +14 -5
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/headless.ts +6 -1
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/insider/index.tsx +4 -4
- package/src/plugins/builtin/iv-history/charts.tsx +21 -7
- package/src/plugins/builtin/iv-history/client.ts +12 -2
- package/src/plugins/builtin/iv-history/format.ts +33 -2
- package/src/plugins/builtin/iv-history/headless.ts +22 -10
- package/src/plugins/builtin/iv-history/index.tsx +3 -3
- package/src/plugins/builtin/iv-history/model.ts +61 -13
- package/src/plugins/builtin/iv-history/pane.tsx +11 -3
- package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
- package/src/plugins/builtin/jobs/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
- package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
- package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
- package/src/plugins/builtin/layout-manager/index.tsx +9 -1
- package/src/plugins/builtin/macro-day/client.ts +24 -0
- package/src/plugins/builtin/macro-day/headless.ts +11 -8
- package/src/plugins/builtin/macro-day/index.tsx +3 -0
- package/src/plugins/builtin/macro-day/model.ts +11 -6
- package/src/plugins/builtin/macro-day/pane.tsx +12 -8
- package/src/plugins/builtin/macro-day/release-table.ts +40 -0
- package/src/plugins/builtin/macro-day/releases.ts +62 -44
- package/src/plugins/builtin/market-heatmap/data.ts +25 -5
- package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
- package/src/plugins/builtin/market-heatmap/model.ts +94 -3
- package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
- package/src/plugins/builtin/market-movers/headless.ts +31 -4
- package/src/plugins/builtin/market-movers/index.tsx +8 -1
- package/src/plugins/builtin/market-movers/model.ts +71 -17
- package/src/plugins/builtin/market-movers/table.ts +50 -23
- package/src/plugins/builtin/news/headless.ts +9 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
- package/src/plugins/builtin/openfda/client.ts +1 -1
- package/src/plugins/builtin/options/carry.ts +95 -0
- package/src/plugins/builtin/options/index.tsx +58 -5
- package/src/plugins/builtin/options/strike-window.ts +183 -0
- package/src/plugins/builtin/options/table.ts +55 -3
- package/src/plugins/builtin/options/types.ts +8 -1
- package/src/plugins/builtin/options/view.tsx +161 -12
- package/src/plugins/builtin/options-positioning/headless.ts +4 -2
- package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
- package/src/plugins/builtin/options-scenario/client.ts +73 -17
- package/src/plugins/builtin/options-scenario/headless.ts +71 -24
- package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
- package/src/plugins/builtin/options-scenario/index.tsx +9 -2
- package/src/plugins/builtin/options-scenario/model.ts +33 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
- package/src/plugins/builtin/pe-band/model.ts +2 -2
- package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
- package/src/plugins/builtin/perps/equity-row.tsx +8 -5
- package/src/plugins/builtin/perps/model.ts +16 -5
- package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
- package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
- package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
- package/src/plugins/builtin/plugin-module.ts +87 -4
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
- package/src/plugins/builtin/realized-vol/headless.ts +3 -0
- package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
- package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
- package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
- package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
- package/src/plugins/builtin/risk-factors/data.ts +23 -9
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
- package/src/plugins/builtin/seasonality/pane.tsx +1 -1
- package/src/plugins/builtin/sec/index.tsx +5 -5
- package/src/plugins/builtin/sectors/client.ts +10 -2
- package/src/plugins/builtin/sectors/headless.ts +35 -6
- package/src/plugins/builtin/sectors/index.tsx +18 -1
- package/src/plugins/builtin/sectors/sector-model.ts +150 -41
- package/src/plugins/builtin/shared/command-tab.ts +51 -0
- package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
- package/src/plugins/builtin/shared/members-menu.tsx +4 -4
- package/src/plugins/builtin/shared/report-freshness.ts +50 -2
- package/src/plugins/builtin/shared/ticker-request.ts +25 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
- package/src/plugins/builtin/shared/volatility/options.ts +1 -46
- package/src/plugins/builtin/short-interest/index.tsx +19 -4
- package/src/plugins/builtin/short-interest/surface.tsx +4 -4
- package/src/plugins/builtin/short-volume/index.tsx +1 -10
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
- package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
- package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
- package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
- package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
- package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
- package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
- package/src/plugins/builtin/vol-surface/client.ts +6 -2
- package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
- package/src/plugins/builtin/vol-surface/headless.ts +124 -20
- package/src/plugins/builtin/vol-surface/model.ts +23 -11
- package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
- package/src/plugins/builtin/vol-surface/tables.ts +124 -0
- package/src/plugins/builtin/world-indices/client.ts +22 -4
- package/src/plugins/builtin/world-indices/headless.ts +54 -27
- package/src/plugins/builtin/world-indices/model.ts +4 -2
- package/src/plugins/builtin/world-indices/table.tsx +13 -2
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/builtin-editorial.ts +126 -0
- package/src/plugins/catalog-backend.ts +16 -9
- package/src/plugins/catalog-browser.ts +127 -63
- package/src/plugins/catalog-manifest.ts +73 -40
- package/src/plugins/catalog-ui.ts +26 -6
- package/src/plugins/extracted-plugins.ts +7 -1
- package/src/plugins/ownership.ts +150 -7
- package/src/plugins/plugin-functions.ts +54 -0
- package/src/plugins/plugin-off.ts +36 -0
- package/src/plugins/registry/host-actions.ts +16 -0
- package/src/plugins/registry/index.ts +84 -24
- package/src/plugins/registry/plugin-state.ts +25 -1
- package/src/plugins/runtime/context.tsx +29 -0
- package/src/react/input.ts +48 -1
- package/src/remote/app-host.tsx +5 -0
- package/src/remote/controller.ts +3 -53
- package/src/remote/matching.ts +69 -0
- package/src/remote/schema.ts +1 -1
- package/src/remote/semantic-tree.tsx +14 -0
- package/src/remote/types.ts +2 -0
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/query-bar.tsx +59 -32
- package/src/renderers/dom/host/box.tsx +1 -1
- package/src/renderers/dom/host/drag-shield.ts +17 -6
- package/src/renderers/dom/host/live-frame.ts +10 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +13 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +11 -5
- package/src/sources/provider-router/financials.ts +42 -15
- package/src/sources/provider-router/history.ts +27 -11
- package/src/sources/provider-router/index.ts +1 -1
- package/src/sources/provider-router/primary.ts +23 -5
- package/src/sources/sec-edgar.ts +68 -8
- package/src/sync/core-contributors.ts +4 -3
- package/src/test-support/cli-context.ts +2 -1
- package/src/test-support/data-provider.ts +34 -0
- package/src/test-support/shell-pane-keys.tsx +1 -0
- package/src/theme/series-colors.ts +46 -0
- package/src/theme/themes.ts +149 -0
- package/src/tickers/search/index.ts +22 -2
- package/src/tickers/search/ranking.ts +45 -1
- package/src/tickers/search/types.ts +3 -1
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/headless.ts +11 -0
- package/src/types/instrument.ts +8 -0
- package/src/types/plugin.ts +49 -3
- package/src/ui/host.tsx +8 -4
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history.ts +39 -6
- package/src/utils/rpc-timeout-error.ts +6 -2
- package/src/utils/sec-eps-basis.ts +159 -30
- package/src/utils/sec.ts +38 -34
- package/src/utils/throttled-fetch.ts +51 -22
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
usePaneStatusFooter,
|
|
20
20
|
type MetricTreemapDirection,
|
|
21
21
|
type MetricTreemapItem,
|
|
22
|
+
type PaneFooterSegment,
|
|
22
23
|
} from "../../../components";
|
|
23
24
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
24
25
|
import { useShortcut } from "../../../react/input";
|
|
@@ -27,7 +28,7 @@ import type { GloomPlugin, PaneProps, PaneSettingField } from "../../../types/pl
|
|
|
27
28
|
import type { TickerRecord } from "../../../types/ticker";
|
|
28
29
|
import { t, tf } from "../../../i18n";
|
|
29
30
|
import { priceColor } from "../../../theme/colors";
|
|
30
|
-
import { clipToDisplayWidth, formatCompact, formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
31
|
+
import { clipToDisplayWidth, displayWidth, formatCompact, formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
31
32
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
32
33
|
import { useAppSelector, usePaneAppConfig, usePaneSettingValue, usePaneStateValue } from "../../../state/app/context";
|
|
33
34
|
import { usePluginTickerActions } from "../../runtime";
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
MARKET_HEATMAP_UNIVERSES,
|
|
49
50
|
fetchMarketHeatmap,
|
|
50
51
|
resetMarketHeatmapCache,
|
|
52
|
+
type MarketHeatmapBoard,
|
|
51
53
|
type MarketHeatmapUniverseId,
|
|
52
54
|
} from "./data";
|
|
53
55
|
import {
|
|
@@ -79,8 +81,20 @@ import {
|
|
|
79
81
|
rankHeatmapQuoteWeights,
|
|
80
82
|
useSettledValue,
|
|
81
83
|
} from "./live";
|
|
84
|
+
import { useMarketHeatmapEvidence } from "./evidence";
|
|
82
85
|
import { marketHeatmapHeadless } from "./headless";
|
|
83
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
HEATMAP_NAME_COUNTS,
|
|
88
|
+
heatmapBoardCaption,
|
|
89
|
+
heatmapMove,
|
|
90
|
+
heatmapNameCount,
|
|
91
|
+
heatmapSessionBasis,
|
|
92
|
+
regularSessionHeatmapAssets,
|
|
93
|
+
resolveHeatmapGrouping,
|
|
94
|
+
sizeWeightedMove,
|
|
95
|
+
type HeatmapGrouping,
|
|
96
|
+
type HeatmapSessionBasis,
|
|
97
|
+
} from "./model";
|
|
84
98
|
import {
|
|
85
99
|
overlayScreenerQuoteEntries,
|
|
86
100
|
resolveScreenerQuoteFeedStatus,
|
|
@@ -93,10 +107,15 @@ const LAYOUT_THROTTLE_MS = 5_000;
|
|
|
93
107
|
/** A size change below this share of a tile's area is not worth moving it. */
|
|
94
108
|
const LAYOUT_TOLERANCE = 0.005;
|
|
95
109
|
const TERMINAL_PAINT_THROTTLE_MS = 250;
|
|
110
|
+
/** "loading", which comes and goes with each refresh. */
|
|
111
|
+
const LOADING_FOOTER_WIDTH = 7;
|
|
96
112
|
const SELECTION_STREAM_SETTLE_MS = 1_500;
|
|
97
113
|
const TOOLTIP_NAME_WIDTH = 28;
|
|
98
114
|
const SIZE_BY_SETTING_KEY = "sizeBy";
|
|
99
115
|
const SQRT_SIZE_VALUE: HeatmapSizeBy = "sqrt-market-cap";
|
|
116
|
+
const SESSION_BASIS_SETTING_KEY = "sessionBasis";
|
|
117
|
+
const REGULAR_SESSION_BASIS: HeatmapSessionBasis = "regular-session";
|
|
118
|
+
const NAME_COUNT_SETTING_KEY = "maxNames";
|
|
100
119
|
const NO_BOARD = { assets: NO_ASSETS, omitted: 0 };
|
|
101
120
|
const EMPTY_TICKERS: TickerRecord[] = [];
|
|
102
121
|
const NO_CURRENCIES: string[] = [];
|
|
@@ -125,6 +144,10 @@ function formatMoneyCompact(value: number | null | undefined, currency: string):
|
|
|
125
144
|
return `${formatCompact(value)} ${currency}`;
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
function footerSegmentWidth(segment: PaneFooterSegment): number {
|
|
148
|
+
return segment.parts.reduce((total, part, index) => total + (index > 0 ? 1 : 0) + displayWidth(part.text), 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
function sizeLabel(asset: HeatmapBoardAsset): string | null {
|
|
129
152
|
if (asset.showSize === false || asset.size == null) return null;
|
|
130
153
|
const label = asset.sizeCaption ?? (asset.sizeKind === "net-assets" ? "Assets" : "Mkt");
|
|
@@ -233,6 +256,10 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
233
256
|
const [linkPortfolio] = usePaneSettingValue<boolean>("linkPortfolio", false);
|
|
234
257
|
const [sizeBySetting] = usePaneSettingValue<string>(SIZE_BY_SETTING_KEY, SQRT_SIZE_VALUE);
|
|
235
258
|
const sizeBy = heatmapSizeBy(sizeBySetting);
|
|
259
|
+
const [sessionBasisSetting] = usePaneSettingValue<unknown>(SESSION_BASIS_SETTING_KEY, null);
|
|
260
|
+
const regularSession = heatmapSessionBasis(sessionBasisSetting) === REGULAR_SESSION_BASIS;
|
|
261
|
+
const [nameCountSetting] = usePaneSettingValue<unknown>(NAME_COUNT_SETTING_KEY, null);
|
|
262
|
+
const nameCount = heatmapNameCount(nameCountSetting);
|
|
236
263
|
const activeUniverse = heatmapTabId(universeSetting);
|
|
237
264
|
const portfolioTab = activeUniverse === PORTFOLIO_HEATMAP_TAB;
|
|
238
265
|
const config = usePaneAppConfig();
|
|
@@ -281,26 +308,38 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
281
308
|
currency: boardCurrency,
|
|
282
309
|
exchangeRates,
|
|
283
310
|
sizeBy,
|
|
311
|
+
regularSession,
|
|
284
312
|
})
|
|
285
313
|
: NO_BOARD),
|
|
286
314
|
[financials],
|
|
287
|
-
[boardCurrency, collectionId, collectionKind, collectionTickers, exchangeRates, financials.size, sizeBy],
|
|
315
|
+
[boardCurrency, collectionId, collectionKind, collectionTickers, exchangeRates, financials.size, regularSession, sizeBy],
|
|
288
316
|
PORTFOLIO_REORDER_THROTTLE_MS,
|
|
289
317
|
);
|
|
290
318
|
const portfolioAssets = portfolioBoard.assets;
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
const [
|
|
319
|
+
// The last market board that loaded, with the universe and count it was
|
|
320
|
+
// asked for. It is drawn only under its own tab, so a switch never paints the
|
|
321
|
+
// previous universe's tiles, and its footer names its own count while a new
|
|
322
|
+
// one is on its way.
|
|
323
|
+
const [board, setBoard] = useState<MarketHeatmapBoard | null>(null);
|
|
296
324
|
// The first load starts before the effect runs; an empty board is not "no data".
|
|
297
325
|
const [loading, setLoading] = useState(true);
|
|
298
326
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
299
|
-
const [stale, setStale] = useState(false);
|
|
300
327
|
const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
|
|
301
328
|
const fetchGenRef = useRef(0);
|
|
302
329
|
const seenCollection = useRef<string | null>(null);
|
|
303
|
-
const
|
|
330
|
+
const shownBoard = !portfolioTab && board?.universe === activeUniverse ? board : null;
|
|
331
|
+
const stale = board?.stale === true;
|
|
332
|
+
const requestCount = isRemoteHeatmapUniverse(activeUniverse)
|
|
333
|
+
? Math.min(MARKET_HEATMAP_REQUEST_COUNT[activeUniverse], nameCount)
|
|
334
|
+
: 0;
|
|
335
|
+
const marketAssets = useMemo(
|
|
336
|
+
() => {
|
|
337
|
+
const assets: readonly HeatmapBoardAsset[] = shownBoard?.assets ?? NO_ASSETS;
|
|
338
|
+
return regularSession ? regularSessionHeatmapAssets(assets, shownBoard?.session) : assets;
|
|
339
|
+
},
|
|
340
|
+
[regularSession, shownBoard],
|
|
341
|
+
);
|
|
342
|
+
const boardAssets = portfolioTab ? portfolioAssets : marketAssets;
|
|
304
343
|
const hasBoard = boardAssets.length > 0;
|
|
305
344
|
|
|
306
345
|
const selectUniverse = useCallback((value: string) => {
|
|
@@ -345,9 +384,12 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
345
384
|
freshnessScopeKey: `market-heatmap:${activeUniverse}`,
|
|
346
385
|
liveStreaming,
|
|
347
386
|
});
|
|
387
|
+
const completedSessionDate = shownBoard?.regularSessionDate ?? null;
|
|
348
388
|
const resolvedAssets = useMemo(
|
|
349
|
-
() => overlayScreenerQuoteEntries(boardAssets, liveQuoteEntries,
|
|
350
|
-
|
|
389
|
+
() => overlayScreenerQuoteEntries(boardAssets, liveQuoteEntries, regularSession
|
|
390
|
+
? { regularSession: true, completedSessionDate }
|
|
391
|
+
: { extendedSessions: true }),
|
|
392
|
+
[boardAssets, completedSessionDate, liveQuoteEntries, regularSession],
|
|
351
393
|
);
|
|
352
394
|
const feedStatus = useMemo(
|
|
353
395
|
() => resolveScreenerQuoteFeedStatus(quoteTargets, liveQuoteEntries, {
|
|
@@ -363,10 +405,11 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
363
405
|
() => resolveHeatmapGrouping(boardAssets, activeUniverse === "us-equity"),
|
|
364
406
|
[activeUniverse, boardAssets],
|
|
365
407
|
);
|
|
408
|
+
// A new board, or a new Colour by, applies at once rather than on the next tick.
|
|
366
409
|
const layoutAssets = useThrottledMemo(
|
|
367
410
|
() => resolvedAssets,
|
|
368
411
|
[resolvedAssets],
|
|
369
|
-
[boardAssets],
|
|
412
|
+
[boardAssets, regularSession],
|
|
370
413
|
LAYOUT_THROTTLE_MS,
|
|
371
414
|
);
|
|
372
415
|
const layoutItems = useSettledLayoutItems(
|
|
@@ -384,11 +427,19 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
384
427
|
const paintAssets = useThrottledMemo(
|
|
385
428
|
() => resolvedAssets,
|
|
386
429
|
[resolvedAssets],
|
|
387
|
-
[boardAssets],
|
|
430
|
+
[boardAssets, regularSession],
|
|
388
431
|
nativePaneChrome ? 0 : TERMINAL_PAINT_THROTTLE_MS,
|
|
389
432
|
);
|
|
390
433
|
const paintItems = useMemo(() => buildPaintItems(paintAssets), [paintAssets]);
|
|
391
434
|
const visibleTiles = scene.tiles;
|
|
435
|
+
useMarketHeatmapEvidence({
|
|
436
|
+
tab: activeUniverse,
|
|
437
|
+
tiles: visibleTiles,
|
|
438
|
+
assets: paintAssets,
|
|
439
|
+
grouped: grouping !== "flat",
|
|
440
|
+
loading: !portfolioTab && loading,
|
|
441
|
+
retained: !portfolioTab && (stale || loadError != null),
|
|
442
|
+
});
|
|
392
443
|
const selectedTileIndex = selectedSymbol ? visibleTiles.findIndex((tile) => tile.item.id === selectedSymbol) : -1;
|
|
393
444
|
const activeIdx = selectedTileIndex >= 0 ? selectedTileIndex : (visibleTiles.length > 0 ? 0 : -1);
|
|
394
445
|
const activeSymbol = activeIdx >= 0 ? visibleTiles[activeIdx]!.item.id : null;
|
|
@@ -396,7 +447,7 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
396
447
|
? resolvedAssets.find((asset) => asset.symbol === activeSymbol) ?? null
|
|
397
448
|
: null;
|
|
398
449
|
|
|
399
|
-
const loadUniverse = useCallback(async (universe: MarketHeatmapUniverseId, options?: { forceRefresh?: boolean }) => {
|
|
450
|
+
const loadUniverse = useCallback(async (universe: MarketHeatmapUniverseId, count: number, options?: { forceRefresh?: boolean }) => {
|
|
400
451
|
fetchGenRef.current += 1;
|
|
401
452
|
const gen = fetchGenRef.current;
|
|
402
453
|
setLoading(true);
|
|
@@ -404,14 +455,11 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
404
455
|
|
|
405
456
|
try {
|
|
406
457
|
const result = await fetchMarketHeatmap(universe, {
|
|
407
|
-
count
|
|
458
|
+
count,
|
|
408
459
|
forceRefresh: options?.forceRefresh,
|
|
409
460
|
});
|
|
410
461
|
if (fetchGenRef.current !== gen) return;
|
|
411
|
-
|
|
412
|
-
setLoadedUniverse(universe);
|
|
413
|
-
setLastUpdated(result.fetchedAt);
|
|
414
|
-
setStale(result.stale === true);
|
|
462
|
+
setBoard(result);
|
|
415
463
|
// Selection is the user's; the effect below only fills it when it is gone.
|
|
416
464
|
} catch {
|
|
417
465
|
if (fetchGenRef.current !== gen) return;
|
|
@@ -424,8 +472,8 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
424
472
|
|
|
425
473
|
useEffect(() => {
|
|
426
474
|
if (!isRemoteHeatmapUniverse(activeUniverse)) return;
|
|
427
|
-
void loadUniverse(activeUniverse);
|
|
428
|
-
}, [activeUniverse, loadUniverse]);
|
|
475
|
+
void loadUniverse(activeUniverse, requestCount);
|
|
476
|
+
}, [activeUniverse, loadUniverse, requestCount]);
|
|
429
477
|
|
|
430
478
|
useEffect(() => {
|
|
431
479
|
const previous = seenCollection.current;
|
|
@@ -460,12 +508,12 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
460
508
|
return;
|
|
461
509
|
}
|
|
462
510
|
if (!isRemoteHeatmapUniverse(activeUniverse)) return;
|
|
463
|
-
void loadUniverse(activeUniverse, { forceRefresh: true });
|
|
464
|
-
}, [activeUniverse, collectionId, collectionKind, collectionTickers, loadUniverse, portfolioTab]);
|
|
511
|
+
void loadUniverse(activeUniverse, requestCount, { forceRefresh: true });
|
|
512
|
+
}, [activeUniverse, collectionId, collectionKind, collectionTickers, loadUniverse, portfolioTab, requestCount]);
|
|
465
513
|
// The list's quotes stream; on a timer only the market boards reload.
|
|
466
514
|
const autoRefresh = useCallback(() => {
|
|
467
|
-
if (isRemoteHeatmapUniverse(activeUniverse)) void loadUniverse(activeUniverse, { forceRefresh: true });
|
|
468
|
-
}, [activeUniverse, loadUniverse]);
|
|
515
|
+
if (isRemoteHeatmapUniverse(activeUniverse)) void loadUniverse(activeUniverse, requestCount, { forceRefresh: true });
|
|
516
|
+
}, [activeUniverse, loadUniverse, requestCount]);
|
|
469
517
|
|
|
470
518
|
const selectAdjacentUniverse = useCallback((direction: -1 | 1) => {
|
|
471
519
|
const index = universeTabs.findIndex((tab) => tab.value === activeUniverse);
|
|
@@ -574,9 +622,11 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
574
622
|
}
|
|
575
623
|
});
|
|
576
624
|
|
|
577
|
-
|
|
625
|
+
// Ages the last check that succeeded, not the snapshot, which the server can
|
|
626
|
+
// hand back unchanged long after it was put together.
|
|
627
|
+
const checked = useUpdatedAgo(shownBoard?.checkedAt ?? null);
|
|
578
628
|
// The snapshot carries sizes and every name the stream does not; the server caches it for a minute.
|
|
579
|
-
useAutoRefresh(
|
|
629
|
+
useAutoRefresh(board?.checkedAt ?? null, autoRefresh, { intervalMs: SNAPSHOT_REFRESH_MS });
|
|
580
630
|
|
|
581
631
|
usePaneStatusFooter({ registrationId: "market-heatmap-retained", stale: hasBoard && stale && !portfolioTab });
|
|
582
632
|
usePaneNoticeFooter({
|
|
@@ -587,41 +637,53 @@ function MarketHeatmapPane({ focused, width, height }: PaneProps) {
|
|
|
587
637
|
focused,
|
|
588
638
|
});
|
|
589
639
|
|
|
590
|
-
usePaneFooter("market-heatmap", () =>
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
640
|
+
usePaneFooter("market-heatmap", () => {
|
|
641
|
+
const selected: PaneFooterSegment[] = selectedAsset ? [{
|
|
642
|
+
id: "selected",
|
|
643
|
+
parts: [
|
|
644
|
+
{ text: selectedAsset.symbol, tone: "label" as const },
|
|
645
|
+
{
|
|
646
|
+
text: heatmapMove(selectedAsset) == null ? "—" : formatPercentRaw(heatmapMove(selectedAsset)!),
|
|
647
|
+
tone: "value" as const,
|
|
648
|
+
color: heatmapMove(selectedAsset) == null ? undefined : priceColor(heatmapMove(selectedAsset)!),
|
|
649
|
+
bold: true,
|
|
650
|
+
},
|
|
651
|
+
...(sessionMark(selectedAsset) ? [{ text: sessionMark(selectedAsset)!, tone: "muted" as const }] : []),
|
|
652
|
+
...(selectedAsset.price > 0
|
|
653
|
+
? [{ text: formatCurrency(selectedAsset.price, selectedAsset.currency), tone: "value" as const }]
|
|
654
|
+
: []),
|
|
655
|
+
...(sizeLabel(selectedAsset) ? [{ text: sizeLabel(selectedAsset)!, tone: "muted" as const }] : []),
|
|
656
|
+
...(selectedAsset.volume != null
|
|
657
|
+
? [{ text: `Vol ${formatCompact(selectedAsset.volume)}`, tone: "muted" as const }]
|
|
658
|
+
: []),
|
|
659
|
+
],
|
|
660
|
+
}] : [];
|
|
661
|
+
// Without a board the body carries the failure.
|
|
662
|
+
const failed: PaneFooterSegment[] = !portfolioTab && loadError && hasBoard
|
|
663
|
+
? [{ id: "error", parts: [{ text: "refresh failed", tone: "warning" as const }] }]
|
|
664
|
+
: [];
|
|
665
|
+
const feed: PaneFooterSegment[] = feedStatus
|
|
666
|
+
? [{ id: "feed", parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }] }]
|
|
667
|
+
: [];
|
|
668
|
+
const loadingSegment: PaneFooterSegment[] = !portfolioTab && loading
|
|
669
|
+
? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }]
|
|
670
|
+
: [];
|
|
671
|
+
// The caption takes the room the rest leaves, with "loading" counted in
|
|
672
|
+
// always, so a refresh never squeezes the row; a narrow pane drops its
|
|
673
|
+
// least important parts rather than truncating every segment.
|
|
674
|
+
const others = [...selected, ...failed, ...feed];
|
|
675
|
+
const room = width - others.reduce((total, segment) => total + footerSegmentWidth(segment) + 1, 0) - LOADING_FOOTER_WIDTH - 1;
|
|
676
|
+
const caption = shownBoard ? heatmapBoardCaption(shownBoard, { now: Date.now(), checked, failed: failed.length > 0, maxWidth: room }) : null;
|
|
677
|
+
return {
|
|
678
|
+
info: [
|
|
679
|
+
...selected,
|
|
680
|
+
...failed,
|
|
681
|
+
...(caption ? [{ id: "board", parts: [{ text: caption, tone: "muted" as const }] }] : []),
|
|
682
|
+
...feed,
|
|
683
|
+
...loadingSegment,
|
|
684
|
+
],
|
|
685
|
+
};
|
|
686
|
+
}, [checked, feedStatus, hasBoard, loadError, loading, portfolioTab, selectedAsset, shownBoard, width]);
|
|
625
687
|
|
|
626
688
|
return (
|
|
627
689
|
<Box flexDirection="column" width={width} height={height}>
|
|
@@ -701,6 +763,8 @@ export const marketHeatmapPlugin: GloomPlugin = {
|
|
|
701
763
|
values: {
|
|
702
764
|
linkPortfolio: context.settings.linkPortfolio === true,
|
|
703
765
|
[SIZE_BY_SETTING_KEY]: heatmapSizeBy(context.settings[SIZE_BY_SETTING_KEY]),
|
|
766
|
+
[SESSION_BASIS_SETTING_KEY]: heatmapSessionBasis(context.settings[SESSION_BASIS_SETTING_KEY]),
|
|
767
|
+
[NAME_COUNT_SETTING_KEY]: String(heatmapNameCount(context.settings[NAME_COUNT_SETTING_KEY])),
|
|
704
768
|
},
|
|
705
769
|
fields: [
|
|
706
770
|
{
|
|
@@ -725,6 +789,23 @@ export const marketHeatmapPlugin: GloomPlugin = {
|
|
|
725
789
|
],
|
|
726
790
|
description: "Square root gives mid-size names room next to the largest. A portfolio is always sized by position value.",
|
|
727
791
|
},
|
|
792
|
+
{
|
|
793
|
+
key: SESSION_BASIS_SETTING_KEY,
|
|
794
|
+
label: "Colour by",
|
|
795
|
+
type: "select",
|
|
796
|
+
options: [
|
|
797
|
+
{ value: REGULAR_SESSION_BASIS, label: "Regular session" },
|
|
798
|
+
{ value: "active-session", label: "Active session" },
|
|
799
|
+
],
|
|
800
|
+
description: "Active session follows pre-market and after-hours trading. Regular session shows the regular session alone: its move while it trades, then its close and move until the next one opens.",
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
key: NAME_COUNT_SETTING_KEY,
|
|
804
|
+
label: "Names",
|
|
805
|
+
type: "select",
|
|
806
|
+
options: HEATMAP_NAME_COUNTS.map((count) => ({ value: String(count), label: `Top ${count}` })),
|
|
807
|
+
description: "How many of the largest names US Stocks draws, and US ETFs up to 160. Fewer names leave fewer in Other.",
|
|
808
|
+
},
|
|
728
809
|
{
|
|
729
810
|
key: "linkPortfolio",
|
|
730
811
|
label: "Link to portfolio",
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { MarketHeatmapAsset } from "../../../api-client/market-discovery";
|
|
2
|
+
import { formatShortDate } from "../../../utils/datetime-format";
|
|
3
|
+
import { displayWidth } from "../../../utils/format";
|
|
4
|
+
import { zonedDateKey, zonedDateTimeParts } from "../../../utils/zoned-date-time";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* The board's grouping and move arithmetic, shared by the pane and the `fn HM`
|
|
@@ -13,14 +16,102 @@ type HeatmapMoveAsset = Pick<MarketHeatmapAsset, "symbol" | "hasChange" | "chang
|
|
|
13
16
|
/**
|
|
14
17
|
* The move, or null when there is none: no change data is not a flat session.
|
|
15
18
|
* A name without the last session's close still has the move of the open
|
|
16
|
-
* pre-market or after-hours session it streams (`extendedSession`)
|
|
19
|
+
* pre-market or after-hours session it streams (`extendedSession`), or of the
|
|
20
|
+
* completed regular session its quote reports (`regularChangePercent`).
|
|
17
21
|
*/
|
|
18
|
-
export function heatmapMove(
|
|
19
|
-
|
|
22
|
+
export function heatmapMove(
|
|
23
|
+
asset: Pick<MarketHeatmapAsset, "hasChange" | "changePercent" | "regularChangePercent"> & { extendedSession?: string },
|
|
24
|
+
): number | null {
|
|
25
|
+
const moved = asset.hasChange || asset.extendedSession != null
|
|
26
|
+
|| (typeof asset.regularChangePercent === "number" && Number.isFinite(asset.regularChangePercent));
|
|
27
|
+
return moved && typeof asset.changePercent === "number" && Number.isFinite(asset.changePercent)
|
|
20
28
|
? asset.changePercent
|
|
21
29
|
: null;
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
/**
|
|
33
|
+
* What a market board's tiles are colored by. The active session, the
|
|
34
|
+
* default, follows an open pre-market or after-hours session; the regular
|
|
35
|
+
* session keeps the regular session's move through extended trading.
|
|
36
|
+
*/
|
|
37
|
+
export type HeatmapSessionBasis = "active-session" | "regular-session";
|
|
38
|
+
|
|
39
|
+
/** A saved choice wins; a pane that never chose follows the active session. */
|
|
40
|
+
export function heatmapSessionBasis(value: unknown): HeatmapSessionBasis {
|
|
41
|
+
return value === "regular-session" ? "regular-session" : "active-session";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How many of the largest names a market board asks for. */
|
|
45
|
+
export const HEATMAP_NAME_COUNTS = [100, 150, 500] as const;
|
|
46
|
+
export type HeatmapNameCount = typeof HEATMAP_NAME_COUNTS[number];
|
|
47
|
+
|
|
48
|
+
/** A saved count, as the settings dialog stores it ("150") or as a number; anything else is the full 500. */
|
|
49
|
+
export function heatmapNameCount(value: unknown): HeatmapNameCount {
|
|
50
|
+
const count = typeof value === "string" && value.trim() ? Number(value) : value;
|
|
51
|
+
return HEATMAP_NAME_COUNTS.find((option) => option === count) ?? 500;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A market board read by its regular session alone. Taken outside the regular
|
|
56
|
+
* session, a tile's move is that session's only when the server dates it as
|
|
57
|
+
* the completed session's (`regularChangePercent`); any other move there is
|
|
58
|
+
* from a session it does not name, so the tile shows none, and no price.
|
|
59
|
+
* Returns the same array when nothing changes.
|
|
60
|
+
*/
|
|
61
|
+
export function regularSessionHeatmapAssets<T extends Pick<MarketHeatmapAsset, "hasChange" | "price" | "regularChangePercent">>(
|
|
62
|
+
assets: readonly T[],
|
|
63
|
+
session: string | null | undefined,
|
|
64
|
+
): readonly T[] {
|
|
65
|
+
if (session == null || session === "REGULAR") return assets;
|
|
66
|
+
let changed = false;
|
|
67
|
+
const next = assets.map((asset) => {
|
|
68
|
+
if (!asset.hasChange || (typeof asset.regularChangePercent === "number" && Number.isFinite(asset.regularChangePercent))) return asset;
|
|
69
|
+
changed = true;
|
|
70
|
+
return { ...asset, hasChange: false, price: 0 };
|
|
71
|
+
});
|
|
72
|
+
return changed ? next : assets;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const NEW_YORK = "America/New_York";
|
|
76
|
+
const twoDigits = (value: number) => String(value).padStart(2, "0");
|
|
77
|
+
/** A New York calendar day ("2026-10-09") as the footer names it: "Oct 9". */
|
|
78
|
+
const dayLabel = (day: string) => formatShortDate(day, { year: false, utc: true, fallback: day });
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* What a market board covers and when it is from, for the footer: how many of
|
|
82
|
+
* the largest names it holds, the completed regular session it shows outside
|
|
83
|
+
* that session, when the server put the snapshot together, in New York time,
|
|
84
|
+
* dated when that day is not the session's (or, while the session trades,
|
|
85
|
+
* today's), and when this client last checked, which is a separate figure.
|
|
86
|
+
* A footer narrower than `maxWidth` loses the check first, then the snapshot,
|
|
87
|
+
* then the session; after a failed refresh (`failed`) the age of the last good
|
|
88
|
+
* check outlasts the snapshot. Null when not even the count fits.
|
|
89
|
+
*/
|
|
90
|
+
export function heatmapBoardCaption(
|
|
91
|
+
board: { topCount: number; fetchedAt: number; regularSessionDate?: string | null },
|
|
92
|
+
{ now, checked = null, failed = false, maxWidth = Number.POSITIVE_INFINITY }:
|
|
93
|
+
{ now: number; checked?: string | null; failed?: boolean; maxWidth?: number },
|
|
94
|
+
): string | null {
|
|
95
|
+
const sessionDate = board.regularSessionDate || null;
|
|
96
|
+
// Display order; `rank` is the order they give way in, highest first.
|
|
97
|
+
const parts: Array<{ text: string; rank: number }> = [{ text: `top ${board.topCount}`, rank: 0 }];
|
|
98
|
+
if (sessionDate) parts.push({ text: `${dayLabel(sessionDate)} session`, rank: 1 });
|
|
99
|
+
if (Number.isFinite(board.fetchedAt) && board.fetchedAt > 0) {
|
|
100
|
+
const { hour, minute } = zonedDateTimeParts(board.fetchedAt, NEW_YORK);
|
|
101
|
+
const clock = `${twoDigits(hour)}:${twoDigits(minute)} ET`;
|
|
102
|
+
const day = zonedDateKey(board.fetchedAt, NEW_YORK);
|
|
103
|
+
const text = `snapshot ${day === (sessionDate ?? zonedDateKey(now, NEW_YORK)) ? clock : `${dayLabel(day)} ${clock}`}`;
|
|
104
|
+
parts.push({ text, rank: failed ? 3 : 2 });
|
|
105
|
+
}
|
|
106
|
+
if (checked) parts.push({ text: `checked ${checked}`, rank: failed ? 2 : 3 });
|
|
107
|
+
const caption = () => parts.map((part) => part.text).join(" · ");
|
|
108
|
+
while (parts.length > 0 && displayWidth(caption()) > maxWidth) {
|
|
109
|
+
const weakest = parts.reduce((found, part, index) => (part.rank > parts[found]!.rank ? index : found), 0);
|
|
110
|
+
parts.splice(weakest, 1);
|
|
111
|
+
}
|
|
112
|
+
return parts.length > 0 ? caption() : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
24
115
|
export type HeatmapGrouping = "sector-industry" | "sector" | "flat";
|
|
25
116
|
|
|
26
117
|
/**
|
|
@@ -3,7 +3,12 @@ import type { MarketHeatmapAsset, MarketHeatmapUniverseId } from "../../../api-c
|
|
|
3
3
|
import { useAppSelector, usePaneStateValue } from "../../../state/app/context";
|
|
4
4
|
import type { ColumnConfig } from "../../../types/config";
|
|
5
5
|
import type { TickerFinancials } from "../../../types/financials";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getRegularSessionMoveDisplay,
|
|
8
|
+
getSessionMoveDisplay,
|
|
9
|
+
type ActiveQuoteDisplay,
|
|
10
|
+
type ExtendedSession,
|
|
11
|
+
} from "../../../market-data/market/status";
|
|
7
12
|
import type { TickerRecord } from "../../../types/ticker";
|
|
8
13
|
import { getSortValue, type ColumnContext } from "../portfolio-list/metrics";
|
|
9
14
|
|
|
@@ -195,6 +200,7 @@ export function buildPortfolioHeatmapAssets({
|
|
|
195
200
|
currency,
|
|
196
201
|
exchangeRates,
|
|
197
202
|
sizeBy = "sqrt-market-cap",
|
|
203
|
+
regularSession = false,
|
|
198
204
|
}: {
|
|
199
205
|
tickers: readonly TickerRecord[];
|
|
200
206
|
financials: ReadonlyMap<string, TickerFinancials>;
|
|
@@ -204,6 +210,8 @@ export function buildPortfolioHeatmapAssets({
|
|
|
204
210
|
currency: string;
|
|
205
211
|
exchangeRates: Map<string, number>;
|
|
206
212
|
sizeBy?: HeatmapSizeBy;
|
|
213
|
+
/** Color by the regular session alone, never an extended print (`getRegularSessionMoveDisplay`). */
|
|
214
|
+
regularSession?: boolean;
|
|
207
215
|
}): { assets: HeatmapBoardAsset[]; omitted: number } {
|
|
208
216
|
const context: ColumnContext = {
|
|
209
217
|
activeTab: kind === "portfolio" ? collectionId : undefined,
|
|
@@ -217,7 +225,9 @@ export function buildPortfolioHeatmapAssets({
|
|
|
217
225
|
const snapshot = financials.get(symbol);
|
|
218
226
|
const quote = snapshot?.quote;
|
|
219
227
|
// Colored like the market boards: an open extended session's move, else the regular session's.
|
|
220
|
-
const display =
|
|
228
|
+
const display: (ActiveQuoteDisplay & { session?: ExtendedSession }) | null = regularSession
|
|
229
|
+
? getRegularSessionMoveDisplay(quote)
|
|
230
|
+
: getSessionMoveDisplay(quote);
|
|
221
231
|
const price = display != null && Number.isFinite(display.price) ? display.price : null;
|
|
222
232
|
const size = positiveNumber(getSortValue(sizeColumn, ticker, snapshot, context));
|
|
223
233
|
const hasChange = display?.changePercent != null && Number.isFinite(display.changePercent);
|
|
@@ -10,14 +10,18 @@ import type {
|
|
|
10
10
|
CloudSessionMoversSide,
|
|
11
11
|
} from "../../../api-client/market-movers";
|
|
12
12
|
import { loadMarketMoverTab, loadSessionMovers, type MarketMoverTabResult } from "./client";
|
|
13
|
-
import { createRows, fiftyTwoWeekPositionPercent, formatMoverPrice, moverReferencePrice, type MarketMoverRow, type ScreenerTabId, type TabId } from "./model";
|
|
13
|
+
import { createRows, fiftyTwoWeekPositionPercent, formatMoverPrice, moverExtendedSessions, moverReferencePrice, type MarketMoverRow, type ScreenerTabId, type TabId } from "./model";
|
|
14
|
+
import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
|
|
15
|
+
import type { HeadlessPaneColumn } from "../../../types/headless";
|
|
14
16
|
import { isSessionTab, resolveSide } from "./session";
|
|
15
17
|
|
|
16
|
-
const
|
|
18
|
+
const rowPrice = (value: unknown, row: Record<string, unknown>) => formatMoverPrice(typeof value === "number" ? value : null, typeof row.currency === "string" ? row.currency : "", moverReferencePrice(row as unknown as MarketMoverRow));
|
|
19
|
+
|
|
20
|
+
const COLUMNS: HeadlessPaneColumn[] = [
|
|
17
21
|
{ key: "rank", header: "Rank", align: "right" as const },
|
|
18
22
|
{ key: "symbol", header: "Symbol" },
|
|
19
23
|
{ key: "name", header: "Name" },
|
|
20
|
-
{ key: "price", header: "Last", align: "right" as const, format:
|
|
24
|
+
{ key: "price", header: "Last", align: "right" as const, format: rowPrice },
|
|
21
25
|
{ key: "changePercent", header: "Change %", align: "right" as const, format: (value: unknown) => value == null ? "—" : formatPercentRaw(Number(value)) },
|
|
22
26
|
{ key: "volume", header: "Volume", align: "right" as const, format: (value: unknown) => value == null ? "—" : formatCompact(Number(value), { fixedDecimals: true }) },
|
|
23
27
|
{ key: "volumeRatio", header: "Vol / Avg", align: "right" as const, format: (value: unknown) => value == null ? "—" : formatNumber(Number(value), 1) },
|
|
@@ -30,6 +34,19 @@ const percent = (value: unknown) => value == null ? "—" : formatPercentRaw(Num
|
|
|
30
34
|
const ratio = (value: unknown) => value == null ? "—" : `${formatNumber(Number(value), 1)}x`;
|
|
31
35
|
const compact = (value: unknown) => value == null ? "—" : formatCompact(Number(value), { fixedDecimals: true });
|
|
32
36
|
const CATALYST_LABEL: Record<string, string> = { halt: "Halt", filing: "8-K", news: "News" };
|
|
37
|
+
|
|
38
|
+
/** The pre-market or after-hours print and its move from the regular close, for the rows that have one. */
|
|
39
|
+
function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
|
|
40
|
+
return {
|
|
41
|
+
key: "extendedPrice",
|
|
42
|
+
header: EXTENDED_SESSION_LABELS[session],
|
|
43
|
+
align: "right",
|
|
44
|
+
format: (value, row) => row.extendedSession === session && typeof value === "number"
|
|
45
|
+
? `${rowPrice(value, row)} ${percent(row.extendedChangePercent)}`
|
|
46
|
+
: "—",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
const SESSION_VOLUME: Record<CloudSessionMoversCategory, string> = {
|
|
34
51
|
premarket: "Pre-market volume",
|
|
35
52
|
afterhours: "After-hours volume",
|
|
@@ -131,8 +148,15 @@ export function createMarketMoversHeadless(
|
|
|
131
148
|
};
|
|
132
149
|
}
|
|
133
150
|
const result = await dependencies.load(args, tab, ctx.marketData);
|
|
134
|
-
const
|
|
151
|
+
const ranked = createRows(result.quotes);
|
|
152
|
+
// Last and Change % are the regular session; an extended print is a column of its own.
|
|
153
|
+
const extendedSessions = moverExtendedSessions(ranked);
|
|
154
|
+
const rows = ranked.map(({ extended, ...row }) => ({
|
|
135
155
|
...row,
|
|
156
|
+
extendedSession: extended?.session ?? null,
|
|
157
|
+
extendedPrice: extended?.price ?? null,
|
|
158
|
+
extendedChange: extended?.change ?? null,
|
|
159
|
+
extendedChangePercent: extended?.changePercent ?? null,
|
|
136
160
|
rangePositionPercent: fiftyTwoWeekPositionPercent(
|
|
137
161
|
row.price,
|
|
138
162
|
row.fiftyTwoWeekLow,
|
|
@@ -140,6 +164,9 @@ export function createMarketMoversHeadless(
|
|
|
140
164
|
),
|
|
141
165
|
}));
|
|
142
166
|
return {
|
|
167
|
+
...(extendedSessions.length > 0
|
|
168
|
+
? { columns: [...COLUMNS.slice(0, 5), ...extendedSessions.map(extendedColumn), ...COLUMNS.slice(5)] }
|
|
169
|
+
: {}),
|
|
143
170
|
rows,
|
|
144
171
|
metadata: { list: tab, source: result.source, stale: result.stale },
|
|
145
172
|
};
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
INDEX_SHORT,
|
|
26
26
|
TABS,
|
|
27
27
|
createRows,
|
|
28
|
+
moverExtendedSessions,
|
|
28
29
|
overlayMarketMoverQuotes,
|
|
29
30
|
resolveSummarySymbols,
|
|
30
31
|
resolveTabs,
|
|
@@ -42,6 +43,7 @@ import { isSessionTab, resolveActiveTab, usSessionAt, type UsSession } from "./s
|
|
|
42
43
|
import { SessionMoversBody } from "./session-body";
|
|
43
44
|
import { marketMoversHeadless } from "./headless";
|
|
44
45
|
import { buildMarketMoverColumns, renderMarketMoverCell } from "./table";
|
|
46
|
+
import type { ExtendedSession } from "../../../market-data/market/status";
|
|
45
47
|
import {
|
|
46
48
|
LIVE_STREAMING_QUICK_SETTING,
|
|
47
49
|
useLiveStreamingSetting,
|
|
@@ -188,8 +190,13 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
188
190
|
}),
|
|
189
191
|
[freshnessNow, liveQuoteEntries, quoteTargets, subscriptionStartedAt],
|
|
190
192
|
);
|
|
191
|
-
const columns = useMemo(() => buildMarketMoverColumns(width), [width]);
|
|
192
193
|
const rankedRows = useMemo(() => createRows(resolvedQuotes), [resolvedQuotes]);
|
|
194
|
+
// A pre-market or after-hours column only while a row has such a print.
|
|
195
|
+
const extendedSessionsKey = moverExtendedSessions(rankedRows).join(",");
|
|
196
|
+
const columns = useMemo(
|
|
197
|
+
() => buildMarketMoverColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
|
|
198
|
+
[extendedSessionsKey, width],
|
|
199
|
+
);
|
|
193
200
|
const rows = useMemo(() => sortRows(rankedRows, sortPreference), [rankedRows, sortPreference]);
|
|
194
201
|
const selectedIdx = selectedSymbol
|
|
195
202
|
? rows.findIndex((row) => moverKey(row) === selectedSymbol)
|