gloomberb 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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { PricePoint, Quote } from "../types/financials";
|
|
2
|
+
import { latestRegularSessionClose, regularSessionCloseOn, sessionCalendarTimeZone } from "../market-data/market/freshness";
|
|
3
|
+
import { quoteTradingDay } from "../market-data/quotes/day-range";
|
|
4
|
+
import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
|
|
5
|
+
import { canonicalExchange, isUsListingExchange } from "../utils/exchanges";
|
|
6
|
+
import { zonedDateKey } from "../utils/zoned-date-time";
|
|
7
|
+
|
|
8
|
+
/** What the footer of an intraday total says while a session has closed but its closing print is not in yet. */
|
|
9
|
+
export const CLOSING_PRINT_PENDING_TEXT = "closing print not in yet";
|
|
10
|
+
|
|
11
|
+
/** A close this far from the session's last trade is another listing's or another unit's, never its auction print. */
|
|
12
|
+
const MAX_CLOSE_MOVE = 0.25;
|
|
13
|
+
|
|
14
|
+
function barTime(point: Pick<PricePoint, "date">): number {
|
|
15
|
+
return new Date(point.date as Date | string | number).getTime();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const positive = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
19
|
+
|
|
20
|
+
/** Whether `close` can be the close of a session whose last trade was `lastTrade`. */
|
|
21
|
+
function nearLastTrade(close: number, lastTrade: number | null): boolean {
|
|
22
|
+
return lastTrade === null || !positive(lastTrade) || Math.abs(close / lastTrade - 1) <= MAX_CLOSE_MOVE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The newest regular session that has closed, when the bars hold it, and its last trade there. */
|
|
26
|
+
export interface ClosedSession {
|
|
27
|
+
/** The session's local date. */
|
|
28
|
+
date: string;
|
|
29
|
+
/** The close of its last regular bar: the last trade before the closing auction. */
|
|
30
|
+
lastTrade: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** When each local date's session closed, read once per date. */
|
|
34
|
+
function sessionCloses(venue: string | undefined) {
|
|
35
|
+
const closes = new Map<string, number | null>();
|
|
36
|
+
return (date: string) => {
|
|
37
|
+
if (!closes.has(date)) closes.set(date, regularSessionCloseOn(venue, date));
|
|
38
|
+
return closes.get(date)!;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Each closed session's last regular bar, by the session's local date (a later bar is after-hours). */
|
|
43
|
+
function lastRegularBars(points: readonly PricePoint[], venue: string | undefined, now: number): Map<string, number> {
|
|
44
|
+
const zone = sessionCalendarTimeZone(venue);
|
|
45
|
+
const latest = latestRegularSessionClose(venue, now);
|
|
46
|
+
const last = new Map<string, number>();
|
|
47
|
+
if (!zone || !latest) return last;
|
|
48
|
+
const closeOn = sessionCloses(venue);
|
|
49
|
+
points.forEach((point, index) => {
|
|
50
|
+
const time = barTime(point);
|
|
51
|
+
if (!Number.isFinite(time)) return;
|
|
52
|
+
const date = zonedDateKey(time, zone);
|
|
53
|
+
if (date > latest.date) return;
|
|
54
|
+
const close = closeOn(date);
|
|
55
|
+
if (close === null || time >= close) return;
|
|
56
|
+
last.set(date, index);
|
|
57
|
+
});
|
|
58
|
+
return last;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The newest regular session that has closed by `now` (the venue's calendar),
|
|
63
|
+
* when the bars hold a regular bar of it: the session whose official close a
|
|
64
|
+
* quote is read for. Null while the venue trades past its last session's
|
|
65
|
+
* bars, for venues without sessions, and when the bars end earlier.
|
|
66
|
+
*/
|
|
67
|
+
export function newestClosedSession(points: readonly PricePoint[], options: { venue: string | undefined; now: number }): ClosedSession | null {
|
|
68
|
+
const latest = latestRegularSessionClose(options.venue, options.now);
|
|
69
|
+
if (!latest) return null;
|
|
70
|
+
const index = lastRegularBars(points, options.venue, options.now).get(latest.date);
|
|
71
|
+
return index === undefined ? null : { date: latest.date, lastTrade: points[index]!.close };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What the newest closed session's last bar ends on: a close, the note that its print is to come, or nothing to change. */
|
|
75
|
+
export type NewestSessionClose = { close: number } | { pending: true } | null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The close the newest closed session ends on, for its last regular bar
|
|
79
|
+
* (`session`, from newestClosedSession). In order:
|
|
80
|
+
*
|
|
81
|
+
* 1. The quote's official close of that session (`regularClose`, dated by
|
|
82
|
+
* `regularCloseSessionDate` or the quote's trading day).
|
|
83
|
+
* 2. Right after a US close (POST) without it, the note that the print is to
|
|
84
|
+
* come: the quote's price then is the last trade before the auction.
|
|
85
|
+
* 3. Otherwise (the venue closed, the next pre-market, a listing that
|
|
86
|
+
* publishes no closing print) the session's finished daily bar, then the
|
|
87
|
+
* quote's own close: its price once closed, its previous close in the
|
|
88
|
+
* next pre-market.
|
|
89
|
+
*
|
|
90
|
+
* Every figure from the quote must be the bars' listing (same venue, same
|
|
91
|
+
* currency where both are known), in the bars' units, and within 25% of the
|
|
92
|
+
* session's last trade; one that is not is not used. A quote of the session
|
|
93
|
+
* still trading, or none, leaves the daily bar's close or nothing.
|
|
94
|
+
*/
|
|
95
|
+
export function newestSessionClose(
|
|
96
|
+
quote: Quote | null | undefined,
|
|
97
|
+
session: ClosedSession & {
|
|
98
|
+
venue: string | undefined;
|
|
99
|
+
/** The listing's currency, where known. */
|
|
100
|
+
currency?: string | null;
|
|
101
|
+
/** The session's finished daily bar's close, where one was read. */
|
|
102
|
+
dailyClose?: number | null;
|
|
103
|
+
},
|
|
104
|
+
): NewestSessionClose {
|
|
105
|
+
const daily = positive(session.dailyClose) && nearLastTrade(session.dailyClose, session.lastTrade) ? { close: session.dailyClose } : null;
|
|
106
|
+
if (!quote || quote.marketState === "REGULAR") return daily;
|
|
107
|
+
const quoteVenue = quote.listingExchangeName || quote.exchangeName;
|
|
108
|
+
if (quoteVenue && session.venue && canonicalExchange(quoteVenue) !== canonicalExchange(session.venue)) return daily;
|
|
109
|
+
if (session.currency && quote.currency && session.currency !== quote.currency) return daily;
|
|
110
|
+
const usable = (value: number | undefined): value is number => positive(value)
|
|
111
|
+
&& !hasLikelyQuoteUnitMismatch({ currency: quote.currency, price: session.lastTrade }, { currency: quote.currency, price: value })
|
|
112
|
+
&& nearLastTrade(value, session.lastTrade);
|
|
113
|
+
const official = positive(quote.regularClose) && (quote.regularCloseSessionDate ?? quoteTradingDay(quote)) === session.date
|
|
114
|
+
? quote.regularClose : undefined;
|
|
115
|
+
if (official !== undefined) return usable(official) ? { close: official } : daily;
|
|
116
|
+
if (quote.marketState === "POST" && isUsListingExchange(canonicalExchange(session.venue))) return { pending: true };
|
|
117
|
+
if (daily) return daily;
|
|
118
|
+
if (quote.marketState === "PRE" || quote.marketState === "PREPRE") {
|
|
119
|
+
const previous = quote.previousClose;
|
|
120
|
+
return (!quote.previousCloseDate || quote.previousCloseDate === session.date) && usable(previous) ? { close: previous } : null;
|
|
121
|
+
}
|
|
122
|
+
return quoteTradingDay(quote) === session.date && usable(quote.price) ? { close: quote.price } : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Intraday bars of a listing that trades in sessions, each closed session's
|
|
127
|
+
* last regular bar closing on that session's official close (`closes`, by
|
|
128
|
+
* the venue's local date) instead of the last trade before the closing
|
|
129
|
+
* auction: an hourly 19:30 UTC bar of a thin ETF ends on a 19:38 trade at
|
|
130
|
+
* 12.31 while the 20:00 closing print is 12.19, which every total measured
|
|
131
|
+
* from the bars then misses. The bar's high and low widen to take the close.
|
|
132
|
+
* A close more than 25% off the bar's own is another unit's, and not taken.
|
|
133
|
+
*
|
|
134
|
+
* A session still trading, bars after its close (after-hours) and venues
|
|
135
|
+
* without sessions are left as served; the points array is the same one when
|
|
136
|
+
* nothing changed.
|
|
137
|
+
*/
|
|
138
|
+
export function withOfficialCloses(
|
|
139
|
+
points: PricePoint[],
|
|
140
|
+
closes: ReadonlyMap<string, number>,
|
|
141
|
+
options: { venue: string | undefined; now: number },
|
|
142
|
+
): PricePoint[] {
|
|
143
|
+
if (points.length === 0 || closes.size === 0) return points;
|
|
144
|
+
let changed = false;
|
|
145
|
+
const next = points.slice();
|
|
146
|
+
for (const [date, index] of lastRegularBars(points, options.venue, options.now)) {
|
|
147
|
+
const close = closes.get(date);
|
|
148
|
+
const bar = next[index]!;
|
|
149
|
+
if (!positive(close) || bar.close === close || !nearLastTrade(close, bar.close)) continue;
|
|
150
|
+
next[index] = {
|
|
151
|
+
...bar,
|
|
152
|
+
close,
|
|
153
|
+
...(positive(bar.high) ? { high: Math.max(bar.high, close) } : {}),
|
|
154
|
+
...(positive(bar.low) ? { low: Math.min(bar.low, close) } : {}),
|
|
155
|
+
};
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
return changed ? next : points;
|
|
159
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { scalarPointValue } from "./alignment";
|
|
2
2
|
import { getTimeSeriesField } from "./field-catalog";
|
|
3
3
|
import type { ChartSpec, ResolvedSeries, TimeSeriesPoint } from "./types";
|
|
4
|
-
import { chartBarSizeSource, type ManualChartResolution } from "./resolution";
|
|
4
|
+
import { CHART_RESOLUTION_STEP_MS, chartBarSizeSource, getChartResolutionLabel, isIntradayResolution, type ManualChartResolution } from "./resolution";
|
|
5
5
|
import type { TimeRange } from "./range";
|
|
6
6
|
import { zonedDateTimeParts } from "../utils/zoned-date-time";
|
|
7
|
+
import { priceComparisonCloseTimes } from "./coin-session-close";
|
|
7
8
|
import { barLastSession } from "../market-data/market/bar-period";
|
|
8
9
|
import {
|
|
9
10
|
barCloseDay, isIntradayDailyBar, isReturnPeriod, LATEST_INTRADAY_TEXT, periodBaseDay, periodBaseIndex, periodDayText,
|
|
@@ -52,6 +53,10 @@ export interface PriceComparisonOptions {
|
|
|
52
53
|
*/
|
|
53
54
|
preset?: TimeRange | null;
|
|
54
55
|
now?: number;
|
|
56
|
+
/** Coin legs whose daily closes were taken at the session venue's close (coinLegsAtSessionClose). */
|
|
57
|
+
atSessionClose?: ReadonlySet<string>;
|
|
58
|
+
/** Of those, the legs whose newest day ends at a session close that has passed: not a price still moving. */
|
|
59
|
+
closedEnds?: ReadonlySet<string>;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export function observationDate(time: number, series: Pick<ResolvedSeries, "timeBasis">): string {
|
|
@@ -156,6 +161,28 @@ export function totalReturnLevelsNotice(spec: ChartSpec, series: readonly Resolv
|
|
|
156
161
|
priceOnly.length ? ` Price only for ${priceOnly.join(", ")}: distribution history unavailable.` : ""}`;
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
/**
|
|
165
|
+
* The bar size intraday legs are matched in: the chart's, or when it has none
|
|
166
|
+
* the coarsest the legs were served in. Null when neither is intraday.
|
|
167
|
+
*/
|
|
168
|
+
function intradayBucketMs(resolution: ManualChartResolution | "auto", series: ReadonlyArray<ResolvedSeries | undefined>): number | null {
|
|
169
|
+
const sizes = [resolution, ...series.map((entry) => entry?.historyResolution)]
|
|
170
|
+
.filter((size): size is ManualChartResolution => !!size && size !== "auto" && size in CHART_RESOLUTION_STEP_MS && isIntradayResolution(size as ManualChartResolution));
|
|
171
|
+
if (resolution !== "auto" && sizes[0] === resolution) return CHART_RESOLUTION_STEP_MS[resolution];
|
|
172
|
+
return sizes.length ? Math.max(...sizes.map((size) => CHART_RESOLUTION_STEP_MS[size])) : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** `1H` for an hour's bucket, as the chart names its bar size. */
|
|
176
|
+
function barLabel(bucketMs: number): string {
|
|
177
|
+
const size = (Object.keys(CHART_RESOLUTION_STEP_MS) as ManualChartResolution[]).find((key) => CHART_RESOLUTION_STEP_MS[key] === bucketMs);
|
|
178
|
+
return size ? getChartResolutionLabel(size) : `${Math.round(bucketMs / 60_000)}m`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** `IBIT and BTC-USD`, `A, B and C`. */
|
|
182
|
+
function listLabels(labels: readonly string[]): string {
|
|
183
|
+
return labels.length <= 1 ? labels.join("") : `${labels.slice(0, -1).join(", ")} and ${labels.at(-1)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
159
186
|
interface ComparedLeg {
|
|
160
187
|
id: string;
|
|
161
188
|
label: string;
|
|
@@ -203,6 +230,9 @@ export function resolvePriceComparison(
|
|
|
203
230
|
const dropped: NonNullable<PriceComparison["dropped"]> = [];
|
|
204
231
|
const reported = new Set<string>();
|
|
205
232
|
const legs: ComparedLeg[] = [];
|
|
233
|
+
// Intraday bars compare by the bar of the chart's size they open in: a fund's 13:30 bar and a coin's 13:00 bar are
|
|
234
|
+
// one hourly bar, so the fund's last bar of the day (19:30, closing at 20:00) meets the coin's bar closing then.
|
|
235
|
+
const bucketMs = calendarBars ? null : intradayBucketMs(resolution, seriesIds.map((id) => byId.get(id)!));
|
|
206
236
|
for (const id of seriesIds) {
|
|
207
237
|
const entry = byId.get(id)!;
|
|
208
238
|
const inWindow = priceObservationWindowFilter(entry, bounds, resolution, dateWindow);
|
|
@@ -211,7 +241,7 @@ export function resolvePriceComparison(
|
|
|
211
241
|
for (const point of entry.points) {
|
|
212
242
|
const time = point.date.getTime();
|
|
213
243
|
if (!Number.isFinite(time) || scalarPointValue(point) === null) continue;
|
|
214
|
-
const key = calendarBars ? observationDate(time, entry) : String(time);
|
|
244
|
+
const key = calendarBars ? observationDate(time, entry) : String(bucketMs ? Math.floor(time / bucketMs) * bucketMs : time);
|
|
215
245
|
points.set(key, point);
|
|
216
246
|
if (inWindow(time)) window.add(key);
|
|
217
247
|
}
|
|
@@ -237,7 +267,16 @@ export function resolvePriceComparison(
|
|
|
237
267
|
return legs.length ? keys(legs[0]!).filter((key) => sets.every((set) => set.has(key))) : [];
|
|
238
268
|
};
|
|
239
269
|
const sharedWindow = shared((leg) => leg.window);
|
|
240
|
-
|
|
270
|
+
// Authored dates (`--from 2026-10-19`) measure from the close before the first date, as RETURN does: the last
|
|
271
|
+
// date the legs share before it. Weekly and monthly bars keep the bar the window starts in.
|
|
272
|
+
const authoredFrom = !preset && dateWindow && /^\d{4}-\d{2}-\d{2}$/.test(dateWindow.start) && (resolution === "1d" || !calendarBars)
|
|
273
|
+
? dateWindow.start : null;
|
|
274
|
+
const firstInWindow = authoredFrom ? legs.map((leg) => leg.window[0]!).sort(order)[0] : undefined;
|
|
275
|
+
const authoredBase = authoredFrom && firstInWindow !== undefined
|
|
276
|
+
? shared((leg) => leg.keys).filter((key) => (calendarBars ? key < authoredFrom : order(key, firstInWindow) < 0)
|
|
277
|
+
&& legs.every((leg) => nonzero(leg, key))).at(-1)
|
|
278
|
+
: undefined;
|
|
279
|
+
const sharedStart = authoredBase ?? sharedWindow.find((key) => legs.every((leg) => nonzero(leg, key)));
|
|
241
280
|
// The period is measured back from the chart's newest observation, by its session day.
|
|
242
281
|
const anchor = period && legs.length
|
|
243
282
|
? legs.map((leg) => calendarBars ? sessionDay(leg.window.at(-1)!) : dayOf(leg, leg.window.at(-1)!)).sort().at(-1)!
|
|
@@ -274,7 +313,14 @@ export function resolvePriceComparison(
|
|
|
274
313
|
const droppedText = dropped.length
|
|
275
314
|
? ` Left out: ${dropped.map((entry) => reported.has(entry.seriesId) ? entry.label : `${entry.label} (${entry.reason})`).join("; ")}.`
|
|
276
315
|
: "";
|
|
277
|
-
if (!compared.length)
|
|
316
|
+
if (!compared.length) {
|
|
317
|
+
// Intraday legs that never trade in the same bar: said, with the bar size that compares them.
|
|
318
|
+
const apart = !calendarBars && legs.length >= 2 && sharedWindow.length === 0 && legs.every((leg) => leg.window.length >= 2);
|
|
319
|
+
const notice = apart
|
|
320
|
+
? `${basis} Comparison unavailable: ${listLabels(legs.map((leg) => leg.label))} share no ${bucketMs ? `${barLabel(bucketMs)} ` : ""}bar times in this range; daily bars (1D) compare them.`
|
|
321
|
+
: unavailable;
|
|
322
|
+
return { seriesIds, start: null, end: null, notice, ...(dropped.length ? { dropped } : {}) };
|
|
323
|
+
}
|
|
278
324
|
const sourceBounds = Object.fromEntries(compared.map(({ leg, start, end }) => [leg.id, {
|
|
279
325
|
start: leg.points.get(start)!.date.getTime(),
|
|
280
326
|
end: leg.points.get(end)!.date.getTime(),
|
|
@@ -284,7 +330,7 @@ export function resolvePriceComparison(
|
|
|
284
330
|
const endDay = calendarBars ? sessionDay(end) : dayOf(leg, end);
|
|
285
331
|
// A daily bar of the session still trading, or a week or month running through it.
|
|
286
332
|
return [leg.id, { baseDay: calendarBars ? sessionDay(start) : dayOf(leg, start), endDay,
|
|
287
|
-
intraday: calendarBars && isIntradayDailyBar(endDay, legVenue(leg.entry), now) }];
|
|
333
|
+
intraday: calendarBars && !options.closedEnds?.has(leg.id) && isIntradayDailyBar(endDay, legVenue(leg.entry), now) }];
|
|
288
334
|
}));
|
|
289
335
|
const span = calendarBars
|
|
290
336
|
? (() => {
|
|
@@ -292,7 +338,10 @@ export function resolvePriceComparison(
|
|
|
292
338
|
const to = legsText(compared.map(({ leg }) => ({ label: leg.label, text: legDays[leg.id]!.intraday
|
|
293
339
|
? `${periodDayText(legDays[leg.id]!.endDay, currentYear)}, ${LATEST_INTRADAY_TEXT}`
|
|
294
340
|
: `${periodDayText(legDays[leg.id]!.endDay, currentYear)} close` })));
|
|
295
|
-
|
|
341
|
+
// A coin beside a listing that closes each day says when it is measured: at that close, or at 00:00 UTC.
|
|
342
|
+
const timing = priceComparisonCloseTimes({ legs: legDays }, compared.map(({ leg }) => leg.entry),
|
|
343
|
+
new Map(compared.map(({ leg }) => [leg.id, leg.label])), options.atSessionClose);
|
|
344
|
+
return `From ${from} to ${to}; ${timing ?? "market session times may differ"}.`;
|
|
296
345
|
})()
|
|
297
346
|
: baseDay && sharedBase !== undefined
|
|
298
347
|
? `From ${periodDayText(legDays[compared[0]!.leg.id]!.baseDay, currentYear)} close to ${displayDate(Math.max(...Object.values(sourceBounds).map((range) => range.end)))}.`
|
|
@@ -43,7 +43,7 @@ import { FORWARD_PE_BASIS_NOTICE, REALIZED_NTM_PE_BASIS_NOTICE } from "./forward
|
|
|
43
43
|
import { valuationPriceWarning } from "./valuation-price";
|
|
44
44
|
import { VALUATION_CURRENCY_WARNING_PREFIX } from "./valuation-currency";
|
|
45
45
|
import { chartPriceHistoryIntegrityNotices } from "./market";
|
|
46
|
-
import { maxStudyWarmupPoints, resolveStudies } from "./studies";
|
|
46
|
+
import { maxStudyWarmupPoints, resolveStudies } from "./studies/resolve";
|
|
47
47
|
import { clipSeriesToWindow } from "./alignment";
|
|
48
48
|
import { totalReturnSeries } from "./total-return";
|
|
49
49
|
import {
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
totalReturnLevelsNotice,
|
|
56
56
|
} from "./price-comparison";
|
|
57
57
|
import { chartQuoteOverrideKeyForSource } from "./live-quotes";
|
|
58
|
+
import { coinLegsAtSessionClose } from "./coin-session-close";
|
|
58
59
|
import { chartSeriesSourceKey } from "../capabilities/chart-series";
|
|
59
60
|
import { rememberSeedListingFacts } from "./parsed-history-cache";
|
|
60
61
|
import {
|
|
@@ -1057,8 +1058,16 @@ async function resolveChartSpecPass(
|
|
|
1057
1058
|
const commonCadence = servedCadences.size === 1 ? [...servedCadences][0] : undefined;
|
|
1058
1059
|
const resolution = commonCadence ?? undefined;
|
|
1059
1060
|
const comparisonResolution = resolution ?? "auto";
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1061
|
+
const comparisonOptions = { preset: hasExplicitWindow ? null : spec.viewport.range, now: referenceNow.getTime() + Math.max(0, Date.now() - resolutionStartedAt) };
|
|
1062
|
+
let priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow, comparisonOptions);
|
|
1063
|
+
// A coin compared in daily closes with a listing that closes each day is measured at that close when its hourly bars reach.
|
|
1064
|
+
const timed = sources.dataProvider && comparisonResolution === "1d"
|
|
1065
|
+
? await coinLegsAtSessionClose(priceComparison, rawSeries, sources.dataProvider, comparisonOptions.now) : null;
|
|
1066
|
+
if (timed?.timing.atSessionClose.size) {
|
|
1067
|
+
rawSeries.splice(0, rawSeries.length, ...timed.series);
|
|
1068
|
+
priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow,
|
|
1069
|
+
{ ...comparisonOptions, atSessionClose: timed.timing.atSessionClose, closedEnds: timed.timing.closedEnds });
|
|
1070
|
+
}
|
|
1062
1071
|
const displayBounds = comparisonDisplayBounds(bounds, priceComparison);
|
|
1063
1072
|
const comparisonBounds = priceComparison && priceComparison.start !== null
|
|
1064
1073
|
? priceComparison : bounds;
|
|
@@ -2,7 +2,7 @@ import { limitSeriesObservations } from "./financial-period-coverage";
|
|
|
2
2
|
import type { ManualChartResolution } from "./resolution";
|
|
3
3
|
import { getTimeSeriesField } from "./field-catalog";
|
|
4
4
|
import { collectPriceHistoryIntegrity } from "./market";
|
|
5
|
-
import { activeStudyInputSeriesIds } from "./studies";
|
|
5
|
+
import { activeStudyInputSeriesIds } from "./studies/resolve";
|
|
6
6
|
import { applyResolvedSeriesTransform } from "./transforms";
|
|
7
7
|
import { clipSeriesToWindow } from "./alignment";
|
|
8
8
|
import type { StudyAnchor } from "./leverage";
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { alignTimeSeries } from "../alignment";
|
|
2
|
+
import type { ChartStudySpec, ResolvedSeries, TimeSeriesPoint } from "../types";
|
|
3
|
+
import { isFiniteNumber } from "../../utils/guards";
|
|
4
|
+
import { GALLONS_PER_BARREL, isBarrelsPerTonne, perBarrelUnit, perTonneUnit } from "../price-units";
|
|
5
|
+
import { derivedPoint, outputSeries } from "./shared";
|
|
6
|
+
import {
|
|
7
|
+
combinedUnit,
|
|
8
|
+
derivedValueDecimals,
|
|
9
|
+
fxConversionIssue,
|
|
10
|
+
isPriceGroup,
|
|
11
|
+
knownUnitSignature,
|
|
12
|
+
legName,
|
|
13
|
+
unitMismatchMessage,
|
|
14
|
+
type SeriesUnit,
|
|
15
|
+
} from "./units";
|
|
16
|
+
|
|
17
|
+
interface FormulaLeg {
|
|
18
|
+
/** The input's place in the study's inputs. */
|
|
19
|
+
index: number;
|
|
20
|
+
/** Its multiplier, its sign included. */
|
|
21
|
+
weight: number;
|
|
22
|
+
/** The multiplier carries the 42 gallons that make a per-gallon price per barrel. */
|
|
23
|
+
perBarrel: boolean;
|
|
24
|
+
/** The barrels in a tonne the multiplier carries (7.45 for gasoil), making a per-barrel price per tonne. */
|
|
25
|
+
perTonne: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FormulaTerms {
|
|
29
|
+
/** The inputs the formula adds up. */
|
|
30
|
+
legs: FormulaLeg[];
|
|
31
|
+
/** The inputs the whole sum is multiplied (1) or divided (-1) by: `/EURUSD=X`. */
|
|
32
|
+
factors: Array<{ index: number; power: 1 | -1 }>;
|
|
33
|
+
divisor: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A formula study's terms: `weight1`, `perBarrel1`, `perTonne1` ... per
|
|
38
|
+
* input it adds up, `power1` ... (1 or -1) per input it multiplies or divides
|
|
39
|
+
* the sum by, and an optional `divisor`. A saved or hand-edited spec can carry
|
|
40
|
+
* a multiplier or divisor no formula was written with (a value that is not a
|
|
41
|
+
* number is dropped when the spec loads, so it arrives missing); that is an
|
|
42
|
+
* error, not a silent 1.
|
|
43
|
+
*/
|
|
44
|
+
export function formulaTerms(spec: ChartStudySpec, inputs: readonly ResolvedSeries[]): FormulaTerms | { error: string } {
|
|
45
|
+
const legs: FormulaLeg[] = [];
|
|
46
|
+
const factors: FormulaTerms["factors"] = [];
|
|
47
|
+
for (const [index, input] of inputs.entries()) {
|
|
48
|
+
const power = spec.parameters[`power${index + 1}`];
|
|
49
|
+
if (power !== undefined) {
|
|
50
|
+
if (power !== 1 && power !== -1) return { error: `The formula neither multiplies nor divides by ${legName(input)}; write the formula again.` };
|
|
51
|
+
factors.push({ index, power });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const weight = spec.parameters[`weight${index + 1}`];
|
|
55
|
+
if (!isFiniteNumber(weight)) return { error: `The multiplier of ${legName(input)} is missing or not a number; write the formula again.` };
|
|
56
|
+
if (weight === 0) return { error: `${legName(input)} has a multiplier of 0, which removes it; leave it out of the formula instead.` };
|
|
57
|
+
const perTonne = spec.parameters[`perTonne${index + 1}`];
|
|
58
|
+
legs.push({
|
|
59
|
+
index,
|
|
60
|
+
weight,
|
|
61
|
+
perBarrel: spec.parameters[`perBarrel${index + 1}`] === 1,
|
|
62
|
+
perTonne: isFiniteNumber(perTonne) && isBarrelsPerTonne(perTonne) ? perTonne : null,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (legs.length === 0) return { error: "The formula has no series to add up, only series it divides by; write the formula again." };
|
|
66
|
+
const divisor = spec.parameters.divisor ?? 1;
|
|
67
|
+
if (!isFiniteNumber(divisor)) return { error: "The formula's divisor is not a number; write the formula again." };
|
|
68
|
+
if (divisor === 0) return { error: "A formula cannot be divided by 0." };
|
|
69
|
+
return { legs, factors, divisor };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A leg's unit once its multiplier is applied: 42 gallons of a per-gallon
|
|
74
|
+
* price are a barrel's worth, and 7.45 barrels of gasoil a tonne's.
|
|
75
|
+
*/
|
|
76
|
+
function formulaLegUnit(leg: FormulaLeg, input: ResolvedSeries): string {
|
|
77
|
+
const unit = leg.perBarrel ? perBarrelUnit(input.unit) : input.unit;
|
|
78
|
+
return leg.perTonne ? perTonneUnit(unit) : unit;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Why a formula's legs cannot be summed, or null when every leg counts the same known unit. */
|
|
82
|
+
function formulaUnitMismatch(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): string | null {
|
|
83
|
+
if (terms.legs.length < 2) return null;
|
|
84
|
+
const legs = terms.legs.map((leg) => ({ series: inputs[leg.index]!, unit: formulaLegUnit(leg, inputs[leg.index]!) }));
|
|
85
|
+
const signatures = legs.map(({ series, unit }) => knownUnitSignature({ unit, unitGroup: series.unitGroup }));
|
|
86
|
+
const other = signatures.findIndex((signature, index) => index > 0 && (signature === null || signature !== signatures[0]));
|
|
87
|
+
if (signatures[0] !== null && other < 0) return null;
|
|
88
|
+
return unitMismatchMessage("formula", legs[0]!, legs[Math.max(1, other)]!);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatFormulaNumber(value: number): string {
|
|
92
|
+
return String(Number(value.toPrecision(10)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A formula as the legend names it, from the terms it was written with:
|
|
97
|
+
* `42×HO=F - CL=F`, `(2×42×RB=F + 42×HO=F - 3×CL=F)/3`, `(42×HO=F - BZ=F)/EURUSD=X`.
|
|
98
|
+
*/
|
|
99
|
+
function formulaLabel(inputs: readonly ResolvedSeries[], terms: FormulaTerms): string {
|
|
100
|
+
const sum = terms.legs.map((leg, position) => {
|
|
101
|
+
const magnitude = Math.abs(leg.weight);
|
|
102
|
+
// The conversions it carries are named as written: 7.45×42×HO=F.
|
|
103
|
+
const conversions = [...(leg.perTonne ? [leg.perTonne] : []), ...(leg.perBarrel ? [GALLONS_PER_BARREL] : [])];
|
|
104
|
+
const count = conversions.reduce((rest, factor) => rest / factor, magnitude);
|
|
105
|
+
const factors = [...(Math.abs(count - 1) < 1e-9 ? [] : [formatFormulaNumber(count)]), ...conversions.map(String)];
|
|
106
|
+
const term = [...factors, legName(inputs[leg.index]!)].join("×");
|
|
107
|
+
if (position === 0) return leg.weight < 0 ? `-${term}` : term;
|
|
108
|
+
return `${leg.weight < 0 ? "-" : "+"} ${term}`;
|
|
109
|
+
}).join(" ");
|
|
110
|
+
if (terms.divisor === 1 && terms.factors.length === 0) return sum;
|
|
111
|
+
return [
|
|
112
|
+
`(${sum})`,
|
|
113
|
+
...(terms.divisor === 1 ? [] : [`/${formatFormulaNumber(terms.divisor)}`]),
|
|
114
|
+
...terms.factors.map(({ index, power }) => `${power < 0 ? "/" : "×"}${legName(inputs[index]!)}`),
|
|
115
|
+
].join("");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Every input's value on each date they share, each carried forward to the others' dates. */
|
|
119
|
+
function alignedSamples(inputs: readonly ResolvedSeries[]): Array<{ point: TimeSeriesPoint; values: number[] }> {
|
|
120
|
+
return alignTimeSeries(inputs, { mode: "intersection", carryForward: true }).flatMap((row) => {
|
|
121
|
+
const aligned = inputs.map((input) => row.values[input.id]);
|
|
122
|
+
if (aligned.some((entry) => !isFiniteNumber(entry?.value))) return [];
|
|
123
|
+
const availability = Math.max(...aligned.map((entry) => entry!.point.availableAt?.getTime() ?? entry!.point.date.getTime()));
|
|
124
|
+
return [{
|
|
125
|
+
values: aligned.map((entry) => entry!.value as number),
|
|
126
|
+
point: {
|
|
127
|
+
date: new Date(row.date),
|
|
128
|
+
observedAt: new Date(row.date),
|
|
129
|
+
availableAt: Number.isFinite(availability) ? new Date(availability) : undefined,
|
|
130
|
+
value: null,
|
|
131
|
+
provenance: { quality: "derived" as const },
|
|
132
|
+
},
|
|
133
|
+
}];
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A weighted sum of the legs over a divisor, in the unit the legs share once
|
|
139
|
+
* converted, then times or over the series it is scaled by, in the unit
|
|
140
|
+
* that arithmetic leaves: a crack in USD / barrel over EURUSD=X is EUR / barrel.
|
|
141
|
+
*/
|
|
142
|
+
/**
|
|
143
|
+
* A formula's unit: its legs' once converted, then times or over each series
|
|
144
|
+
* it is scaled by. Legs that count different units, or an FX rate the wrong
|
|
145
|
+
* way up, are an error that says why: a crack over USDJPY=X is no price, and
|
|
146
|
+
* times it JPY / barrel.
|
|
147
|
+
*/
|
|
148
|
+
export function formulaUnit(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): SeriesUnit | { error: string } {
|
|
149
|
+
const mismatch = formulaUnitMismatch(terms, inputs);
|
|
150
|
+
if (mismatch) return { error: mismatch };
|
|
151
|
+
const first = inputs[terms.legs[0]!.index]!;
|
|
152
|
+
let unit: SeriesUnit = { unit: formulaLegUnit(terms.legs[0]!, first), unitGroup: first.unitGroup };
|
|
153
|
+
for (const factor of terms.factors) {
|
|
154
|
+
const series = inputs[factor.index]!;
|
|
155
|
+
const next = combinedUnit(unit, series, factor.power);
|
|
156
|
+
const issue = fxConversionIssue(unit, series, factor.power, next);
|
|
157
|
+
if (issue) return { error: issue };
|
|
158
|
+
unit = next;
|
|
159
|
+
}
|
|
160
|
+
return unit;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveFormula(
|
|
164
|
+
spec: ChartStudySpec,
|
|
165
|
+
inputs: readonly ResolvedSeries[],
|
|
166
|
+
terms: FormulaTerms,
|
|
167
|
+
unit: SeriesUnit,
|
|
168
|
+
color: string,
|
|
169
|
+
): ResolvedSeries[] {
|
|
170
|
+
const first = inputs[terms.legs[0]!.index]!;
|
|
171
|
+
// Rounded off the float's last digits: 42 × 4.6233 - 96.12 is 98.0586, not 98.05860000000001.
|
|
172
|
+
const points = alignedSamples(inputs).map(({ point, values }) => {
|
|
173
|
+
let value: number | null = terms.legs.reduce((sum, leg) => sum + values[leg.index]! * leg.weight, 0) / terms.divisor;
|
|
174
|
+
for (const factor of terms.factors) {
|
|
175
|
+
const scale = values[factor.index]!;
|
|
176
|
+
value = value === null || (factor.power < 0 && scale === 0) ? null : factor.power < 0 ? value / scale : value * scale;
|
|
177
|
+
}
|
|
178
|
+
return derivedPoint({ point, value: 0 }, value === null ? null : Number(value.toPrecision(12)));
|
|
179
|
+
});
|
|
180
|
+
const frequencies = new Set(inputs.map((input) => input.nativeFrequency));
|
|
181
|
+
const resolutions = new Set(inputs.map((input) => input.historyResolution));
|
|
182
|
+
const scaled = terms.factors.length > 0;
|
|
183
|
+
// A price per quantity reads with its unit beside it, as a future's does.
|
|
184
|
+
const statedUnit = scaled
|
|
185
|
+
? isPriceGroup(unit.unitGroup) && unit.unit.includes("/") ? unit.unit : null
|
|
186
|
+
: inputs.some((input) => input.statedUnit) ? unit.unit : null;
|
|
187
|
+
const valueDecimals = !scaled || isPriceGroup(unit.unitGroup) ? derivedValueDecimals(inputs, points) : undefined;
|
|
188
|
+
return [outputSeries(spec, first, {
|
|
189
|
+
label: formulaLabel(inputs, terms),
|
|
190
|
+
points,
|
|
191
|
+
color,
|
|
192
|
+
unit: unit.unit,
|
|
193
|
+
unitGroup: unit.unitGroup,
|
|
194
|
+
statedUnit,
|
|
195
|
+
// A weighted sum is no traded price: its decimals are its legs', not a contract's tick.
|
|
196
|
+
priceAssetCategory: null,
|
|
197
|
+
...(valueDecimals !== undefined ? { valueDecimals } : {}),
|
|
198
|
+
// As the pair formulas: as-of levels, constant until an input changes.
|
|
199
|
+
style: "step",
|
|
200
|
+
interpolation: "step-after",
|
|
201
|
+
axis: "left",
|
|
202
|
+
nativeFrequency: frequencies.size === 1 ? first.nativeFrequency : "auto",
|
|
203
|
+
historyResolution: resolutions.size === 1 ? first.historyResolution : null,
|
|
204
|
+
})];
|
|
205
|
+
}
|