@gloom-sh/terminal 0.19.1 → 0.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +58 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +101 -27
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +14 -10
- package/src/cli/commands/search.ts +84 -25
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +6 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/glossary.ts +2 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +189 -11
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +51 -5
- package/src/cli/listing-arg.ts +63 -10
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +49 -6
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +51 -6
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +10 -5
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +8 -2
- package/src/components/layout/pane/footer/model.ts +73 -6
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +4 -2
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +60 -26
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +16 -3
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +165 -90
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +324 -0
- package/src/data/sqlite/checkpointer.ts +285 -0
- package/src/data/sqlite/database.ts +80 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +5 -4
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +6 -4
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +19 -3
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +16 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +127 -29
- package/src/plugins/builtin/thirteenf/data.ts +165 -35
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
- package/src/plugins/builtin/thirteenf/types.ts +14 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +64 -42
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +5 -2
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +6 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/resolve.ts +2 -1
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +12 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +25 -9
- package/src/telemetry/stall-reports.ts +218 -13
- package/src/test-support/outage.ts +8 -0
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +84 -1
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +104 -8
- package/src/tickers/search/index.ts +5 -2
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +75 -8
- package/src/time-series/resolve.ts +32 -4
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/time-series/types.ts +7 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +40 -3
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +17 -1
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +122 -5
- package/src/utils/perf-marks.ts +58 -12
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/time-series/studies.ts +0 -1628
|
@@ -62,6 +62,8 @@ import { describeFundamentalMarketCap } from "../../../utils/market-capitalizati
|
|
|
62
62
|
import { liveFiftyTwoWeekRange, liveMarketCapitalization } from "../../../market-data/quotes/live-valuation";
|
|
63
63
|
import { instrumentTypeLabel } from "../../../tickers/instrument-kind";
|
|
64
64
|
import { ClippedText } from "../../../components/ui/display";
|
|
65
|
+
import { DelayedMark, delayedMarkWidth } from "../../../components/ui/delayed-mark";
|
|
66
|
+
import { useFreeQuoteDelay } from "../../../api-client/plan-access";
|
|
65
67
|
import { safeExternalUrl } from "../../../utils/external-url";
|
|
66
68
|
import { cashRunway, SHORT_RUNWAY_HISTORY } from "../../../utils/cash-runway";
|
|
67
69
|
import { useOfferingNote } from "./financials/offerings";
|
|
@@ -126,6 +128,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
126
128
|
const figureCells = useFigureCells();
|
|
127
129
|
|
|
128
130
|
const quote = financials?.quote;
|
|
131
|
+
const quoteDelayMinutes = useFreeQuoteDelay(quote);
|
|
129
132
|
const fundamentals = financials?.fundamentals;
|
|
130
133
|
const capitalization = liveMarketCapitalization(quote, fundamentals);
|
|
131
134
|
const profile = financials?.profile;
|
|
@@ -195,8 +198,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
195
198
|
id: `${ticker.metadata.ticker}:price`,
|
|
196
199
|
label: `${ticker.metadata.ticker} Price`,
|
|
197
200
|
color: priceColor(chartDelta),
|
|
198
|
-
|
|
199
|
-
unit: unknownHistoryBasis ? "unknown" : priceSeriesUnit(quoteCurrency, instrumentType),
|
|
201
|
+
unit: unknownHistoryBasis ? "unknown" : priceSeriesUnit(quoteCurrency, instrumentType, quote?.priceBasis),
|
|
200
202
|
style: "area",
|
|
201
203
|
axis: "right",
|
|
202
204
|
panelId: "price",
|
|
@@ -251,7 +253,10 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
251
253
|
const venueText = listingVenue ? (companyName ? ` (${listingVenue})` : listingVenue) : "";
|
|
252
254
|
const sessionLabel = quoteSessionLabel(quote);
|
|
253
255
|
const marketStateText = sessionLabel ? t(sessionLabel) : "";
|
|
254
|
-
|
|
256
|
+
// Signed out or free, the headline price is the delayed one: the mark says so after the session.
|
|
257
|
+
const delayMinutes = sessionQuote ? quoteDelayMinutes : null;
|
|
258
|
+
const delayCells = delayMinutes != null ? delayedMarkWidth(delayMinutes, nativePaneChrome === true) + 1 : 0;
|
|
259
|
+
const hasIdentityLine = Boolean(companyName || venueText || marketStateText || delayMinutes != null);
|
|
255
260
|
// The desktop logo is a square from the top of the company name's capitals down to the
|
|
256
261
|
// price's baseline, so its edges meet the text's and the three read as one block. The
|
|
257
262
|
// extended-hours line hangs below, as it comes and goes with the session. Without a
|
|
@@ -290,7 +295,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
290
295
|
+ quoteChangeCells + 1 + displayWidth(changeBasisText) <= quoteTextWidth;
|
|
291
296
|
const companyNameWidth = Math.max(8, quoteTextWidth
|
|
292
297
|
- displayWidth(venueText)
|
|
293
|
-
- (marketStateText ? displayWidth(marketStateText) + 1 : 0)
|
|
298
|
+
- (marketStateText ? displayWidth(marketStateText) + 1 : 0)
|
|
299
|
+
- delayCells);
|
|
294
300
|
// A listing that no longer trades keeps its delisting date whole on a narrow pane: the venue gives way, then the name.
|
|
295
301
|
const delistedRoom = quote?.delisting && marketStateText ? quoteTextWidth - displayWidth(marketStateText) - 1 : null;
|
|
296
302
|
const identityVenue = delistedRoom !== null && delistedRoom - displayWidth(venueText) < 8 ? "" : venueText;
|
|
@@ -401,7 +407,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
401
407
|
/>
|
|
402
408
|
<Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
403
409
|
{hasIdentityLine && (
|
|
404
|
-
<Box flexDirection="row" minWidth={0} overflow="hidden">
|
|
410
|
+
<Box flexDirection="row" alignItems="center" minWidth={0} overflow="hidden">
|
|
405
411
|
{identityName && (
|
|
406
412
|
<ClippedText fg={colors.textDim} text={identityName} width={identityNameWidth} />
|
|
407
413
|
)}
|
|
@@ -411,6 +417,11 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
411
417
|
{identityName || identityVenue ? " " : ""}{marketStateText}
|
|
412
418
|
</Text>
|
|
413
419
|
)}
|
|
420
|
+
{delayMinutes != null && (
|
|
421
|
+
<Box flexShrink={0} paddingLeft={identityName || identityVenue || marketStateText ? 1 : 0}>
|
|
422
|
+
<DelayedMark minutes={delayMinutes} />
|
|
423
|
+
</Box>
|
|
424
|
+
)}
|
|
414
425
|
</Box>
|
|
415
426
|
)}
|
|
416
427
|
|
|
@@ -546,7 +557,6 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
546
557
|
series={[priceSeries]}
|
|
547
558
|
panels={[{ id: "price" }]}
|
|
548
559
|
levels={trade && researchTradeMatches(trade, instrumentFromTicker(ticker)) ? { seriesId: priceSeries.id, items: trade.levels } : undefined}
|
|
549
|
-
axisWidth={8}
|
|
550
560
|
showLegend={false}
|
|
551
561
|
/>
|
|
552
562
|
</Box>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
2
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
3
|
import { timeSalesHeadless } from "./headless";
|
|
4
|
-
import { TimeSalesPane } from "./pane";
|
|
5
4
|
export const timeSalesModule: PluginModule = {
|
|
6
|
-
panes: [{ id: "time-sales", name: "Time and Sales", icon: "T", component: TimeSalesPane,
|
|
5
|
+
panes: [{ id: "time-sales", name: "Time and Sales", icon: "T", get component(): typeof import("./pane").TimeSalesPane { const { TimeSalesPane } = require("./pane"); return TimeSalesPane; },
|
|
7
6
|
defaultPosition: "right", tickerFollower: true, defaultMode: "floating", defaultFloatingSize: { width: 104, height: 28 },
|
|
8
7
|
tableExport: true, headless: timeSalesHeadless, settings: { title: "Time and Sales", fields: [{ key: "tab", label: "View", type: "select", options: [{ value: "trades", label: "Trades" }, { value: "quotes", label: "NBBO" }] }] } }],
|
|
9
8
|
paneTemplates: [
|
|
@@ -100,8 +100,10 @@ function TimeSalesView({ width, height, focused, symbol, exchange }: PaneProps &
|
|
|
100
100
|
}, [data]);
|
|
101
101
|
const tradeDecision = useRef<TapeDigits | null>(null);
|
|
102
102
|
const quoteDecision = useRef<TapeDigits | null>(null);
|
|
103
|
+
// Decimals come from the prints alone: a session extreme can sit a fraction of a cent off a tick (NVDA's 200.9199 low)
|
|
104
|
+
// and must not turn every trade into four decimals. The session range reads at the prints' decimals instead.
|
|
103
105
|
const tradeDigits = useMemo(() => (tradeDecision.current = stickyTapePriceDigits(tradeDecision.current, symbol,
|
|
104
|
-
|
|
106
|
+
data?.trades.map((trade) => trade.price) ?? []))?.digits ?? 2, [data, symbol]);
|
|
105
107
|
const quoteDigits = useMemo(() => (quoteDecision.current = stickyTapePriceDigits(quoteDecision.current, symbol,
|
|
106
108
|
data?.quotes.flatMap((quote) => [quote.bid, quote.ask]) ?? []))?.digits ?? 2, [data, symbol]);
|
|
107
109
|
// The session range stands beside the tab's own prices, so it reads at their decimals: the quote tab's bid and ask, not the trades'.
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
quotedCompanyName,
|
|
9
9
|
type CorporateBond,
|
|
10
10
|
} from "./client";
|
|
11
|
-
import { bondStackPriority, formatBondChange, formatBondDecimal, type BondColumnId } from "./model";
|
|
11
|
+
import { bondStackPriority, formatBondChange, formatBondDecimal, sharesOneIssuer, type BondColumnId } from "./model";
|
|
12
12
|
|
|
13
13
|
const decimal = (digits: number) => (value: unknown) => (
|
|
14
14
|
typeof value === "number" ? formatBondDecimal(value, digits) : "—"
|
|
@@ -33,6 +33,15 @@ const COLUMNS: HeadlessPaneColumn[] = [
|
|
|
33
33
|
column("grade", { key: "traceGradeCode", header: "Grade" }),
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* One issuer's tape (`sharesOneIssuer`) leaves the issuer column out of the
|
|
38
|
+
* text table, as the pane does; exports and piped output keep it.
|
|
39
|
+
*/
|
|
40
|
+
function reportColumns(bonds: readonly CorporateBond[]): HeadlessPaneColumn[] {
|
|
41
|
+
if (!sharesOneIssuer(bonds)) return COLUMNS;
|
|
42
|
+
return COLUMNS.map((spec) => spec.key === "issuerName" ? { ...spec, minTableWidth: Number.MAX_SAFE_INTEGER } : spec);
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function reportRow(bond: CorporateBond) {
|
|
37
46
|
return {
|
|
38
47
|
id: bond.id,
|
|
@@ -85,7 +94,7 @@ export const traceBondsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
85
94
|
sections: [{
|
|
86
95
|
// An empty answer is its message alone, dated in its own words.
|
|
87
96
|
title: bonds.length > 0 ? (tradeDate ? `Last sales ${tradeDate}` : "Last sales") : query ? "" : "Last sales",
|
|
88
|
-
columns:
|
|
97
|
+
columns: reportColumns(bonds),
|
|
89
98
|
rows: bonds.map(reportRow),
|
|
90
99
|
...(query ? { empty: noPrintsText(query, tradeDate, issuer) } : {}),
|
|
91
100
|
}],
|
|
@@ -85,26 +85,32 @@ export function sortBonds(bonds: readonly CorporateBond[], sort: BondSort): Corp
|
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* What a sale says leads: its price, yield and CUSIP, after the issuer when
|
|
90
|
+
* the rows are several issuers', before what describes the bond (coupon,
|
|
91
|
+
* maturity) and the rest of the sale.
|
|
92
|
+
*/
|
|
88
93
|
const BOND_COLUMNS: readonly BondColumn[] = [
|
|
89
94
|
{ id: "issuer", label: "Issuer", width: 18, align: "left", flexGrow: 1 },
|
|
95
|
+
{ id: "price", label: "Price", width: 9, align: "right" },
|
|
96
|
+
{ id: "yield", label: "Yield", width: 8, align: "right" },
|
|
90
97
|
{ id: "cusip", label: "CUSIP", width: 10, align: "left" },
|
|
91
98
|
{ id: "coupon", label: "Coupon", width: 7, align: "right" },
|
|
92
99
|
{ id: "maturity", label: "Maturity", width: 11, align: "left" },
|
|
93
|
-
{ id: "price", label: "Price", width: 9, align: "right" },
|
|
94
|
-
{ id: "yield", label: "Yield", width: 8, align: "right" },
|
|
95
100
|
{ id: "change", label: "Chg", width: 8, align: "right" },
|
|
96
101
|
{ id: "traded", label: "Traded", width: 11, align: "left" },
|
|
97
102
|
{ id: "grade", label: "Grade", width: 6, align: "left" },
|
|
98
103
|
];
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
|
-
* What a narrow tape gives up first. The
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* moves its columns under the rows in the same order
|
|
106
|
+
* What a narrow tape gives up first. The price and yield are what was printed
|
|
107
|
+
* and the CUSIP tells two issues with one coupon and maturity apart, so they
|
|
108
|
+
* never go, nor does the issuer while it names the row; the descriptive
|
|
109
|
+
* columns go before them, the coupon last as it is the narrowest. The
|
|
110
|
+
* headless report moves its columns under the rows in the same order
|
|
111
|
+
* (`bondStackPriority`).
|
|
106
112
|
*/
|
|
107
|
-
const BOND_NARROW_DROP_ORDER: readonly BondColumnId[] = ["grade", "change", "
|
|
113
|
+
const BOND_NARROW_DROP_ORDER: readonly BondColumnId[] = ["grade", "change", "traded", "maturity", "coupon"];
|
|
108
114
|
|
|
109
115
|
/** `stackPriority` of each column in the text report: the higher, the sooner it moves under its row. */
|
|
110
116
|
export function bondStackPriority(columnId: BondColumnId): number {
|
|
@@ -112,6 +118,34 @@ export function bondStackPriority(columnId: BondColumnId): number {
|
|
|
112
118
|
return index < 0 ? 0 : BOND_NARROW_DROP_ORDER.length - index;
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
/** The tape's symbol root of an issue (`WHR` of `WHR4800338` and `WHR.GM`), or null without one. */
|
|
122
|
+
function symbolRoot(symbol: string | null): string | null {
|
|
123
|
+
return symbol?.match(/^[A-Z]+/)?.[0] ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether every row is the same issuer's, so an issuer column would only
|
|
128
|
+
* repeat it: more than one row, all under one symbol root, or all one name
|
|
129
|
+
* where a row has no symbol. One row keeps the name, the only thing naming it.
|
|
130
|
+
*/
|
|
131
|
+
export function sharesOneIssuer(bonds: readonly Pick<CorporateBond, "issuerName" | "symbol">[]): boolean {
|
|
132
|
+
if (bonds.length < 2) return false;
|
|
133
|
+
const roots = bonds.map((bond) => symbolRoot(bond.symbol));
|
|
134
|
+
if (roots.every((root) => root != null)) return new Set(roots).size === 1;
|
|
135
|
+
return new Set(bonds.map((bond) => bond.issuerName.trim().toUpperCase())).size === 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The sort the tape shows: a saved issuer sort, while one issuer's tape leaves
|
|
140
|
+
* the issuer column out, sorts as the default, which the header can show; the
|
|
141
|
+
* saved sort comes back with the column.
|
|
142
|
+
*/
|
|
143
|
+
export function shownBondSort(sort: BondSort, sharedIssuer: boolean): BondSort {
|
|
144
|
+
return sharedIssuer && sort.columnId === "issuer" ? DEFAULT_BOND_SORT : sort;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The columns at `width`, without the issuer's when every row shares it (`sharesOneIssuer`). */
|
|
148
|
+
export function buildBondColumns(width: number, { sharedIssuer = false }: { sharedIssuer?: boolean } = {}): BondColumn[] {
|
|
149
|
+
const columns = sharedIssuer ? BOND_COLUMNS.filter((column) => column.id !== "issuer") : BOND_COLUMNS;
|
|
150
|
+
return fitChartTableColumns(columns, width, BOND_NARROW_DROP_ORDER);
|
|
117
151
|
}
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
formatBondChange,
|
|
41
41
|
formatBondDecimal,
|
|
42
42
|
isBondColumnId,
|
|
43
|
+
sharesOneIssuer,
|
|
44
|
+
shownBondSort,
|
|
43
45
|
sortBonds,
|
|
44
46
|
type BondColumn,
|
|
45
47
|
type BondSort,
|
|
@@ -73,8 +75,11 @@ export function TraceBondsPane({ focused, width, height }: PaneProps) {
|
|
|
73
75
|
useAutoRefresh(updatedAt, load);
|
|
74
76
|
usePaneRefreshKey(() => void reload(), { focused, enabled: !searchActive });
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
+
// One issuer's tape says its name once, in what was asked, not on every row.
|
|
79
|
+
const sharedIssuer = useMemo(() => sharesOneIssuer(bonds), [bonds]);
|
|
80
|
+
const columns = useMemo(() => buildBondColumns(width, { sharedIssuer }), [sharedIssuer, width]);
|
|
81
|
+
const shownSort = shownBondSort(sort, sharedIssuer);
|
|
82
|
+
const rows = useMemo(() => sortBonds(bonds, shownSort), [bonds, shownSort]);
|
|
78
83
|
const tradeDate = answered ? data.tradeDate : null;
|
|
79
84
|
const footerInfo = useMemo(
|
|
80
85
|
() => (tradeDate ? [{ id: "trade-date", parts: [{ text: tradeDate }] }] : []),
|
|
@@ -96,11 +101,11 @@ export function TraceBondsPane({ focused, width, height }: PaneProps) {
|
|
|
96
101
|
|
|
97
102
|
const onHeaderClick = useCallback((columnId: string) => {
|
|
98
103
|
if (!isBondColumnId(columnId)) return;
|
|
99
|
-
setSort((current) => nextHeaderSort(current, columnId, {
|
|
104
|
+
setSort((current) => nextHeaderSort(shownBondSort(current, sharedIssuer), columnId, {
|
|
100
105
|
firstDirection: firstBondSortDirection,
|
|
101
106
|
resetTo: DEFAULT_BOND_SORT,
|
|
102
107
|
}));
|
|
103
|
-
}, [setSort]);
|
|
108
|
+
}, [setSort, sharedIssuer]);
|
|
104
109
|
|
|
105
110
|
const handleRootKeyDown = useCallback((
|
|
106
111
|
event: DataTableKeyEvent,
|
|
@@ -190,13 +195,13 @@ export function TraceBondsPane({ focused, width, height }: PaneProps) {
|
|
|
190
195
|
}}
|
|
191
196
|
columns={columns}
|
|
192
197
|
items={rows}
|
|
193
|
-
sortColumnId={
|
|
194
|
-
sortDirection={
|
|
198
|
+
sortColumnId={shownSort.columnId}
|
|
199
|
+
sortDirection={shownSort.direction}
|
|
195
200
|
onHeaderClick={onHeaderClick}
|
|
196
201
|
onSortChange={(columnId, direction) => {
|
|
197
202
|
if (isBondColumnId(columnId)) setSort({ columnId, direction });
|
|
198
203
|
}}
|
|
199
|
-
resetScrollKey={`${needle}:${
|
|
204
|
+
resetScrollKey={`${needle}:${shownSort.columnId ?? ""}:${shownSort.direction}`}
|
|
200
205
|
getItemKey={(row) => row.id}
|
|
201
206
|
renderCell={renderCell}
|
|
202
207
|
selectedTextOverridesCellColor
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createPluginCache } from "../../../data/plugin-cache";
|
|
1
|
+
import { createPluginCache, savedCopyForTransientFailure } from "../../../data/plugin-cache";
|
|
2
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
2
3
|
import type { ConnectionHealthRegistry } from "../../../core/connection-health";
|
|
3
4
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
4
5
|
import {
|
|
@@ -37,6 +38,8 @@ export interface TreasuryAuctionsResult {
|
|
|
37
38
|
fetchedAt: number;
|
|
38
39
|
/** True when the network failed and expired cache was served instead. */
|
|
39
40
|
stale: boolean;
|
|
41
|
+
/** The refresh failed and the copy saved at this time (ms) answered instead. */
|
|
42
|
+
savedAt?: number;
|
|
40
43
|
refreshError?: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -84,11 +87,18 @@ export async function loadTreasuryAuctions(
|
|
|
84
87
|
window: AuctionHistoryWindow = AUCTION_HISTORY_DAYS,
|
|
85
88
|
): Promise<TreasuryAuctionsResult> {
|
|
86
89
|
if (window === AUCTION_HISTORY_ALL) return loadAllTreasuryAuctions(force, loader);
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
// A saved board answers only for a failure asking again may get past; a refusal or an invalid page is the error.
|
|
91
|
+
const result = savedCopyForTransientFailure(await cache.load(`recent:${window}`, walk(loader, () => auctionWindowQuery(window)), { force }));
|
|
92
|
+
return answered({
|
|
89
93
|
auctions: result.data, fetchedAt: result.fetchedAt, stale: result.stale,
|
|
94
|
+
...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}),
|
|
90
95
|
...(result.refreshError ? { refreshError: result.refreshError } : {}),
|
|
91
|
-
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A board a saved copy answered: marked as one, so a command ends by saying when it was saved, not with its failure. */
|
|
100
|
+
function answered(result: TreasuryAuctionsResult): TreasuryAuctionsResult {
|
|
101
|
+
return result.savedAt != null ? servedFromSavedCopy(result, result.savedAt) : result;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/**
|
|
@@ -100,16 +110,18 @@ export async function loadTreasuryAuctions(
|
|
|
100
110
|
async function loadAllTreasuryAuctions(force: boolean, loader: AuctionLoader): Promise<TreasuryAuctionsResult> {
|
|
101
111
|
const boundary = auctionArchiveBoundary();
|
|
102
112
|
const [recent, archive] = await Promise.all([
|
|
103
|
-
cache.load(`since:${boundary}`, walk(loader, () => ({ from: boundary })), { force }),
|
|
113
|
+
cache.load(`since:${boundary}`, walk(loader, () => ({ from: boundary })), { force }).then(savedCopyForTransientFailure),
|
|
104
114
|
archiveCache.load(`archive:${boundary}`, walk(loader, () => ({
|
|
105
115
|
from: AUCTION_HISTORY_START, before: boundary, pageSize: AUCTION_ARCHIVE_PAGE_SIZE,
|
|
106
|
-
}))),
|
|
116
|
+
}))).then(savedCopyForTransientFailure),
|
|
107
117
|
]);
|
|
108
118
|
const seen = new Set<string>();
|
|
109
119
|
const auctions = [...recent.data, ...archive.data].filter((auction) => !seen.has(auction.id) && Boolean(seen.add(auction.id)));
|
|
110
120
|
const refreshError = recent.refreshError ?? archive.refreshError;
|
|
111
|
-
|
|
121
|
+
const savedAt = Math.min(...[recent, archive].map((part) => part.source === "stale-fallback" ? part.fetchedAt : Infinity));
|
|
122
|
+
return answered({
|
|
112
123
|
auctions, fetchedAt: recent.fetchedAt, stale: recent.stale || archive.stale,
|
|
124
|
+
...(Number.isFinite(savedAt) ? { savedAt } : {}),
|
|
113
125
|
...(refreshError ? { refreshError } : {}),
|
|
114
|
-
};
|
|
126
|
+
});
|
|
115
127
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
1
2
|
import { createThrottledFetch } from "../../../utils/throttled-fetch";
|
|
2
3
|
import type { TreasuryAuction, TreasuryAuctionRaw } from "./types";
|
|
3
4
|
|
|
@@ -222,9 +223,8 @@ export async function fetchTreasuryAuctions(
|
|
|
222
223
|
): Promise<TreasuryAuction[]> {
|
|
223
224
|
return fetchAuctionPages(async (page) => {
|
|
224
225
|
const response = await TREASURY_FETCH.fetch(buildAuctionsUrl(query, page));
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
226
|
+
// The status stays: an overloaded server is a failure a saved board may answer for, a refusal is not.
|
|
227
|
+
if (!response.ok) throw new ApiRequestError(`Treasury Fiscal Data request failed (${response.status})`, response.status);
|
|
228
228
|
return response.json();
|
|
229
229
|
});
|
|
230
230
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import { formatCompact } from "../../../utils/format";
|
|
6
6
|
import { AUCTION_HISTORY_ALL, AUCTION_HISTORY_START, fetchTreasuryAuctions, type AuctionHistoryWindow } from "./client";
|
|
7
7
|
import { loadTreasuryAuctions, type TreasuryAuctionsResult } from "./cache";
|
|
8
|
+
import { unansweredRefreshError } from "../../../data/saved-copies";
|
|
8
9
|
import type { TreasuryAuction } from "./types";
|
|
9
10
|
import { FIXED_INCOME_VIEW_STATE } from "../fixed-income/model";
|
|
10
11
|
import {
|
|
@@ -156,7 +157,8 @@ function createTreasuryAuctionsHeadless(
|
|
|
156
157
|
];
|
|
157
158
|
return {
|
|
158
159
|
complete: !result.refreshError,
|
|
159
|
-
|
|
160
|
+
// A saved copy answering the failed refresh ends the report by saying when it was saved instead.
|
|
161
|
+
errors: [unansweredRefreshError(result)].filter((error): error is string => !!error),
|
|
160
162
|
notes,
|
|
161
163
|
rows: auctions.map((auction) => ({
|
|
162
164
|
...auction,
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
type DataTableRootKeyContext,
|
|
14
14
|
} from "../../../components";
|
|
15
15
|
import { loadingErrorFooterInfo, usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
16
|
+
import { ageFooterSegment, savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
17
|
+
import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
|
|
16
18
|
import { usePaneInstance } from "../../../state/app/context";
|
|
17
19
|
import { colors } from "../../../theme/colors";
|
|
18
20
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -208,8 +210,10 @@ export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
|
|
|
208
210
|
const auctions = resource.data?.auctions ?? NO_AUCTIONS;
|
|
209
211
|
const stale = resource.data?.stale ?? false;
|
|
210
212
|
const fetchedAt = resource.data?.fetchedAt ?? null;
|
|
213
|
+
// A refresh that failed shows the auctions an earlier read saved, and says when that copy is from.
|
|
214
|
+
const savedAt = resource.data?.savedAt ?? null;
|
|
211
215
|
// A cache that could not be refreshed arrives as a result, not a failure.
|
|
212
|
-
const error = resource.error ?? (resource.loading ? null : resource.data?.refreshError ?? null);
|
|
216
|
+
const error = resource.error ?? (resource.loading || savedAt != null ? null : resource.data?.refreshError ?? null);
|
|
213
217
|
// Once auctions have loaded, a refresh runs without a loading state.
|
|
214
218
|
const loading = resource.loading && !resource.data;
|
|
215
219
|
const [filter, setFilter] = usePluginPaneState<AuctionFilter>("activeTab", "all");
|
|
@@ -302,9 +306,10 @@ export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
|
|
|
302
306
|
|
|
303
307
|
usePaneFooter(TREASURY_AUCTIONS_PANE_ID, () => {
|
|
304
308
|
const info = loadingErrorFooterInfo(loading, error);
|
|
305
|
-
if (
|
|
306
|
-
if (
|
|
307
|
-
|
|
309
|
+
if (savedAt != null) info.push(...savedCopyFooterInfo([savedAt]));
|
|
310
|
+
else if (stale) info.push(STALE_SEGMENT);
|
|
311
|
+
if (fetchedAt && savedAt == null) {
|
|
312
|
+
info.push(ageFooterSegment(formatRelativeAge(fetchedAt)));
|
|
308
313
|
}
|
|
309
314
|
return {
|
|
310
315
|
info,
|
|
@@ -323,6 +328,7 @@ export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
|
|
|
323
328
|
fetchedAt,
|
|
324
329
|
focusSearch,
|
|
325
330
|
loading,
|
|
331
|
+
savedAt,
|
|
326
332
|
stale,
|
|
327
333
|
]);
|
|
328
334
|
|
|
@@ -17,7 +17,14 @@ import { expiredExpiryText, findListedExpiry, isLiveOptionExpiry, liveOptionExpi
|
|
|
17
17
|
import { optionModelSpot } from "../options/model-spot";
|
|
18
18
|
|
|
19
19
|
const DEFAULT_SURFACE_EXPIRY_LIMIT = 18;
|
|
20
|
+
/** Expiries read at a time from a source that takes them one by one. */
|
|
20
21
|
const SURFACE_LOAD_CONCURRENCY = 4;
|
|
22
|
+
/**
|
|
23
|
+
* Expiries asked at once from a source that batches them (`batchesExpiries`):
|
|
24
|
+
* the API client sends one listing's expiries asked in one moment in batch
|
|
25
|
+
* requests of six, side by side.
|
|
26
|
+
*/
|
|
27
|
+
const SURFACE_BATCHED_CONCURRENCY = 48;
|
|
21
28
|
/** Each kept expiry must be at least this much further out than the previous one. */
|
|
22
29
|
const SURFACE_TENOR_RATIO = 1.35;
|
|
23
30
|
|
|
@@ -43,8 +50,10 @@ export function selectSurfaceExpiries(catalogue: readonly number[], limit: numbe
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface SurfaceLoaderDependencies {
|
|
46
|
-
loadOptions(request: OptionsRequest, options?: { forceRefresh?: boolean }): Promise<QueryEntry<OptionsChain>>;
|
|
53
|
+
loadOptions(request: OptionsRequest, options?: { forceRefresh?: boolean; batch?: boolean }): Promise<QueryEntry<OptionsChain>>;
|
|
47
54
|
loadYieldCurve(): Promise<YieldPoint[]>;
|
|
55
|
+
/** Whether `loadOptions` reaches a source that batches expiries asked together; absent, four are read at a time. */
|
|
56
|
+
batchesExpiries?(): boolean | Promise<boolean>;
|
|
48
57
|
now?: () => number;
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -58,6 +67,7 @@ export function createSurfaceDependencies(
|
|
|
58
67
|
loadOptions: (request, options) => coordinator
|
|
59
68
|
? coordinator.loadOptions(request, options)
|
|
60
69
|
: Promise.reject(new Error("Market data coordinator unavailable")),
|
|
70
|
+
batchesExpiries: () => coordinator?.batchesOptions() ?? false,
|
|
61
71
|
loadYieldCurve: () => cloudApi.getCloudYieldCurve(),
|
|
62
72
|
};
|
|
63
73
|
}
|
|
@@ -179,6 +189,8 @@ export async function loadVolatilitySurface(
|
|
|
179
189
|
};
|
|
180
190
|
checkAbort();
|
|
181
191
|
publish();
|
|
192
|
+
// Asked while the catalogue loads, so it costs the expiries no wait.
|
|
193
|
+
const batched = Promise.resolve().then(() => dependencies.batchesExpiries?.()).then((answer) => answer === true, () => false);
|
|
182
194
|
const treasury = Promise.resolve().then(() => dependencies.loadYieldCurve()).then((value) => {
|
|
183
195
|
if (request.signal?.aborted) return;
|
|
184
196
|
curve = value;
|
|
@@ -233,6 +245,9 @@ export async function loadVolatilitySurface(
|
|
|
233
245
|
publish();
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
// A source that takes expiries one by one is asked four at a time, and a cancelled load asks for no more.
|
|
249
|
+
const batch = await abortable(batched, request.signal, CANCELLED);
|
|
250
|
+
const concurrency = batch ? SURFACE_BATCHED_CONCURRENCY : SURFACE_LOAD_CONCURRENCY;
|
|
236
251
|
let next = 0;
|
|
237
252
|
const worker = async () => {
|
|
238
253
|
while (!request.signal?.aborted && next < selected.length) {
|
|
@@ -240,7 +255,7 @@ export async function loadVolatilitySurface(
|
|
|
240
255
|
try {
|
|
241
256
|
// The catalogue response already refreshed its own slice, unless it came from cache.
|
|
242
257
|
const entry = await dependencies.loadOptions({ instrument: request.instrument, expirationDate: expiration },
|
|
243
|
-
{ forceRefresh: !!request.forceRefresh && (!!request.expiries || expiration !== catalogueExpiration) });
|
|
258
|
+
{ forceRefresh: !!request.forceRefresh && (!!request.expiries || expiration !== catalogueExpiration), ...(batch ? { batch } : {}) });
|
|
244
259
|
if (request.signal?.aborted) return;
|
|
245
260
|
entries.set(expiration, entry);
|
|
246
261
|
if (!resolveEntryValue(entry) && !entry.error) errors.set(expiration, "Options chain unavailable");
|
|
@@ -251,7 +266,7 @@ export async function loadVolatilitySurface(
|
|
|
251
266
|
publish();
|
|
252
267
|
}
|
|
253
268
|
};
|
|
254
|
-
await abortable(Promise.all([treasury, ...Array.from({ length: Math.min(
|
|
269
|
+
await abortable(Promise.all([treasury, ...Array.from({ length: Math.min(concurrency, selected.length) }, worker)]), request.signal, CANCELLED);
|
|
255
270
|
checkAbort();
|
|
256
271
|
finished = true;
|
|
257
272
|
const result = snapshot();
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
2
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
|
-
import { VolSurfacePane } from "./pane";
|
|
4
3
|
import { volSurfaceHeadless } from "./headless";
|
|
5
4
|
|
|
6
5
|
export const volSurfaceModule: PluginModule = {
|
|
7
6
|
panes: [{
|
|
8
|
-
id: "vol-surface", name: "Volatility Surface", icon: "V", component: VolSurfacePane,
|
|
7
|
+
id: "vol-surface", name: "Volatility Surface", icon: "V", get component(): typeof import("./pane").VolSurfacePane { const { VolSurfacePane } = require("./pane"); return VolSurfacePane; },
|
|
9
8
|
defaultPosition: "right", tickerFollower: true, defaultMode: "floating", defaultFloatingSize: { width: 126, height: 36 },
|
|
10
9
|
tableExport: true, headless: volSurfaceHeadless,
|
|
11
10
|
settings: {
|
|
@@ -14,6 +14,7 @@ import { buildVolatilityData, buildVolatilityTermHistory, IMPLIED_CORRELATION_RO
|
|
|
14
14
|
type VolatilitySeriesId, type VolatilitySeriesInput, type VolatilityTermHistory } from "./model";
|
|
15
15
|
import { abortable, abortError } from "../../../utils/async-deadline";
|
|
16
16
|
import { errorMessage } from "../../../utils/errors";
|
|
17
|
+
import { savedCopyAt } from "../../../data/saved-copies";
|
|
17
18
|
|
|
18
19
|
const VOLATILITY_LOAD_CONCURRENCY = 4;
|
|
19
20
|
const VOLATILITY_HISTORY_LIMIT = 400;
|
|
@@ -22,6 +23,8 @@ export interface VolatilityLoadResult {
|
|
|
22
23
|
/** The daily inputs the data was built from, so live index levels can be laid over them. */
|
|
23
24
|
inputs: VolatilityInputs;
|
|
24
25
|
stale: boolean;
|
|
26
|
+
/** The oldest copy a failed refresh left answering, by when it was saved (ms); its failure is not in `errors`. */
|
|
27
|
+
savedAt?: number;
|
|
25
28
|
errors: string[];
|
|
26
29
|
phase: "loading" | "partial" | "ready" | "error";
|
|
27
30
|
loaded: number;
|
|
@@ -76,14 +79,16 @@ const legError = (legs: ReadonlyArray<{ error?: string | null } | undefined>) =>
|
|
|
76
79
|
* The 3M/30D ratio's history as the board builds it: the board's own VIX and
|
|
77
80
|
* VIX 3M daily closes (one year, the same request and cache), FRED's
|
|
78
81
|
* republished closes from `start` on the dates those lack. When Cboe's closes
|
|
79
|
-
* fail FRED stands in, a day behind, and the warning says so
|
|
80
|
-
* from
|
|
81
|
-
*
|
|
82
|
+
* fail FRED stands in, a day behind, and the warning says so. A ratio read
|
|
83
|
+
* from closes a failed refresh left in place carries when they were saved
|
|
84
|
+
* (`savedAt`), which the chart dates as every pane does; other cached closes
|
|
85
|
+
* name whose. With neither publisher's pair it rejects with both reasons
|
|
86
|
+
* rather than return an empty history.
|
|
82
87
|
*/
|
|
83
88
|
export async function loadVolatilityTermHistory(
|
|
84
89
|
{ start, signal }: { start?: Date; signal?: AbortSignal } = {},
|
|
85
90
|
dependencies: VolatilityLoaderDependencies & { loadFredPoints?: typeof loadFredPoints } = termDependencies(),
|
|
86
|
-
): Promise<{ term: VolatilityTermHistory; warning?: string }> {
|
|
91
|
+
): Promise<{ term: VolatilityTermHistory; warning?: string; savedAt?: number }> {
|
|
87
92
|
const loadFred = dependencies.loadFredPoints ?? loadFredPoints;
|
|
88
93
|
const now = dependencies.now?.() ?? Date.now();
|
|
89
94
|
const history: NonNullable<VolatilityInputs["history"]> = {};
|
|
@@ -101,7 +106,7 @@ export async function loadVolatilityTermHistory(
|
|
|
101
106
|
try {
|
|
102
107
|
const loaded = await loadFred(seriesId, start);
|
|
103
108
|
fred[seriesId] = { observations: loaded.points, info: null, stale: loaded.points.length > 0 && loaded.stale,
|
|
104
|
-
error: loaded.refreshError ?? null };
|
|
109
|
+
error: loaded.refreshError ?? null, ...(loaded.savedAt != null && loaded.points.length ? { savedAt: loaded.savedAt } : {}) };
|
|
105
110
|
} catch (error) {
|
|
106
111
|
fred[seriesId] = { observations: [], info: null, error: errorMessage(error) };
|
|
107
112
|
}
|
|
@@ -119,12 +124,14 @@ export async function loadVolatilityTermHistory(
|
|
|
119
124
|
} else if (fredError && !term.ratioHistory.some((point) => point.publisher === "FRED")) {
|
|
120
125
|
notes.push(`FRED closes unavailable (${fredError}); showing Cboe's from ${term.ratioHistory[0]!.date}.`);
|
|
121
126
|
}
|
|
122
|
-
|
|
127
|
+
const legs = term.publisher === "FRED" ? VOLATILITY_SERIES.map(({ seriesId }) => fred[seriesId]) : [history.vix, history.vix3m];
|
|
128
|
+
const savedAt = term.stale ? oldestSavedAt(legs) : undefined;
|
|
129
|
+
if (term.stale && savedAt == null) {
|
|
123
130
|
const error = term.publisher === "FRED" ? fredError : cboeError;
|
|
124
131
|
notes.push(error ? `Refresh failed (${error}); showing cached ${term.publisher} closes.`
|
|
125
132
|
: `Showing cached ${term.publisher} closes; the source has not updated since.`);
|
|
126
133
|
}
|
|
127
|
-
return notes.length ? {
|
|
134
|
+
return { term, ...(notes.length ? { warning: notes.join(" ") } : {}), ...(savedAt != null ? { savedAt } : {}) };
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
/** A CBOE daily close as a daily bar dated at the 16:15 New York settlement. */
|
|
@@ -141,23 +148,39 @@ function validateFredData(seriesId: VolatilitySeriesId, data: FredSeriesData): F
|
|
|
141
148
|
}
|
|
142
149
|
return data;
|
|
143
150
|
}
|
|
144
|
-
function fredInput(result: Pick<FredSeriesLoadResult, "data" | "stale" | "fetchedAt"> &
|
|
151
|
+
function fredInput(result: Pick<FredSeriesLoadResult, "data" | "stale" | "fetchedAt"> & Partial<Pick<FredSeriesLoadResult, "source">>
|
|
152
|
+
& { refreshError?: string }): VolatilitySeriesInput {
|
|
153
|
+
const saved = result.source === "stale-fallback" && result.data.observations.length > 0;
|
|
145
154
|
return { observations: result.data.observations, info: result.data.info, fetchedAt: result.fetchedAt,
|
|
146
155
|
stale: result.data.observations.length > 0 && result.stale,
|
|
147
|
-
error: result.refreshError ?? (result.data.observations.length ? null : "Daily FRED observations unavailable")
|
|
156
|
+
error: result.refreshError ?? (result.data.observations.length ? null : "Daily FRED observations unavailable"),
|
|
157
|
+
...(saved ? { savedAt: result.fetchedAt } : {}) };
|
|
148
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* A history answered from the copy saved before its refetch failed is one only
|
|
161
|
+
* when the router says so (its saved-copy mark, which a command's answers
|
|
162
|
+
* carry): closes kept from an earlier answer, or a fresh answer with no data,
|
|
163
|
+
* say their error instead.
|
|
164
|
+
*/
|
|
149
165
|
function historyInput(entry: QueryEntry<PricePoint[]>, now: number): VolatilityHistoryInput {
|
|
150
166
|
const history = resolveEntryValue(entry) ?? [];
|
|
167
|
+
const savedAt = history.length > 0 ? savedCopyAt(entry.history) : null;
|
|
151
168
|
return { history, source: entry.source, fetchedAt: entry.fetchedAt,
|
|
152
|
-
stale: history.length > 0 && (!!entry.error || (entry.staleAt != null && entry.staleAt <= now)),
|
|
153
|
-
error: entry.error?.message ?? (history.length ? null : "Daily index history unavailable")
|
|
169
|
+
stale: history.length > 0 && (savedAt != null || !!entry.error || (entry.staleAt != null && entry.staleAt <= now)),
|
|
170
|
+
error: entry.error?.message ?? (history.length ? null : "Daily index history unavailable"),
|
|
171
|
+
...(savedAt != null ? { savedAt } : {}) };
|
|
172
|
+
}
|
|
173
|
+
/** The oldest of `inputs`' saved copies, by when each was saved. */
|
|
174
|
+
function oldestSavedAt(inputs: ReadonlyArray<{ savedAt?: number | null } | undefined>): number | undefined {
|
|
175
|
+
const saved = inputs.flatMap((input) => input?.savedAt != null ? [input.savedAt] : []);
|
|
176
|
+
return saved.length ? Math.min(...saved) : undefined;
|
|
154
177
|
}
|
|
155
178
|
/** Seeds the first paint; the load that replaces each input settles its freshness, so cache age is not stale. */
|
|
156
179
|
function cachedInputs(dependencies: VolatilityLoaderDependencies, now: number): VolatilityInputs {
|
|
157
180
|
const inputs: VolatilityInputs = { history: {}, fred: {} };
|
|
158
181
|
for (const definition of VOLATILITY_INDICES) {
|
|
159
182
|
const entry = dependencies.getChartEntry?.(volatilityHistoryRequest(definition.symbol));
|
|
160
|
-
if (entry && resolveEntryValue(entry) != null) inputs.history![definition.id] = { ...historyInput(entry, now), stale: false };
|
|
183
|
+
if (entry && resolveEntryValue(entry) != null) inputs.history![definition.id] = { ...historyInput(entry, now), stale: false, savedAt: null };
|
|
161
184
|
}
|
|
162
185
|
for (const { seriesId } of VOLATILITY_SERIES) {
|
|
163
186
|
const cached = getCachedFredSeries(requestFor(seriesId), { allowExpired: true });
|
|
@@ -176,12 +199,16 @@ function historyError(id: string, error: string): string {
|
|
|
176
199
|
}
|
|
177
200
|
function project(inputs: VolatilityInputs, loaded: number, total: number, pending: boolean): VolatilityLoadResult {
|
|
178
201
|
const data = buildVolatilityData(inputs);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
// A saved copy answering a failed refresh says when it was saved (`savedAt`) instead of the failure.
|
|
203
|
+
const errors = [...Object.entries(inputs.history ?? {}).flatMap(([id, value]) => value.error && value.savedAt == null ? [historyError(id, value.error)] : []),
|
|
204
|
+
...Object.entries(inputs.fred ?? {}).flatMap(([id, value]) => value.error && value.savedAt == null ? [`${id}: ${value.error}`] : [])];
|
|
205
|
+
const all = [...Object.values(inputs.history ?? {}), ...Object.values(inputs.fred ?? {})];
|
|
206
|
+
const stale = all.some((value) => value.stale);
|
|
207
|
+
const savedAt = oldestSavedAt(all);
|
|
182
208
|
const phase = pending ? hasValues(data) ? "partial" : "loading" : !hasValues(data) ? "error"
|
|
183
209
|
: errors.length || stale || data.warnings.length || data.board.some((row) => row.status !== "available") ? "partial" : "ready";
|
|
184
|
-
return { data, inputs: { history: { ...inputs.history }, fred: { ...inputs.fred } }, stale,
|
|
210
|
+
return { data, inputs: { history: { ...inputs.history }, fred: { ...inputs.fred } }, stale, ...(savedAt != null ? { savedAt } : {}),
|
|
211
|
+
errors, phase, loaded, total };
|
|
185
212
|
}
|
|
186
213
|
export function getCachedVolatilityData(dependencies: VolatilityLoaderDependencies = createVolatilityDependencies()): VolatilityLoadResult | null {
|
|
187
214
|
const inputs = cachedInputs(dependencies, dependencies.now?.() ?? Date.now());
|