@gloom-sh/terminal 0.19.1 → 0.19.3
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 +7 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +58 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +101 -27
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +14 -10
- package/src/cli/commands/search.ts +84 -25
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +6 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/glossary.ts +2 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +189 -11
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +51 -5
- package/src/cli/listing-arg.ts +63 -10
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +49 -6
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +51 -6
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +10 -5
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +8 -2
- package/src/components/layout/pane/footer/model.ts +73 -6
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +4 -2
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +60 -26
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +16 -3
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +165 -90
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +324 -0
- package/src/data/sqlite/checkpointer.ts +285 -0
- package/src/data/sqlite/database.ts +80 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +5 -4
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +6 -4
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +19 -3
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +16 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +127 -29
- package/src/plugins/builtin/thirteenf/data.ts +165 -35
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
- package/src/plugins/builtin/thirteenf/types.ts +14 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +64 -42
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +5 -2
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +6 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/resolve.ts +2 -1
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +12 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +25 -9
- package/src/telemetry/stall-reports.ts +218 -13
- package/src/test-support/outage.ts +8 -0
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +84 -1
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +104 -8
- package/src/tickers/search/index.ts +5 -2
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +75 -8
- package/src/time-series/resolve.ts +32 -4
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/time-series/types.ts +7 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +40 -3
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +17 -1
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +122 -5
- package/src/utils/perf-marks.ts +58 -12
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/time-series/studies.ts +0 -1628
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DebtHistoryPoint } from "../../../api-client/debt-maturities";
|
|
2
2
|
import type { SeriesDefinition } from "../../../series/types";
|
|
3
|
-
import {
|
|
3
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
4
4
|
import { loadDebtMaturities } from "./client";
|
|
5
5
|
|
|
6
6
|
type HistoryField = "totalPrincipal" | "next12MonthsShare" | "next3YearsShare";
|
|
@@ -36,10 +36,10 @@ export const DEBT_MATURITY_SERIES: readonly SeriesDefinition[] = FIELDS.map(({ i
|
|
|
36
36
|
// An amount stays in the unit it is listed in: a filing in another currency is a gap, not a converted figure.
|
|
37
37
|
const listed = (point: DebtHistoryPoint) => unit !== "USD" || point.currency === "USD";
|
|
38
38
|
const other = resource.payload.history.find((point) => !listed(point))?.currency;
|
|
39
|
-
const warning = other ? `Filings reported in ${other} are left out.` : cachedSeriesWarning(resource);
|
|
40
39
|
return {
|
|
41
40
|
points: resource.payload.history.map((point) => ({ date: point.asOf, value: listed(point) ? point[field] : null })),
|
|
42
|
-
...(
|
|
41
|
+
...cachedSeriesNote(resource),
|
|
42
|
+
...(other ? { warning: `Filings reported in ${other} are left out.` } : {}),
|
|
43
43
|
};
|
|
44
44
|
},
|
|
45
45
|
}));
|
|
@@ -288,7 +288,7 @@ export const debugPlugin: GloomPlugin = {
|
|
|
288
288
|
name: "Debug Log",
|
|
289
289
|
icon: "D",
|
|
290
290
|
component: DebugPane,
|
|
291
|
-
reportFreshness: { status: "not-a-feed", basis: "debug log" },
|
|
291
|
+
reportFreshness: { status: "not-a-feed", basis: "debug log", local: true },
|
|
292
292
|
defaultPosition: "right",
|
|
293
293
|
defaultMode: "floating",
|
|
294
294
|
defaultFloatingSize: { width: 120, height: 30 },
|
|
@@ -75,6 +75,19 @@ function statedRateSummary(stated: StatedForwardRate): QuoteSummaryDividendField
|
|
|
75
75
|
interface DividendReadOptions {
|
|
76
76
|
/** The forward rate fundamentals state (statedForwardRate), asked only when the summary is missing. */
|
|
77
77
|
loadStatedForwardRate?: () => Promise<StatedForwardRate | null> | StatedForwardRate | null;
|
|
78
|
+
/** The history already asked for (requestDividendHistory), so it need not wait for the quote the price comes from. */
|
|
79
|
+
history?: Promise<DividendHistoryResponse>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type DividendHistoryResponse = Awaited<ReturnType<typeof apiClient.getMarketDividends>>;
|
|
83
|
+
|
|
84
|
+
/** Asks Gloom Cloud for a listing's dividend history, for fetchDividendData to read once the price is known. */
|
|
85
|
+
export function requestDividendHistory(symbol: string, exchange = ""): Promise<DividendHistoryResponse> {
|
|
86
|
+
const pending = trackRequest("dividends", () => apiClient.getMarketDividends(symbol, exchange));
|
|
87
|
+
pending.catch(() => {
|
|
88
|
+
// Intentional ignore: fetchDividendData awaits this same promise and reports its failure.
|
|
89
|
+
});
|
|
90
|
+
return pending;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
let connectionHealth: ConnectionHealthRegistry | null = null;
|
|
@@ -125,8 +138,22 @@ function declaredCashByExDate(declared: DeclaredCash | undefined, cashCurrency:
|
|
|
125
138
|
? [[row.exDate, { amount: row.amount / unit.divisor, currency: unit.currency }] as const] : []));
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The currency the company declares in when it pays the listing another
|
|
143
|
+
* (South32's dollars, which its rand line converts): the listing's amounts are
|
|
144
|
+
* conversions of it. Undefined for a listing paid in what was declared.
|
|
145
|
+
*/
|
|
146
|
+
function declaredCurrencyOf(declared: DeclaredCash | undefined, cashCurrency: string): string | undefined {
|
|
147
|
+
const currency = resolveCurrencyUnit(declared?.currency).currency;
|
|
148
|
+
return currency && currency !== cashCurrency ? currency : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
export interface DividendData {
|
|
129
152
|
payments: DividendPayment[];
|
|
153
|
+
/** The currency the company declares in, when its amounts here are converted from it (declaredCurrencyOf). */
|
|
154
|
+
convertedFrom?: string;
|
|
155
|
+
/** Beside convertedFrom, the sub-unit the source states the cash in (GBp, ZAc): converted amounts are written in it. */
|
|
156
|
+
convertedSubUnit?: string;
|
|
130
157
|
/** Declared and unpaid, in `currency`; never in `payments` or any figure. Absent from older servers. */
|
|
131
158
|
declared?: DeclaredDividendAction[];
|
|
132
159
|
metrics: DividendMetrics;
|
|
@@ -155,7 +182,7 @@ export async function fetchDividendData(
|
|
|
155
182
|
currentPriceCurrency?: string,
|
|
156
183
|
options: DividendReadOptions = {},
|
|
157
184
|
): Promise<DividendData> {
|
|
158
|
-
const response = await
|
|
185
|
+
const response = await (options.history ?? requestDividendHistory(symbol, exchange));
|
|
159
186
|
if (!response.data) throw new Error(`No dividend data found for ${symbol}`);
|
|
160
187
|
const { actions: sourceActions, quote: rawQuote, summary, triedAt } = response.data;
|
|
161
188
|
// In the major unit as the history arrives, as every other reader of it gets it.
|
|
@@ -180,6 +207,9 @@ export async function fetchDividendData(
|
|
|
180
207
|
historyError = MISSING_DIVIDEND_CURRENCY;
|
|
181
208
|
}
|
|
182
209
|
const declaredCash = cashCurrency ? declaredCashByExDate(actions?.declaredCash, cashCurrency) : new Map();
|
|
210
|
+
const convertedFrom = cashCurrency ? declaredCurrencyOf(actions?.declaredCash, cashCurrency) : undefined;
|
|
211
|
+
const sourceUnit = sourceActions?.currency;
|
|
212
|
+
const convertedSubUnit = convertedFrom && sourceUnit && resolveCurrencyUnit(sourceUnit).divisor !== 1 ? sourceUnit : undefined;
|
|
183
213
|
const payments = actions && cashCurrency ? actions.dividends.flatMap(action => {
|
|
184
214
|
const payment = toDividendPayment(action.exDate, action.amount, cashCurrency);
|
|
185
215
|
const declared = declaredCash.get(action.exDate);
|
|
@@ -203,7 +233,7 @@ export async function fetchDividendData(
|
|
|
203
233
|
throw new Error(`No dividend data found for ${symbol}`);
|
|
204
234
|
}
|
|
205
235
|
return {
|
|
206
|
-
payments, ...(declared.length > 0 ? { declared } : {}), metrics, price, currency: currency || undefined, historyAvailable,
|
|
236
|
+
payments, ...(declared.length > 0 ? { declared } : {}), ...(convertedFrom ? { convertedFrom } : {}), ...(convertedSubUnit ? { convertedSubUnit } : {}), metrics, price, currency: currency || undefined, historyAvailable,
|
|
207
237
|
...(suppliedPrice == null && price != null && quote ? dividendQuotePriceMetadata(quote) : {}),
|
|
208
238
|
...(historyError ? { historyError } : {}), ...(summaryError ? { summaryError } : {}),
|
|
209
239
|
...(statedFields && metrics.forwardRate != null ? { forwardFromFundamentals: true } : {}),
|
|
@@ -4,19 +4,21 @@ import type {
|
|
|
4
4
|
HeadlessPaneDefinition,
|
|
5
5
|
HeadlessPaneLoadArgs,
|
|
6
6
|
} from "../../../types/plugin";
|
|
7
|
-
import { fetchDividendData, statedForwardRate, type DividendData } from "./client";
|
|
7
|
+
import { fetchDividendData, requestDividendHistory, statedForwardRate, type DividendData } from "./client";
|
|
8
8
|
import { dividendReferencePrice } from "../../../utils/trailing-dividend";
|
|
9
9
|
import type { DividendPayment, GrowthPayments } from "./types";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
convertedFromText, formatConvertedAmount, formatDividendYield, growthPaymentsText, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText,
|
|
12
|
+
} from "./view";
|
|
11
13
|
import { formatDistributionAmount, formatPercent } from "../../../utils/format";
|
|
12
14
|
import { forwardDividendBasis, noCurrentDividend, TRAILING_DIVIDEND_BASIS, UNEVEN_REGULAR_PAYMENTS } from "../../../utils/forward-dividend";
|
|
13
15
|
import { currencyPriceMinDigits, formatMarketPriceWithCurrency } from "../../../market-data/market/format";
|
|
14
16
|
import type { Quote } from "../../../types/financials";
|
|
15
17
|
import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
16
18
|
import { dividendPriceStatus, dividendQuotePriceMetadata } from "./reference-price";
|
|
17
|
-
import { mapWithConcurrency } from "../../../utils/concurrency";
|
|
18
19
|
import { logFailure } from "../../../utils/log-failure";
|
|
19
|
-
import { projectDividendRanking, roundedRatio
|
|
20
|
+
import { projectDividendRanking, roundedRatio } from "./ranking";
|
|
21
|
+
import { MAX_RANKED_SYMBOLS, readDividendRanking, type DividendRankingReads } from "./ranking-reads";
|
|
20
22
|
import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
|
|
21
23
|
import type { FundBondFacts } from "../../../api-client/fund-facts";
|
|
22
24
|
import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
|
|
@@ -27,31 +29,35 @@ import {
|
|
|
27
29
|
type TaxEquivalentAnalysis, type TreasuryTenor,
|
|
28
30
|
} from "../../../market-data/tax-equivalent";
|
|
29
31
|
|
|
30
|
-
/** Symbols one `fn DVD` ranks at most, and how many it reads at once. */
|
|
31
|
-
const MAX_RANKED_SYMBOLS = 25;
|
|
32
|
-
const RANKING_CONCURRENCY = 4;
|
|
33
32
|
/** The history options' defaults: a ranking lists no history, so only these leave it unchanged. */
|
|
34
33
|
const DEFAULT_TYPE = "all";
|
|
35
34
|
const DEFAULT_LIMIT = 40;
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
/** The history's columns; amounts converted from the currency the company declares in go to two decimals of their source's unit. */
|
|
37
|
+
function paymentColumns(converted: boolean, subUnit?: string) {
|
|
38
|
+
const amount = converted ? (value: number, currency: string) => formatConvertedAmount(value, currency, subUnit) : formatDistributionAmount;
|
|
39
|
+
return [
|
|
40
|
+
{ key: "exDate", header: "Ex-date" },
|
|
41
|
+
{ key: "amount", header: "Amount", align: "right" as const,
|
|
42
|
+
format: (value: unknown, row: Record<string, unknown>) => typeof value === "number" && typeof row.currency === "string" && row.currency
|
|
43
|
+
? amount(value, row.currency) : value == null ? "-" : String(value) },
|
|
44
|
+
{ key: "currency", header: "CCY" },
|
|
45
|
+
{ key: "type", header: "Type" },
|
|
46
|
+
];
|
|
47
|
+
}
|
|
45
48
|
|
|
46
49
|
/** After the amount, for a company declaring in another currency than the listing pays: what it declared. */
|
|
47
50
|
const DECLARED_COLUMN = { key: "declared", header: "Declared", align: "right" as const,
|
|
48
51
|
format: (value: unknown, row: Record<string, unknown>) => typeof value === "number" && typeof row.declaredCurrency === "string"
|
|
49
52
|
? formatDistributionAmount(value, row.declaredCurrency) : "-" };
|
|
50
53
|
|
|
54
|
+
/** What a ticker's read needs: the market data, Gloom Cloud, and the listing a typed key stands for. */
|
|
55
|
+
export type DividendReadContext = Pick<HeadlessPaneContext, "marketData" | "apiClient" | "resolveInstrument">;
|
|
56
|
+
|
|
51
57
|
export interface DividendYieldHeadlessDependencies {
|
|
52
|
-
loadData(symbol: string, context:
|
|
58
|
+
loadData(symbol: string, context: DividendReadContext): Promise<DividendData>;
|
|
53
59
|
/** A US-listed fund's bond facts (SEC yield, yield to worst, duration); null for any other listing. */
|
|
54
|
-
loadBond?(symbol: string, context:
|
|
60
|
+
loadBond?(symbol: string, context: DividendReadContext): Promise<FundBondFacts | null>;
|
|
55
61
|
/** The Treasury par curve GC shows, for the muni/Treasury ratio; null when it cannot be read. */
|
|
56
62
|
loadTreasury?(context: HeadlessPaneContext): Promise<TreasuryTenor[] | null>;
|
|
57
63
|
}
|
|
@@ -68,6 +74,8 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
|
|
|
68
74
|
const instrument = await context.resolveInstrument?.(symbol);
|
|
69
75
|
const listingSymbol = instrument?.symbol ?? symbol;
|
|
70
76
|
const exchange = instrument?.exchange ?? "";
|
|
77
|
+
// The history is asked for beside the quote, not after it: only its yields need the price.
|
|
78
|
+
const history = requestDividendHistory(listingSymbol, exchange);
|
|
71
79
|
try {
|
|
72
80
|
const quote = await context.marketData.getQuote(listingSymbol, exchange);
|
|
73
81
|
// The regular session's price: after the close, the close rather than an after-hours print.
|
|
@@ -79,6 +87,7 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
|
|
|
79
87
|
currentPrice = null;
|
|
80
88
|
}
|
|
81
89
|
const data = await fetchDividendData(listingSymbol, currentPrice, exchange, currentPriceCurrency, {
|
|
90
|
+
history,
|
|
82
91
|
// Read only when the summary is missing: the forward rate fundamentals state, as `fundamentals` shows it.
|
|
83
92
|
loadStatedForwardRate: async () => statedForwardRate(await context.marketData.getTickerFinancials(listingSymbol, exchange)),
|
|
84
93
|
});
|
|
@@ -102,6 +111,25 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
|
|
|
102
111
|
loadTreasury: (context) => loadTreasuryTenors(context.apiClient),
|
|
103
112
|
};
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* How a ranking reads each ticker: `fn DVD`'s, and the DVD pane's with the
|
|
116
|
+
* app's market data, so both read the same listing at the same price. A
|
|
117
|
+
* fund's bond facts never fail its row: without them it ranks as before.
|
|
118
|
+
*/
|
|
119
|
+
export function dividendRankingReads(
|
|
120
|
+
context: DividendReadContext,
|
|
121
|
+
dependencies: Pick<DividendYieldHeadlessDependencies, "loadData" | "loadBond"> = defaultDependencies,
|
|
122
|
+
): DividendRankingReads {
|
|
123
|
+
const { loadBond } = dependencies;
|
|
124
|
+
return {
|
|
125
|
+
loadData: (symbol) => dependencies.loadData(symbol, context),
|
|
126
|
+
...loadBond ? {
|
|
127
|
+
loadBond: (symbol: string) => loadBond(symbol, context).catch((error) => error instanceof BondClassificationError ? null
|
|
128
|
+
: logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)),
|
|
129
|
+
} : {},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
/** A growth figure, or what its short year holds: "n/a: 1 of 2 payments". */
|
|
106
134
|
function growthText(growth: number | null, payments: GrowthPayments | null | undefined): string {
|
|
107
135
|
return growth == null && payments ? growthPaymentsText(payments) : formatPercent(growth ?? undefined);
|
|
@@ -147,10 +175,16 @@ export function projectDividendYieldHeadless(
|
|
|
147
175
|
...(row.declared ? { declared: row.declared.amount, declaredCurrency: row.declared.currency } : {}),
|
|
148
176
|
};
|
|
149
177
|
});
|
|
178
|
+
// A listing paid in another currency than its company declares (South32's rand) has its amounts converted from that one.
|
|
179
|
+
const convertedFrom = data.convertedFrom;
|
|
180
|
+
const subUnit = data.convertedSubUnit;
|
|
181
|
+
const paymentColumnList = paymentColumns(!!convertedFrom, subUnit);
|
|
150
182
|
const declaredColumns = rows.some((row) => row.declared)
|
|
151
|
-
? [...
|
|
183
|
+
? [...paymentColumnList.slice(0, 2), DECLARED_COLUMN, ...paymentColumnList.slice(2)] : paymentColumnList;
|
|
152
184
|
const metrics = data.metrics;
|
|
153
185
|
const currency = data.currency ?? data.payments[0]?.currency ?? "";
|
|
186
|
+
const cashRate = (value: number | null) => !currency ? "—" : !convertedFrom ? formatDistributionAmount(value ?? undefined, currency)
|
|
187
|
+
: value == null ? formatConvertedAmount(value, currency) : `${formatConvertedAmount(value, currency, subUnit)} (${convertedFromText(convertedFrom)})`;
|
|
154
188
|
const priceStatus = dividendPriceStatus(data.price, data.priceAsOf, data.priceStale);
|
|
155
189
|
// A regular payer's year with fewer payments than its cadence pays: the yield says how many it counts.
|
|
156
190
|
const shortfall = trailingPaymentsText(metrics.trailingPayments, { cadence: true });
|
|
@@ -180,8 +214,8 @@ export function projectDividendYieldHeadless(
|
|
|
180
214
|
value: figure.value,
|
|
181
215
|
formatted: `${figure.text}${figure.date ? ` (as of ${figure.date})` : ""}`,
|
|
182
216
|
})) : []),
|
|
183
|
-
{ label: "Trailing rate", value: metrics.trailingRate, formatted:
|
|
184
|
-
{ label: "Forward rate", value: metrics.forwardRate, formatted:
|
|
217
|
+
{ label: "Trailing rate", value: metrics.trailingRate, formatted: cashRate(metrics.trailingRate) },
|
|
218
|
+
{ label: "Forward rate", value: metrics.forwardRate, formatted: cashRate(metrics.forwardRate) },
|
|
185
219
|
{ label: "Earnings Payout", value: roundedRatio(metrics.payoutRatio), formatted: metrics.payoutNotMeaningful ? PAYOUT_NOT_MEANINGFUL : formatDividendYield(metrics.payoutRatio) },
|
|
186
220
|
{ label: "1Y Cash Growth", value: roundedRatio(metrics.growth1Y), formatted: growthText(metrics.growth1Y, metrics.growth1YPayments) },
|
|
187
221
|
{ label: "3Y Cash CAGR", value: roundedRatio(metrics.growth3Y), formatted: growthText(metrics.growth3Y, metrics.growth3YPayments) },
|
|
@@ -198,7 +232,7 @@ export function projectDividendYieldHeadless(
|
|
|
198
232
|
entries: taxEquivalentLines(tax).map((line) => ({ label: line.label, value: line.value, formatted: line.text })),
|
|
199
233
|
}] : []),
|
|
200
234
|
{
|
|
201
|
-
title: "Dividend history",
|
|
235
|
+
title: convertedFrom ? `Dividend history (amounts ${convertedFromText(convertedFrom)})` : "Dividend history",
|
|
202
236
|
columns: declaredColumns,
|
|
203
237
|
rows,
|
|
204
238
|
// Each amount is in its own row's currency.
|
|
@@ -211,6 +245,8 @@ export function projectDividendYieldHeadless(
|
|
|
211
245
|
truncated: rows.length < matching.length,
|
|
212
246
|
type,
|
|
213
247
|
currency: data.currency ?? data.payments[0]?.currency ?? null,
|
|
248
|
+
// The currency the company declares in, when the amounts are converted from it; null for one paid as declared.
|
|
249
|
+
convertedFrom: convertedFrom ?? null,
|
|
214
250
|
historyAvailable: data.historyAvailable ?? true,
|
|
215
251
|
historyError: data.historyError ?? null,
|
|
216
252
|
summaryError: data.summaryError ?? null,
|
|
@@ -321,18 +357,7 @@ export function createDividendYieldHeadless(
|
|
|
321
357
|
if (historyOptions.length > 0) {
|
|
322
358
|
throw new Error(`${historyOptions.join(" and ")} ${historyOptions.length === 1 ? "filters" : "filter"} one ticker's payment history, and a ranking of several lists none. Drop ${historyOptions.length === 1 ? "it" : "them"}, or run DVD on one ticker.`);
|
|
323
359
|
}
|
|
324
|
-
|
|
325
|
-
// A fund's bond facts never fail its row: without them it ranks as before.
|
|
326
|
-
const bond = dependencies.loadBond?.(symbol, context)
|
|
327
|
-
.catch((error) => error instanceof BondClassificationError ? null
|
|
328
|
-
: logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)) ?? null;
|
|
329
|
-
try {
|
|
330
|
-
return { symbol, data: await dependencies.loadData(symbol, context), bond: await bond };
|
|
331
|
-
} catch (error) {
|
|
332
|
-
return { symbol, error: error instanceof Error ? error.message : String(error) };
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
return projectDividendRanking(entries);
|
|
360
|
+
return projectDividendRanking(await readDividendRanking(args.symbols, dividendRankingReads(context, dependencies)));
|
|
336
361
|
},
|
|
337
362
|
};
|
|
338
363
|
}
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
} from "./client";
|
|
9
9
|
import { DividendYieldPane } from "./pane";
|
|
10
10
|
import { dividendYieldHeadless } from "./headless";
|
|
11
|
+
import { RANK_SYMBOLS_SETTING } from "./ranking-view";
|
|
12
|
+
import { MAX_RANKED_SYMBOLS } from "./ranking-reads";
|
|
13
|
+
import type { PaneTemplateCreateOptions, PaneTemplateDef } from "../../../types/plugin";
|
|
11
14
|
|
|
12
15
|
function createDividendYieldModule({
|
|
13
16
|
component = DividendYieldPane,
|
|
@@ -65,7 +68,7 @@ function createDividendYieldModule({
|
|
|
65
68
|
|
|
66
69
|
paneTemplates: [
|
|
67
70
|
{
|
|
68
|
-
...createTickerSurfacePaneTemplate({
|
|
71
|
+
...withRanking(createTickerSurfacePaneTemplate({
|
|
69
72
|
id: "dividend-yield-pane",
|
|
70
73
|
paneId: "dividend-yield",
|
|
71
74
|
label: "Dividend Yield",
|
|
@@ -75,11 +78,43 @@ function createDividendYieldModule({
|
|
|
75
78
|
"dividend growth", "payout ratio", "drip", "reinvest", "reinvestment", "dividend reinvestment", "rank dividend payers",
|
|
76
79
|
"sec yield", "distribution yield", "muni", "munis", "municipal", "municipal bonds", "tax-exempt", "tax-equivalent yield", "muni treasury ratio"],
|
|
77
80
|
shortcut: "DVD",
|
|
78
|
-
}),
|
|
81
|
+
})),
|
|
79
82
|
headless,
|
|
80
83
|
},
|
|
81
84
|
],
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The tickers a DVD is opened with when there are several: the list a command
|
|
90
|
+
* passes (`shot DVD KO JNJ`), or a comma list typed as its argument.
|
|
91
|
+
*/
|
|
92
|
+
function rankedSymbols(options: PaneTemplateCreateOptions | undefined): string[] {
|
|
93
|
+
const list = options?.symbols?.length ? options.symbols : (options?.arg ?? options?.symbol ?? "").split(",");
|
|
94
|
+
// The first 25, as `fn DVD` ranks at most.
|
|
95
|
+
const symbols = [...new Set(list.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean))].slice(0, MAX_RANKED_SYMBOLS);
|
|
96
|
+
return symbols.length > 1 ? symbols : [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** DVD with several tickers opens one pane that ranks them by yield, as `fn DVD A B C` does; with one, its own pane. */
|
|
100
|
+
function withRanking(template: PaneTemplateDef): PaneTemplateDef {
|
|
101
|
+
return {
|
|
102
|
+
...template,
|
|
103
|
+
// `fn` and `shot` pass a typed list as `symbols`, so `shot DVD KO JNJ` draws the ranking `fn DVD KO JNJ` prints.
|
|
104
|
+
opensTickerList: true,
|
|
105
|
+
canCreate: (context, options) => rankedSymbols(options).length > 1 || (template.canCreate?.(context, options) ?? true),
|
|
106
|
+
createInstance: (context, options) => {
|
|
107
|
+
const symbols = rankedSymbols(options);
|
|
108
|
+
if (symbols.length < 2) return template.createInstance?.(context, options) ?? null;
|
|
109
|
+
return {
|
|
110
|
+
instanceId: `dividend-yield:rank:${symbols.map((symbol) => encodeURIComponent(symbol)).join(",")}`,
|
|
111
|
+
title: `DVD ${symbols.join(", ")}`,
|
|
112
|
+
binding: { kind: "none" },
|
|
113
|
+
placement: "floating",
|
|
114
|
+
settings: { [RANK_SYMBOLS_SETTING]: symbols },
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
export const dividendYieldModule = createDividendYieldModule();
|
|
@@ -31,7 +31,7 @@ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/resea
|
|
|
31
31
|
import { fetchDividendData, repriceDividendMetrics, statedForwardRate, type DividendData } from "./client";
|
|
32
32
|
import { dividendReferencePrice } from "../../../utils/trailing-dividend";
|
|
33
33
|
import { useTickerQuoteStream } from "../../../state/hooks/live-ticker-financials";
|
|
34
|
-
import { buildTrailingCashChartPoints, formatDividendYield, growthPaymentsDetail, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText, type DividendRow } from "./view";
|
|
34
|
+
import { buildTrailingCashChartPoints, convertedFromText, formatConvertedAmount, formatDividendYield, growthPaymentsDetail, PAYOUT_NOT_MEANINGFUL, staleHistoryText, toDividendRows, trailingPaymentsText, type DividendRow } from "./view";
|
|
35
35
|
import {
|
|
36
36
|
DEFAULT_SORT_PREFERENCE,
|
|
37
37
|
buildDividendColumns,
|
|
@@ -46,11 +46,14 @@ import type { FundBondFacts } from "../../../api-client/fund-facts";
|
|
|
46
46
|
import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
|
|
47
47
|
import { mayBeUsEquityOrFundTicker } from "../../../utils/sec";
|
|
48
48
|
import { useFundFacts } from "../sec/fund-facts-view";
|
|
49
|
+
import { usePaneInstance } from "../../../state/app/context";
|
|
50
|
+
import { DividendRankingView, rankSymbolsOf } from "./ranking-view";
|
|
49
51
|
import { bondFigures } from "../../../market-data/bond-funds";
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
/** A per-share rate; one converted from the currency the company declares in goes to two decimals of its source's unit. */
|
|
54
|
+
function formatRate(value: number | null, currency: string, converted = false, subUnit?: string): string {
|
|
52
55
|
if (value == null) return "—";
|
|
53
|
-
return formatDistributionAmount(value, currency);
|
|
56
|
+
return converted ? formatConvertedAmount(value, currency, subUnit) : formatDistributionAmount(value, currency);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function formatGrowth(value: number | null): string {
|
|
@@ -90,7 +93,9 @@ function formatFrequency(freq: DividendMetrics["paymentFrequency"]): string {
|
|
|
90
93
|
* say they are not its SEC yield, and the SEC yield follows them with its date.
|
|
91
94
|
*/
|
|
92
95
|
function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory: boolean, short: boolean,
|
|
93
|
-
declared: DividendData["declared"], bond: FundBondFacts | null = null): StatItem[] {
|
|
96
|
+
declared: DividendData["declared"], bond: FundBondFacts | null = null, convertedFrom?: string, convertedSubUnit?: string): StatItem[] {
|
|
97
|
+
// The rates of a listing paid in a conversion of what its company declares say what they were converted from.
|
|
98
|
+
const converted = convertedFrom ? { detail: convertedFromText(convertedFrom) } : {};
|
|
94
99
|
const exDate: StatItem[] = metrics.nextExDividendDate
|
|
95
100
|
? [{ id: "next-ex", label: "Next ex", value: formatDate(metrics.nextExDividendDate) }]
|
|
96
101
|
: hasHistory ? [] : [{ id: "last-ex", label: "Last ex", value: formatDate(metrics.lastExDividendDate) }];
|
|
@@ -116,8 +121,10 @@ function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory
|
|
|
116
121
|
...(forwardNote ? { detail: forwardNote } : {}) },
|
|
117
122
|
// The 30-day SEC yield as the fund's sponsor publishes it, dated.
|
|
118
123
|
...(sec ? [{ id: "sec-yield", label: short ? "SEC yld" : "SEC yield", value: sec.text, ...(sec.date ? { detail: `as of ${sec.date}` } : {}) }] : []),
|
|
119
|
-
{ id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency
|
|
120
|
-
|
|
124
|
+
{ id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency, !!convertedFrom, convertedSubUnit),
|
|
125
|
+
...metrics.trailingRate != null ? converted : {} },
|
|
126
|
+
{ id: "forward-rate", label: short ? "Fwd/sh" : "Fwd/share", value: formatRate(metrics.forwardRate, currency, !!convertedFrom, convertedSubUnit),
|
|
127
|
+
...metrics.forwardRate != null ? converted : {} },
|
|
121
128
|
growthItem("growth-1y", short ? "1Y chg" : "1Y growth", metrics.growth1Y, metrics.growth1YPayments),
|
|
122
129
|
growthItem("cagr-3y", "3Y CAGR", metrics.growth3Y, metrics.growth3YPayments),
|
|
123
130
|
{ id: "payout", label: "Payout", value: metrics.payoutNotMeaningful ? PAYOUT_NOT_MEANINGFUL : metrics.payoutRatio != null ? `${(metrics.payoutRatio * 100).toFixed(1)}%` : "—" },
|
|
@@ -144,13 +151,15 @@ const rowKey = (row: DividendRow) => row.key;
|
|
|
144
151
|
function renderCell(
|
|
145
152
|
row: DividendRow,
|
|
146
153
|
column: DividendColumn,
|
|
154
|
+
converted = false,
|
|
155
|
+
subUnit?: string,
|
|
147
156
|
): DataTableCell {
|
|
148
157
|
switch (column.id) {
|
|
149
158
|
case "exDate":
|
|
150
159
|
return { text: row.exDate, color: colors.textDim };
|
|
151
160
|
case "amount":
|
|
152
161
|
return {
|
|
153
|
-
text: formatDistributionAmount(row.amount, row.currency),
|
|
162
|
+
text: converted ? formatConvertedAmount(row.amount, row.currency, subUnit) : formatDistributionAmount(row.amount, row.currency),
|
|
154
163
|
color: colors.textBright,
|
|
155
164
|
attributes: TextAttributes.BOLD,
|
|
156
165
|
};
|
|
@@ -161,9 +170,15 @@ function renderCell(
|
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
type DividendPaneProps = { focused: boolean; width: number; height: number; loadData?: typeof fetchDividendData };
|
|
174
|
+
|
|
175
|
+
/** One ticker's dividends, or several ranked by yield when the pane was opened with a list (`shot DVD KO JNJ`). */
|
|
176
|
+
export function DividendYieldPane(props: DividendPaneProps) {
|
|
177
|
+
const ranked = rankSymbolsOf(usePaneInstance()?.settings);
|
|
178
|
+
return ranked.length > 1 ? <DividendRankingView symbols={ranked} {...props} /> : <DividendTickerPane {...props} />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function DividendTickerPane({ focused, width, height, loadData = fetchDividendData }: DividendPaneProps) {
|
|
167
182
|
const { symbol, ticker, financials } = usePaneTicker();
|
|
168
183
|
const { nativePaneChrome } = useUiCapabilities();
|
|
169
184
|
// Yields are repriced from the quote on every render, so streaming it is all
|
|
@@ -275,13 +290,17 @@ export function DividendYieldPane({ focused, width, height, loadData = fetchDivi
|
|
|
275
290
|
});
|
|
276
291
|
// The header row, plus the scrollbar row once the columns overflow the pane.
|
|
277
292
|
const tableChromeRows = chartTableChromeRows(columns, width);
|
|
278
|
-
const
|
|
293
|
+
const convertedFrom = historyData?.convertedFrom;
|
|
294
|
+
const convertedSubUnit = historyData?.convertedSubUnit;
|
|
295
|
+
const renderRowCell = useCallback((row: DividendRow, column: DividendColumn) => renderCell(row, column, !!convertedFrom, convertedSubUnit),
|
|
296
|
+
[convertedFrom, convertedSubUnit]);
|
|
297
|
+
const fullFigures = metrics ? buildMetricItems(metrics, currency, sortedRows.length > 0, false, data?.declared, bond, convertedFrom, convertedSubUnit) : [];
|
|
279
298
|
const keptFigures = chartTableLayout({
|
|
280
299
|
width, height, figures: fullFigures, tableRows: sortedRows.length, tableChromeRows, chart: chartPoints.length >= 2 ? {} : null,
|
|
281
300
|
}).figures;
|
|
282
301
|
// The desktop grid never clips a label; the terminal shortens them rather than clip.
|
|
283
302
|
const figures = metrics && !nativePaneChrome && labelsClip(keptFigures, width)
|
|
284
|
-
? buildMetricItems(metrics, currency, sortedRows.length > 0, true, data?.declared, bond) : fullFigures;
|
|
303
|
+
? buildMetricItems(metrics, currency, sortedRows.length > 0, true, data?.declared, bond, convertedFrom, convertedSubUnit) : fullFigures;
|
|
285
304
|
|
|
286
305
|
const handleHeaderClick = useCallback((columnId: string) => {
|
|
287
306
|
setSortPreference((current) => nextHeaderSort(current, columnId as DividendColumnId, {
|
|
@@ -324,7 +343,7 @@ export function DividendYieldPane({ focused, width, height, loadData = fetchDivi
|
|
|
324
343
|
sortDirection={sortPreference.direction}
|
|
325
344
|
onHeaderClick={handleHeaderClick}
|
|
326
345
|
getItemKey={rowKey}
|
|
327
|
-
renderCell={
|
|
346
|
+
renderCell={renderRowCell}
|
|
328
347
|
selectedTextOverridesCellColor
|
|
329
348
|
emptyStateTitle={emptyTitle}
|
|
330
349
|
/>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
2
|
+
import type { FundBondFacts } from "../../../api-client/fund-facts";
|
|
3
|
+
import { createConcurrencyLimiter } from "../../../utils/concurrency";
|
|
4
|
+
import type { DividendData } from "./client";
|
|
5
|
+
import type { DividendRankingEntry } from "./ranking";
|
|
6
|
+
|
|
7
|
+
/** Tickers one ranking reads at most: `fn DVD`, `shot DVD` and the DVD pane alike. */
|
|
8
|
+
export const MAX_RANKED_SYMBOLS = 25;
|
|
9
|
+
/** Tickers read at once across every ranking in the process, so a ranking never floods Gloom Cloud. */
|
|
10
|
+
const RANKING_CONCURRENCY = 4;
|
|
11
|
+
/** A rate-limited read waits what the server asks, a second when it names nothing, and never longer than this. */
|
|
12
|
+
const RATE_LIMIT_WAIT_MS = 1_000;
|
|
13
|
+
const RATE_LIMIT_WAIT_MAX_MS = 5_000;
|
|
14
|
+
|
|
15
|
+
/** The one gate every ranking's reads pass through. */
|
|
16
|
+
const rankingReads = createConcurrencyLimiter(RANKING_CONCURRENCY);
|
|
17
|
+
|
|
18
|
+
/** How a ranking reads one ticker. */
|
|
19
|
+
export interface DividendRankingReads {
|
|
20
|
+
loadData(symbol: string): Promise<DividendData>;
|
|
21
|
+
/** A fund's bond facts, null for any other listing. It never rejects: without them the fund ranks as before. */
|
|
22
|
+
loadBond?(symbol: string): Promise<FundBondFacts | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface RankingReadOptions {
|
|
26
|
+
/** How the one retry of a rate-limited read waits; tests pass their own. */
|
|
27
|
+
sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
|
|
32
|
+
/** The 429 a read was refused with, wrapped or not; null for any other failure. */
|
|
33
|
+
function rateLimited(error: unknown): ApiRequestError | null {
|
|
34
|
+
for (let current: unknown = error, depth = 0; current && typeof current === "object" && depth < 4; depth += 1) {
|
|
35
|
+
if (current instanceof ApiRequestError) return current.status === 429 ? current : null;
|
|
36
|
+
current = (current as { cause?: unknown }).cause;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A read Gloom Cloud refused with 429, asked once more after the wait it names; a second refusal is the row's error. */
|
|
42
|
+
async function retryRateLimitedOnce<T>(read: () => Promise<T>, sleep: (ms: number) => Promise<void>): Promise<T> {
|
|
43
|
+
try {
|
|
44
|
+
return await read();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const limited = rateLimited(error);
|
|
47
|
+
if (!limited) throw error;
|
|
48
|
+
await sleep(Math.min(Math.max(0, limited.retryAfterMs ?? RATE_LIMIT_WAIT_MS), RATE_LIMIT_WAIT_MAX_MS));
|
|
49
|
+
return read();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Each ticker of a ranking, read into its entry in the order given: the first
|
|
55
|
+
* 25, at most four at once through the gate every ranking shares, so `fn`,
|
|
56
|
+
* `shot` and any open DVD pane together never ask Gloom Cloud for more. A
|
|
57
|
+
* ticker that fails keeps its row with its error, after one more try when it
|
|
58
|
+
* was rate limited.
|
|
59
|
+
*/
|
|
60
|
+
export function readDividendRanking(
|
|
61
|
+
symbols: readonly string[],
|
|
62
|
+
reads: DividendRankingReads,
|
|
63
|
+
{ sleep = wait }: RankingReadOptions = {},
|
|
64
|
+
): Promise<DividendRankingEntry[]> {
|
|
65
|
+
return Promise.all(symbols.slice(0, MAX_RANKED_SYMBOLS).map((symbol) => rankingReads(async (): Promise<DividendRankingEntry> => {
|
|
66
|
+
const bond = reads.loadBond?.(symbol) ?? null;
|
|
67
|
+
try {
|
|
68
|
+
return { symbol, data: await retryRateLimitedOnce(() => reads.loadData(symbol), sleep), bond: await bond };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return { symbol, error: error instanceof Error ? error.message : String(error) };
|
|
71
|
+
}
|
|
72
|
+
})));
|
|
73
|
+
}
|