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,20 +1,23 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
3
|
-
import type { FundMembersPayload, FundChangesPayload, MemberFundsPayload } from "../../../api-client/members";
|
|
3
|
+
import type { FundMembersPayload, FundChangesPayload, MemberFundsPayload, FundBreadthPayload } from "../../../api-client/members";
|
|
4
4
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
|
+
import { companyNameCase } from "../../../tickers/company-name";
|
|
5
6
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
6
7
|
import { coveredFund, notCoveredMessage } from "./model";
|
|
7
8
|
const policy = { staleMs: 60_000, expireMs: 2 * 60 * 60_000 };
|
|
8
9
|
export const fundListCache = createPluginCache<MemberFundsPayload>({ kind: "member-funds", source: "gloom-cloud", schemaVersion: 1, policy });
|
|
9
10
|
export const fundMembersCache = createPluginCache<FundMembersPayload>({ kind: "fund-members", source: "gloom-cloud", schemaVersion: 1, policy });
|
|
10
11
|
export const fundChangesCache = createPluginCache<FundChangesPayload>({ kind: "fund-changes", source: "gloom-cloud", schemaVersion: 1, policy });
|
|
12
|
+
export const fundBreadthCache = createPluginCache<FundBreadthPayload>({ kind: "fund-breadth", source: "gloom-cloud", schemaVersion: 1, policy });
|
|
11
13
|
type Client = Pick<typeof apiClient, "getMemberFunds" | "getFundMembers" | "getFundChanges">;
|
|
12
14
|
const number = (value: unknown) => value === null || typeof value === "number" && Number.isFinite(value);
|
|
13
15
|
function validateMembers(data: FundMembersPayload) {
|
|
14
16
|
if (!data?.fund?.ticker || !Number.isFinite(Date.parse(data.asOf)) || !Array.isArray(data.members)
|
|
15
17
|
|| !data.members.every((row) => typeof row.id === "string" && typeof row.name === "string" && [row.weight, row.price, row.changePercent, row.return1WPercent, row.return1MPercent, row.returnYtdPercent, row.contribution].every(number))
|
|
16
18
|
|| !data.aggregate || !number(data.aggregate.sum)) throw new Error("Unreadable fund holdings response");
|
|
17
|
-
|
|
19
|
+
// A holdings file writes every name in capitals; the pane, the report and a watchlist made from it read them as other panes do.
|
|
20
|
+
return { ...data, members: data.members.map((row) => ({ ...row, name: companyNameCase(row.name) })) };
|
|
18
21
|
}
|
|
19
22
|
function validateChanges(data: FundChangesPayload) {
|
|
20
23
|
if (!data?.fund?.ticker || !Array.isArray(data.changes) || !data.changes.every((row) => typeof row.id === "string" && typeof row.reason === "string" && Array.isArray(row.estimates))) throw new Error("Unreadable fund changes response");
|
|
@@ -33,13 +36,13 @@ async function fetchPayload<T>(fetcher: () => Promise<T>, validate: (data: T) =>
|
|
|
33
36
|
* same. The covered list tells them apart: a fund missing from a readable list is not covered; a list
|
|
34
37
|
* that cannot be read, or that holds the fund, leaves the server-side message.
|
|
35
38
|
*/
|
|
36
|
-
async function fetchFundPayload<T>(fund: string, fetcher: () => Promise<T>, validate: (data: T) => T, client: Client) {
|
|
39
|
+
async function fetchFundPayload<T>(fund: string, fetcher: () => Promise<T>, validate: (data: T) => T, client: Client, unavailable = UNAVAILABLE) {
|
|
37
40
|
try { return validate(await fetcher()); } catch (error) {
|
|
38
41
|
if (error instanceof ApiRequestError && error.status === 404) {
|
|
39
42
|
const funds = await fetchFunds(client).then((payload) => payload.funds, () => []);
|
|
40
43
|
if (funds.length && !coveredFund(funds, fund)) throw new Error(notCoveredMessage(fund, funds));
|
|
41
44
|
}
|
|
42
|
-
throw unavailableOnServer(error,
|
|
45
|
+
throw unavailableOnServer(error, unavailable);
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
export const fetchFunds = (client: Client = apiClient) => fetchPayload(() => client.getMemberFunds(), validateFunds);
|
|
@@ -51,3 +54,15 @@ export const cachedMembers = (fund: string) => cachedCloudResource(fundMembersCa
|
|
|
51
54
|
export const loadMembers = (fund: string, force = false) => loadCloudResource(fundMembersCache, fund, () => fetchMembers(fund), { force, validate: validateMembers });
|
|
52
55
|
export const cachedChanges = (fund: string) => cachedCloudResource(fundChangesCache, fund, validateChanges);
|
|
53
56
|
export const loadChanges = (fund: string, force = false) => loadCloudResource(fundChangesCache, fund, () => fetchChanges(fund), { force, validate: validateChanges });
|
|
57
|
+
|
|
58
|
+
function validateBreadth(data: FundBreadthPayload) {
|
|
59
|
+
const row = (value: FundBreadthPayload["history"][number]) => value && /^\d{4}-\d{2}-\d{2}$/.test(value.date)
|
|
60
|
+
&& [value.memberCount, value.observed, value.advances, value.declines, value.unchanged, value.advanceDeclineEligible, value.newHighs, value.newLows, value.highLowEligible].every(n => Number.isInteger(n) && n >= 0)
|
|
61
|
+
&& [value.ma20, value.ma50, value.ma200].every(m => m && Number.isInteger(m.eligible) && m.eligible >= 0 && Number.isInteger(m.above) && m.above >= 0 && m.above <= m.eligible && number(m.percent))
|
|
62
|
+
&& number(value.adLine);
|
|
63
|
+
if (!data?.fund?.ticker || !Array.isArray(data.history) || !data.history.every(row) || (data.latest !== null && !row(data.latest))) throw new Error("Unreadable fund breadth response");
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
export const fetchBreadth = (fund: string, client: Client & Pick<typeof apiClient, "getFundBreadth"> = apiClient) => fetchFundPayload(fund, () => client.getFundBreadth(fund), validateBreadth, client, "Fund breadth is not available on this server yet.");
|
|
67
|
+
export const cachedBreadth = (fund: string) => cachedCloudResource(fundBreadthCache, fund, validateBreadth);
|
|
68
|
+
export const loadBreadth = (fund: string, force = false) => loadCloudResource(fundBreadthCache, fund, () => fetchBreadth(fund), { force, validate: validateBreadth });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
|
-
import { fetchChanges, fetchFunds, fetchMembers } from "./client";
|
|
3
|
-
import { canonicalFund, changeReason, coverageText, decimal, DEFAULT_SORT, holdingsCaveats, holdingsSourceText, memberRows, percent } from "./model";
|
|
2
|
+
import { fetchBreadth, fetchChanges, fetchFunds, fetchMembers } from "./client";
|
|
3
|
+
import { canonicalFund, changeReason, coverageText, decimal, DEFAULT_SORT, holdingsCaveats, holdingsSourceText, memberRows, membersFeed, percent } from "./model";
|
|
4
4
|
const columns: HeadlessPaneColumn[] = [
|
|
5
5
|
{ key: "symbol", header: "Ticker" }, { key: "name", header: "Name" },
|
|
6
6
|
{ key: "weight", header: "Weight %", align: "right", format: (value) => value == null ? "--" : decimal(Number(value) * 100) },
|
|
@@ -9,14 +9,22 @@ const columns: HeadlessPaneColumn[] = [
|
|
|
9
9
|
...["changePercent", "return1WPercent", "return1MPercent", "returnYtdPercent"].map((key, i) => ({ key, header: ["1D", "1W", "1M", "YTD"][i]!, align: "right" as const, format: (value: unknown) => percent(value as number | null) })),
|
|
10
10
|
];
|
|
11
11
|
export const membersHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
12
|
-
shape: "rows", argument: { kind: "
|
|
13
|
-
options: [{ key: "tab", type: "enum", values: [{ value: "members" }, { value: "movers" }, { value: "changes" }], defaultValue: "members", description: "Members, movers or
|
|
14
|
-
discovery: { aliases: ["MEMB", "MRR", "IMOV"], limitations: ["ETF holdings, with file dates.", "Member prices are real-time with Pro and fifteen minutes delayed otherwise; partial 1D coverage.", "Nasdaq-100 is not covered."] },
|
|
12
|
+
shape: "rows", argument: { kind: "free-text", optional: true, placeholder: "fund", description: "Covered fund or index alias; omit for the fund list." },
|
|
13
|
+
options: [{ key: "tab", type: "enum", values: [{ value: "members" }, { value: "movers" }, { value: "changes" }, { value: "breadth" }], defaultValue: "members", description: "Members, movers, changes or daily breadth." }],
|
|
14
|
+
discovery: { aliases: ["MEMB", "MRR", "IMOV"], limitations: ["ETF holdings, with file dates.", "Member prices are real-time with Pro and fifteen minutes delayed otherwise; partial 1D coverage.", "Nasdaq-100 is not covered.", "Breadth history uses current members (survivorship bias)."] },
|
|
15
15
|
describe: (args) => args.rawArgument ? `MEMB ${args.rawArgument}` : "Index and ETF members",
|
|
16
16
|
async load(args, context) {
|
|
17
17
|
const input = args.symbols[0] ?? args.rawArgument ?? "";
|
|
18
18
|
if (!input) return { freshness: { status: "not-a-feed", basis: "fund holdings files" }, rows: (await fetchFunds(context.apiClient)).funds.map((fund) => ({ ...fund })), columns: [{ key: "ticker", header: "Fund" }, { key: "name", header: "Index" }, { key: "asOf", header: "Holdings as of" }] };
|
|
19
19
|
const fund = canonicalFund(input);
|
|
20
|
+
if (args.options.tab === "breadth") {
|
|
21
|
+
const data = await fetchBreadth(fund, context.apiClient);
|
|
22
|
+
return { freshness: { status: data.stale ? "stale" : data.basis === "intraday" ? data.access === "realtime" ? "live" : "delayed" : "not-a-feed", basis: data.basis === "intraday" ? "regular session" : "daily closes", delayMinutes: 15 },
|
|
23
|
+
rows: [...data.history, ...(data.basis === "intraday" && data.latest ? [data.latest] : [])].reverse().map(row => ({ ...row, above20: row.ma20.percent, above50: row.ma50.percent, above200: row.ma200.percent })),
|
|
24
|
+
columns: [{ key: "date", header: "Date" }, ...["advances", "declines", "unchanged", "above20", "above50", "above200", "newHighs", "newLows", "adLine", "advanceDeclineEligible", "memberCount"].map((key, index) => ({ key, header: ["Adv", "Dec", "Unch", ">20D %", ">50D %", ">200D %", "52W high", "52W low", "A/D line", "A/D covered", "Members"][index]!, align: "right" as const }))],
|
|
25
|
+
complete: !data.stale && !!data.latest && data.latest.advanceDeclineEligible === data.latest.memberCount,
|
|
26
|
+
metadata: { ...data, history: undefined, notices: ["Breadth history uses current members (survivorship bias).", "History uses daily closes; percentages exclude members without the full window. 52-week records strictly break the prior 252 sessions' high/low."] } };
|
|
27
|
+
}
|
|
20
28
|
if (args.options.tab === "changes") {
|
|
21
29
|
const data = await fetchChanges(fund, context.apiClient);
|
|
22
30
|
return { freshness: { status: "not-a-feed", basis: "index changes" }, rows: data.changes.map((row) => ({ ...row, reason: changeReason(row) })), columns: [{ key: "effectiveDate", header: "Effective" }, { key: "added", header: "Added" }, { key: "removed", header: "Removed" }, { key: "daysToGo", header: "In days", align: "right" }, { key: "reason", header: "Reason" }],
|
|
@@ -28,10 +36,15 @@ export const membersHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
28
36
|
// Member prices are the pane's quotes: real-time for a Pro account, 15 minutes delayed otherwise. As in the
|
|
29
37
|
// pane, the fund's snapshot says whether it is stale; the members' own flags are not what the pane reads.
|
|
30
38
|
const live = data.access === "realtime";
|
|
31
|
-
|
|
39
|
+
// Outside the regular session a Pro account's members are the session's close, not a live feed: the report
|
|
40
|
+
// is as of that close and says no feed, as the pane's footer names the close instead of "live".
|
|
41
|
+
const feed = membersFeed(data.access, Date.now());
|
|
42
|
+
const closed = feed.kind === "close";
|
|
43
|
+
const status = data.stale ? { status: "stale" as const, delayMinutes: 15 } : closed ? {} : { status: live ? "live" as const : "delayed" as const, delayMinutes: 15 };
|
|
44
|
+
return { freshness: { ...status, ignoreStaleFlags: true }, rows, columns: movers ? [...columns.slice(0, 3), { key: "contribution", header: "Contrib. pp", align: "right", format: (value) => percent(value as number | null, "", 3) }] : columns,
|
|
32
45
|
complete: !data.stale && data.aggregate.fresh1D === data.aggregate.total,
|
|
33
46
|
unavailableSymbols: data.members.flatMap((row) => row.symbol && row.changePercent === null ? [row.symbol] : []),
|
|
34
|
-
metadata: { fund: data.fund, asOf: data.asOf, snapshotAsOf: data.snapshotAsOf, aggregate: data.aggregate, stale: data.stale, notices: [
|
|
47
|
+
metadata: { fund: data.fund, asOf: data.asOf, ...(closed ? { sessionExchange: "NYSE", observedAt: new Date(feed.closedAt).toISOString() } : {}), snapshotAsOf: data.snapshotAsOf, aggregate: data.aggregate, stale: data.stale, notices: [
|
|
35
48
|
holdingsSourceText(input, data),
|
|
36
49
|
// The pane's coverage figure, and once what the table's dashes mean instead of leaving them bare.
|
|
37
50
|
[coverageText(data.aggregate), data.aggregate.fresh1D < data.aggregate.total ? `1D -- means no fresh ${live ? "" : "delayed "}quote; those holdings are not counted` : ""].filter(Boolean).join(" | "),
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
2
|
-
import { fundChangesCache, fundListCache, fundMembersCache } from "./client";
|
|
2
|
+
import { fundBreadthCache, fundChangesCache, fundListCache, fundMembersCache } from "./client";
|
|
3
3
|
import { membersHeadless } from "./headless";
|
|
4
4
|
import { MembersPane } from "./pane";
|
|
5
|
-
import { membersTitle } from "./model";
|
|
5
|
+
import { membersTitle, TABS } from "./model";
|
|
6
6
|
export const membersPlugin: GloomPlugin = {
|
|
7
|
-
id: "members", name: "Index and ETF members", version: "1.0.0", description: "ETF holdings, member returns, contributions and index changes", toggleable: true,
|
|
8
|
-
setup(ctx) { fundListCache.attach(ctx.persistence); fundMembersCache.attach(ctx.persistence); fundChangesCache.attach(ctx.persistence); },
|
|
9
|
-
dispose() { fundListCache.reset(); fundMembersCache.reset(); fundChangesCache.reset(); },
|
|
7
|
+
id: "members", name: "Index and ETF members", version: "1.0.0", description: "ETF holdings, member returns, contributions, breadth and index changes", toggleable: true,
|
|
8
|
+
setup(ctx) { fundBreadthCache.attach(ctx.persistence); fundListCache.attach(ctx.persistence); fundMembersCache.attach(ctx.persistence); fundChangesCache.attach(ctx.persistence); },
|
|
9
|
+
dispose() { fundBreadthCache.reset(); fundListCache.reset(); fundMembersCache.reset(); fundChangesCache.reset(); },
|
|
10
10
|
panes: [{ id: "members", name: "Index and ETF members", icon: "M", component: MembersPane, tickerFollower: true,
|
|
11
11
|
defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 132, height: 32 }, tableExport: true,
|
|
12
|
-
settings: { fields: [{ key: "tab", label: "View", type: "select", options:
|
|
13
|
-
paneTemplates: [{ id: "members-pane", paneId: "members", label: "Index and ETF members", description: "ETF holdings, member returns, daily contributions and index changes.",
|
|
14
|
-
keywords: ["members", "index", "ETF", "holdings", "weights", "rebalance", "MRR", "IMOV"],
|
|
15
|
-
shortcut: { prefix: "MEMB", argKind: "
|
|
12
|
+
settings: { fields: [{ key: "tab", label: "View", type: "select", options: TABS }] } }],
|
|
13
|
+
paneTemplates: [{ id: "members-pane", paneId: "members", label: "Index and ETF members", description: "ETF holdings, member returns, daily contributions, breadth and index changes.",
|
|
14
|
+
keywords: ["members", "index", "ETF", "holdings", "weights", "rebalance", "breadth", "advancers", "decliners", "MRR", "IMOV"],
|
|
15
|
+
shortcut: { prefix: "MEMB", argKind: "text", argPlaceholder: "fund", argOptional: true, openWithoutArg: true }, headless: membersHeadless,
|
|
16
16
|
createInstance: (_context, options) => { const symbol = (options?.arg ?? options?.symbol)?.trim().toUpperCase();
|
|
17
17
|
return { placement: "floating", title: membersTitle(symbol ?? ""),
|
|
18
18
|
...(symbol ? { binding: { kind: "fixed", symbol } } : {}), settings: { tab: options?.values?.tab ?? "members" } }; },
|
|
@@ -2,13 +2,15 @@ import type { FundChange, FundMember, FundMembersPayload, MemberFundsPayload } f
|
|
|
2
2
|
import type { DataTableColumn } from "../../../components";
|
|
3
3
|
import { buildSectionedRows, type SectionedRow } from "../../../components/data-table/sections";
|
|
4
4
|
import { getTableWidth } from "../../../components/ui/table-layout";
|
|
5
|
+
import { isRegularSessionTime, latestRegularSessionClose } from "../../../market-data/market/freshness";
|
|
5
6
|
import { compareSortValues, type SortPreference } from "../../../utils/sort-values";
|
|
6
|
-
|
|
7
|
-
export
|
|
7
|
+
import { closeLabel } from "../themes/model";
|
|
8
|
+
export type MembersTab = "members" | "movers" | "changes" | "breadth";
|
|
9
|
+
export const TABS = [{ value: "members", label: "Members" }, { value: "movers", label: "Movers" }, { value: "changes", label: "Changes" }, { value: "breadth", label: "Breadth" }];
|
|
8
10
|
export const isTab = (value: unknown): value is MembersTab => TABS.some((tab) => tab.value === value);
|
|
9
11
|
const COVERED = [{ ticker: "IVV", name: "S&P 500" }, { ticker: "IJH", name: "S&P 400" }, { ticker: "IJR", name: "S&P 600" }, { ticker: "IWM", name: "Russell 2000" }, { ticker: "IWB", name: "Russell 1000" }];
|
|
10
12
|
/** Symbols the app reads as a covered fund before asking the server, which knows its own aliases too. */
|
|
11
|
-
const LOCAL_ALIASES: Record<string, string> = { SPX: "IVV", SPY: "IVV", "^GSPC": "IVV" };
|
|
13
|
+
const LOCAL_ALIASES: Record<string, string> = { SPX: "IVV", "^SPX": "IVV", SPY: "IVV", "^GSPC": "IVV", "^MID": "IJH", SP400: "IJH", "^SP400": "IJH", "^SML": "IJR", SP600: "IJR", "^SP600": "IJR", RUT: "IWM", "^RUT": "IWM", RUI: "IWB", "^RUI": "IWB" };
|
|
12
14
|
const requestedSymbol = (input: string) => input.split(":")[0]!.trim().toUpperCase();
|
|
13
15
|
export function canonicalFund(input: string) { const symbol = requestedSymbol(input); return LOCAL_ALIASES[symbol] ?? symbol; }
|
|
14
16
|
/** The covered fund the user asked for, by ticker or by an alias such as SPX, or undefined when it is not covered. */
|
|
@@ -30,6 +32,17 @@ export function coverageText(aggregate: FundMembersPayload["aggregate"], short =
|
|
|
30
32
|
const partial = aggregate.fresh1D < aggregate.total;
|
|
31
33
|
return `${partial && !short ? "Partial " : ""}1D ${aggregate.fresh1D}/${aggregate.total} · ${decimal(aggregate.coveredWeight * 100, 1)}% wt`;
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* What a fund's member quotes are, as read at `readAt`. A Pro account's are live while the regular US session
|
|
37
|
+
* trades; outside it the server prices the members from the last completed session, so they are that session's
|
|
38
|
+
* close (`Fri 23 Oct close`), as THEM names it, never live. Everyone else's are 15 minutes delayed, in or out of
|
|
39
|
+
* a session. Hours that cannot be told read as live.
|
|
40
|
+
*/
|
|
41
|
+
export function membersFeed(access: FundMembersPayload["access"], readAt: number): { kind: "live" | "delayed" } | { kind: "close"; date: string; closedAt: number; label: string } {
|
|
42
|
+
if (access !== "realtime") return { kind: "delayed" };
|
|
43
|
+
const close = isRegularSessionTime("NYSE", readAt) === false ? latestRegularSessionClose("NYSE", readAt) : null;
|
|
44
|
+
return close ? { kind: "close", date: close.date, closedAt: close.close, label: closeLabel(close.date) } : { kind: "live" };
|
|
45
|
+
}
|
|
33
46
|
/** "S&P 500, IVV holdings as of 2026-10-08 (for SPY)": the holdings file the members come from. */
|
|
34
47
|
export function holdingsSourceText(input: string, data: Pick<FundMembersPayload, "fund" | "asOf">) {
|
|
35
48
|
const requested = requestedSymbol(input);
|
|
@@ -52,7 +65,13 @@ export function membersTitle(input: string) {
|
|
|
52
65
|
: requested ? `MEMB ${requested}` : "Index and ETF members";
|
|
53
66
|
}
|
|
54
67
|
export const DEFAULT_SORT: SortPreference<string> = { columnId: "weight", direction: "desc" };
|
|
55
|
-
|
|
68
|
+
/** A signed figure; one that rounds to zero is unsigned (0.00, never +0.00 or -0.00). */
|
|
69
|
+
export function percent(value: number | null, suffix = "%", digits = 2) {
|
|
70
|
+
if (value == null || !Number.isFinite(value)) return "--";
|
|
71
|
+
const fixed = value.toFixed(digits);
|
|
72
|
+
if (!/[1-9]/.test(fixed)) return `${fixed.replace("-", "")}${suffix}`;
|
|
73
|
+
return `${value > 0 ? "+" : ""}${fixed}${suffix}`;
|
|
74
|
+
}
|
|
56
75
|
export const decimal = (value: number | null, digits = 2) => value == null || !Number.isFinite(value) ? "--" : value.toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
57
76
|
export function sortMembers(rows: readonly FundMember[], sort: SortPreference<string>) {
|
|
58
77
|
return [...rows].sort((a, b) => compareSortValues(a[sort.columnId as keyof FundMember] as number | string | null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Breadth } from "./breadth-pane";
|
|
1
2
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
3
|
import type { FundChange, FundMember } from "../../../api-client/members";
|
|
3
4
|
import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
@@ -5,7 +6,7 @@ import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBo
|
|
|
5
6
|
type DataTableCell, type DataTableColumn, type PaneFooterSegment, type SectionedRow } from "../../../components";
|
|
6
7
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
7
8
|
import { useAsyncResource, useAutoRefresh, usePaneInstance, usePaneTickerIdentity, usePaneTitle, usePluginPaneState, usePluginTickerActions } from "../../../public/react";
|
|
8
|
-
import {
|
|
9
|
+
import { shownPriceColor } from "../../../theme/colors";
|
|
9
10
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
10
11
|
import type { PaneProps } from "../../../types/plugin";
|
|
11
12
|
import { Box, type ScrollBoxRenderable } from "../../../ui";
|
|
@@ -13,7 +14,7 @@ import { nextHeaderSort, type SortPreference } from "../../../utils/sort-values"
|
|
|
13
14
|
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
|
|
14
15
|
import { useMembersMenu, type MemberList } from "../shared/members-menu";
|
|
15
16
|
import { cachedChanges, cachedFunds, cachedMembers, loadChanges, loadFunds, loadMembers } from "./client";
|
|
16
|
-
import { canonicalFund, changeColumns, changeLabel, changeReason, coverageText, coveredFund, decimal, DEFAULT_SORT, holdingsCaveats, isTab, memberColumns, memberRows, membersTitle, notCoveredMessage, percent, TABS, type MembersTab } from "./model";
|
|
17
|
+
import { canonicalFund, changeColumns, changeLabel, changeReason, coverageText, coveredFund, decimal, DEFAULT_SORT, holdingsCaveats, isTab, memberColumns, memberRows, membersFeed, membersTitle, notCoveredMessage, percent, TABS, type MembersTab } from "./model";
|
|
17
18
|
|
|
18
19
|
type ViewProps = Pick<PaneProps, "width" | "height" | "focused"> & { fund: string; active: boolean };
|
|
19
20
|
const rowKey = (row: SectionedRow<FundMember>) => row.key;
|
|
@@ -47,25 +48,29 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
|
|
|
47
48
|
usePaneRefreshKey(() => void resource.reload(), { focused, enabled: active && !search.active });
|
|
48
49
|
const info = useMemo<PaneFooterSegment[]>(() => {
|
|
49
50
|
if (!data) return [];
|
|
50
|
-
|
|
51
|
+
// Outside the regular session a Pro account's members are the session's close, so the footer names it, not "live".
|
|
52
|
+
const feed = membersFeed(data.access, resource.updatedAt ?? Date.now());
|
|
53
|
+
const feedText = feed.kind === "close" ? feed.label : feed.kind === "live" ? "live" : "15m delayed";
|
|
51
54
|
const dates = [...new Set(data.members.flatMap((row) => row.changePercent !== null && row.dailyAsOf ? [row.dailyAsOf.slice(0, 10)] : []))].sort();
|
|
52
55
|
const date = dates.length === 1 ? dates[0] : dates.length ? `${dates[0]} to ${dates.at(-1)}` : data.fundDailyAsOf?.slice(0, 10);
|
|
56
|
+
// The close names its own date, so a quote date that is the same day is not said twice.
|
|
57
|
+
const dateSaid = feed.kind === "close" && dates.length === 1 && dates[0] === feed.date;
|
|
53
58
|
const partial = data.aggregate.fresh1D < data.aggregate.total;
|
|
54
59
|
return [
|
|
55
60
|
{ id: "coverage", parts: [{ text: coverageText(data.aggregate, width < 60), tone: partial ? "warning" : "muted" }] },
|
|
56
61
|
// A capture's status line says the quotes' date and delay too; it does not say returns are unavailable.
|
|
57
62
|
...(data.quotesUnavailable
|
|
58
|
-
? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · ${
|
|
63
|
+
? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · ${feedText} · holdings ${data.asOf}`, parts: [{ text: "returns unavailable", tone: "muted" as const }] }]
|
|
59
64
|
: [
|
|
60
|
-
|
|
61
|
-
? { id: "quote-
|
|
65
|
+
feed.kind === "live" ? { id: "quote-live", quoteStatus: true, parts: [{ text: "live", tone: "positive" as const }] }
|
|
66
|
+
: feed.kind === "close" ? { id: "quote-close", title: `${feed.label} · holdings ${data.asOf}`, quoteStatus: true, parts: [{ text: feed.label, tone: "muted" as const }] }
|
|
62
67
|
: delayedFooterSegment({ id: "quote-delay", detail: "15m delayed", delay: "15m", quoteStatus: true }),
|
|
63
|
-
...(date ? [{ id: "quote-date", title: `${date} · ${
|
|
68
|
+
...(date && !dateSaid ? [{ id: "quote-date", title: `${date} · ${feedText} · holdings ${data.asOf}`, quoteStatus: true,
|
|
64
69
|
parts: [{ text: width < 60 && dates.length === 1 ? date.slice(5) : date, tone: "muted" as const }] }] : []),
|
|
65
70
|
]),
|
|
66
71
|
...(width >= 60 ? [{ id: "holdings-date", asOf: true, parts: [{ text: `holdings ${data.asOf}`, tone: "muted" as const }] }] : []),
|
|
67
72
|
];
|
|
68
|
-
}, [data, width]);
|
|
73
|
+
}, [data, resource.updatedAt, width]);
|
|
69
74
|
usePaneStatusFooter({ registrationId: "members:status", loading: !!data && resource.loading,
|
|
70
75
|
stale: !!data && (data.stale || resource.data!.stale || !!resource.error), info });
|
|
71
76
|
usePaneNoticeFooter({ registrationId: "members:notices", focused, notices: [
|
|
@@ -82,14 +87,15 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
|
|
|
82
87
|
if (column.id === "shares") return { text: decimal(row.shares, 0), value: row.shares };
|
|
83
88
|
if (column.id === "price") return { text: decimal(row.price), value: row.price };
|
|
84
89
|
const value = row[column.id as "changePercent"];
|
|
85
|
-
|
|
90
|
+
const digits = column.id === "contribution" ? 3 : 2;
|
|
91
|
+
return { text: percent(value, column.id === "contribution" ? "" : "%", digits), value, color: value === null ? colors.textMuted : shownPriceColor(value, digits, colors) };
|
|
86
92
|
}, [colors]);
|
|
87
93
|
return <PaneStatusBody subject="fund holdings" loading={!data && resource.loading} error={!data ? resource.error : null}>
|
|
88
94
|
{data && <DataTableView<SectionedRow<FundMember>> items={rows} columns={columns} rootWidth={width} rootHeight={height} focused={focused && !search.active}
|
|
89
95
|
rootBefore={<Box flexDirection="column"><QueryBar width={width} search={{ value: query, onChange: setQuery, placeholder: "ticker, name or sector", focused, ...search.searchProps }} />
|
|
90
96
|
{movers && <StatGrid width={width} items={[
|
|
91
|
-
{ label: "Covered contrib.", value: percent(data.aggregate.sum, " pp"), color:
|
|
92
|
-
{ label: `${fund} 1D`, value: percent(data.aggregate.fundReturn), color:
|
|
97
|
+
{ label: "Covered contrib.", value: percent(data.aggregate.sum, " pp"), color: shownPriceColor(data.aggregate.sum ?? 0, 2, colors) },
|
|
98
|
+
{ label: `${fund} 1D`, value: percent(data.aggregate.fundReturn), color: shownPriceColor(data.aggregate.fundReturn ?? 0, 2, colors) },
|
|
93
99
|
{ label: "Difference", value: percent(data.aggregate.residual, " pp"), tone: data.aggregate.withinTolerance === false ? "warning" : "muted" },
|
|
94
100
|
]} />}</Box>}
|
|
95
101
|
selection={{ kind: "id", selectedId: selected, getId: rowKey, onChange: (id) => setSelected(String(id)) }} getItemKey={rowKey}
|
|
@@ -150,14 +156,18 @@ function FundView({ fund, ...props }: Pick<PaneProps, "width" | "height" | "focu
|
|
|
150
156
|
const initial = isTab(instance?.settings?.tab) ? instance.settings.tab : "members";
|
|
151
157
|
const [storedTab, setTab] = usePluginPaneState<MembersTab>("tab", initial);
|
|
152
158
|
const tab = isTab(storedTab) ? storedTab : "members";
|
|
153
|
-
const
|
|
154
|
-
const
|
|
159
|
+
const holdings = tab === "members" || tab === "movers";
|
|
160
|
+
const [visited, setVisited] = useState({ holdings, changes: tab === "changes", breadth: tab === "breadth" });
|
|
161
|
+
const selectTab = (value: string) => { if (isTab(value)) { setTab(value); setVisited((previous) => ({ holdings: previous.holdings || value === "members" || value === "movers", changes: previous.changes || value === "changes", breadth: previous.breadth || value === "breadth" })); } };
|
|
155
162
|
const { strip, rows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: selectTab, focused: props.focused, compact: true, variant: "bare" });
|
|
156
163
|
const height = Math.max(1, props.height - rows);
|
|
157
164
|
return <Box width={props.width} height={props.height} flexDirection="column">
|
|
158
165
|
{strip}
|
|
159
|
-
{visited.holdings && <Box visible={
|
|
160
|
-
<Holdings fund={fund} {...props} height={height} tab={tab} active={
|
|
166
|
+
{visited.holdings && <Box visible={holdings} flexGrow={1} flexBasis={0} overflow="hidden"><PaneFooterScope active={holdings}>
|
|
167
|
+
<Holdings fund={fund} {...props} height={height} tab={tab} active={holdings} focused={props.focused && holdings} />
|
|
168
|
+
</PaneFooterScope></Box>}
|
|
169
|
+
{visited.breadth && <Box visible={tab === "breadth"} flexGrow={1} flexBasis={0} overflow="hidden"><PaneFooterScope active={tab === "breadth"}>
|
|
170
|
+
<Breadth fund={fund} {...props} height={height} active={tab === "breadth"} focused={props.focused && tab === "breadth"} />
|
|
161
171
|
</PaneFooterScope></Box>}
|
|
162
172
|
{visited.changes && <Box visible={tab === "changes"} flexGrow={1} flexBasis={0} overflow="hidden"><PaneFooterScope active={tab === "changes"}>
|
|
163
173
|
<Changes fund={fund} {...props} height={height} active={tab === "changes"} focused={props.focused && tab === "changes"} />
|
|
@@ -112,6 +112,7 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
|
|
|
112
112
|
? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
|
|
113
113
|
: undefined,
|
|
114
114
|
onPopOut: readableArticle ? popOutReadable : undefined,
|
|
115
|
+
feedAccess: newsState.access,
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
if (!ticker) {
|
|
@@ -29,10 +29,13 @@ const CATEGORY_LABELS: Record<string, string> = {
|
|
|
29
29
|
us: "US",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
function categoryKey(value: string | null | undefined): string {
|
|
33
|
+
return value?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
export function formatNewsCategory(value: string | null | undefined): string {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
-
const key = raw.toLowerCase().replace(/[\s-]+/g, "_");
|
|
37
|
+
const key = categoryKey(value);
|
|
38
|
+
if (!key) return "";
|
|
36
39
|
const mapped = CATEGORY_LABELS[key];
|
|
37
40
|
if (mapped) return mapped;
|
|
38
41
|
const words = key.split("_").filter(Boolean);
|
|
@@ -46,6 +49,19 @@ export function formatNewsCategory(value: string | null | undefined): string {
|
|
|
46
49
|
.join(" ");
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Labels for the news tables' category column, which is ten cells wide, for the
|
|
54
|
+
* ids whose label above would be cut in it. Anywhere with room (a story's
|
|
55
|
+
* detail) keeps the full label.
|
|
56
|
+
*/
|
|
57
|
+
const COLUMN_CATEGORY_LABELS: Record<string, string> = {
|
|
58
|
+
product_approval: "Approval",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function formatNewsCategoryForColumn(value: string | null | undefined): string {
|
|
62
|
+
return COLUMN_CATEGORY_LABELS[categoryKey(value)] ?? formatNewsCategory(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
const KNOWN_TICKERS = new Set([
|
|
50
66
|
"AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "JPM", "JNJ", "UNH",
|
|
51
67
|
"PG", "XOM", "CVX", "HD", "BAC", "V", "MA", "PFE", "KO", "PEP", "ABBV", "MRK",
|
|
@@ -125,6 +125,7 @@ function IndustryNewsBody({ focused, width, height, entry, code, codeFilter }: P
|
|
|
125
125
|
defaultSort: DEFAULT_SORT,
|
|
126
126
|
refreshing: loaded.loading && loaded.articles.length > 0,
|
|
127
127
|
error: loaded.error,
|
|
128
|
+
feedAccess: loaded.query ? loaded.newsState.access : null,
|
|
128
129
|
});
|
|
129
130
|
const { setSelectedArticleId } = stack;
|
|
130
131
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { PaneFooterSegment } from "../../../../../components";
|
|
3
|
+
import { delayedFooterSegment } from "../../../../../components/layout/pane/footer/model";
|
|
4
|
+
import type { NewsAccess } from "../../../../../news/types";
|
|
3
5
|
import { t, tf } from "../../../../../i18n";
|
|
4
6
|
import { useAppLanguage } from "../../../../../i18n/react";
|
|
5
7
|
import { useCloudAccessFooter } from "../../../shared/cloud-upgrade";
|
|
@@ -22,6 +24,18 @@ interface UseNewsArticleFooterOptions {
|
|
|
22
24
|
loading?: boolean;
|
|
23
25
|
error?: string | null;
|
|
24
26
|
onPopOut?: () => void;
|
|
27
|
+
/** Which news the list on screen is, from the service's answer; null or absent when it did not say. */
|
|
28
|
+
feedAccess?: NewsAccess | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the stories are the live wire: what the service's answer said when
|
|
33
|
+
* it said; otherwise held back for an account without real-time news, and
|
|
34
|
+
* not said at all for one with it. The plan never claims the live wire.
|
|
35
|
+
*/
|
|
36
|
+
function newsFeedRealtime(feedAccess: NewsAccess | null | undefined, hasProAccess: boolean): boolean | null {
|
|
37
|
+
if (feedAccess) return feedAccess === "realtime";
|
|
38
|
+
return hasProAccess ? null : false;
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
export function useNewsArticleFooter({
|
|
@@ -32,32 +46,44 @@ export function useNewsArticleFooter({
|
|
|
32
46
|
loading = false,
|
|
33
47
|
error,
|
|
34
48
|
onPopOut,
|
|
49
|
+
feedAccess = null,
|
|
35
50
|
}: UseNewsArticleFooterOptions) {
|
|
36
51
|
const language = useAppLanguage();
|
|
52
|
+
const delayLabel = tf("{count}h", { count: CLOUD_NEWS_DELAY_HOURS });
|
|
37
53
|
const { access, hint: upgradeHint, segment } = useCloudAccessFooter({
|
|
38
|
-
delayLabel
|
|
54
|
+
delayLabel,
|
|
39
55
|
focused,
|
|
56
|
+
// A feed the service answered live has nothing to upgrade.
|
|
57
|
+
degraded: feedAccess !== "realtime",
|
|
40
58
|
segmentId: "news-access",
|
|
41
59
|
placement: "news-footer",
|
|
42
60
|
shortcutScope: `${registrationId}:news-upgrade`,
|
|
43
61
|
});
|
|
62
|
+
const realtime = newsFeedRealtime(feedAccess, access.hasProAccess);
|
|
44
63
|
|
|
45
64
|
// A report or capture of the pane says how far its stories are held back, as the footer does, and dates the
|
|
46
65
|
// list by its newest story, named so it is not read as the time now.
|
|
47
66
|
useRemoteUiNode({
|
|
48
67
|
role: "report-freshness",
|
|
49
68
|
label: "News freshness",
|
|
50
|
-
getMetadata: () => (
|
|
69
|
+
getMetadata: () => (realtime === true
|
|
51
70
|
? { status: "live", asOfLabel: NEWEST_STORY }
|
|
52
|
-
:
|
|
71
|
+
: realtime === false
|
|
72
|
+
? { status: "delayed", delayMinutes: CLOUD_NEWS_DELAY_HOURS * 60, asOfLabel: NEWEST_STORY }
|
|
73
|
+
: { status: "not-a-feed", basis: "published stories", asOfLabel: NEWEST_STORY }),
|
|
53
74
|
});
|
|
54
75
|
|
|
55
76
|
const accessInfo = useMemo<PaneFooterSegment[]>(() => {
|
|
56
|
-
|
|
77
|
+
// A trial keeps its days-left segment, which manages the plan.
|
|
78
|
+
if (realtime === true && !access.isTrialActive) {
|
|
57
79
|
return [{ id: "news-access", parts: [{ text: t("real-time news"), tone: "positive" }] }];
|
|
58
80
|
}
|
|
81
|
+
// Held back although the account has Pro (a read that went out before its session): say so, without a pitch.
|
|
82
|
+
if (feedAccess === "delayed" && access.hasProAccess) {
|
|
83
|
+
return [delayedFooterSegment({ id: "news-access", detail: tf("{delay} delayed", { delay: delayLabel }), delay: delayLabel })];
|
|
84
|
+
}
|
|
59
85
|
return segment ? [segment] : [];
|
|
60
|
-
}, [access.
|
|
86
|
+
}, [access.hasProAccess, access.isTrialActive, delayLabel, feedAccess, language, realtime, segment]);
|
|
61
87
|
const footerInfo = useMemo(() => [...accessInfo, ...(info ?? [])], [accessInfo, info]);
|
|
62
88
|
|
|
63
89
|
// The story's header names its source, so the footer keeps to the link.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { Box } from "../../../../../ui";
|
|
3
|
-
import type { NewsArticle, NewsQuery } from "../../../../../news/types";
|
|
3
|
+
import type { NewsAccess, NewsArticle, NewsQuery } from "../../../../../news/types";
|
|
4
4
|
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
|
|
5
5
|
import type { PaneProps } from "../../../../../types/plugin";
|
|
6
6
|
import { useDebouncedPluginPaneState, usePluginConfigState, usePluginPaneState } from "../../../../runtime";
|
|
@@ -31,6 +31,8 @@ interface NewsArticleStackOptions {
|
|
|
31
31
|
/** Rows stay on screen while a refresh runs, so the footer says it is loading. */
|
|
32
32
|
refreshing: boolean;
|
|
33
33
|
error: string | null;
|
|
34
|
+
/** Which news the rows are, from the service's answer, for the footer's real-time or delayed label. */
|
|
35
|
+
feedAccess?: NewsAccess | null;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -46,6 +48,7 @@ export function useNewsArticleStack({
|
|
|
46
48
|
defaultSort,
|
|
47
49
|
refreshing,
|
|
48
50
|
error,
|
|
51
|
+
feedAccess,
|
|
49
52
|
}: NewsArticleStackOptions) {
|
|
50
53
|
const [selectedArticleId, setSelectedArticleId] = useDebouncedPluginPaneState<string | null>(
|
|
51
54
|
`${paneKey}:selectedArticleId`,
|
|
@@ -95,6 +98,7 @@ export function useNewsArticleStack({
|
|
|
95
98
|
loading: detailLoading || refreshing,
|
|
96
99
|
error: [error, detailError].filter(Boolean).join(" ") || null,
|
|
97
100
|
onPopOut: readableArticle ? popOutReadable : undefined,
|
|
101
|
+
feedAccess,
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
return {
|
|
@@ -172,6 +176,7 @@ export function NewsPresetPane({
|
|
|
172
176
|
defaultSort,
|
|
173
177
|
refreshing: loading && persisted.length > 0,
|
|
174
178
|
error,
|
|
179
|
+
feedAccess: newsState.access,
|
|
175
180
|
});
|
|
176
181
|
const emptyCopy = newsListEmptyCopy({
|
|
177
182
|
query: searchQuery,
|
|
@@ -17,7 +17,7 @@ import { useLoadNewsStory } from "../../../../../news/hooks";
|
|
|
17
17
|
import { formatFeedTime } from "../../../../../utils/datetime-format";
|
|
18
18
|
import { logFailure } from "../../../../../utils/log-failure";
|
|
19
19
|
import { nextHeaderSort } from "../../../../../utils/sort-values";
|
|
20
|
-
import {
|
|
20
|
+
import { formatNewsCategoryForColumn } from "../categories";
|
|
21
21
|
import { useOpenTickerChoice } from "../../../shared/ticker-choice";
|
|
22
22
|
import { usePinnedMoment } from "../../../../../components/use-pinned-moment";
|
|
23
23
|
|
|
@@ -359,7 +359,7 @@ export function NewsArticleStackView({
|
|
|
359
359
|
}
|
|
360
360
|
case "categories":
|
|
361
361
|
return {
|
|
362
|
-
text:
|
|
362
|
+
text: formatNewsCategoryForColumn(item.categories[0]) || "-",
|
|
363
363
|
color: tone(colors.textDim),
|
|
364
364
|
};
|
|
365
365
|
case "sentiment": {
|
|
@@ -68,6 +68,17 @@ export function parseOpenFdaDate(value: unknown): Date | null {
|
|
|
68
68
|
|
|
69
69
|
const normalizeWords = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
70
70
|
|
|
71
|
+
/** Each term once, in the order first coded, whatever its case: `Nausea` and `NAUSEA` are one. */
|
|
72
|
+
function uniqueTerms(terms: readonly string[]): string[] {
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
return terms.filter((term) => {
|
|
75
|
+
const key = normalizeWords(term);
|
|
76
|
+
if (seen.has(key)) return false;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
/** The search's words appear together in one of the fields, as openFDA matches a phrase. */
|
|
72
83
|
function fieldsMatch(fields: Array<string | undefined>, query: string): boolean {
|
|
73
84
|
const words = normalizeWords(query);
|
|
@@ -230,9 +241,10 @@ export function parseDrugReport(raw: unknown, query = "", role: DrugRole | null
|
|
|
230
241
|
product: shown?.name ?? "Unnamed drug",
|
|
231
242
|
role: shown?.role ?? null,
|
|
232
243
|
drugs: drugs.map(({ name, role }) => ({ name, role })),
|
|
233
|
-
|
|
244
|
+
// A report can code one reaction more than once (once per drug, or in another case): each is listed once.
|
|
245
|
+
reactions: uniqueTerms((Array.isArray(patient.reaction) ? patient.reaction : [])
|
|
234
246
|
.map((entry) => asString(asRecord(entry).reactionmeddrapt))
|
|
235
|
-
.filter((entry): entry is string => !!entry),
|
|
247
|
+
.filter((entry): entry is string => !!entry)),
|
|
236
248
|
outcome,
|
|
237
249
|
outcomes,
|
|
238
250
|
manufacturer: shown && shown.manufacturers.length === 1 ? shown.manufacturers[0]! : null,
|
|
@@ -3,8 +3,9 @@ import { OpenFdaPane } from "./pane";
|
|
|
3
3
|
import { OPENFDA_PANE_ID, OPENFDA_PLUGIN_ID } from "./types";
|
|
4
4
|
|
|
5
5
|
const createOpenFdaPaneInstance = (options?: PaneTemplateCreateOptions) => {
|
|
6
|
-
// A search of product and firm names
|
|
7
|
-
//
|
|
6
|
+
// A search of product and firm names: FDA records name the firm as it filed,
|
|
7
|
+
// not a symbol. A ticker that matches nothing is searched as its company's
|
|
8
|
+
// name, which the pane looks up (`FDA AZN` is AstraZeneca's reports).
|
|
8
9
|
const query = (options?.arg ?? options?.values?.query ?? "").trim();
|
|
9
10
|
const encoded = encodeURIComponent(query).replace(/%/g, "~");
|
|
10
11
|
return {
|
|
@@ -65,6 +65,15 @@ const TONES: Partial<Record<ColumnId, Record<string, StateTone>>> = {
|
|
|
65
65
|
status: { Ongoing: "warning", Completed: "text", Terminated: "muted" },
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/** The most terms a row's list cell names before counting the rest; the record names them all. */
|
|
69
|
+
const MAX_CELL_TERMS = 3;
|
|
70
|
+
|
|
71
|
+
/** A report's coded terms as a row shows them: the first few, then how many more (`Nausea, Rash, Fatigue +9 more`). */
|
|
72
|
+
export function termsCellText(terms: readonly string[]): string {
|
|
73
|
+
const more = terms.length - MAX_CELL_TERMS;
|
|
74
|
+
return `${terms.slice(0, MAX_CELL_TERMS).join(", ")}${more > 0 ? ` +${more} more` : ""}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
export function recordTone(column: ColumnId, text: string): StateTone {
|
|
69
78
|
return TONES[column]?.[text] ?? "text";
|
|
70
79
|
}
|