gloomberb 0.18.4 → 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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +169 -5
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +31 -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 +171 -11
- package/src/api-client/live-scan.ts +91 -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 +13 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +6 -12
- package/src/api-client/types.ts +74 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +42 -27
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +1 -1
- 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/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +138 -46
- package/src/cli/commands/options-chain.ts +107 -10
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +176 -39
- package/src/cli/commands/search.ts +39 -3
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +57 -35
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +10 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +73 -1
- package/src/cli/history-rows.ts +87 -16
- package/src/cli/index.ts +2 -0
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +195 -56
- 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 +111 -16
- package/src/cli/pane-functions/function-help.ts +23 -13
- package/src/cli/pane-functions/headless.ts +56 -12
- package/src/cli/pane-functions/index.ts +58 -16
- package/src/cli/pane-functions/market-session.ts +81 -9
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +35 -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 +30 -13
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +26 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +61 -5
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +46 -7
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- 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/types.ts +4 -0
- 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/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
- 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/display.tsx +5 -1
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/saved-copies.ts +55 -16
- 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 +427 -216
- package/src/i18n/es.ts +19 -3
- package/src/i18n/ja.ts +19 -3
- package/src/i18n/ko.ts +19 -3
- package/src/i18n/zh-cn.ts +19 -3
- package/src/i18n/zh-tw.ts +19 -3
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -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 +48 -7
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +141 -4
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- 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/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +380 -82
- package/src/plugins/builtin/alerts/command.ts +31 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +113 -8
- package/src/plugins/builtin/alerts/format.ts +80 -4
- package/src/plugins/builtin/alerts/headless.ts +30 -7
- package/src/plugins/builtin/alerts/history-rows.ts +7 -3
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +135 -22
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +134 -0
- package/src/plugins/builtin/alerts/metric-data.ts +111 -0
- package/src/plugins/builtin/alerts/metric-model.ts +324 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +90 -39
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +37 -10
- package/src/plugins/builtin/alerts/types.ts +7 -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/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +10 -5
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- 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 +7 -3
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
- package/src/plugins/builtin/chart-composer/headless.ts +19 -3
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -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/cloud/plugin.tsx +1 -1
- 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/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +4 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
- package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- 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 +70 -15
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- 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 +11 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
- package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -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/keybindings-editor.tsx +18 -14
- 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 +36 -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/pane.tsx +7 -5
- 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/footer.ts +2 -2
- 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 +82 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- 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 +54 -6
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +6 -9
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +42 -12
- package/src/plugins/builtin/options-positioning/client.ts +30 -0
- package/src/plugins/builtin/options-positioning/headless.ts +74 -21
- package/src/plugins/builtin/options-positioning/index.tsx +4 -4
- package/src/plugins/builtin/options-positioning/model.ts +122 -14
- package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
- 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/freshness.ts +12 -10
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +32 -31
- 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 +12 -7
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- 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/summary/index.tsx +11 -8
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- 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 +11 -9
- 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/feed.ts +2 -2
- 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 +40 -6
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +138 -0
- package/src/plugins/builtin/scanner/live-model.ts +362 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- 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 +3 -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/macro-releases.ts +65 -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/scanner-capture.ts +3 -0
- 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 +10 -3
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +17 -8
- package/src/plugins/builtin/themes/pane.tsx +7 -7
- 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-footer.ts +6 -1
- 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/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +9 -17
- package/src/plugins/builtin/vol-surface/headless.ts +63 -14
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +4 -4
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +5 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/remote/resources.ts +2 -1
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- 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 +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
- 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/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +30 -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/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -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/renderers/opentui/toast-host.tsx +71 -43
- 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/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +149 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +70 -19
- package/src/sync/core-contributors.ts +90 -31
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- 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/telemetry/usage-counts.ts +0 -18
- package/src/test-support/preload.ts +14 -0
- package/src/theme/colors.ts +8 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +103 -3
- package/src/tickers/listing-venues.ts +38 -3
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +2 -4
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +198 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +7 -1
- package/src/time-series/resolve.ts +116 -15
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +5 -3
- package/src/time-series/studies.ts +278 -74
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +3 -3
- package/src/time-series/types.ts +27 -1
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +73 -2
- package/src/types/headless.ts +61 -5
- package/src/types/instrument.ts +3 -0
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +10 -1
- package/src/ui/host.tsx +3 -0
- package/src/updater/command-names.ts +12 -7
- 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/financial-statements.ts +6 -1
- 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/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +12 -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
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PaneFooterSegment } from "../../../components";
|
|
2
|
-
import {
|
|
2
|
+
import { shownPriceColor } from "../../../theme/colors";
|
|
3
3
|
import { formatPercentRaw } from "../../../utils/format";
|
|
4
4
|
import { INDEX_SHORT } from "./model";
|
|
5
5
|
import type { MarketSummaryQuote } from "./screener";
|
|
@@ -12,7 +12,7 @@ export function summaryFooterSegments(summaryQuotes: readonly MarketSummaryQuote
|
|
|
12
12
|
id: `summary:${idx.symbol}`,
|
|
13
13
|
parts: [
|
|
14
14
|
{ text: short, tone: "label" as const },
|
|
15
|
-
{ text: formatPercentRaw(idx.changePercent), tone: "value" as const, color:
|
|
15
|
+
{ text: formatPercentRaw(idx.changePercent), tone: "value" as const, color: shownPriceColor(idx.changePercent, 2), bold: true },
|
|
16
16
|
],
|
|
17
17
|
};
|
|
18
18
|
});
|
|
@@ -14,6 +14,7 @@ import { createRows, fiftyTwoWeekPositionPercent, formatMoverPrice, MOVER_RANKIN
|
|
|
14
14
|
import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
|
|
15
15
|
import type { HeadlessPaneColumn } from "../../../types/headless";
|
|
16
16
|
import { isSessionTab, resolveSide } from "./session";
|
|
17
|
+
import { hasPartialFloat, minFloatShares, normalizeMinFloat, PARTIAL_FLOAT_NOTE } from "./min-float";
|
|
17
18
|
|
|
18
19
|
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
|
|
|
@@ -69,15 +70,19 @@ function sessionColumns(view: CloudSessionMoversCategory) {
|
|
|
69
70
|
];
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
const FLOAT_COLUMN: HeadlessPaneColumn = { key: "floatShares", header: "Float", align: "right", format: compact };
|
|
74
|
+
|
|
72
75
|
export interface MarketMoversHeadlessDependencies {
|
|
73
76
|
load(
|
|
74
77
|
args: HeadlessPaneLoadArgs,
|
|
75
78
|
tab: ScreenerTabId,
|
|
76
79
|
provider: DataProvider,
|
|
80
|
+
minFloat: number | null,
|
|
77
81
|
): Promise<MarketMoverTabResult>;
|
|
78
82
|
loadSession(
|
|
79
83
|
view: CloudSessionMoversCategory,
|
|
80
84
|
side: CloudSessionMoversSide,
|
|
85
|
+
minFloat: number | null,
|
|
81
86
|
): Promise<CloudSessionMoversPayload>;
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -89,8 +94,8 @@ export interface MarketMoversHeadlessDependencies {
|
|
|
89
94
|
const ONE_SHOT_METADATA_WAIT_MS = 10_000;
|
|
90
95
|
|
|
91
96
|
const defaultDependencies: MarketMoversHeadlessDependencies = {
|
|
92
|
-
load: (_args, tab, provider) => loadMarketMoverTab(tab, provider, { metadataWaitMs: ONE_SHOT_METADATA_WAIT_MS }),
|
|
93
|
-
loadSession: (view, side) => loadSessionMovers(view, side),
|
|
97
|
+
load: (_args, tab, provider, minFloat) => loadMarketMoverTab(tab, provider, { metadataWaitMs: ONE_SHOT_METADATA_WAIT_MS, minFloat }),
|
|
98
|
+
loadSession: (view, side, minFloat) => loadSessionMovers(view, side, { minFloat }),
|
|
94
99
|
};
|
|
95
100
|
|
|
96
101
|
export function createMarketMoversHeadless(
|
|
@@ -128,6 +133,14 @@ export function createMarketMoversHeadless(
|
|
|
128
133
|
],
|
|
129
134
|
defaultValue: "up",
|
|
130
135
|
pluginState: { pluginId: "market-overview", key: "sessionSide" },
|
|
136
|
+
}, {
|
|
137
|
+
key: "min-float",
|
|
138
|
+
description: "Keep the rows whose float (the one EQS serves) is at least this many shares, such as 10M or 500k; any lifts it. A row with no float is left out. Trending carries no float and ignores it.",
|
|
139
|
+
type: "string",
|
|
140
|
+
placeholder: "shares",
|
|
141
|
+
example: "--min-float 10M",
|
|
142
|
+
normalize: normalizeMinFloat,
|
|
143
|
+
pluginState: { pluginId: "market-overview", key: "minFloat" },
|
|
131
144
|
}],
|
|
132
145
|
columns: COLUMNS,
|
|
133
146
|
describe: (args) => isSessionTab(String(args.options.list))
|
|
@@ -135,21 +148,26 @@ export function createMarketMoversHeadless(
|
|
|
135
148
|
: `Market Movers | ${String(args.options.list)}`,
|
|
136
149
|
async load(args, ctx) {
|
|
137
150
|
const tab = args.options.list as TabId;
|
|
151
|
+
const minFloatValue = typeof args.options["min-float"] === "string" ? args.options["min-float"] : null;
|
|
152
|
+
const minFloat = minFloatShares(minFloatValue);
|
|
138
153
|
if (isSessionTab(tab)) {
|
|
139
154
|
const side = resolveSide(tab, String(args.options.side));
|
|
140
|
-
const payload = await dependencies.loadSession(tab, side);
|
|
155
|
+
const payload = await dependencies.loadSession(tab, side, minFloat);
|
|
141
156
|
return {
|
|
142
157
|
columns: sessionColumns(tab),
|
|
143
158
|
rows: payload.items.map((item) => ({
|
|
144
159
|
...item,
|
|
145
160
|
events: item.catalysts.map((catalyst) => CATALYST_LABEL[catalyst] ?? catalyst).join(" "),
|
|
146
161
|
})),
|
|
147
|
-
|
|
162
|
+
...(hasPartialFloat(payload.items) ? { notes: [PARTIAL_FLOAT_NOTE] } : {}),
|
|
163
|
+
metadata: { list: tab, side, session: payload.session, phase: payload.phase, asOf: payload.asOf, stale: payload.stale === true, minFloat },
|
|
148
164
|
// A Pro account's list is the live board; the free one says it is delayed.
|
|
149
165
|
...(payload.access === "delayed" ? {} : { freshness: { status: "live" as const } }),
|
|
150
166
|
};
|
|
151
167
|
}
|
|
152
|
-
|
|
168
|
+
// Trending carries no float, so a minimum does not apply to it.
|
|
169
|
+
const floatFiltered = minFloat != null && tab !== "trending";
|
|
170
|
+
const result = await dependencies.load(args, tab, ctx.marketData, floatFiltered ? minFloat : null);
|
|
153
171
|
const ranked = createRows(result.quotes);
|
|
154
172
|
// Last and Change % are the regular session; an extended print is a column of its own.
|
|
155
173
|
const extendedSessions = moverExtendedSessions(ranked);
|
|
@@ -168,16 +186,18 @@ export function createMarketMoversHeadless(
|
|
|
168
186
|
// Beside an extended column, say which move the order follows.
|
|
169
187
|
const rankedBy = result.rankedBy ?? null;
|
|
170
188
|
const quoteError = marketMoverQuoteError(result);
|
|
189
|
+
const columns = [...COLUMNS.slice(0, 5), ...extendedSessions.map(extendedColumn), ...COLUMNS.slice(5), ...(floatFiltered ? [FLOAT_COLUMN] : [])];
|
|
190
|
+
const notes = [
|
|
191
|
+
...(rankedBy && rankedBy !== "volume" && extendedSessions.length > 0 ? [`Ranked by ${MOVER_RANKING_LABELS[rankedBy]}.`] : []),
|
|
192
|
+
...(minFloat != null && !floatFiltered ? ["Trending carries no float, so --min-float does not apply to it."] : []),
|
|
193
|
+
...(floatFiltered && hasPartialFloat(rows) ? [PARTIAL_FLOAT_NOTE] : []),
|
|
194
|
+
];
|
|
171
195
|
return {
|
|
172
|
-
...(
|
|
173
|
-
? { columns: [...COLUMNS.slice(0, 5), ...extendedSessions.map(extendedColumn), ...COLUMNS.slice(5)] }
|
|
174
|
-
: {}),
|
|
196
|
+
...(columns.length !== COLUMNS.length ? { columns } : {}),
|
|
175
197
|
rows,
|
|
176
198
|
...(quoteError ? { complete: false, unavailableSymbols: result.unavailableSymbols, errors: [quoteError] } : {}),
|
|
177
|
-
...(
|
|
178
|
-
|
|
179
|
-
: {}),
|
|
180
|
-
metadata: { list: tab, source: result.source, stale: result.stale, rankedBy },
|
|
199
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
200
|
+
metadata: { list: tab, source: result.source, stale: result.stale, rankedBy, minFloat: floatFiltered ? minFloat : null },
|
|
181
201
|
// A Pro account's Gainers, Losers and Most Active are the real-time lists.
|
|
182
202
|
...(result.source === "cloud" ? { freshness: { status: "live" as const } } : {}),
|
|
183
203
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box, useUiCapabilities } from "../../../ui";
|
|
2
2
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { DataTableView, EmptyState, usePaneFooter, usePaneTabs, type DataTableKeyEvent } from "../../../components";
|
|
4
|
+
import { DataTableView, EmptyState, QueryBar, usePaneFooter, usePaneTabs, type DataTableKeyEvent } from "../../../components";
|
|
5
5
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
6
6
|
import type { PaneProps } from "../../../types/plugin";
|
|
7
7
|
import type { PluginModule } from "../plugin-module";
|
|
@@ -41,6 +41,7 @@ import { loadMarketMoverTab, marketMoverQuoteError } from "./client";
|
|
|
41
41
|
import { summaryFooterSegments } from "./footer";
|
|
42
42
|
import { isSessionTab, resolveActiveTab, usSessionAt, type UsSession } from "./session";
|
|
43
43
|
import { SessionMoversBody } from "./session-body";
|
|
44
|
+
import { ANY_FLOAT, minFloatEmptyTitle, minFloatFilter, minFloatShares, partialFloatFooter } from "./min-float";
|
|
44
45
|
import { marketMoversHeadless } from "./headless";
|
|
45
46
|
import { buildMarketMoverColumns, renderMarketMoverCell } from "./table";
|
|
46
47
|
import type { ExtendedSession } from "../../../market-data/market/status";
|
|
@@ -152,16 +153,21 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
152
153
|
}) {
|
|
153
154
|
const dataProvider = useAssetData();
|
|
154
155
|
const { pinTicker } = usePluginTickerActions();
|
|
156
|
+
const [minFloatValue, setMinFloatValue] = usePluginPaneState<string>("minFloat", ANY_FLOAT);
|
|
157
|
+
// Trending carries no float, so the minimum is not offered there.
|
|
158
|
+
const floatFiltered = activeTab !== "trending";
|
|
159
|
+
const minFloat = floatFiltered ? minFloatShares(minFloatValue) : null;
|
|
160
|
+
const listKey = `${activeTab}:${minFloat ?? ""}`;
|
|
155
161
|
const [quotes, setQuotes] = useState<MoverQuote[]>([]);
|
|
156
|
-
const [
|
|
157
|
-
const visibleQuotes =
|
|
162
|
+
const [loadedKey, setLoadedKey] = useState<string | null>(null);
|
|
163
|
+
const visibleQuotes = loadedKey === listKey ? quotes : NO_QUOTES;
|
|
158
164
|
// The first load starts before the effect runs; an empty board is not "no data".
|
|
159
165
|
const [loading, setLoading] = useState(true);
|
|
160
166
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
161
167
|
const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
|
|
162
168
|
const [sortPreference, setSortPreference] = useState<MarketMoverSortPreference>(DEFAULT_SORT_PREFERENCE);
|
|
163
169
|
const [moversStale, setMoversStale] = useState(false);
|
|
164
|
-
const [lastSuccess, setLastSuccess] = useState<{
|
|
170
|
+
const [lastSuccess, setLastSuccess] = useState<{ key: string; at: number } | null>(null);
|
|
165
171
|
|
|
166
172
|
const fetchGenRef = useRef(0);
|
|
167
173
|
|
|
@@ -192,8 +198,8 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
192
198
|
// A pre-market or after-hours column only while a row has such a print.
|
|
193
199
|
const extendedSessionsKey = moverExtendedSessions(rankedRows).join(",");
|
|
194
200
|
const columns = useMemo(
|
|
195
|
-
() => buildMarketMoverColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
|
|
196
|
-
[extendedSessionsKey, width],
|
|
201
|
+
() => buildMarketMoverColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], { float: minFloat != null }),
|
|
202
|
+
[extendedSessionsKey, minFloat, width],
|
|
197
203
|
);
|
|
198
204
|
const rows = useMemo(() => sortRows(rankedRows, sortPreference), [rankedRows, sortPreference]);
|
|
199
205
|
const selectedIdx = selectedSymbol
|
|
@@ -223,6 +229,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
223
229
|
try {
|
|
224
230
|
const result = await loadMarketMoverTab(tab, dataProvider, {
|
|
225
231
|
forceRefresh: options?.forceRefresh,
|
|
232
|
+
minFloat,
|
|
226
233
|
});
|
|
227
234
|
if (fetchGenRef.current !== gen) return;
|
|
228
235
|
|
|
@@ -230,12 +237,12 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
230
237
|
// A partial list shows only its current rows; a total failure keeps this list's prior rows.
|
|
231
238
|
if (!quoteError || result.quotes.length > 0) {
|
|
232
239
|
setQuotes(result.quotes);
|
|
233
|
-
|
|
240
|
+
setLoadedKey(`${tab}:${minFloat ?? ""}`);
|
|
234
241
|
if (!options?.background) setSelectedSymbol(null);
|
|
235
242
|
}
|
|
236
243
|
setMoversStale(result.stale);
|
|
237
244
|
setLoadError(quoteError);
|
|
238
|
-
if (!quoteError && !result.stale) setLastSuccess({ tab
|
|
245
|
+
if (!quoteError && !result.stale) setLastSuccess({ key: `${tab}:${minFloat ?? ""}`, at: Date.now() });
|
|
239
246
|
} catch {
|
|
240
247
|
if (fetchGenRef.current !== gen) return;
|
|
241
248
|
setMoversStale(true);
|
|
@@ -244,7 +251,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
244
251
|
finally {
|
|
245
252
|
if (fetchGenRef.current === gen && !options?.background) setLoading(false);
|
|
246
253
|
}
|
|
247
|
-
}, [dataProvider]);
|
|
254
|
+
}, [dataProvider, minFloat]);
|
|
248
255
|
|
|
249
256
|
useEffect(() => {
|
|
250
257
|
setSelectedSymbol(null);
|
|
@@ -254,7 +261,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
254
261
|
const backgroundRefresh = useCallback(() => {
|
|
255
262
|
void loadTab(activeTab, { background: true });
|
|
256
263
|
}, [activeTab, loadTab]);
|
|
257
|
-
useAutoRefresh(lastSuccess?.
|
|
264
|
+
useAutoRefresh(lastSuccess?.key === listKey ? lastSuccess.at : null, backgroundRefresh);
|
|
258
265
|
|
|
259
266
|
const openSymbol = useCallback((row: MarketMoverRow) => {
|
|
260
267
|
pinTicker(moverKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
|
|
@@ -281,16 +288,21 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
281
288
|
id: "feed",
|
|
282
289
|
parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
|
|
283
290
|
}] : []),
|
|
284
|
-
...(moversStale &&
|
|
291
|
+
...(moversStale && loadedKey === listKey ? [{
|
|
285
292
|
id: "stale",
|
|
286
293
|
parts: [{ text: "stale", tone: "muted" as const }],
|
|
287
294
|
}] : []),
|
|
295
|
+
// The FLOAT column shows while a minimum is set; a float the source does not date says so once.
|
|
296
|
+
...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
|
|
288
297
|
],
|
|
289
|
-
}), [
|
|
298
|
+
}), [columns, feedStatus, listKey, loadedKey, loadError, loading, moversStale, rows, summaryQuotes]);
|
|
290
299
|
|
|
291
300
|
return (
|
|
292
301
|
<DataTableView<MarketMoverRow, MarketMoverColumn>
|
|
293
302
|
focused={focused}
|
|
303
|
+
rootBefore={floatFiltered ? (
|
|
304
|
+
<QueryBar width={Math.max(1, width - 2)} filters={[minFloatFilter(minFloatValue, setMinFloatValue)]} />
|
|
305
|
+
) : undefined}
|
|
294
306
|
selection={{
|
|
295
307
|
kind: "id",
|
|
296
308
|
selectedId: selectedSymbol,
|
|
@@ -298,7 +310,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
298
310
|
onChange: (symbol) => setSelectedSymbol(symbol),
|
|
299
311
|
}}
|
|
300
312
|
onRootKeyDown={handleTableKeyDown}
|
|
301
|
-
resetScrollKey={
|
|
313
|
+
resetScrollKey={listKey}
|
|
302
314
|
sortable
|
|
303
315
|
columns={columns}
|
|
304
316
|
items={rows}
|
|
@@ -309,7 +321,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
309
321
|
onActivate={openSymbol}
|
|
310
322
|
renderCell={renderCell}
|
|
311
323
|
selectedTextOverridesCellColor
|
|
312
|
-
emptyStateTitle={loading ? "Loading movers..." : loadError ?? "No movers returned."}
|
|
324
|
+
emptyStateTitle={loading ? "Loading movers..." : loadError ?? (minFloat != null ? minFloatEmptyTitle(minFloatValue) : "No movers returned.")}
|
|
313
325
|
emptyContent={loadError ? (
|
|
314
326
|
<Box paddingX={1} paddingY={1}>
|
|
315
327
|
<EmptyState title={loadError} message="Try again in a moment." />
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PaneFooterSegment } from "../../../components";
|
|
2
|
+
import type { QueryBarSelectFilter } from "../../../components/ui/query-bar";
|
|
3
|
+
import { formatCompact } from "../../../utils/format";
|
|
4
|
+
|
|
5
|
+
/** No minimum: every row, with or without a float. */
|
|
6
|
+
export const ANY_FLOAT = "any";
|
|
7
|
+
/** The minimums the pane's menu offers; `--min-float` takes any other. */
|
|
8
|
+
const PRESETS = ["1M", "5M", "10M", "20M", "50M", "100M"] as const;
|
|
9
|
+
const SCALE: Record<string, number> = { k: 1e3, m: 1e6, b: 1e9 };
|
|
10
|
+
|
|
11
|
+
/** Shares in `text` (10M, 500k, 2.5M, 25000000), or null when it is not a share count. */
|
|
12
|
+
function parseShares(text: string): number | null {
|
|
13
|
+
const match = /^((?:\d+(?:\.\d*)?|\.\d+))([kmb])?$/i.exec(text.trim().replace(/[_,]/g, ""));
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const value = Number(match[1]) * (match[2] ? SCALE[match[2].toLowerCase()]! : 1);
|
|
16
|
+
return Number.isFinite(value) ? value : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The minimum a saved or typed value sets, in shares; null for none (`any`, 0 or a value that is not a count). */
|
|
20
|
+
export function minFloatShares(value: string | null | undefined): number | null {
|
|
21
|
+
if (!value || value.trim().toLowerCase() === ANY_FLOAT) return null;
|
|
22
|
+
const shares = parseShares(value);
|
|
23
|
+
return shares != null && shares > 0 ? shares : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `--min-float` as the pane keeps it: `10M` for 10m or 10000000, `any` for none. Throws on a value that is not a share count. */
|
|
27
|
+
export function normalizeMinFloat(value: string): string {
|
|
28
|
+
if (value.trim().toLowerCase() === ANY_FLOAT) return ANY_FLOAT;
|
|
29
|
+
const shares = parseShares(value);
|
|
30
|
+
if (shares == null) throw new Error(`--min-float takes a share count such as 10M or 500k, or any; got "${value}".`);
|
|
31
|
+
return shares > 0 ? formatCompact(shares) : ANY_FLOAT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A row passes with a float at or above the minimum; a row whose float is not known does not. */
|
|
35
|
+
export function meetsMinFloat(floatShares: number | null | undefined, minimum: number | null): boolean {
|
|
36
|
+
return minimum == null || (floatShares != null && floatShares >= minimum);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The pane's Min float menu: Any and the presets, with a minimum set from the CLI kept as its own choice. */
|
|
40
|
+
export function minFloatFilter(value: string, onChange: (value: string) => void): QueryBarSelectFilter {
|
|
41
|
+
const current = minFloatShares(value) == null ? ANY_FLOAT : value;
|
|
42
|
+
const presets: string[] = [...PRESETS];
|
|
43
|
+
const options = [ANY_FLOAT, ...(presets.includes(current) || current === ANY_FLOAT ? [] : [current]), ...presets]
|
|
44
|
+
.map((option) => ({ value: option, label: option === ANY_FLOAT ? "Any" : option }));
|
|
45
|
+
return {
|
|
46
|
+
id: "min-float",
|
|
47
|
+
label: "Min float",
|
|
48
|
+
title: "Minimum float",
|
|
49
|
+
value: current,
|
|
50
|
+
defaultValue: ANY_FLOAT,
|
|
51
|
+
options,
|
|
52
|
+
onChange,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a list says under its Float column while a shown float is partial. */
|
|
57
|
+
export const PARTIAL_FLOAT_NOTE = "Float is partial: the source does not say when it measured it.";
|
|
58
|
+
|
|
59
|
+
/** Whether any row's float is partial, which the list then says once. */
|
|
60
|
+
export function hasPartialFloat(rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): boolean {
|
|
61
|
+
return rows.some((row) => row.floatShares != null && row.floatSharesState === "partial");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The footer's word for a FLOAT column of partial floats, or none. */
|
|
65
|
+
export function partialFloatFooter(shown: boolean, rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): PaneFooterSegment[] {
|
|
66
|
+
return shown && hasPartialFloat(rows)
|
|
67
|
+
? [{ id: "float-partial", parts: [{ text: "FLOAT", tone: "label" }, { text: "partial, undated", tone: "muted" }] }]
|
|
68
|
+
: [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The empty list's title while a minimum hides every row. */
|
|
72
|
+
export function minFloatEmptyTitle(value: string): string {
|
|
73
|
+
return `No movers with a float of ${value} or more.`;
|
|
74
|
+
}
|
|
@@ -50,7 +50,8 @@ type MarketMoverColumnId =
|
|
|
50
50
|
| "volume"
|
|
51
51
|
| "volumeRatio"
|
|
52
52
|
| "range"
|
|
53
|
-
| "marketCap"
|
|
53
|
+
| "marketCap"
|
|
54
|
+
| "floatShares";
|
|
54
55
|
export type MarketMoverColumn = DataTableColumn & { id: MarketMoverColumnId };
|
|
55
56
|
/**
|
|
56
57
|
* A list row. Price and change are the regular session's, as `ticker` reads
|
|
@@ -123,6 +124,8 @@ function getSortValue(
|
|
|
123
124
|
return fiftyTwoWeekPositionPercent(row.price, row.fiftyTwoWeekLow, row.fiftyTwoWeekHigh);
|
|
124
125
|
case "marketCap":
|
|
125
126
|
return row.marketCap ?? null;
|
|
127
|
+
case "floatShares":
|
|
128
|
+
return row.floatShares ?? null;
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -220,6 +220,17 @@ function parseScreenerSessions(row: Record<string, unknown>): CloudMoverSessionF
|
|
|
220
220
|
return sessions;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** A positive float with its state, or nothing: a row without one fails any minimum. */
|
|
224
|
+
function floatOf(source: { floatShares?: unknown; floatSharesState?: unknown; floatSharesReason?: unknown } | null | undefined): Pick<ScreenerQuote, "floatShares" | "floatSharesState" | "floatSharesReason"> {
|
|
225
|
+
const shares = screenerNumber(source?.floatShares);
|
|
226
|
+
if (shares == null || !(shares > 0)) return {};
|
|
227
|
+
return {
|
|
228
|
+
floatShares: shares,
|
|
229
|
+
...(source?.floatSharesState === "partial" ? { floatSharesState: "partial" as const } : {}),
|
|
230
|
+
...(source?.floatSharesState === "partial" && typeof source.floatSharesReason === "string" ? { floatSharesReason: source.floatSharesReason } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
223
234
|
/** Validate numeric fields at the API boundary so absent values stay unavailable. */
|
|
224
235
|
export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
|
|
225
236
|
if (!data || typeof data !== "object" || !("quotes" in data) || !Array.isArray(data.quotes)) {
|
|
@@ -239,6 +250,7 @@ export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
|
|
|
239
250
|
previousClose: screenerNumber(q.previousClose) ?? undefined, exchange: typeof q.exchange === "string" ? q.exchange : "",
|
|
240
251
|
lastUpdated: typeof q.lastUpdated === "number" && Number.isFinite(q.lastUpdated) && q.lastUpdated > 0 ? q.lastUpdated : undefined,
|
|
241
252
|
sessions: parseScreenerSessions(q),
|
|
253
|
+
...floatOf(q),
|
|
242
254
|
}];
|
|
243
255
|
});
|
|
244
256
|
}
|
|
@@ -325,6 +337,8 @@ function mergeCloudScreenerItem(
|
|
|
325
337
|
lastUpdated: item.lastUpdated,
|
|
326
338
|
previousClose: screenerNumber(item.previousClose) ?? undefined,
|
|
327
339
|
sessions: parseScreenerSessions(item as unknown as Record<string, unknown>),
|
|
340
|
+
// The live list's own float, the delayed list's when it sends none, each with its own state.
|
|
341
|
+
...floatOf(item.floatShares != null ? item : metadata),
|
|
328
342
|
};
|
|
329
343
|
}
|
|
330
344
|
|
|
@@ -20,6 +20,7 @@ import { nextHeaderSort } from "../../../utils/sort-values";
|
|
|
20
20
|
import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
21
21
|
import { ProWall, SignInWall } from "../cloud/auth-actions";
|
|
22
22
|
import { loadSessionMovers, SessionMoversAccessError } from "./client";
|
|
23
|
+
import { ANY_FLOAT, minFloatEmptyTitle, minFloatFilter, minFloatShares, partialFloatFooter } from "./min-float";
|
|
23
24
|
import { summaryFooterSegments } from "./footer";
|
|
24
25
|
import type { MarketSummaryQuote } from "./screener";
|
|
25
26
|
import {
|
|
@@ -104,7 +105,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
104
105
|
const { pinTicker } = usePluginTickerActions();
|
|
105
106
|
const [savedSide, setSavedSide] = usePluginPaneState<CloudSessionMoversSide>("sessionSide", "up");
|
|
106
107
|
const side = resolveSide(view, savedSide);
|
|
107
|
-
const
|
|
108
|
+
const [minFloatValue, setMinFloatValue] = usePluginPaneState<string>("minFloat", ANY_FLOAT);
|
|
109
|
+
const minFloat = minFloatShares(minFloatValue);
|
|
110
|
+
const listKey = `${view}:${side}:${minFloat ?? ""}`;
|
|
108
111
|
const [loaded, setLoaded] = useState<LoadedList | null>(null);
|
|
109
112
|
// The first frame would otherwise claim an empty list.
|
|
110
113
|
const [loading, setLoading] = useState(true);
|
|
@@ -132,9 +135,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
132
135
|
setLoadError(null);
|
|
133
136
|
}
|
|
134
137
|
try {
|
|
135
|
-
const next = await loadSessionMovers(view, side, options);
|
|
138
|
+
const next = await loadSessionMovers(view, side, { ...options, minFloat });
|
|
136
139
|
if (fetchGenRef.current !== gen) return;
|
|
137
|
-
setLoaded({ key: `${view}:${side}`, payload: next });
|
|
140
|
+
setLoaded({ key: `${view}:${side}:${minFloat ?? ""}`, payload: next });
|
|
138
141
|
setLoadError(null);
|
|
139
142
|
setLastLoadedAt(Date.now());
|
|
140
143
|
} catch (error) {
|
|
@@ -147,7 +150,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
147
150
|
} finally {
|
|
148
151
|
if (fetchGenRef.current === gen && !options?.background) setLoading(false);
|
|
149
152
|
}
|
|
150
|
-
}, [onDenied, side, view]);
|
|
153
|
+
}, [minFloat, onDenied, side, view]);
|
|
151
154
|
|
|
152
155
|
useEffect(() => {
|
|
153
156
|
setSelectedId(null);
|
|
@@ -167,7 +170,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
167
170
|
freshnessScopeKey: `market-movers:${listKey}`,
|
|
168
171
|
liveStreaming,
|
|
169
172
|
});
|
|
170
|
-
const columns = useMemo(() => buildSessionMoverColumns(view, width), [view, width]);
|
|
173
|
+
const columns = useMemo(() => buildSessionMoverColumns(view, width, { float: minFloat != null }), [minFloat, view, width]);
|
|
171
174
|
// The sort survives a change of list, but not onto a column this list or width does not show.
|
|
172
175
|
const sort = columns.some((column) => column.id === sortPreference.columnId) ? sortPreference : DEFAULT_SORT;
|
|
173
176
|
const rows = useMemo(
|
|
@@ -199,8 +202,10 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
199
202
|
parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
|
|
200
203
|
}] : []),
|
|
201
204
|
...(payload?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "muted" as const }] }] : []),
|
|
205
|
+
// A float the source does not date says so once, while the FLOAT column shows.
|
|
206
|
+
...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
|
|
202
207
|
],
|
|
203
|
-
}), [asOfLabel, feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
|
|
208
|
+
}), [asOfLabel, columns, feedStatus, loadError, loading, payload?.stale, rows, sessionLabel, summaryQuotes]);
|
|
204
209
|
|
|
205
210
|
const openRow = useCallback((row: SessionMoverRow) => {
|
|
206
211
|
pinTicker(rowKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
|
|
@@ -217,7 +222,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
217
222
|
const sides = sessionSides(view);
|
|
218
223
|
// Until this list's first answer the table is loading, including the frame
|
|
219
224
|
// between a change of list and its request going out.
|
|
220
|
-
const emptyTitle = payload ? "No movers yet." : loadError ?? "Loading movers...";
|
|
225
|
+
const emptyTitle = payload ? (minFloat != null ? minFloatEmptyTitle(minFloatValue) : "No movers yet.") : loadError ?? "Loading movers...";
|
|
221
226
|
return (
|
|
222
227
|
<DataTableView<SessionMoverRow, SessionMoverColumn>
|
|
223
228
|
focused={focused}
|
|
@@ -231,7 +236,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
231
236
|
value: side,
|
|
232
237
|
options: sides,
|
|
233
238
|
onChange: (value: CloudSessionMoversSide) => setSavedSide(value),
|
|
234
|
-
}]}
|
|
239
|
+
}, minFloatFilter(minFloatValue, setMinFloatValue)]}
|
|
235
240
|
/>
|
|
236
241
|
)}
|
|
237
242
|
selection={{ kind: "id", selectedId, getId: rowKey, onChange: setSelectedId }}
|
|
@@ -21,9 +21,10 @@ const VOLUME_LABEL: Record<CloudSessionMoversCategory, string> = {
|
|
|
21
21
|
* Pre-market and after hours: last, change from the reference close, session
|
|
22
22
|
* volume, relative volume, float and the catalyst. Gaps add the gap and the
|
|
23
23
|
* distance from VWAP. The name gives way first, then the columns a narrow pane
|
|
24
|
-
* can do without (the day's volume, then the float
|
|
24
|
+
* can do without (the day's volume, then the float, which stays while a
|
|
25
|
+
* minimum float is set: `float`).
|
|
25
26
|
*/
|
|
26
|
-
export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number): SessionMoverColumn[] {
|
|
27
|
+
export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number, options: { float?: boolean } = {}): SessionMoverColumn[] {
|
|
27
28
|
const columns: SessionMoverColumn[] = [
|
|
28
29
|
{ id: "rank", label: "#", width: 3, align: "left" },
|
|
29
30
|
{ id: "symbol", label: "TICKER", width: 8, align: "left" },
|
|
@@ -37,7 +38,7 @@ export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width
|
|
|
37
38
|
{ id: "floatShares", label: "FLOAT", width: 7, align: "right" },
|
|
38
39
|
{ id: "catalyst", label: "EVENT", width: 6, align: "left" },
|
|
39
40
|
];
|
|
40
|
-
const optional: SessionMoverColumnId[] = view === "gaps" ? ["volume",
|
|
41
|
+
const optional: SessionMoverColumnId[] = [...(view === "gaps" ? ["volume" as const] : []), ...(options.float ? [] : ["floatShares" as const])];
|
|
41
42
|
// Measured the way the table draws it: a header widens its column, and a
|
|
42
43
|
// left-aligned column after a right-aligned one gets a lead gap. The name
|
|
43
44
|
// sits between left-aligned columns, so it adds only its width and a gutter.
|
|
@@ -26,15 +26,20 @@ function fiftyTwoWeekPosition(price: number | null, low: number | undefined, hig
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const EXTENDED_COLUMN_WIDTH = 9;
|
|
29
|
-
/** With an extended column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
|
|
29
|
+
/** With an extended or float column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
|
|
30
30
|
const EXTENDED_MIN_NAME_WIDTH = 12;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* The list's columns at `width`. A pre-market or after-hours column follows
|
|
34
|
-
* CHG% for each session in `extendedSessions
|
|
35
|
-
*
|
|
34
|
+
* CHG% for each session in `extendedSessions`, and FLOAT ends the row while a
|
|
35
|
+
* minimum float is set (`float`); to make room for them a narrow pane gives up
|
|
36
|
+
* MCAP, then 52W%, before it cuts the names short.
|
|
36
37
|
*/
|
|
37
|
-
export function buildMarketMoverColumns(
|
|
38
|
+
export function buildMarketMoverColumns(
|
|
39
|
+
width: number,
|
|
40
|
+
extendedSessions: readonly ExtendedSession[] = [],
|
|
41
|
+
options: { float?: boolean } = {},
|
|
42
|
+
): MarketMoverColumn[] {
|
|
38
43
|
const lead: MarketMoverColumn[] = [
|
|
39
44
|
{ id: "rank", label: "#", width: 3, align: "left" },
|
|
40
45
|
{ id: "symbol", label: "TICKER", width: 8, align: "left" },
|
|
@@ -55,12 +60,13 @@ export function buildMarketMoverColumns(width: number, extendedSessions: readonl
|
|
|
55
60
|
{ id: "volumeRatio", label: "V/AVG", width: 6, align: "right" },
|
|
56
61
|
{ id: "range", label: "52W%", width: 6, align: "right" },
|
|
57
62
|
{ id: "marketCap", label: "MCAP", width: 8, align: "right" },
|
|
63
|
+
...(options.float ? [{ id: "floatShares" as const, label: "FLOAT", width: 7, align: "right" as const }] : []),
|
|
58
64
|
];
|
|
59
65
|
// Measured the way the table draws them (a header and its sort mark widen a
|
|
60
66
|
// column, gaps between them); the name adds its own width and a gap.
|
|
61
67
|
const nameRoom = () => width - getTableWidth([...lead, ...quote, ...trailing]) - 1;
|
|
62
68
|
for (const id of ["marketCap", "range"] as const) {
|
|
63
|
-
if (extendedSessions.length === 0 || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
|
|
69
|
+
if ((extendedSessions.length === 0 && !options.float) || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
|
|
64
70
|
trailing.splice(trailing.findIndex((column) => column.id === id), 1);
|
|
65
71
|
}
|
|
66
72
|
return [...lead, { id: "name", label: "NAME", width: Math.max(6, nameRoom()), align: "left" }, ...quote, ...trailing];
|
|
@@ -121,5 +127,11 @@ export function renderMarketMoverCell(
|
|
|
121
127
|
value: row.marketCap,
|
|
122
128
|
color: colors.textDim,
|
|
123
129
|
};
|
|
130
|
+
case "floatShares":
|
|
131
|
+
return {
|
|
132
|
+
text: row.floatShares != null ? formatCompact(row.floatShares, { fixedDecimals: true }) : "—",
|
|
133
|
+
value: row.floatShares ?? null,
|
|
134
|
+
color: colors.textDim,
|
|
135
|
+
};
|
|
124
136
|
}
|
|
125
137
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import type { BreadthDay } from "../../../api-client/members";
|
|
3
|
+
import { ChartTableHeader, DataTableView, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, type DataTableColumn } from "../../../components";
|
|
4
|
+
import { staticSeries } from "../../../components/chart/static/series";
|
|
5
|
+
import { fitChartTableColumns } from "../../../components/chart-table/layout";
|
|
6
|
+
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
7
|
+
import { useAsyncResource, useAutoRefresh, usePaneVisible, usePluginPaneState } from "../../../public/react";
|
|
8
|
+
import { useThemeColors } from "../../../theme/theme-context";
|
|
9
|
+
import type { PaneProps } from "../../../types/plugin";
|
|
10
|
+
import { cachedBreadth, loadBreadth } from "./client";
|
|
11
|
+
import { useLiveSessionRefresh } from "../shared/volatility/live-session";
|
|
12
|
+
import { decimal } from "./model";
|
|
13
|
+
|
|
14
|
+
const percent = (value: number | null) => value == null ? "--" : `${value.toFixed(1)}%`;
|
|
15
|
+
const CHARTS = [{ value: "adLine", label: "A/D line" }, { value: "ma200", label: "Above 200D" }, { value: "ma50", label: "Above 50D" }, { value: "ma20", label: "Above 20D" }];
|
|
16
|
+
type Metric = "adLine" | "ma200" | "ma50" | "ma20";
|
|
17
|
+
const dateOf = (row: BreadthDay) => new Date(`${row.date}T00:00:00Z`);
|
|
18
|
+
const keyOf = (row: BreadthDay) => row.date;
|
|
19
|
+
const valueOf = (row: BreadthDay, metric: Metric) => metric === "adLine" ? row.adLine : row[metric].percent;
|
|
20
|
+
const COLUMNS: DataTableColumn[] = [
|
|
21
|
+
{ id: "date", label: "Date", width: 12, align: "left" },
|
|
22
|
+
...["advances", "declines", "unchanged", "ma20", "ma50", "ma200", "newHighs", "newLows", "coverage"].map((id, index) => ({
|
|
23
|
+
id, label: ["Adv", "Dec", "Unch", ">20D %", ">50D %", ">200D %", "52W high", "52W low", "A/D covered"][index]!, width: index === 8 ? 13 : 10, flexGrow: 1, align: "right" as const,
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function Breadth({ fund, width, height, focused, active }: Pick<PaneProps, "width" | "height" | "focused"> & { fund: string; active: boolean }) {
|
|
28
|
+
const colors = useThemeColors();
|
|
29
|
+
const visible = usePaneVisible();
|
|
30
|
+
const resource = useAsyncResource(useCallback((force: boolean) => loadBreadth(fund, force), [fund]), { initialData: useCallback(() => cachedBreadth(fund), [fund]) });
|
|
31
|
+
const data = resource.data?.payload;
|
|
32
|
+
const latest = data?.latest;
|
|
33
|
+
const [metric, setMetric] = usePluginPaneState<Metric>("breadth:metric", "adLine");
|
|
34
|
+
const [selected, setSelected] = usePluginPaneState<string | null>(`breadth:selected:${fund}`, null);
|
|
35
|
+
const history = useMemo(() => data ? [...data.history, ...(data.basis === "intraday" && data.latest ? [data.latest] : [])] : [], [data]);
|
|
36
|
+
const rows = useMemo(() => [...history].reverse(), [history]);
|
|
37
|
+
const columns = useMemo(() => fitChartTableColumns(COLUMNS, width, ["unchanged", "ma20", "ma50", "coverage"]), [width]);
|
|
38
|
+
const series = useMemo(() => [staticSeries(history.map(row => ({ date: dateOf(row), observedAt: new Date(row.asOf), value: valueOf(row, metric) })), {
|
|
39
|
+
id: `breadth-${metric}`, label: metric === "adLine" ? "Cumulative advances − declines" : `% above ${metric.slice(2)}-day average`,
|
|
40
|
+
color: colors.warning, calendarSpaced: true,
|
|
41
|
+
})], [history, metric, colors.warning]);
|
|
42
|
+
const link = useChartTableSelection({ rows, getId: keyOf, getDate: dateOf, selectedId: selected ?? latest?.date ?? null, onSelect: setSelected, focused });
|
|
43
|
+
// The resource keeps a failed load as its error (stale, and a warning below), so this refresh never rejects.
|
|
44
|
+
const sessionOpen = useLiveSessionRefresh(() => resource.load(true), data?.access === "realtime" ? 15_000 : 60_000, active && visible, resource.updatedAt).live;
|
|
45
|
+
useAutoRefresh(resource.updatedAt, () => { if (active && visible && !sessionOpen) void resource.load(data?.basis === "intraday"); });
|
|
46
|
+
usePaneRefreshKey(() => void resource.reload(), { focused, enabled: active });
|
|
47
|
+
usePaneStatusFooter({ registrationId: "members:breadth", loading: !!data && resource.loading,
|
|
48
|
+
stale: !!data && (data.stale || resource.data!.stale || !!resource.error), info: latest ? [
|
|
49
|
+
{ id: "as-of", asOf: true, parts: [{ text: `${data?.basis === "intraday" ? data.access === "realtime" ? "live" : "15m delayed" : "close"} ${latest.date}`, tone: "muted" }] },
|
|
50
|
+
{ id: "coverage", parts: [{ text: `A/D ${latest.advanceDeclineEligible}/${latest.memberCount}`, tone: latest.advanceDeclineEligible < latest.memberCount * 0.8 ? "warning" : "muted" }] },
|
|
51
|
+
] : [] });
|
|
52
|
+
usePaneNoticeFooter({ registrationId: "members:breadth-notices", focused, notices: [
|
|
53
|
+
...(resource.error || resource.data?.refreshError ? [resource.error ?? resource.data!.refreshError!] : []),
|
|
54
|
+
...(latest && [latest.advanceDeclineEligible, latest.ma20.eligible, latest.ma50.eligible, latest.ma200.eligible, latest.highLowEligible].some(n => n < latest.memberCount * 0.8)
|
|
55
|
+
? [`Partial coverage: A/D ${latest.advanceDeclineEligible}, 20D ${latest.ma20.eligible}, 50D ${latest.ma50.eligible}, 200D ${latest.ma200.eligible}, 52W ${latest.highLowEligible} of ${latest.memberCount} members. Missing windows are excluded.`] : []),
|
|
56
|
+
] });
|
|
57
|
+
return <PaneStatusBody subject="fund breadth" loading={!data && resource.loading} error={!data ? resource.error : null}
|
|
58
|
+
empty={!!data && !latest} emptyTitle="Breadth is awaiting daily price history.">
|
|
59
|
+
{data && latest && <DataTableView<BreadthDay> items={rows} columns={columns} rootWidth={width} rootHeight={height} focused={focused}
|
|
60
|
+
getItemKey={keyOf} selection={{ kind: "id", selectedId: selected ?? latest.date, getId: keyOf, onChange: id => setSelected(String(id)) }}
|
|
61
|
+
sortColumnId="date" sortDirection="desc" freezeFirstColumn emptyStateTitle="No daily breadth history."
|
|
62
|
+
renderCell={(row, column) => {
|
|
63
|
+
if (column.id === "date") return { text: row.date };
|
|
64
|
+
if (column.id === "coverage") return { text: `${row.advanceDeclineEligible}/${row.memberCount}` };
|
|
65
|
+
if (column.id === "ma20" || column.id === "ma50" || column.id === "ma200") return { text: percent(row[column.id].percent), value: row[column.id].percent };
|
|
66
|
+
const value = row[column.id as "advances"];
|
|
67
|
+
const eligible = column.id === "newHighs" || column.id === "newLows" ? row.highLowEligible : row.advanceDeclineEligible;
|
|
68
|
+
return { text: eligible ? decimal(value, 0) : "--", value: eligible ? value : null,
|
|
69
|
+
color: column.id === "advances" || column.id === "newHighs" ? colors.positive : column.id === "declines" || column.id === "newLows" ? colors.negative : colors.text };
|
|
70
|
+
}}
|
|
71
|
+
rootBefore={<ChartTableHeader width={width} height={height} tableRows={rows.length} tableColumns={columns}
|
|
72
|
+
query={<QueryBar width={width} meta="History uses current members (survivorship bias)." filters={[{ kind: "select", id: "chart", label: "History", value: metric, options: CHARTS, onChange: value => setMetric(value as Metric) }]} />}
|
|
73
|
+
figures={[
|
|
74
|
+
{ label: "Adv / Dec", value: latest.advanceDeclineEligible ? `${latest.advances} / ${latest.declines}` : "--", detail: `${latest.unchanged} unchanged` },
|
|
75
|
+
...([20, 50, 200] as const).map(window => ({ label: `Above ${window}D`, value: percent(latest[`ma${window}`].percent), detail: `${latest[`ma${window}`].eligible}/${latest.memberCount} covered` })),
|
|
76
|
+
{ label: "52W high / low", value: latest.highLowEligible ? `${latest.newHighs} / ${latest.newLows}` : "--", detail: `${latest.highLowEligible}/${latest.memberCount} covered` },
|
|
77
|
+
]}
|
|
78
|
+
chart={{ series, ...link, formatValue: metric === "adLine" ? value => decimal(value, 0) : percent,
|
|
79
|
+
formatAxisValue: metric === "adLine" ? value => decimal(value, 0) : percent, remoteKind: "member-breadth", empty: "History is awaiting sufficient daily bars." }} />}
|
|
80
|
+
/>}
|
|
81
|
+
</PaneStatusBody>;
|
|
82
|
+
}
|