@gloom-sh/terminal 0.19.2 → 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 +1 -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 +20 -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.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +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 +92 -23
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +9 -4
- package/src/cli/commands/search.ts +71 -12
- 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 +5 -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/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +154 -4
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +40 -5
- package/src/cli/listing-arg.ts +46 -9
- 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 +20 -0
- 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 +24 -1
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/header.tsx +8 -3
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +7 -2
- package/src/components/layout/pane/footer/model.ts +40 -5
- 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 +2 -1
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +54 -23
- 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/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +8 -1
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +34 -31
- package/src/data/sqlite/access.ts +19 -8
- package/src/data/sqlite/checkpointer.ts +95 -8
- package/src/data/sqlite/database.ts +40 -7
- 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/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/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/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
- 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 +26 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -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/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/index.tsx +1 -2
- 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/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- 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 +2 -0
- 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/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/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- 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-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/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- 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/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 +15 -2
- 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/help/index.tsx +1 -1
- 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/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/index.tsx +7 -1
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
- 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/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/index.tsx +1 -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/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/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 +60 -0
- 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/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- 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/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- 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 +116 -13
- package/src/plugins/builtin/thirteenf/data.ts +132 -30
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
- package/src/plugins/builtin/thirteenf/types.ts +12 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
- 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/pane.tsx +2 -2
- 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/index.tsx +1 -2
- 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/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 +15 -4
- 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 +2 -0
- 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/initialization.ts +3 -2
- 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/create.ts +4 -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/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/standalone.ts +10 -1
- package/src/series/types.ts +7 -0
- package/src/series/validate.ts +11 -2
- 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 +12 -7
- package/src/telemetry/stall-reports.ts +34 -5
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name.ts +19 -0
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +13 -2
- package/src/tickers/search/index.ts +4 -1
- 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/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +55 -6
- package/src/time-series/resolve.ts +12 -3
- 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/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +22 -0
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +11 -0
- 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/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/main-thread-stall.ts +75 -3
- package/src/utils/perf-marks.ts +36 -2
- 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/renderers/electrobun/launcher/preload.ts +0 -13
- package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
- package/src/time-series/studies.ts +0 -1628
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/plugin";
|
|
2
2
|
import type { CdxBoardIndex, CreditIndexQuote } from "../../../api-client/credit-boards";
|
|
3
3
|
import { CDX_MAX_DAYS, fetchCdxBoard, fetchSovrBoard, loadCurrencyMoves } from "./client";
|
|
4
|
+
import { readsInBatches } from "../../../market-data/batched-reads";
|
|
4
5
|
import {
|
|
5
6
|
cdxDay,
|
|
6
7
|
cdxRows,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
formatCurrencyMove,
|
|
11
12
|
formatLevel,
|
|
12
13
|
formatMove,
|
|
14
|
+
levelKinds,
|
|
13
15
|
partialDayText,
|
|
14
16
|
quoteUnits,
|
|
15
17
|
sovrRows,
|
|
@@ -74,6 +76,8 @@ function historySection(index: CdxBoardIndex, days: number, now: number) {
|
|
|
74
76
|
const priced = index.quote === "price" && index.points.some((point) => point.spreadBp !== undefined);
|
|
75
77
|
const partial = today.partial && latest === index.date;
|
|
76
78
|
const symbol = cdxSeries(index.id);
|
|
79
|
+
const shown = index.points.filter((point) => point.date > from).reverse();
|
|
80
|
+
const kinds = levelKinds(shown, (point) => point.kind);
|
|
77
81
|
return {
|
|
78
82
|
title: `${index.name} 5Y daily ${index.quote === "spread" ? "spread" : "price"}`,
|
|
79
83
|
columns: [
|
|
@@ -83,11 +87,12 @@ function historySection(index: CdxBoardIndex, days: number, now: number) {
|
|
|
83
87
|
{ key: "prints", header: "Prints", align: "right" as const },
|
|
84
88
|
{ key: "maturity", header: "Contract", textFormat: (value: unknown) => formatContract(typeof value === "string" ? value : null) },
|
|
85
89
|
...(partial ? [dayColumn] : []),
|
|
90
|
+
...kinds.columns,
|
|
86
91
|
],
|
|
87
|
-
rows:
|
|
92
|
+
rows: shown.map((point) => ({
|
|
88
93
|
day: point.date, level: point.level, unit,
|
|
89
94
|
...(priced ? { spreadBp: point.spreadBp ?? null, spreadUnit: "bp" } : {}),
|
|
90
|
-
prints: point.prints, maturity: point.maturity, series: symbol,
|
|
95
|
+
prints: point.prints, maturity: point.maturity, series: symbol, ...kinds.row(point),
|
|
91
96
|
...(partial && point.date === latest ? { session: partialDayText(point.prints, today.lastPrintAt), partial: true, lastPrintAt: today.lastPrintAt } : {}),
|
|
92
97
|
})),
|
|
93
98
|
rowUnitKey: "unit",
|
|
@@ -133,6 +138,7 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
133
138
|
const shown = index ? [index] : payload.indexes;
|
|
134
139
|
// A server that predates the spread equivalent sends none: no column of blanks.
|
|
135
140
|
const spreads = shown.some((entry) => entry.spreadBp !== undefined);
|
|
141
|
+
const kinds = levelKinds(shown, (entry) => entry.kind);
|
|
136
142
|
const rows = cdxRows(shown, payload.asOf, now).map((row) => {
|
|
137
143
|
const units = quoteUnits(row.index.quote);
|
|
138
144
|
const priced = spreads && row.index.quote === "price";
|
|
@@ -151,6 +157,7 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
151
157
|
contract: formatContract(row.index.maturity),
|
|
152
158
|
date: row.index.date,
|
|
153
159
|
changeDate: row.day.changeDate,
|
|
160
|
+
...kinds.row(row.index),
|
|
154
161
|
...(row.day.partial ? { session: partialDayText(row.index.prints, row.day.lastPrintAt), partial: true, lastPrintAt: row.day.lastPrintAt } : {}),
|
|
155
162
|
};
|
|
156
163
|
});
|
|
@@ -158,7 +165,7 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
158
165
|
return {
|
|
159
166
|
sections: [
|
|
160
167
|
{
|
|
161
|
-
title: "On the run, 5Y", columns: [...CDX_COLUMNS.filter((column) => spreads || column !== spreadColumn), ...(anyPartial ? [dayColumn] : [])], rows,
|
|
168
|
+
title: "On the run, 5Y", columns: [...CDX_COLUMNS.filter((column) => spreads || column !== spreadColumn), ...(anyPartial ? [dayColumn] : []), ...kinds.columns], rows,
|
|
162
169
|
rowUnitKey: "unit", rowSymbolKey: "series", rowAsOfKey: "date", interval: "daily",
|
|
163
170
|
},
|
|
164
171
|
...(index ? [historySection(index, days, now)] : []),
|
|
@@ -170,6 +177,15 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
170
177
|
},
|
|
171
178
|
};
|
|
172
179
|
|
|
180
|
+
const SOVR_COLUMNS: HeadlessPaneColumn[] = [
|
|
181
|
+
{ key: "name", header: "Sovereign" },
|
|
182
|
+
levelColumn,
|
|
183
|
+
moveColumn("change1M", "1M", "unit"),
|
|
184
|
+
{ key: "currency", header: "Ccy 1M", align: "right" },
|
|
185
|
+
{ key: "percentile", header: "Pctl 1Y", align: "right", format: percentile },
|
|
186
|
+
{ key: "date", header: "As of" },
|
|
187
|
+
];
|
|
188
|
+
|
|
173
189
|
export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
174
190
|
discovery: { aliases: ["SOVR", "WCDS"], dataRequirements: ["Gloom Cloud sovereign CDS endpoint"],
|
|
175
191
|
limitations: ["Daily 5Y levels from public DTCC prints; thinly traded names can miss days."] },
|
|
@@ -178,14 +194,7 @@ export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
178
194
|
argument: { kind: "free-text", optional: true, placeholder: "country",
|
|
179
195
|
description: "A country (Brazil, BR) to list its row alone; omit for every sovereign." },
|
|
180
196
|
options: [],
|
|
181
|
-
columns:
|
|
182
|
-
{ key: "name", header: "Sovereign" },
|
|
183
|
-
levelColumn,
|
|
184
|
-
moveColumn("change1M", "1M", "unit"),
|
|
185
|
-
{ key: "currency", header: "Ccy 1M", align: "right" },
|
|
186
|
-
{ key: "percentile", header: "Pctl 1Y", align: "right", format: percentile },
|
|
187
|
-
{ key: "date", header: "As of" },
|
|
188
|
-
],
|
|
197
|
+
columns: SOVR_COLUMNS,
|
|
189
198
|
describe: "Sovereign CDS",
|
|
190
199
|
async load(args, ctx) {
|
|
191
200
|
const payload = await fetchSovrBoard(ctx.apiClient);
|
|
@@ -193,10 +202,13 @@ export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
193
202
|
const country = query ? findSovereign(payload.sovereigns, query) : null;
|
|
194
203
|
if (query && !country) throw new Error(`No sovereign ${query}. Use one of: ${payload.sovereigns.map((entry) => entry.name).join(", ")}.`);
|
|
195
204
|
const sovereigns = country ? [country] : payload.sovereigns;
|
|
205
|
+
const batch = await readsInBatches(ctx.marketData, "getPriceHistory");
|
|
196
206
|
const moves = await loadCurrencyMoves(sovereigns.map((row) => row.currency),
|
|
197
|
-
(symbol) => ctx.marketData.getPriceHistory(symbol, "", "3M"));
|
|
207
|
+
(symbol) => ctx.marketData.getPriceHistory(symbol, "", "3M", batch ? { batch } : undefined), batch);
|
|
198
208
|
const publisher = boardPublisher(payload.source);
|
|
209
|
+
const kinds = levelKinds(sovereigns, (row) => row.kind);
|
|
199
210
|
return {
|
|
211
|
+
...(kinds.columns.length ? { columns: [...SOVR_COLUMNS, ...kinds.columns] } : {}),
|
|
200
212
|
rows: sovrRows(sovereigns, payload.asOf, moves).map((row) => ({
|
|
201
213
|
name: row.label,
|
|
202
214
|
series: sovereignSeries(row.sovereign.id),
|
|
@@ -206,6 +218,7 @@ export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
206
218
|
currency: formatCurrencyMove(row.sovereign.currency, row.currencyMove),
|
|
207
219
|
percentile: rank(row.percentile),
|
|
208
220
|
date: row.sovereign.date,
|
|
221
|
+
...kinds.row(row.sovereign),
|
|
209
222
|
})),
|
|
210
223
|
rowUnitKey: "unit",
|
|
211
224
|
rowSymbolKey: "series",
|
|
@@ -2,12 +2,14 @@ import type {
|
|
|
2
2
|
CdxBoardIndex,
|
|
3
3
|
CreditBoardPoint,
|
|
4
4
|
CreditIndexQuote,
|
|
5
|
+
CreditLevelKind,
|
|
5
6
|
SovrBoardSovereign,
|
|
6
7
|
} from "../../../api-client/credit-boards";
|
|
7
8
|
import { spanAxisFormatter, formatBpAxis } from "../../../components/chart-table/axis";
|
|
8
9
|
import { historyStatistics } from "../../../components/chart/curve/model";
|
|
9
10
|
import type { MarketBoardRow } from "../../../components/market-board";
|
|
10
11
|
import type { PricePoint } from "../../../types/financials";
|
|
12
|
+
import type { HeadlessPaneColumn } from "../../../types/plugin";
|
|
11
13
|
|
|
12
14
|
const YEAR_DAYS = 365;
|
|
13
15
|
/** A level older than this is not the current market. */
|
|
@@ -93,6 +95,33 @@ function utcClock(instant: string): string | null {
|
|
|
93
95
|
return Number.isFinite(time) ? new Date(time).toISOString().slice(11, 16) : null;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* How a level that is the day's settlement price, not its trades, is marked
|
|
100
|
+
* beside its date.
|
|
101
|
+
*/
|
|
102
|
+
export const SETTLEMENT_TEXT = "settlement";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A report's mark for the levels that are settlement prices: blank for a
|
|
106
|
+
* level from the day's trades. Shown only when a row is one, and each row
|
|
107
|
+
* then carries `kind`.
|
|
108
|
+
*/
|
|
109
|
+
const levelKindColumn: HeadlessPaneColumn = {
|
|
110
|
+
key: "kind", header: "Level",
|
|
111
|
+
description: "Blank for a level from the day's trades. settlement for the day's end-of-day settlement price, which stands in "
|
|
112
|
+
+ "on a day without enough agreeing trades; its prints are 0.",
|
|
113
|
+
format: (value) => value === "settlement" ? SETTLEMENT_TEXT : "",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** `kind` on every row once one level is a settlement; nothing otherwise, so a report without one is unchanged. */
|
|
117
|
+
export function levelKinds<T>(items: readonly T[], kindOf: (item: T) => CreditLevelKind | null | undefined) {
|
|
118
|
+
const shown = items.some((item) => kindOf(item) === "settlement");
|
|
119
|
+
return {
|
|
120
|
+
columns: shown ? [levelKindColumn] : [],
|
|
121
|
+
row: (item: T) => shown ? { kind: kindOf(item) ?? "traded" } : {},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
/**
|
|
97
126
|
* What a day still trading rests on: `so far, 38 prints, last 08:09 UTC`.
|
|
98
127
|
* The print time is left out when the server does not say it.
|
|
@@ -25,12 +25,14 @@ import { formatAsOf } from "../cds/model";
|
|
|
25
25
|
import { FIXED_INCOME_PANE_ID } from "../fixed-income/model";
|
|
26
26
|
import { useCommandValue } from "../shared/command-tab";
|
|
27
27
|
import { getCachedSovrBoard, loadCurrencyMoves, loadSovrBoard } from "./client";
|
|
28
|
+
import { readsInBatches } from "../../../market-data/batched-reads";
|
|
28
29
|
import {
|
|
29
30
|
findSovereign,
|
|
30
31
|
formatContract,
|
|
31
32
|
formatCurrencyMove,
|
|
32
33
|
formatLevel,
|
|
33
34
|
formatMove,
|
|
35
|
+
SETTLEMENT_TEXT,
|
|
34
36
|
sovrRows,
|
|
35
37
|
type SovrRow,
|
|
36
38
|
} from "./model";
|
|
@@ -65,7 +67,7 @@ function SovereignDetail({ row, width, height, focused }: { row: SovrRow; width:
|
|
|
65
67
|
const { sovereign, year } = row;
|
|
66
68
|
const items: StatItem[] = [
|
|
67
69
|
{ id: "level", label: "5Y spread", value: formatLevel("spread", sovereign.level),
|
|
68
|
-
detail:
|
|
70
|
+
detail: [formatPercentileRank(year.percentile, "1Y"), sovereign.date, ...(sovereign.kind === "settlement" ? [SETTLEMENT_TEXT] : [])].join(" · ") },
|
|
69
71
|
{ id: "1m", label: "1M", value: formatMove("spread", sovereign.change1M) },
|
|
70
72
|
{ id: "1w", label: "1W", value: formatMove("spread", sovereign.change1W) },
|
|
71
73
|
{ id: "fx", label: "Currency 1M", value: formatCurrencyMove(sovereign.currency, row.currencyMove) },
|
|
@@ -99,9 +101,12 @@ export function SovrPane({ focused, width, height }: PaneProps) {
|
|
|
99
101
|
// Each currency's month against the dollar, from the same daily closes the FX panes use.
|
|
100
102
|
const currencies = useMemo(() => [...new Set(payload?.sovereigns.map((row) => row.currency) ?? [])].sort().join(","),
|
|
101
103
|
[payload]);
|
|
102
|
-
const movesLoader = useCallback(() =>
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
const movesLoader = useCallback(async () => {
|
|
105
|
+
if (!provider || !currencies) return NO_MOVES;
|
|
106
|
+
// Every currency at once, in batch requests, from a source that batches them; a few at a time otherwise.
|
|
107
|
+
const batch = await readsInBatches(provider, "getPriceHistory");
|
|
108
|
+
return loadCurrencyMoves(currencies.split(","), (symbol) => provider.getPriceHistory(symbol, "", "3M", batch ? { batch } : undefined), batch);
|
|
109
|
+
}, [currencies, provider]);
|
|
105
110
|
const moves = useAsyncResource(movesLoader, { keepPreviousData: true });
|
|
106
111
|
const rows = useMemo(() => payload ? sovrRows(payload.sovereigns, payload.asOf, moves.data ?? NO_MOVES) : EMPTY_ROWS,
|
|
107
112
|
[moves.data, payload]);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
3
|
+
import type { BitcoinNetworkDailyValue, BitcoinNetworkPayload } from "../../../api-client/crypto-markets";
|
|
4
|
+
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
|
+
import type { SeriesAttribution, SeriesDefinition, SeriesPoint } from "../../../series/types";
|
|
6
|
+
import { isFiniteNumber } from "../../../utils/guards";
|
|
7
|
+
import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
8
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
9
|
+
|
|
10
|
+
const bitcoinNetworkCache = createPluginCache<BitcoinNetworkPayload>({
|
|
11
|
+
kind: "bitcoin-network",
|
|
12
|
+
source: "gloom-cloud",
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
// The server reads the estimate every half hour and the daily figures every six hours.
|
|
15
|
+
policy: { staleMs: 30 * 60_000, expireMs: 30 * 86_400_000 },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const BITCOIN_NETWORK_CACHES = [bitcoinNetworkCache] as const;
|
|
19
|
+
|
|
20
|
+
const MAX_DAYS = 20_000;
|
|
21
|
+
const date = (value: unknown): value is string => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
22
|
+
const instant = (value: unknown): value is string => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
23
|
+
const daily = (rows: unknown): rows is BitcoinNetworkDailyValue[] => Array.isArray(rows) && rows.length <= MAX_DAYS
|
|
24
|
+
&& rows.every((row) => !!row && date((row as BitcoinNetworkDailyValue).date) && isFiniteNumber((row as BitcoinNetworkDailyValue).value));
|
|
25
|
+
|
|
26
|
+
function validateBitcoinNetwork(data: BitcoinNetworkPayload): BitcoinNetworkPayload {
|
|
27
|
+
const retarget = data?.retarget;
|
|
28
|
+
if (
|
|
29
|
+
!data
|
|
30
|
+
|| data.version !== 1
|
|
31
|
+
|| !instant(data.generatedAt)
|
|
32
|
+
|| typeof data.source?.name !== "string" || !data.source.name.trim()
|
|
33
|
+
|| !["available", "stale", "unavailable"].includes(data.status)
|
|
34
|
+
|| !daily(data.hashrate)
|
|
35
|
+
|| !daily(data.hashprice)
|
|
36
|
+
|| !daily(data.retargetHistory)
|
|
37
|
+
|| !Array.isArray(data.difficulty)
|
|
38
|
+
|| !data.difficulty.every((row) => !!row && date(row.date) && isFiniteNumber(row.difficulty) && row.difficulty > 0)
|
|
39
|
+
|| (retarget !== null && (!retarget || !isFiniteNumber(retarget.changePercent) || !instant(retarget.estimatedAt) || !instant(retarget.readAt)))
|
|
40
|
+
|| !Array.isArray(data.warnings)
|
|
41
|
+
) {
|
|
42
|
+
throw new Error("The server returned invalid bitcoin network data");
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchBitcoinNetwork(client: Pick<typeof apiClient, "getCloudBitcoinNetwork"> = apiClient): Promise<BitcoinNetworkPayload> {
|
|
48
|
+
try {
|
|
49
|
+
return validateBitcoinNetwork(await client.getCloudBitcoinNetwork());
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof ApiRequestError && error.status === 503) throw unavailableOnServer(error, "Bitcoin network data is temporarily unavailable", [503]);
|
|
52
|
+
throw unavailableOnServer(error, "Bitcoin network data is not available yet.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadBitcoinNetwork() {
|
|
57
|
+
return loadCloudResource(bitcoinNetworkCache, "all", () => fetchBitcoinNetwork(), { validate: validateBitcoinNetwork });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const points = (rows: readonly BitcoinNetworkDailyValue[]): SeriesPoint[] =>
|
|
61
|
+
rows.map((row) => ({ date: Date.parse(`${row.date}T00:00:00Z`), value: row.value }));
|
|
62
|
+
|
|
63
|
+
const TAGS = ["bitcoin", "BTC", "BTC-USD", "mining", "miner", "miners", "bitcoin mining", "network", "on-chain"];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A series of the network board: its points, cited as the server names its
|
|
67
|
+
* source, with the board's warnings about this figure (`concerns`: "Hash
|
|
68
|
+
* rate, difficulty and hashprice could not refresh" is three series', "The
|
|
69
|
+
* retarget estimate is unavailable" the estimate's alone) and the
|
|
70
|
+
* cached-copy note.
|
|
71
|
+
*/
|
|
72
|
+
function networkSeries(
|
|
73
|
+
definition: Omit<Extract<SeriesDefinition, { load: unknown }>, "load" | "frequency" | "group">
|
|
74
|
+
& { frequency?: SeriesDefinition["frequency"] },
|
|
75
|
+
read: (payload: BitcoinNetworkPayload) => SeriesPoint[],
|
|
76
|
+
{ concerns, derived }: { concerns: RegExp; derived?: string },
|
|
77
|
+
): SeriesDefinition {
|
|
78
|
+
return {
|
|
79
|
+
frequency: "daily",
|
|
80
|
+
group: "Crypto",
|
|
81
|
+
refreshMs: 30 * 60_000,
|
|
82
|
+
...definition,
|
|
83
|
+
load: async () => {
|
|
84
|
+
const resource = await loadBitcoinNetwork();
|
|
85
|
+
const found = read(resource.payload);
|
|
86
|
+
if (!found.length) throw new Error(`${definition.name} is not available yet.`);
|
|
87
|
+
const note = cachedSeriesNote(resource);
|
|
88
|
+
const own = resource.payload.warnings.filter((line) => concerns.test(line)).join(" ");
|
|
89
|
+
const warning = note.warning ?? (own || undefined);
|
|
90
|
+
const source = resource.payload.source.name.trim();
|
|
91
|
+
const attribution: SeriesAttribution = { source: derived ? `${source}, ${derived}` : source };
|
|
92
|
+
return { points: found, attribution, ...note, ...warning ? { warning } : {} };
|
|
93
|
+
},
|
|
94
|
+
} as SeriesDefinition;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The bitcoin network for `fn G`: hash rate, difficulty, the next
|
|
99
|
+
* retarget's estimate and hashprice. `fn G hashrate`, `difficulty`,
|
|
100
|
+
* `retarget` and `hashprice` chart them by their aliases; `catalog
|
|
101
|
+
* hashrate`, `hashprice`, `difficulty`, `mining` and `miner` find them by
|
|
102
|
+
* their tags.
|
|
103
|
+
*/
|
|
104
|
+
export const BITCOIN_NETWORK_SERIES: readonly SeriesDefinition[] = [
|
|
105
|
+
networkSeries({
|
|
106
|
+
id: "bitcoin:hashrate",
|
|
107
|
+
name: "Bitcoin network hash rate",
|
|
108
|
+
unit: "EH/s",
|
|
109
|
+
tags: [...TAGS, "hashrate", "hash rate"],
|
|
110
|
+
aliases: ["hashrate"],
|
|
111
|
+
description: "The bitcoin network's hash rate in exahashes per second, each UTC day's average, estimated from the blocks found; the newest day is the day so far.",
|
|
112
|
+
}, (payload) => points(payload.hashrate), { concerns: /\bhash rate\b/i }),
|
|
113
|
+
networkSeries({
|
|
114
|
+
id: "bitcoin:difficulty",
|
|
115
|
+
name: "Bitcoin mining difficulty (trillions)",
|
|
116
|
+
unit: "number",
|
|
117
|
+
frequency: "irregular",
|
|
118
|
+
style: "step",
|
|
119
|
+
tags: [...TAGS, "difficulty", "retarget", "difficulty adjustment"],
|
|
120
|
+
aliases: ["difficulty"],
|
|
121
|
+
description: "The difficulty each retarget set, every 2,016 blocks, in trillions, dated by the block that set it.",
|
|
122
|
+
}, (payload) => payload.difficulty.map((row) => ({ date: Date.parse(`${row.date}T00:00:00Z`), value: Number((row.difficulty / 1e12).toFixed(4)) })),
|
|
123
|
+
{ concerns: /\bdifficulty\b/i }),
|
|
124
|
+
networkSeries({
|
|
125
|
+
id: "bitcoin:retarget-estimate",
|
|
126
|
+
name: "Bitcoin next difficulty retarget estimate",
|
|
127
|
+
unit: "%",
|
|
128
|
+
tags: [...TAGS, "difficulty", "retarget", "difficulty adjustment", "estimate"],
|
|
129
|
+
aliases: ["retarget"],
|
|
130
|
+
description: "The estimated change in difficulty at the next retarget, in percent, as read each UTC day since Gloom began keeping it in October 2026.",
|
|
131
|
+
}, (payload) => points(payload.retargetHistory), { concerns: /\bretarget\b/i }),
|
|
132
|
+
networkSeries({
|
|
133
|
+
id: "bitcoin:hashprice",
|
|
134
|
+
name: "Bitcoin hashprice (derived)",
|
|
135
|
+
unit: "USD/PH/s/day",
|
|
136
|
+
tags: [...TAGS, "hashprice", "hash price", "miner revenue"],
|
|
137
|
+
aliases: ["hashprice"],
|
|
138
|
+
description: "Derived, not quoted: what 1 PH/s is expected to earn in a day, in USD, at the day's difficulty, average block reward (subsidy and fees) and BTC price.",
|
|
139
|
+
}, (payload) => points(payload.hashprice), {
|
|
140
|
+
concerns: /\bhashprice\b/i,
|
|
141
|
+
derived: "derived from the block rewards, the BTC price and the difficulty",
|
|
142
|
+
}),
|
|
143
|
+
];
|
|
@@ -31,15 +31,16 @@ const COIN_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
31
31
|
{ key: "rank", header: "#", align: "right" },
|
|
32
32
|
{ key: "code", header: "Coin" },
|
|
33
33
|
{ key: "name", header: "Name" },
|
|
34
|
-
|
|
34
|
+
// Every coin is priced, traded and valued in US dollars: the text headers say so; an export keeps its headers.
|
|
35
|
+
{ key: "price", header: "Price", textHeader: "Price USD", align: "right", format: (_value, row) => String(row.priceText ?? "—") },
|
|
35
36
|
// Since 00:00 UTC, the day `quote` gives a coin (CRYPTO_CHANGE_BASIS_LABEL): the text says so, as the pane's
|
|
36
37
|
// CHG% UTC does, and an export keeps the header it has always had.
|
|
37
38
|
{ key: "changePercent", header: "Chg %", textHeader: "Chg % UTC", align: "right", format: percent },
|
|
38
39
|
{ key: "return7d", header: "7D %", align: "right", format: percent },
|
|
39
40
|
{ key: "return30d", header: "30D %", align: "right", format: percent },
|
|
40
41
|
{ key: "return1y", header: "1Y %", align: "right", format: percent },
|
|
41
|
-
{ key: "volume24h", header: "Vol 24h", align: "right", format: compact },
|
|
42
|
-
{ key: "marketCap", header: "Market cap", align: "right", format: compact },
|
|
42
|
+
{ key: "volume24h", header: "Vol 24h", textHeader: "Vol 24h USD", align: "right", format: compact },
|
|
43
|
+
{ key: "marketCap", header: "Market cap", textHeader: "Market cap USD", align: "right", format: compact },
|
|
43
44
|
];
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
2
|
import { LIVE_STREAMING_QUICK_SETTING, withLiveStreamingSetting } from "../../../state/hooks/live-streaming";
|
|
3
3
|
import { cryptoMarketsCache, DEFI_TVL_CACHES, STABLECOIN_SUPPLY_CACHES } from "./client";
|
|
4
|
+
import { BITCOIN_NETWORK_CACHES } from "./bitcoin-network";
|
|
4
5
|
import { cryptoBoardHeadless } from "./headless";
|
|
5
|
-
import { CryptoBoardPane } from "./pane";
|
|
6
6
|
import { CRYPTO_SERIES } from "./series";
|
|
7
7
|
|
|
8
8
|
export const cryptoBoardModule: PluginModule = {
|
|
@@ -12,7 +12,7 @@ export const cryptoBoardModule: PluginModule = {
|
|
|
12
12
|
id: "crypto-board",
|
|
13
13
|
name: "Crypto",
|
|
14
14
|
icon: "C",
|
|
15
|
-
component: CryptoBoardPane,
|
|
15
|
+
get component(): typeof import("./pane").CryptoBoardPane { const { CryptoBoardPane } = require("./pane"); return CryptoBoardPane; },
|
|
16
16
|
defaultPosition: "right",
|
|
17
17
|
defaultMode: "floating",
|
|
18
18
|
defaultFloatingSize: { width: 120, height: 32 },
|
|
@@ -40,10 +40,12 @@ export const cryptoBoardModule: PluginModule = {
|
|
|
40
40
|
cryptoMarketsCache.attach(ctx.persistence);
|
|
41
41
|
for (const cache of STABLECOIN_SUPPLY_CACHES) cache.attach(ctx.persistence);
|
|
42
42
|
for (const cache of DEFI_TVL_CACHES) cache.attach(ctx.persistence);
|
|
43
|
+
for (const cache of BITCOIN_NETWORK_CACHES) cache.attach(ctx.persistence);
|
|
43
44
|
},
|
|
44
45
|
dispose() {
|
|
45
46
|
cryptoMarketsCache.reset();
|
|
46
47
|
for (const cache of STABLECOIN_SUPPLY_CACHES) cache.reset();
|
|
47
48
|
for (const cache of DEFI_TVL_CACHES) cache.reset();
|
|
49
|
+
for (const cache of BITCOIN_NETWORK_CACHES) cache.reset();
|
|
48
50
|
},
|
|
49
51
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SeriesDefinition, SeriesPoint } from "../../../series/types";
|
|
2
2
|
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
3
|
import { loadDefiTvl, loadStablecoinSupply, loadStablecoinSupplyHistory } from "./client";
|
|
4
|
+
import { BITCOIN_NETWORK_SERIES } from "./bitcoin-network";
|
|
4
5
|
|
|
5
6
|
const DAY_MS = 86_400_000;
|
|
6
7
|
|
|
@@ -114,4 +115,5 @@ export const CRYPTO_SERIES: readonly SeriesDefinition[] = [
|
|
|
114
115
|
},
|
|
115
116
|
...STABLECOIN_SUPPLY_SERIES,
|
|
116
117
|
DEFI_TVL_SERIES,
|
|
118
|
+
...BITCOIN_NETWORK_SERIES,
|
|
117
119
|
];
|
|
@@ -18,7 +18,9 @@ const textOrNull = (value: unknown) => value === null || text(value);
|
|
|
18
18
|
/**
|
|
19
19
|
* The flows as served, or an error: the asset asked for, its funds once each,
|
|
20
20
|
* every day newest first with a figure or none for each fund, and where they
|
|
21
|
-
* are from. A payload that breaks any of it is not drawn.
|
|
21
|
+
* are from. A payload that breaks any of it is not drawn. A day's price move
|
|
22
|
+
* that is not a number or null is dropped from that day alone: the flows
|
|
23
|
+
* stand without it.
|
|
22
24
|
*/
|
|
23
25
|
export function validateCryptoEtfFlows(payload: CryptoEtfFlowsPayload, asset: CryptoEtfFlowAsset): CryptoEtfFlowsPayload {
|
|
24
26
|
const invalid = (): never => { throw new Error("The server returned invalid crypto ETF flows"); };
|
|
@@ -35,7 +37,12 @@ export function validateCryptoEtfFlows(payload: CryptoEtfFlowsPayload, asset: Cr
|
|
|
35
37
|
|| !Array.isArray(day.pending) || day.pending.some((ticker) => !known.has(ticker))) return invalid();
|
|
36
38
|
}
|
|
37
39
|
if (payload.days[0] && payload.days[0].date !== payload.asOf) return invalid();
|
|
38
|
-
return payload;
|
|
40
|
+
if (payload.days.every((day) => day.priceChangePct === undefined || flow(day.priceChangePct))) return payload;
|
|
41
|
+
return { ...payload, days: payload.days.map((day) => {
|
|
42
|
+
if (day.priceChangePct === undefined || flow(day.priceChangePct)) return day;
|
|
43
|
+
const { priceChangePct: _dropped, ...rest } = day;
|
|
44
|
+
return rest;
|
|
45
|
+
}) };
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export async function fetchCryptoEtfFlows(
|
|
@@ -2,6 +2,7 @@ import type { CryptoEtfFlowsPayload } from "../../../api-client/crypto-etf-flows
|
|
|
2
2
|
import type { HeadlessPaneColumn } from "../../../types/headless";
|
|
3
3
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
4
4
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
5
|
+
import { readPriceWindow, type PriceWindow } from "../../../utils/price-window";
|
|
5
6
|
import { fetchCryptoEtfFlows } from "./client";
|
|
6
7
|
import {
|
|
7
8
|
cryptoEtfFlowAsset,
|
|
@@ -9,16 +10,22 @@ import {
|
|
|
9
10
|
cryptoEtfFlowSummary,
|
|
10
11
|
formatFlow,
|
|
11
12
|
formatFlowAmount,
|
|
13
|
+
formatPriceChange,
|
|
14
|
+
hasFundPriceChanges,
|
|
12
15
|
pendingText,
|
|
13
16
|
} from "./model";
|
|
14
17
|
|
|
15
18
|
const flowCell = (value: unknown) => formatFlow(typeof value === "number" ? value : null);
|
|
19
|
+
const optionText = (value: unknown) => typeof value === "string" && value.trim() ? value : undefined;
|
|
16
20
|
|
|
17
|
-
/** Date, the day's total and each fund, in US$ millions as published. */
|
|
21
|
+
/** Date, the day's total, the funds' price move when served, and each fund, in US$ millions as published. */
|
|
18
22
|
function flowColumns(payload: CryptoEtfFlowsPayload): HeadlessPaneColumn[] {
|
|
19
23
|
return [
|
|
20
24
|
{ key: "date", header: "Date", shrink: false },
|
|
21
25
|
{ key: "total", header: "Total", align: "right", format: flowCell },
|
|
26
|
+
...hasFundPriceChanges(payload)
|
|
27
|
+
? [{ key: "priceChangePct", header: "Price %", literalHeader: true, align: "right" as const, format: (value: unknown) => formatPriceChange(typeof value === "number" ? value : null) }]
|
|
28
|
+
: [],
|
|
22
29
|
// A fund the day still waits on reads "pending", as in the pane; none at all is a dash.
|
|
23
30
|
...payload.funds.map((fund): HeadlessPaneColumn => ({
|
|
24
31
|
key: fund.ticker, header: fund.ticker, literalHeader: true, align: "right",
|
|
@@ -28,6 +35,13 @@ function flowColumns(payload: CryptoEtfFlowsPayload): HeadlessPaneColumn[] {
|
|
|
28
35
|
];
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/** "Net flows of the US spot bitcoin ETFs in US$ millions. 2026-10-19 to 2026-10-23: +$301.20M over 5 trading days." */
|
|
39
|
+
function windowSummaryLine(payload: CryptoEtfFlowsPayload, window: PriceWindow, days: CryptoEtfFlowsPayload["days"]): string {
|
|
40
|
+
const total = days.some((day) => day.total != null) ? days.reduce((sum, day) => sum + (day.total ?? 0), 0) : null;
|
|
41
|
+
return `Net flows of the US spot ${cryptoEtfFlowAssetLabel(payload.asset).toLowerCase()} ETFs in US$ millions. ${window.from} to ${window.to}: `
|
|
42
|
+
+ `${formatFlowAmount(total)} over ${days.length} trading ${days.length === 1 ? "day" : "days"}.`;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
/** "Net flows of the US spot bitcoin ETFs in US$ millions. 2026-10-20: -$6.70M (IBIT pending); 5 days ..., since 2024-01-11 +$57.47B." */
|
|
32
46
|
function summaryLine(payload: CryptoEtfFlowsPayload): string {
|
|
33
47
|
const summary = cryptoEtfFlowSummary(payload);
|
|
@@ -43,6 +57,15 @@ function summaryLine(payload: CryptoEtfFlowsPayload): string {
|
|
|
43
57
|
].join(" ");
|
|
44
58
|
}
|
|
45
59
|
|
|
60
|
+
/**
|
|
61
|
+
* "Funds: IBIT iShares Bitcoin Trust ETF, ..., BTC Grayscale Bitcoin Mini Trust ETF.": a column is headed by
|
|
62
|
+
* the fund's ticker, and BTC or ETH there is Grayscale's mini fund, not the coin. Null when no fund is named.
|
|
63
|
+
*/
|
|
64
|
+
function fundsLine(payload: CryptoEtfFlowsPayload): string | null {
|
|
65
|
+
const named = payload.funds.flatMap((fund) => fund.name ? [`${fund.ticker} ${fund.name}`] : []);
|
|
66
|
+
return named.length ? `Funds: ${named.join(", ")}.` : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
export const cryptoEtfFlowsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
47
70
|
shape: "rows",
|
|
48
71
|
argument: { kind: "none" },
|
|
@@ -69,6 +92,22 @@ export const cryptoEtfFlowsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
69
92
|
minimum: 1,
|
|
70
93
|
maximum: 2000,
|
|
71
94
|
},
|
|
95
|
+
{
|
|
96
|
+
key: "from",
|
|
97
|
+
type: "string",
|
|
98
|
+
placeholder: "year|date",
|
|
99
|
+
description: "First US trading day listed, as a year (2024) or a date (2026-10-19); lists every day of the window in place of --limit.",
|
|
100
|
+
example: "--from 2026-10-19 --to 2026-10-23",
|
|
101
|
+
shot: { reportOnly: "lists the report's days for those dates" },
|
|
102
|
+
replaces: ["limit"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "to",
|
|
106
|
+
type: "string",
|
|
107
|
+
placeholder: "year|date",
|
|
108
|
+
description: "Last US trading day listed, with --from (default the newest).",
|
|
109
|
+
shot: { reportOnly: "lists the report's days for those dates" },
|
|
110
|
+
},
|
|
72
111
|
],
|
|
73
112
|
columns: [
|
|
74
113
|
{ key: "date", header: "Date" },
|
|
@@ -81,24 +120,36 @@ export const cryptoEtfFlowsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
81
120
|
limitations: [
|
|
82
121
|
"US spot bitcoin and ether ETFs only, net creations and redemptions in US$ millions as Farside Investors publishes them.",
|
|
83
122
|
"The newest day fills in fund by fund; a fund that has not reported shows as pending.",
|
|
123
|
+
"0.0 is the publisher's zero for the fund that day; a dash is a day it published no figure for.",
|
|
124
|
+
"Price % is the funds' price move that day: the median of their close-to-close changes.",
|
|
84
125
|
],
|
|
85
126
|
},
|
|
86
|
-
|
|
127
|
+
// Stale once a trading day's flows are missing (the load's `cadence`), except in a window that ended before the newest day.
|
|
128
|
+
freshness: { source: "Farside Investors", publisher: "Farside Investors", status: "not-a-feed", basis: "daily fund flows", tradingDayMarket: "US" },
|
|
87
129
|
async load(args, ctx) {
|
|
88
130
|
const asset = cryptoEtfFlowAsset(args.options.asset);
|
|
131
|
+
// --from 2026-10-19 --to 2026-10-23 lists those days, every one of them, in place of the newest --limit.
|
|
132
|
+
const window = readPriceWindow(optionText(args.options.from), optionText(args.options.to));
|
|
89
133
|
const payload = await fetchCryptoEtfFlows(asset, ctx.apiClient);
|
|
90
134
|
const limit = Math.max(1, Number(args.options.limit) || 20);
|
|
91
|
-
const
|
|
135
|
+
const days = window ? payload.days.filter((day) => day.date >= window.from && day.date <= window.to) : payload.days.slice(0, limit);
|
|
136
|
+
const priced = hasFundPriceChanges(payload);
|
|
137
|
+
const rows = days.map((day) => ({
|
|
92
138
|
date: day.date,
|
|
93
139
|
total: day.total,
|
|
140
|
+
...(priced ? { priceChangePct: day.priceChangePct ?? null } : {}),
|
|
94
141
|
...Object.fromEntries(payload.funds.map((fund) => [fund.ticker, day.flows[fund.ticker] ?? null])),
|
|
95
142
|
pending: day.pending,
|
|
96
143
|
}));
|
|
97
144
|
const summary = cryptoEtfFlowSummary(payload);
|
|
145
|
+
const funds = fundsLine(payload);
|
|
146
|
+
// A window that ends before the newest day is dated by its own last day.
|
|
147
|
+
const asOf = window ? days[0]?.date ?? null : payload.asOf;
|
|
98
148
|
return {
|
|
99
149
|
rows,
|
|
100
150
|
columns: flowColumns(payload),
|
|
101
|
-
|
|
151
|
+
...(window && !days.length ? { errors: [`No US trading day with flows from ${window.from} to ${window.to}.`] } : {}),
|
|
152
|
+
notes: [window ? windowSummaryLine(payload, window, days) : summaryLine(payload), ...funds ? [funds] : [], `Source: ${payload.source.publisher}, ${payload.source.url}`],
|
|
102
153
|
complete: !payload.stale,
|
|
103
154
|
...(payload.stale ? { warnings: [`The flows were last read ${payload.fetchedAt.slice(0, 10)}; newer days may be missing.`] } : {}),
|
|
104
155
|
freshness: {
|
|
@@ -106,10 +157,12 @@ export const cryptoEtfFlowsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
106
157
|
publisher: payload.source.publisher,
|
|
107
158
|
status: "not-a-feed",
|
|
108
159
|
basis: "daily fund flows",
|
|
109
|
-
cadence: "daily",
|
|
160
|
+
...(window && asOf !== payload.asOf ? {} : { cadence: "daily" as const }),
|
|
110
161
|
tradingDayMarket: "US",
|
|
111
|
-
asOf:
|
|
162
|
+
...(asOf ? { asOf } : {}),
|
|
112
163
|
oldest: null,
|
|
164
|
+
// When the server last read the publisher's table, which is what this report retrieved.
|
|
165
|
+
retrievedAt: payload.fetchedAt,
|
|
113
166
|
},
|
|
114
167
|
metadata: {
|
|
115
168
|
asset,
|
|
@@ -117,6 +170,8 @@ export const cryptoEtfFlowsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
117
170
|
asOf: payload.asOf,
|
|
118
171
|
fetchedAt: payload.fetchedAt,
|
|
119
172
|
stale: payload.stale,
|
|
173
|
+
...(window ? { requestedWindow: window } : {}),
|
|
174
|
+
...(priced ? { priceChangeBasis: "median of the funds' close-to-close price changes, percent" } : {}),
|
|
120
175
|
source: payload.source,
|
|
121
176
|
funds: payload.funds,
|
|
122
177
|
summary: {
|
|
@@ -2,7 +2,6 @@ import type { PluginModule } from "../plugin-module";
|
|
|
2
2
|
import { cryptoEtfFlowsCache } from "./client";
|
|
3
3
|
import { cryptoEtfFlowsHeadless } from "./headless";
|
|
4
4
|
import { CRYPTO_ETF_FLOWS_PANE_ID } from "./model";
|
|
5
|
-
import { CryptoEtfFlowsPane } from "./pane";
|
|
6
5
|
|
|
7
6
|
/** ETFF: the daily net flows of the US spot bitcoin and ether ETFs, per fund. */
|
|
8
7
|
export const cryptoEtfFlowsModule: PluginModule = {
|
|
@@ -11,7 +10,7 @@ export const cryptoEtfFlowsModule: PluginModule = {
|
|
|
11
10
|
id: CRYPTO_ETF_FLOWS_PANE_ID,
|
|
12
11
|
name: "Crypto ETF Flows",
|
|
13
12
|
icon: "F",
|
|
14
|
-
component: CryptoEtfFlowsPane,
|
|
13
|
+
get component(): typeof import("./pane").CryptoEtfFlowsPane { const { CryptoEtfFlowsPane } = require("./pane"); return CryptoEtfFlowsPane; },
|
|
15
14
|
defaultPosition: "right",
|
|
16
15
|
defaultMode: "floating",
|
|
17
16
|
defaultFloatingSize: { width: 120, height: 32 },
|
|
@@ -103,11 +103,24 @@ export function pendingText(day: CryptoEtfFlowDay | null | undefined): string |
|
|
|
103
103
|
|
|
104
104
|
export type CryptoEtfFlowColumn = Omit<DataTableColumn, "id"> & { id: string };
|
|
105
105
|
|
|
106
|
-
/**
|
|
106
|
+
/** Whether the server sent the funds' daily price move beside the flows. */
|
|
107
|
+
export const hasFundPriceChanges = (payload: CryptoEtfFlowsPayload | null | undefined) =>
|
|
108
|
+
!!payload?.days.some((day) => day.priceChangePct !== undefined);
|
|
109
|
+
|
|
110
|
+
/** A day's price move as the table shows it: `+2.15%`, `-0.40%`, `—` for none. */
|
|
111
|
+
export function formatPriceChange(value: number | null | undefined): string {
|
|
112
|
+
if (value == null || !Number.isFinite(value)) return "—";
|
|
113
|
+
return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Date, the day's total and the funds' price move that day, then each fund in the publisher's order. */
|
|
107
117
|
export function cryptoEtfFlowColumns(payload: CryptoEtfFlowsPayload | null | undefined): CryptoEtfFlowColumn[] {
|
|
108
118
|
return [
|
|
109
119
|
{ id: "date", label: "Date", width: 12, align: "left" },
|
|
110
120
|
{ id: "total", label: "Total", width: 9, align: "right", title: "The day's net flow across the funds, US$ millions" },
|
|
121
|
+
...hasFundPriceChanges(payload)
|
|
122
|
+
? [{ id: "price", label: "Price %", width: 9, align: "right" as const, title: "The funds' price move that day: the median of their close-to-close changes" }]
|
|
123
|
+
: [],
|
|
111
124
|
...(payload?.funds ?? []).map((fund): CryptoEtfFlowColumn => ({
|
|
112
125
|
id: `fund:${fund.ticker}`,
|
|
113
126
|
label: fund.ticker,
|