gloomberb 0.18.3 → 0.18.4
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 +8 -8
- package/README.zh-CN.md +11 -11
- package/bin/{gloomberb → gloom} +3 -1
- package/package.json +6 -2
- package/src/account/checkout-handoff.tsx +225 -0
- package/src/account/device-signin-dialog.tsx +2 -2
- package/src/account/device-signin.ts +2 -2
- package/src/account/upgrade-dialog.tsx +1 -1
- package/src/api-client/askg.ts +2 -2
- package/src/api-client/auth.ts +2 -2
- package/src/api-client/central-bank-rates.ts +6 -0
- package/src/api-client/collections.ts +5 -1
- package/src/api-client/cot.ts +2 -0
- package/src/api-client/crypto-etf-flows.ts +37 -0
- package/src/api-client/crypto-markets.ts +60 -0
- package/src/api-client/data.ts +84 -5
- package/src/api-client/doe.ts +15 -0
- package/src/api-client/equity-screener.ts +28 -0
- package/src/api-client/exchange-announcements.ts +38 -0
- package/src/api-client/fund-facts.ts +39 -0
- package/src/api-client/futures-curve.ts +9 -1
- package/src/api-client/hiring.ts +301 -0
- package/src/api-client/index.ts +22 -5
- package/src/api-client/market-discovery.ts +14 -0
- package/src/api-client/members.ts +6 -0
- package/src/api-client/mna.ts +6 -0
- package/src/api-client/paths.ts +18 -1
- package/src/api-client/request.ts +21 -4
- package/src/api-client/short-side-publications.ts +37 -0
- package/src/api-client/socket.ts +64 -4
- package/src/api-client/swap-curve.ts +35 -0
- package/src/api-client/themes.ts +7 -1
- package/src/api-client/types.ts +90 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -2
- package/src/app/runtime/desktop-deeplink.ts +5 -5
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +13 -0
- package/src/app/star-prompt/status-line.tsx +2 -2
- package/src/architecture/core-builtin-allowlist.json +5 -2
- package/src/architecture/ui-literals-allowlist.json +3 -3
- package/src/architecture/ui-tokens-allowlist.json +1 -1
- package/src/brokers/broker-portfolio-sync.ts +37 -0
- package/src/brokers/directory.ts +11 -0
- package/src/brokers/logos.ts +3 -1
- package/src/brokers/signed-in/adapter.ts +304 -26
- package/src/brokers/signed-in/client.ts +14 -2
- package/src/brokers/signed-in/connect.ts +10 -2
- package/src/brokers/sync-broker-instance.ts +9 -1
- package/src/capabilities/chart-series.ts +22 -6
- package/src/cli/bank-section.ts +1 -1
- package/src/cli/base-converter.ts +23 -14
- package/src/cli/central-bank-events.ts +1 -1
- package/src/cli/command-options.ts +59 -4
- package/src/cli/commands/api.ts +9 -6
- package/src/cli/commands/broker.ts +2 -2
- package/src/cli/commands/command-utils.ts +2 -2
- package/src/cli/commands/crypto-hints.ts +7 -20
- package/src/cli/commands/fred-series.ts +4 -2
- package/src/cli/commands/keybindings.ts +2 -2
- package/src/cli/commands/market.ts +1117 -264
- package/src/cli/commands/news-hints.ts +2 -2
- package/src/cli/commands/news-search.ts +59 -6
- package/src/cli/commands/options-leaps.ts +41 -26
- package/src/cli/commands/overview.ts +121 -48
- package/src/cli/commands/plugins.ts +9 -9
- package/src/cli/commands/remote.ts +13 -10
- package/src/cli/commands/search.ts +64 -44
- package/src/cli/commands/system.ts +27 -19
- package/src/cli/commands/themes.ts +1 -1
- package/src/cli/commands/ticker.ts +192 -47
- package/src/cli/context.ts +3 -2
- package/src/cli/desktop-pane-shot-view.ts +2 -2
- package/src/cli/desktop-pane-shot.ts +1 -1
- package/src/cli/earnings-calendar.ts +4 -4
- package/src/cli/entry.ts +18 -1
- package/src/cli/errors.ts +20 -2
- package/src/cli/exit-codes.ts +1 -1
- package/src/cli/export-source.ts +10 -4
- package/src/cli/freshness.ts +2 -8
- package/src/cli/fx-forward.ts +130 -0
- package/src/cli/fx-pair.ts +10 -9
- package/src/cli/glossary.ts +64 -20
- package/src/cli/help.ts +19 -13
- package/src/cli/history-export.ts +126 -0
- package/src/cli/history-rows.ts +196 -43
- package/src/cli/index-roots.ts +5 -45
- package/src/cli/index.ts +24 -18
- package/src/cli/known-gaps.ts +118 -25
- package/src/cli/listing-arg.ts +300 -41
- package/src/cli/name-entries.ts +30 -18
- package/src/cli/native-smoke.ts +2 -0
- package/src/cli/not-a-ticker.ts +19 -9
- package/src/cli/opentui-libc.ts +5 -0
- package/src/cli/options.ts +12 -0
- package/src/cli/pane-functions/capabilities.ts +75 -20
- package/src/cli/pane-functions/catalog.ts +24 -23
- package/src/cli/pane-functions/discovery.ts +1 -1
- package/src/cli/pane-functions/dom.ts +10 -7
- package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
- package/src/cli/pane-functions/freshness.ts +174 -23
- package/src/cli/pane-functions/function-help.ts +66 -27
- package/src/cli/pane-functions/headless.ts +24 -11
- package/src/cli/pane-functions/index.ts +18 -16
- package/src/cli/pane-functions/options.ts +22 -2
- package/src/cli/pane-functions/report-notices.ts +12 -4
- package/src/cli/pane-functions/report.ts +5 -5
- package/src/cli/pane-functions/resolver.ts +47 -10
- package/src/cli/pane-functions/screenshot-assessment.ts +322 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +152 -0
- package/src/cli/pane-functions/screenshot-chart-evidence.ts +396 -0
- package/src/cli/pane-functions/screenshot-data-evidence.ts +383 -0
- package/src/cli/pane-functions/screenshot-evidence.ts +1 -1
- package/src/cli/pane-functions/screenshot-payload.ts +453 -0
- package/src/cli/pane-functions/screenshot-render.ts +316 -0
- package/src/cli/pane-functions/screenshot-types.ts +162 -0
- package/src/cli/pane-functions/shot-options.ts +8 -3
- package/src/cli/peso-dollar-rates.ts +79 -0
- package/src/cli/quote-answer.ts +38 -3
- package/src/cli/report-tables.ts +61 -16
- package/src/cli/restore-plugins.ts +29 -14
- package/src/cli/result.ts +111 -22
- package/src/cli/series-search.ts +5 -5
- package/src/cli/statement-filings.ts +19 -0
- package/src/cli/suffixed-listing-rows.ts +49 -0
- package/src/cli/symbol-args.ts +50 -15
- package/src/cli/symbol-cells.ts +120 -0
- package/src/cli/symbol-list.ts +198 -68
- package/src/cli/symbol-rows.ts +131 -25
- package/src/cli/ticker-resolution.ts +108 -11
- package/src/cli/treasury-yields.ts +5 -5
- package/src/cli/types.ts +2 -0
- package/src/cli/uncovered-instrument.ts +3 -3
- package/src/cli/unknown-command.ts +6 -6
- package/src/components/chart/composite/chart-layout.ts +131 -0
- package/src/components/chart/composite/composite-chart.tsx +373 -16
- package/src/components/chart/composite/interactions.ts +7 -2
- package/src/components/chart/composite/legend.tsx +75 -3
- package/src/components/chart/composite/levels.ts +5 -0
- package/src/components/chart/composite/panel-gestures.ts +41 -0
- package/src/components/chart/composite/panel-labels.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +305 -11
- package/src/components/chart/composite/price-axis-labels.tsx +27 -3
- package/src/components/chart/composite/rasterizer.ts +44 -19
- package/src/components/chart/composite/scene.ts +16 -4
- package/src/components/chart/composite/text-renderer.ts +48 -21
- package/src/components/chart/composite/toolbar.tsx +52 -5
- package/src/components/chart/composite/types.ts +13 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +120 -13
- package/src/components/chart/composite/use-composite-value-scales.ts +44 -0
- package/src/components/chart/composite/use-composite-viewport.ts +12 -3
- package/src/components/chart/composite/value-scale.ts +149 -0
- package/src/components/chart/composite/volume-profile-paint.ts +6 -3
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart-table/index.ts +2 -0
- package/src/components/chart-table/read.tsx +74 -0
- package/src/components/command-bar/commands/collection-list.ts +41 -19
- package/src/components/command-bar/commands/collection.ts +9 -3
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/commands/direct/runtime.ts +3 -2
- package/src/components/command-bar/pane-templates/items.ts +59 -6
- package/src/components/command-bar/pane-templates/workflow.ts +10 -1
- package/src/components/command-bar/routes/root/plugin-install.ts +1 -1
- package/src/components/command-bar/routes/root/provider-search.ts +42 -11
- package/src/components/command-bar/routes/root/results.ts +18 -4
- package/src/components/command-bar/routes/root/runtime.ts +15 -3
- package/src/components/command-bar/routes/root/search-providers.ts +25 -2
- package/src/components/command-bar/routes/root/selection.ts +6 -1
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +4 -1
- package/src/components/command-bar/routes/root/shortcuts.ts +30 -3
- package/src/components/command-bar/routes/ticker-search/results.ts +73 -4
- package/src/components/command-bar/selection-runtime.ts +50 -10
- package/src/components/command-bar/surface/index.tsx +24 -2
- package/src/components/command-bar/view-model.ts +7 -0
- package/src/components/command-bar/workflow/broker.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +46 -7
- package/src/components/command-bar/workflow/collection-ticker.ts +32 -4
- package/src/components/command-bar/workflow/fields.ts +8 -2
- package/src/components/command-bar/workflow/ops.ts +18 -4
- package/src/components/command-bar/workflow/portfolio-position.ts +53 -4
- package/src/components/command-bar/workflow/submit.ts +3 -2
- package/src/components/command-bar/workflow/tickers.ts +5 -1
- package/src/components/company-logo.tsx +39 -0
- package/src/components/data-table/sections.ts +4 -2
- package/src/components/form-modal/confirm.tsx +2 -1
- package/src/components/form-modal/request.ts +5 -0
- package/src/components/form-modal/routes.ts +4 -1
- package/src/components/index.ts +12 -6
- package/src/components/layout/header.tsx +7 -6
- package/src/components/layout/market-summary.ts +9 -6
- package/src/components/layout/pane/capture-status.ts +1 -1
- package/src/components/layout/pane/footer/model.ts +11 -5
- package/src/components/layout/shell/chrome.ts +19 -6
- package/src/components/layout/shell/index.tsx +2 -1
- package/src/components/layout/shell/pane/layers.tsx +19 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/market-board/index.tsx +11 -3
- package/src/components/onboarding/onboarding-wizard.tsx +1 -0
- package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +2 -2
- package/src/components/onboarding/wizard-broker-sync.ts +4 -1
- package/src/components/pane-settings-dialog/desktop.tsx +20 -7
- package/src/components/pane-settings-dialog/tui.tsx +17 -9
- package/src/components/pane-settings-dialog/value.ts +8 -1
- package/src/components/pane-settings-dialog.tsx +44 -13
- package/src/components/sign-in-code-panel.tsx +27 -19
- package/src/components/ui/column-grid.tsx +48 -4
- package/src/components/ui/country-flag-art.tsx +180 -0
- package/src/components/ui/country-flag.tsx +118 -12
- package/src/components/ui/data-table/opentui/index.tsx +61 -2
- package/src/components/ui/data-table/opentui/model.ts +109 -2
- package/src/components/ui/data-table/types.ts +1 -1
- package/src/components/ui/display.tsx +5 -1
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/logo-mark.tsx +11 -2
- package/src/components/ui/query-bar.tsx +1 -1
- package/src/components/ui/row-notes.ts +79 -0
- package/src/core/state/app/layout.ts +4 -4
- package/src/data/cached-query.ts +8 -1
- package/src/data/config/chart-settings.ts +3 -3
- package/src/data/config/home.ts +10 -2
- package/src/data/config/layout.ts +4 -1
- package/src/data/config/store/node.ts +4 -0
- package/src/data/config/store/normalize.ts +13 -3
- package/src/data/config/store/portfolio-as-of-column.ts +2 -2
- package/src/data/json-storage.ts +2 -0
- package/src/data/saved-copies.ts +84 -0
- package/src/docs/snapshot.json +65 -32
- package/src/i18n/es.ts +29 -12
- package/src/i18n/ja.ts +29 -12
- package/src/i18n/ko.ts +29 -12
- package/src/i18n/zh-cn.ts +29 -12
- package/src/i18n/zh-tw.ts +29 -12
- package/src/layout/desks.ts +1 -1
- package/src/layout/launch-layout.ts +1 -1
- package/src/layout/pane-manager/floating-actions.ts +5 -3
- package/src/layout/pane-manager/floating.ts +43 -1
- package/src/layout/pane-manager/index.ts +2 -0
- package/src/layout/selected-ticker.ts +74 -0
- package/src/market-data/depositary-pair.ts +251 -0
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +124 -40
- package/src/market-data/fx-closes.ts +17 -2
- package/src/market-data/hooks.tsx +19 -11
- package/src/market-data/market/change-basis.ts +49 -7
- package/src/market-data/market/freshness.ts +34 -5
- package/src/market-data/market/period-return.ts +163 -0
- package/src/market-data/market/trading-sessions.ts +13 -0
- package/src/market-data/market/year-to-date.ts +8 -13
- package/src/market-data/performance.ts +20 -14
- package/src/market-data/perpetual-premium.ts +32 -0
- package/src/market-data/published-apac-sessions.ts +2 -1
- package/src/market-data/published-futures-sessions.ts +69 -0
- package/src/market-data/quotes/contributions.ts +5 -1
- package/src/market-data/quotes/freshness.ts +42 -0
- package/src/market-data/quotes/intraday-bar.ts +37 -0
- package/src/market-data/quotes/regular-session-valuation.ts +13 -8
- package/src/market-data/quotes/resolution.ts +4 -2
- package/src/market-data/tax-equivalent.ts +1 -1
- package/src/market-data/ticker-price-returns.ts +3 -1
- package/src/news/aggregator.ts +5 -1
- package/src/news/news-model.ts +4 -0
- package/src/news/types.ts +11 -0
- package/src/notifications/app-notifier.ts +1 -1
- package/src/plugins/auto-update.ts +2 -2
- package/src/plugins/builtin/account-management/index.tsx +1 -0
- package/src/plugins/builtin/account-management/pane.tsx +9 -8
- package/src/plugins/builtin/alerts/alert-engine.ts +29 -19
- package/src/plugins/builtin/alerts/calendar-events.ts +378 -0
- package/src/plugins/builtin/alerts/cli.ts +485 -76
- package/src/plugins/builtin/alerts/command.ts +5 -10
- package/src/plugins/builtin/alerts/event-rows.ts +9 -5
- package/src/plugins/builtin/alerts/events-pane.tsx +71 -56
- package/src/plugins/builtin/alerts/events.ts +107 -5
- package/src/plugins/builtin/alerts/fired-log.ts +78 -0
- package/src/plugins/builtin/alerts/format.ts +2 -1
- package/src/plugins/builtin/alerts/headless.ts +100 -39
- package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
- package/src/plugins/builtin/alerts/history-rows.ts +89 -0
- package/src/plugins/builtin/alerts/index.tsx +45 -18
- package/src/plugins/builtin/alerts/iv-level.ts +365 -0
- package/src/plugins/builtin/alerts/levels.ts +3 -3
- package/src/plugins/builtin/alerts/pane.tsx +97 -29
- package/src/plugins/builtin/alerts/price.ts +35 -0
- package/src/plugins/builtin/alerts/quotes.ts +24 -0
- package/src/plugins/builtin/alerts/research-builder.ts +84 -11
- package/src/plugins/builtin/alerts/research-rules.ts +76 -3
- package/src/plugins/builtin/alerts/scheduled-check.ts +206 -32
- package/src/plugins/builtin/alerts/scheduled-sources.ts +26 -3
- package/src/plugins/builtin/alerts/scheduled.ts +188 -29
- package/src/plugins/builtin/alerts/storage.ts +13 -10
- package/src/plugins/builtin/alerts/targets.ts +12 -3
- package/src/plugins/builtin/analytics/index.tsx +8 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -1
- package/src/plugins/builtin/analytics/risk-book.ts +215 -0
- package/src/plugins/builtin/analytics/risk-client.ts +13 -6
- package/src/plugins/builtin/analytics/risk-headless.ts +55 -21
- package/src/plugins/builtin/analytics/risk-math.ts +6 -3
- package/src/plugins/builtin/analytics/risk-model.ts +297 -104
- package/src/plugins/builtin/analytics/risk-options.ts +17 -2
- package/src/plugins/builtin/analytics/risk-pane.tsx +97 -36
- package/src/plugins/builtin/attention/pane.tsx +6 -2
- package/src/plugins/builtin/awards/headless.ts +7 -3
- package/src/plugins/builtin/awards/model.ts +18 -0
- package/src/plugins/builtin/awards/pane.tsx +6 -3
- package/src/plugins/builtin/backtest/headless.ts +3 -3
- package/src/plugins/builtin/backtest/rules.ts +41 -5
- package/src/plugins/builtin/broker-manager/add-flow.tsx +12 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +47 -19
- package/src/plugins/builtin/broker-manager/footer.ts +27 -11
- package/src/plugins/builtin/broker-manager/index.tsx +21 -12
- package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
- package/src/plugins/builtin/broker-manager/model.ts +12 -2
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +183 -57
- package/src/plugins/builtin/broker-manager/table.ts +28 -12
- package/src/plugins/builtin/broker-trading/account-picker.tsx +2 -1
- package/src/plugins/builtin/broker-trading/chart-levels.ts +82 -13
- package/src/plugins/builtin/broker-trading/controller.ts +45 -13
- package/src/plugins/builtin/broker-trading/dock.tsx +22 -4
- package/src/plugins/builtin/broker-trading/headless.ts +2 -1
- package/src/plugins/builtin/broker-trading/index.tsx +7 -1
- package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +75 -7
- package/src/plugins/builtin/broker-trading/orders.tsx +109 -31
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +9 -3
- package/src/plugins/builtin/broker-trading/pane-model.ts +30 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +110 -23
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
- package/src/plugins/builtin/broker-trading/ticket.tsx +112 -31
- package/src/plugins/builtin/buildout/cells.tsx +4 -0
- package/src/plugins/builtin/buildout/detail/index.tsx +5 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +7 -4
- package/src/plugins/builtin/buildout/pane/index.tsx +25 -12
- package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/detail.tsx +3 -2
- package/src/plugins/builtin/catalysts/index.tsx +1 -1
- package/src/plugins/builtin/catalysts/model.ts +49 -4
- package/src/plugins/builtin/catalysts/pane.tsx +6 -8
- package/src/plugins/builtin/central-bank-rates/client.ts +5 -0
- package/src/plugins/builtin/central-bank-rates/headless.ts +36 -10
- package/src/plugins/builtin/central-bank-rates/model.ts +33 -8
- package/src/plugins/builtin/changelog/index.tsx +1 -0
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +1 -0
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +2 -1
- package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +2 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +4 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +28 -26
- package/src/plugins/builtin/chart-composer/headless.ts +253 -35
- package/src/plugins/builtin/chart-composer/index.tsx +12 -2
- package/src/plugins/builtin/chart-composer/leverage-overlay.ts +142 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +127 -28
- package/src/plugins/builtin/chart-composer/series-catalog.ts +1 -0
- package/src/plugins/builtin/chart-composer/series-expression.ts +65 -6
- package/src/plugins/builtin/chart-composer/session-history.ts +15 -21
- package/src/plugins/builtin/chart-composer/session-range.ts +4 -2
- package/src/plugins/builtin/chart-composer/settings.ts +31 -19
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +5 -6
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +18 -8
- package/src/plugins/builtin/chat/attachments/model.ts +3 -1
- package/src/plugins/builtin/chat/content/index.tsx +1 -1
- package/src/plugins/builtin/chat/pane-links/open.ts +1 -1
- package/src/plugins/builtin/chat/people.ts +1 -1
- package/src/plugins/builtin/clinical-trials/client.ts +5 -2
- package/src/plugins/builtin/clinical-trials/model.ts +13 -4
- package/src/plugins/builtin/clinical-trials/pane.tsx +32 -12
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +17 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +1 -1
- package/src/plugins/builtin/cloud/askg/lock-note.ts +1 -1
- package/src/plugins/builtin/cloud/askg/tool-display.ts +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +3 -3
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +105 -47
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +42 -4
- package/src/plugins/builtin/cloud/team/pane.tsx +204 -64
- package/src/plugins/builtin/cloud/thesis/store.ts +45 -22
- package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
- package/src/plugins/builtin/commodities-energy/index.tsx +29 -2
- package/src/plugins/builtin/commodities-energy/view-state.ts +2 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +11 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +7 -5
- package/src/plugins/builtin/company-attention/building-model.ts +222 -0
- package/src/plugins/builtin/company-attention/building.tsx +172 -0
- package/src/plugins/builtin/company-attention/client.ts +117 -1
- package/src/plugins/builtin/company-attention/evidence.ts +16 -3
- package/src/plugins/builtin/company-attention/headless.ts +101 -5
- package/src/plugins/builtin/company-attention/index.tsx +6 -3
- package/src/plugins/builtin/company-attention/model.ts +33 -7
- package/src/plugins/builtin/company-attention/moves-model.ts +253 -0
- package/src/plugins/builtin/company-attention/moves.tsx +159 -0
- package/src/plugins/builtin/company-attention/pane.tsx +79 -23
- package/src/plugins/builtin/company-attention/signals-model.ts +229 -0
- package/src/plugins/builtin/company-attention/signals.tsx +160 -0
- package/src/plugins/builtin/company-kpis/headless.ts +3 -2
- package/src/plugins/builtin/company-kpis/model.ts +2 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +1 -1
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/congress-trades/headless.ts +1 -1
- package/src/plugins/builtin/connections/index.ts +1 -0
- package/src/plugins/builtin/correlation/headless-schema.ts +3 -2
- package/src/plugins/builtin/correlation/headless.ts +5 -3
- package/src/plugins/builtin/cot/headless.ts +28 -6
- package/src/plugins/builtin/cot/model.ts +12 -0
- package/src/plugins/builtin/cot/pane.tsx +3 -2
- package/src/plugins/builtin/cpi/headless.ts +5 -1
- package/src/plugins/builtin/cpi/pane.tsx +11 -9
- package/src/plugins/builtin/credit-boards/model.ts +2 -0
- package/src/plugins/builtin/credit-documents/headless.ts +1 -1
- package/src/plugins/builtin/crypto-board/client.ts +86 -0
- package/src/plugins/builtin/crypto-board/defi-view.tsx +218 -0
- package/src/plugins/builtin/crypto-board/defi.ts +239 -0
- package/src/plugins/builtin/crypto-board/headless.ts +117 -6
- package/src/plugins/builtin/crypto-board/index.tsx +8 -3
- package/src/plugins/builtin/crypto-board/model.ts +40 -8
- package/src/plugins/builtin/crypto-board/pane.tsx +55 -21
- package/src/plugins/builtin/crypto-board/series.ts +19 -1
- package/src/plugins/builtin/crypto-etf-flows/client.ts +60 -0
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +132 -0
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +44 -0
- package/src/plugins/builtin/crypto-etf-flows/model.ts +121 -0
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +193 -0
- package/src/plugins/builtin/debug/index.tsx +1 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/docs/index.tsx +1 -1
- package/src/plugins/builtin/docs/pane.tsx +2 -2
- package/src/plugins/builtin/docs/targets.ts +1 -1
- package/src/plugins/builtin/doe/client.ts +5 -1
- package/src/plugins/builtin/doe/headless.ts +12 -6
- package/src/plugins/builtin/doe/model.ts +43 -6
- package/src/plugins/builtin/doe/pane.tsx +18 -11
- package/src/plugins/builtin/earnings/board-model.ts +13 -6
- package/src/plugins/builtin/earnings/board-table.ts +4 -1
- package/src/plugins/builtin/earnings/board.tsx +253 -50
- package/src/plugins/builtin/earnings/calendar-model.ts +188 -0
- package/src/plugins/builtin/earnings/calendar.tsx +338 -0
- package/src/plugins/builtin/earnings/client.ts +4 -3
- package/src/plugins/builtin/earnings/headless.ts +53 -15
- package/src/plugins/builtin/earnings/history-model.ts +53 -36
- package/src/plugins/builtin/earnings/index.tsx +15 -22
- package/src/plugins/builtin/earnings/model.ts +17 -12
- package/src/plugins/builtin/earnings/scope.ts +89 -0
- package/src/plugins/builtin/earnings-calls/calendar.tsx +310 -0
- package/src/plugins/builtin/earnings-calls/data.ts +146 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +151 -9
- package/src/plugins/builtin/earnings-calls/index.tsx +3 -3
- package/src/plugins/builtin/earnings-calls/lock-note.ts +1 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +149 -31
- package/src/plugins/builtin/earnings-calls/upcoming.ts +195 -0
- package/src/plugins/builtin/earnings-ripple/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +132 -20
- package/src/plugins/builtin/econ/detail-view.tsx +48 -10
- package/src/plugins/builtin/econ/fred-catalog.ts +23 -6
- package/src/plugins/builtin/econ/fred-series-map.ts +4 -0
- package/src/plugins/builtin/econ/index.tsx +112 -25
- package/src/plugins/builtin/econ/print-watch.ts +2 -1
- package/src/plugins/builtin/econ/types.ts +32 -1
- package/src/plugins/builtin/econ-statistics/client.ts +16 -3
- package/src/plugins/builtin/econ-statistics/defs.ts +4 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +2 -2
- package/src/plugins/builtin/econ-statistics/headless.ts +13 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +3 -3
- package/src/plugins/builtin/econ-statistics/pane.tsx +2 -1
- package/src/plugins/builtin/econ-statistics/stats.ts +103 -0
- package/src/plugins/builtin/econ-statistics/view.ts +16 -1
- package/src/plugins/builtin/equity-screener/client.ts +21 -3
- package/src/plugins/builtin/equity-screener/headless.ts +68 -6
- package/src/plugins/builtin/equity-screener/live.ts +1 -1
- package/src/plugins/builtin/equity-screener/model.ts +6 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +45 -10
- package/src/plugins/builtin/equity-screener/screen-check.ts +90 -0
- package/src/plugins/builtin/equity-screener/ticker-check.tsx +148 -0
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -1
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +3 -3
- package/src/plugins/builtin/estimate-revisions/model.ts +8 -10
- package/src/plugins/builtin/exposure/headless.ts +51 -6
- package/src/plugins/builtin/exposure/index.tsx +28 -2
- package/src/plugins/builtin/exposure/model.ts +7 -0
- package/src/plugins/builtin/exposure/pane.tsx +6 -1
- package/src/plugins/builtin/fear-greed/index.tsx +1 -1
- package/src/plugins/builtin/filing-events/index.tsx +1 -1
- package/src/plugins/builtin/futures/model.ts +10 -9
- package/src/plugins/builtin/futures/returns.ts +8 -36
- package/src/plugins/builtin/futures/table.tsx +1 -1
- package/src/plugins/builtin/futures-curve/client.ts +40 -9
- package/src/plugins/builtin/futures-curve/headless.ts +39 -17
- package/src/plugins/builtin/futures-curve/model.ts +258 -69
- package/src/plugins/builtin/futures-curve/pane.tsx +46 -17
- package/src/plugins/builtin/fx-matrix/client.ts +15 -8
- package/src/plugins/builtin/fx-matrix/headless.ts +32 -14
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -11
- package/src/plugins/builtin/fx-matrix/live-legs.ts +22 -0
- package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
- package/src/plugins/builtin/gpu/headless.ts +9 -4
- package/src/plugins/builtin/help/index.tsx +13 -3
- package/src/plugins/builtin/holders/beneficial-model.ts +8 -11
- package/src/plugins/builtin/holders/beneficial-report.ts +6 -5
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/client.ts +13 -1
- package/src/plugins/builtin/holders/format.ts +28 -17
- package/src/plugins/builtin/holders/headless.ts +46 -7
- package/src/plugins/builtin/holders/index-holders.ts +264 -0
- package/src/plugins/builtin/holders/pane.tsx +79 -15
- package/src/plugins/builtin/holders/report-rows.ts +35 -7
- package/src/plugins/builtin/holders/table-model.ts +2 -1
- package/src/plugins/builtin/holders/treemap.tsx +2 -2
- package/src/plugins/builtin/insider/headless.ts +1 -1
- package/src/plugins/builtin/insider/signals-client.ts +1 -1
- package/src/plugins/builtin/insider/signals.tsx +1 -1
- package/src/plugins/builtin/ipo-calendar/pane.tsx +7 -4
- package/src/plugins/builtin/iv-history/client.ts +20 -0
- package/src/plugins/builtin/iv-history/headless.ts +4 -4
- package/src/plugins/builtin/iv-history/index.tsx +1 -1
- package/src/plugins/builtin/iv-history/model.ts +13 -4
- package/src/plugins/builtin/iv-history/series.ts +3 -1
- package/src/plugins/builtin/jobs/lock-note.ts +1 -1
- package/src/plugins/builtin/jobs/model.ts +4 -15
- package/src/plugins/builtin/jobs/pane.tsx +7 -5
- package/src/plugins/builtin/jobs/share-bars.tsx +11 -5
- package/src/plugins/builtin/jodi/pane.tsx +10 -4
- package/src/plugins/builtin/layout-manager/index.tsx +1 -0
- package/src/plugins/builtin/macro-day/headless.ts +20 -7
- package/src/plugins/builtin/macro-day/index.tsx +3 -2
- package/src/plugins/builtin/macro-day/model.ts +29 -8
- package/src/plugins/builtin/macro-day/pane.tsx +46 -10
- package/src/plugins/builtin/macro-day/releases.ts +55 -13
- package/src/plugins/builtin/market-heatmap/data.ts +9 -2
- package/src/plugins/builtin/market-heatmap/evidence.ts +1 -1
- package/src/plugins/builtin/market-heatmap/headless.ts +9 -6
- package/src/plugins/builtin/market-heatmap/index.tsx +3 -3
- package/src/plugins/builtin/market-heatmap/model.ts +10 -7
- package/src/plugins/builtin/market-movers/client.ts +33 -19
- package/src/plugins/builtin/market-movers/headless.ts +8 -2
- package/src/plugins/builtin/market-movers/index.tsx +16 -10
- package/src/plugins/builtin/market-movers/screener.ts +29 -8
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -2
- package/src/plugins/builtin/market-movers/session-table.ts +4 -2
- package/src/plugins/builtin/market-movers/table.ts +4 -1
- package/src/plugins/builtin/market-valuation/headless.ts +2 -0
- package/src/plugins/builtin/market-valuation/view.ts +5 -0
- package/src/plugins/builtin/members/headless.ts +6 -5
- package/src/plugins/builtin/members/pane.tsx +11 -4
- package/src/plugins/builtin/mna/client.ts +1 -0
- package/src/plugins/builtin/mna/detail.tsx +20 -32
- package/src/plugins/builtin/mna/distress/detail.tsx +16 -4
- package/src/plugins/builtin/mna/distress/model.ts +3 -2
- package/src/plugins/builtin/mna/distress/pane.tsx +7 -4
- package/src/plugins/builtin/mna/headless.ts +6 -13
- package/src/plugins/builtin/mna/model.ts +93 -15
- package/src/plugins/builtin/mna/pane.tsx +5 -4
- package/src/plugins/builtin/news/headless.ts +12 -6
- package/src/plugins/builtin/news/index.tsx +10 -1
- package/src/plugins/builtin/news/wire/index.tsx +3 -2
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +26 -10
- package/src/plugins/builtin/news/wire/news/query-presets.ts +6 -2
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -3
- package/src/plugins/builtin/notes/cli.ts +49 -14
- package/src/plugins/builtin/openfda/pane.tsx +1 -1
- package/src/plugins/builtin/options/carry.ts +2 -2
- package/src/plugins/builtin/options/index.tsx +1 -1
- package/src/plugins/builtin/options/view.tsx +10 -10
- package/src/plugins/builtin/options-calculator/headless.ts +46 -7
- package/src/plugins/builtin/options-scenario/client.ts +123 -28
- package/src/plugins/builtin/options-scenario/collar.ts +8 -6
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +127 -0
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +165 -0
- package/src/plugins/builtin/options-scenario/csp-screen.ts +258 -0
- package/src/plugins/builtin/options-scenario/csp.ts +101 -21
- package/src/plugins/builtin/options-scenario/headless.ts +36 -15
- package/src/plugins/builtin/options-scenario/hedge.ts +20 -14
- package/src/plugins/builtin/options-scenario/index.tsx +28 -3
- package/src/plugins/builtin/options-scenario/model.ts +13 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +9 -5
- package/src/plugins/builtin/perps/equity-row.tsx +11 -3
- package/src/plugins/builtin/perps/freshness.ts +8 -5
- package/src/plugins/builtin/perps/headless.ts +3 -1
- package/src/plugins/builtin/perps/list-perpetuals.ts +25 -4
- package/src/plugins/builtin/perps/live-rows.ts +161 -0
- package/src/plugins/builtin/perps/markets-section.tsx +154 -0
- package/src/plugins/builtin/perps/pane.tsx +36 -17
- package/src/plugins/builtin/perps/venue-markets.ts +96 -0
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -1
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +143 -59
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +72 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
- package/src/plugins/builtin/portfolio-list/cli/render.ts +49 -20
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +3 -3
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +29 -24
- package/src/plugins/builtin/portfolio-list/headless.ts +6 -3
- package/src/plugins/builtin/portfolio-list/income.ts +13 -2
- package/src/plugins/builtin/portfolio-list/index.tsx +9 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +21 -8
- package/src/plugins/builtin/portfolio-list/pane/purchase-date-fx.ts +46 -0
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +16 -9
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
- package/src/plugins/builtin/power/client.ts +4 -1
- package/src/plugins/builtin/power/evidence.ts +22 -7
- package/src/plugins/builtin/power/gas.tsx +5 -1
- package/src/plugins/builtin/power/pane.tsx +8 -1
- package/src/plugins/builtin/power/prices.tsx +10 -3
- package/src/plugins/builtin/research/analyst-headless.ts +25 -8
- package/src/plugins/builtin/research/analyst-model.ts +68 -3
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +63 -14
- package/src/plugins/builtin/research/event-model.ts +3 -1
- package/src/plugins/builtin/research/event-sources.ts +12 -4
- package/src/plugins/builtin/research/index.tsx +2 -1
- package/src/plugins/builtin/research/relative-valuation-basis.ts +58 -0
- package/src/plugins/builtin/research/relative-valuation-columns.ts +31 -2
- package/src/plugins/builtin/research/relative-valuation-currency.ts +12 -5
- package/src/plugins/builtin/research/relative-valuation-headless.ts +84 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +88 -19
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +76 -9
- package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +6 -2
- package/src/plugins/builtin/risk-factors/index.tsx +2 -2
- package/src/plugins/builtin/scanner/lock-note.ts +1 -1
- package/src/plugins/builtin/sec/forms.ts +13 -0
- package/src/plugins/builtin/sec/fund-abroad.ts +32 -0
- package/src/plugins/builtin/sec/fund-facts.ts +68 -8
- package/src/plugins/builtin/sec/headless.ts +85 -8
- package/src/plugins/builtin/sec/index.tsx +58 -12
- package/src/plugins/builtin/sectors/client.ts +2 -1
- package/src/plugins/builtin/sectors/sector-model.ts +11 -15
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
- package/src/plugins/builtin/shared/cloud-upgrade.ts +34 -6
- package/src/plugins/builtin/shared/earnings-calendar.ts +375 -0
- package/src/plugins/builtin/{earnings/estimate-basis.ts → shared/earnings-estimate-basis.ts} +1 -1
- package/src/plugins/builtin/shared/earnings-report.ts +3 -3
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +23 -1
- package/src/plugins/builtin/shared/job-posting.ts +129 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +3 -3
- package/src/plugins/builtin/shared/report-freshness.ts +3 -0
- package/src/plugins/builtin/shared/research-trading.tsx +21 -0
- package/src/plugins/builtin/shared/series-cache.ts +2 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
- package/src/plugins/builtin/short-interest/client.ts +2 -0
- package/src/plugins/builtin/short-interest/headless.ts +41 -12
- package/src/plugins/builtin/short-interest/index.tsx +23 -1
- package/src/plugins/builtin/short-interest/model.ts +70 -19
- package/src/plugins/builtin/short-interest/pane.tsx +19 -2
- package/src/plugins/builtin/short-interest/publications-client.ts +82 -0
- package/src/plugins/builtin/short-interest/publications-headless.ts +29 -0
- package/src/plugins/builtin/short-interest/publications-model.ts +50 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +66 -0
- package/src/plugins/builtin/short-interest/schedule.ts +35 -2
- package/src/plugins/builtin/short-interest/surface.tsx +11 -4
- package/src/plugins/builtin/short-interest/types.ts +9 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +14 -8
- package/src/plugins/builtin/short-interest/watch-model.ts +12 -25
- package/src/plugins/builtin/short-interest/watch-pane.tsx +5 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +33 -9
- package/src/plugins/builtin/supply-chain/model.ts +51 -3
- package/src/plugins/builtin/supply-chain/pane.tsx +78 -39
- package/src/plugins/builtin/supply-chain/row-evidence.tsx +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -5
- package/src/plugins/builtin/themes/model.ts +14 -4
- package/src/plugins/builtin/themes/pane.tsx +24 -5
- package/src/plugins/builtin/thirteenf/headless.ts +5 -5
- package/src/plugins/builtin/thirteenf/signals.ts +3 -1
- package/src/plugins/builtin/ticker-detail/adr-pair/headless.ts +123 -0
- package/src/plugins/builtin/ticker-detail/adr-pair/model.ts +132 -0
- package/src/plugins/builtin/ticker-detail/adr-pair/tab.tsx +87 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +40 -26
- package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +8 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +67 -3
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +112 -5
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +17 -2
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +21 -7
- package/src/plugins/builtin/ticker-detail/financials/model.ts +36 -1
- package/src/plugins/builtin/ticker-detail/financials/offerings.ts +56 -0
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +62 -13
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +30 -4
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +14 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +148 -35
- package/src/plugins/builtin/ticker-detail/index.tsx +37 -5
- package/src/plugins/builtin/ticker-detail/overview/model.ts +65 -10
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +9 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +68 -11
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -0
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +8 -3
- package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
- package/src/plugins/builtin/vol-surface/client.ts +5 -4
- package/src/plugins/builtin/vol-surface/headless.ts +12 -3
- package/src/plugins/builtin/vol-surface/model.ts +4 -2
- package/src/plugins/builtin/volatility/model.ts +5 -10
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +4 -3
- package/src/plugins/builtin/world-venue-map/basemap.ts +3 -1
- package/src/plugins/builtin/world-venue-map/chokepoint-chart.ts +40 -0
- package/src/plugins/builtin/world-venue-map/evidence.ts +1 -1
- package/src/plugins/builtin/world-venue-map/geo-series.ts +90 -11
- package/src/plugins/builtin/world-venue-map/headless.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +3 -26
- package/src/plugins/builtin/world-venue-map/layers.ts +1 -1
- package/src/plugins/builtin/world-venue-map/map.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/pane.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/venue-header.tsx +15 -4
- package/src/plugins/builtin/yield-curve/client.ts +25 -0
- package/src/plugins/builtin/yield-curve/curves.ts +7 -4
- package/src/plugins/builtin/yield-curve/headless.ts +38 -4
- package/src/plugins/builtin/yield-curve/history.ts +4 -3
- package/src/plugins/builtin/yield-curve/index.tsx +12 -6
- package/src/plugins/builtin/yield-curve/swaps-model.ts +93 -0
- package/src/plugins/builtin/yield-curve/swaps.tsx +99 -0
- package/src/plugins/bundle.ts +1 -1
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/compat.ts +3 -3
- package/src/plugins/dependencies.ts +75 -14
- package/src/plugins/extracted-plugins.ts +132 -34
- package/src/plugins/host-link.ts +149 -34
- package/src/plugins/installer.ts +35 -10
- package/src/plugins/loader.ts +206 -38
- package/src/plugins/manager-node.ts +2 -2
- package/src/plugins/marketplace/deactivation.ts +22 -0
- package/src/plugins/marketplace/model.ts +2 -2
- package/src/plugins/marketplace/packs.ts +1 -1
- package/src/plugins/ownership.ts +1 -1
- package/src/plugins/peer-retry.ts +155 -0
- package/src/plugins/registry/contributions.ts +3 -1
- package/src/plugins/registry/index.ts +4 -3
- package/src/plugins/registry/plugin-state.ts +3 -0
- package/src/plugins/seed-notice.ts +20 -0
- package/src/plugins/shortcut-argument.ts +39 -0
- package/src/portfolio/allocation.ts +1 -1
- package/src/portfolio/collection-name.ts +31 -0
- package/src/portfolio/column-values.ts +10 -8
- package/src/portfolio/cost-fx.ts +296 -19
- package/src/portfolio/manual-lots.ts +75 -0
- package/src/portfolio/mutations.ts +60 -22
- package/src/portfolio/suffixed-listings.ts +209 -0
- package/src/portfolio/totals-currency.ts +24 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +14 -0
- package/src/public/time-series.ts +11 -8
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/config-host.ts +3 -0
- package/src/renderers/browser/main.tsx +23 -3
- package/src/renderers/dom/boot-frame-page.ts +187 -0
- package/src/renderers/dom/boot-frame.ts +237 -0
- package/src/renderers/dom/build-assets.ts +46 -8
- package/src/renderers/dom/dom-ui-host.tsx +4 -29
- package/src/renderers/dom/host/ascii-text.ts +16 -2
- package/src/renderers/dom/host/box.tsx +43 -2
- package/src/renderers/dom/host/chart-surface.tsx +2 -0
- package/src/renderers/dom/host/hls.chunk.ts +3 -0
- package/src/renderers/dom/host/image-surface.tsx +49 -0
- package/src/renderers/dom/host/media-surface.tsx +24 -18
- package/src/renderers/dom/host/mouse.ts +30 -0
- package/src/renderers/dom/host/size-observer.ts +1 -1
- package/src/renderers/dom/host/style.ts +1 -1
- package/src/renderers/dom/styles.css +76 -0
- package/src/renderers/dom/test-utils.tsx +2 -1
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +46 -3
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +1 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -3
- package/src/renderers/electrobun/bun/desktop/initialization.ts +81 -18
- package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
- package/src/renderers/electrobun/bun/index.ts +11 -1
- package/src/renderers/electrobun/bun/window/boot-frame.ts +67 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -1
- package/src/renderers/electrobun/shared/boot-frame.ts +7 -0
- package/src/renderers/electrobun/shared/protocol.ts +6 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/config-host.ts +3 -0
- package/src/renderers/electrobun/view/fatal-screen.tsx +1 -1
- package/src/renderers/electrobun/view/main.tsx +30 -4
- package/src/renderers/electrobun/view/news-source.ts +5 -5
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/browser-open.ts +88 -0
- package/src/renderers/opentui/host.tsx +5 -14
- package/src/renderers/opentui/image/loader.ts +3 -2
- package/src/renderers/opentui/native-smoke.ts +10 -1
- package/src/renderers/opentui/pane-image/index.ts +2 -1
- package/src/renderers/opentui/render-lib.ts +253 -0
- package/src/renderers/opentui/render-timing-smoke.ts +1 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/share/layout-view.tsx +2 -2
- package/src/renderers/share/main.tsx +4 -4
- package/src/renderers/share/view.tsx +1 -1
- package/src/series/resolve.ts +22 -6
- package/src/series/standalone.ts +30 -2
- package/src/series/types.ts +8 -1
- package/src/shares/api.ts +4 -1
- package/src/shares/pane.ts +1 -1
- package/src/sources/gloomberb-cloud/fred-series.ts +46 -1
- package/src/sources/gloomberb-cloud/index.ts +32 -16
- package/src/sources/gloomberb-cloud/news.ts +39 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +14 -1
- package/src/sources/gloomberb-cloud/perpetuals.ts +49 -1
- package/src/sources/listing-symbols.ts +37 -0
- package/src/sources/provider-errors.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +21 -3
- package/src/sources/provider-router/financials.ts +15 -2
- package/src/sources/provider-router/history.ts +30 -8
- package/src/sources/provider-router/primary.ts +1 -1
- package/src/state/app/bootstrap.ts +23 -0
- package/src/state/hooks/inline-tickers.ts +30 -5
- package/src/sync/controller.ts +1 -1
- package/src/sync/core-contributors.ts +46 -14
- package/src/team/collections-sync.ts +2 -1
- package/src/team/collections.ts +178 -17
- package/src/telemetry/crash-reports.ts +17 -4
- package/src/test-support/builtin-panes.ts +1 -1
- package/src/theme/colors.ts +3 -30
- package/src/theme/ui-tokens.ts +37 -1
- package/src/tickers/company-name-listing.ts +210 -33
- package/src/tickers/index-roots.ts +47 -0
- package/src/tickers/instrument-label.ts +5 -1
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/perpetual-listing.ts +5 -2
- package/src/tickers/search/home-listing.ts +75 -0
- package/src/tickers/search/index.ts +76 -27
- package/src/tickers/search/ranking.ts +54 -4
- package/src/tickers/search/upsert.ts +29 -0
- package/src/tickers/terminal-codes.ts +416 -40
- package/src/time-series/bar-aggregation.ts +243 -0
- package/src/time-series/base-series.ts +307 -0
- package/src/time-series/chart-bounds.ts +349 -0
- package/src/time-series/chart-seed.ts +142 -0
- package/src/time-series/economic.ts +5 -0
- package/src/time-series/fundamentals.ts +18 -3
- package/src/time-series/history-acquisition.ts +499 -0
- package/src/time-series/leverage.ts +100 -0
- package/src/time-series/market-resolution.ts +4 -2
- package/src/time-series/market.ts +2 -1
- package/src/time-series/price-comparison.ts +175 -30
- package/src/time-series/range.ts +9 -1
- package/src/time-series/resolution.ts +174 -16
- package/src/time-series/resolve-context.ts +157 -0
- package/src/time-series/resolve.ts +126 -1577
- package/src/time-series/series-name.ts +38 -0
- package/src/time-series/series-presentation.ts +208 -0
- package/src/time-series/spec.ts +11 -6
- package/src/time-series/studies.ts +109 -12
- package/src/time-series/tail-reconcile.ts +121 -0
- package/src/time-series/types.ts +29 -2
- package/src/time-series/use-chart-resolution.ts +4 -5
- package/src/types/broker.ts +10 -0
- package/src/types/config.ts +14 -0
- package/src/types/data-provider.ts +14 -0
- package/src/types/exchange-rate.ts +6 -0
- package/src/types/financials.ts +104 -7
- package/src/types/headless.ts +76 -6
- package/src/types/instrument.ts +3 -0
- package/src/types/plugin.ts +65 -8
- package/src/types/price-history.ts +2 -0
- package/src/types/ticker.ts +13 -0
- package/src/types/trading.ts +59 -1
- package/src/ui/ascii-font.ts +13 -1
- package/src/ui/host.tsx +6 -1
- package/src/updater/command-names.ts +175 -0
- package/src/updater/index.ts +10 -5
- package/src/utils/calendar-date.ts +32 -15
- package/src/utils/cash-runway.ts +241 -0
- package/src/utils/central-banks.ts +42 -0
- package/src/utils/cli-output.ts +259 -80
- package/src/utils/contract-month.ts +16 -0
- package/src/utils/crypto-pair.ts +61 -3
- package/src/utils/currency-units.ts +29 -0
- package/src/utils/datetime-format.ts +10 -0
- package/src/utils/debug-log.ts +1 -1
- package/src/utils/decimal-comma.ts +39 -6
- package/src/utils/exchange-announcements.ts +71 -0
- package/src/utils/exchange-rate-snapshot.ts +10 -0
- package/src/utils/financial-statements.ts +83 -4
- package/src/utils/format.ts +34 -16
- package/src/utils/fundamentals.ts +150 -34
- package/src/utils/fx-market-hours.ts +10 -1
- package/src/utils/latest-financial-period.ts +8 -0
- package/src/utils/linux-libc.ts +54 -0
- package/src/utils/market-capitalization.ts +111 -15
- package/src/utils/max-drawdown.ts +64 -0
- package/src/utils/network-failure.ts +83 -0
- package/src/utils/option-expiry.ts +139 -7
- package/src/utils/price-earnings.ts +11 -5
- package/src/utils/price-history.ts +18 -8
- package/src/utils/price-window.ts +131 -0
- package/src/utils/release-date.ts +47 -0
- package/src/utils/sec.ts +3 -0
- package/src/utils/throttled-fetch.ts +11 -4
- package/src/utils/trailing-eps.ts +67 -0
- package/src/utils/typed-text.ts +21 -0
- package/src/utils/utc-time.ts +24 -1
- package/src/version.ts +6 -0
- package/src/cli/pane-functions/screenshot.ts +0 -2122
|
@@ -1,2122 +0,0 @@
|
|
|
1
|
-
import { logFailure } from "../../utils/log-failure";
|
|
2
|
-
import { isNoSourceAnswer } from "../../sources/provider-errors";
|
|
3
|
-
import { fetchTape } from "../../plugins/builtin/time-sales/client";
|
|
4
|
-
import { captureScannerEvent, SCANNER_PANE_KINDS, type ScannerCapture } from "../../plugins/builtin/shared/scanner-capture";
|
|
5
|
-
import { isOptionsUnavailableError } from "../../market-data/options-alternatives";
|
|
6
|
-
import type { TapeCapture } from "../../plugins/builtin/time-sales/snapshot-client";
|
|
7
|
-
import { apiClient } from "../../api-client";
|
|
8
|
-
import { snapshotInstrumentKey, type SnapshotMarketData } from "../../market-data/snapshot-provider";
|
|
9
|
-
import { toMarketDataContext } from "../../market-data/selectors";
|
|
10
|
-
import type { ChartPaneModel } from "../../plugins/builtin/chart-composer/headless";
|
|
11
|
-
import { buildHeadlessPaneLoadArgs, loadResolvedHeadlessPaneModel, reportCoverage, reportGaps, type LoadedHeadlessPaneModel } from "./headless";
|
|
12
|
-
import { dirname, resolve } from "path";
|
|
13
|
-
import { mkdir } from "fs/promises";
|
|
14
|
-
import type { PaneRuntimeState } from "../../core/state/app/state";
|
|
15
|
-
import { CHART_COMPOSER_PANE_ID, type AppConfig } from "../../types/config";
|
|
16
|
-
import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
17
|
-
import type { TickerRecord } from "../../types/ticker";
|
|
18
|
-
import { slugifyName } from "../../utils/slugify";
|
|
19
|
-
import {
|
|
20
|
-
readRequestInit,
|
|
21
|
-
toResponseEnvelope,
|
|
22
|
-
type HttpProxyRequestEnvelope,
|
|
23
|
-
type HttpProxyResponseEnvelope,
|
|
24
|
-
} from "../../utils/http-proxy-response";
|
|
25
|
-
import { requireThemeId } from "../commands/themes";
|
|
26
|
-
import { isRecord } from "../../utils/guards";
|
|
27
|
-
|
|
28
|
-
const DEFAULT_SHOT_DEVICE_SCALE_FACTOR = 2;
|
|
29
|
-
import {
|
|
30
|
-
renderDesktopPaneScreenshot,
|
|
31
|
-
type DesktopPaneShotApiProxy,
|
|
32
|
-
type DesktopPaneShotBridge,
|
|
33
|
-
type DesktopPaneShotIntradayHistory,
|
|
34
|
-
type DesktopPaneShotPayload,
|
|
35
|
-
type DesktopPaneShotRenderResult,
|
|
36
|
-
} from "../desktop-pane-shot";
|
|
37
|
-
import { optionPaneState, SHOT_SIZE_LIMITS, type ShotSizeClamp } from "./options";
|
|
38
|
-
import type { ResolvedPaneFunction } from "./resolver";
|
|
39
|
-
import type { MarketContext } from "../types";
|
|
40
|
-
import { capabilityPluginState, findCapabilityOption } from "./capabilities";
|
|
41
|
-
import { resolvePersistedCloudSessionToken } from "./cloud-session";
|
|
42
|
-
import type { RemoteUiNodeSnapshot } from "../../remote/types";
|
|
43
|
-
import {
|
|
44
|
-
graphRowsForFinancials,
|
|
45
|
-
limitGraphRowsBySymbol,
|
|
46
|
-
metricDef,
|
|
47
|
-
} from "../../time-series/reporting";
|
|
48
|
-
import { getTimeSeriesField } from "../../time-series/field-catalog";
|
|
49
|
-
import {
|
|
50
|
-
buildFinancialTableModel,
|
|
51
|
-
financialStatementCurrency,
|
|
52
|
-
formatFinancialHeader,
|
|
53
|
-
} from "../../plugins/builtin/ticker-detail/financials/model";
|
|
54
|
-
import type {
|
|
55
|
-
FundamentalPeriod,
|
|
56
|
-
GraphKind,
|
|
57
|
-
GraphMetricKey,
|
|
58
|
-
} from "../../time-series/reporting";
|
|
59
|
-
import type { TimeRange } from "../../time-series/range";
|
|
60
|
-
import { appendLiveQuotePoint } from "../../time-series/chart-data";
|
|
61
|
-
import { subtractTimeRange } from "../../time-series/date-window";
|
|
62
|
-
import {
|
|
63
|
-
buildPresetDateWindow,
|
|
64
|
-
getVisibleWindowForDateRange,
|
|
65
|
-
} from "../../components/chart/core/date-window";
|
|
66
|
-
import { parseChartSpec } from "../../plugins/builtin/chart-composer/chart-spec";
|
|
67
|
-
import {
|
|
68
|
-
defaultValuationSeriesLoader,
|
|
69
|
-
requiredSeries as valuationRequiredSeries,
|
|
70
|
-
} from "../../plugins/builtin/market-valuation/client";
|
|
71
|
-
import type { DatedObservation } from "../../plugins/builtin/market-valuation/series";
|
|
72
|
-
import { defaultStatLoader } from "../../plugins/builtin/econ-statistics/client";
|
|
73
|
-
import { STATS } from "../../plugins/builtin/econ-statistics/stats";
|
|
74
|
-
import { parsePublicTickerKey, publicTickerKey } from "../../utils/exchanges";
|
|
75
|
-
import { getCloudApiBaseUrl } from "../../api-client/request";
|
|
76
|
-
import { collectExternalPluginBundles } from "../../renderers/electrobun/bun/external-plugins";
|
|
77
|
-
import type { ResolvedSeries } from "../../time-series/types";
|
|
78
|
-
import { getQuoteMonitorPaneSettings } from "../../plugins/builtin/ticker-detail/settings";
|
|
79
|
-
import { isNotATickerMessage } from "../not-a-ticker";
|
|
80
|
-
import { HISTORY_DEFAULT_RANGE } from "../../plugins/builtin/ticker-detail/data-panes/history-bars";
|
|
81
|
-
import { accessGateSentence, accessGateStatus } from "./access-gate";
|
|
82
|
-
import { findCollection } from "../../plugins/builtin/portfolio-list/cli/render";
|
|
83
|
-
import { peekCachedBrokerAccounts } from "../../plugins/builtin/portfolio-list/cached-account";
|
|
84
|
-
import {
|
|
85
|
-
paneEvidenceMismatches,
|
|
86
|
-
paneScreenshotEvidenceHook,
|
|
87
|
-
renderedPaneEvidence,
|
|
88
|
-
type PaneScreenshotEvidence,
|
|
89
|
-
} from "./screenshot-evidence";
|
|
90
|
-
import {
|
|
91
|
-
collectShotSymbols,
|
|
92
|
-
clipPriceHistoryToRange,
|
|
93
|
-
createFallbackTicker,
|
|
94
|
-
fetchTickerFinancials,
|
|
95
|
-
loadBrokerMarkedHolding,
|
|
96
|
-
loadStoredShotTicker,
|
|
97
|
-
isFinancialAnalysisFunction,
|
|
98
|
-
readsDailyReturns,
|
|
99
|
-
withShotDailyReturns,
|
|
100
|
-
withShotPeriodEndHistory,
|
|
101
|
-
withShotPriceHistory,
|
|
102
|
-
withShotReturnHistory,
|
|
103
|
-
withShotSeasonalityHistory,
|
|
104
|
-
} from "./data";
|
|
105
|
-
import { declaredReportFreshness, renderedReportFreshness, renderedReportNotices, renderedReportObservations } from "./report-notices";
|
|
106
|
-
import { captureStatusLine, deriveCaptureReport, deriveHeadlessFreshness, deriveRenderedFreshness, type CaptureReport, type CaptureStatusLine, type ReportFreshness } from "./freshness";
|
|
107
|
-
import { quotesFreshness } from "../freshness";
|
|
108
|
-
import {
|
|
109
|
-
financialRatioRenderMismatches,
|
|
110
|
-
financialRatioShotEvidence,
|
|
111
|
-
financialRatioShotGap,
|
|
112
|
-
shotRatioTab,
|
|
113
|
-
type PaneScreenshotFinancialRatioEvidence,
|
|
114
|
-
} from "./financial-ratio-shot";
|
|
115
|
-
|
|
116
|
-
const DESKTOP_CELL_WIDTH_PX = 8;
|
|
117
|
-
const DESKTOP_CELL_HEIGHT_PX = 18;
|
|
118
|
-
const OPTIONS_PANE_ID = "options";
|
|
119
|
-
const MARKET_VALUATION_PANE_ID = "market-valuation";
|
|
120
|
-
const ECON_STATISTICS_PANE_ID = "econ-statistics";
|
|
121
|
-
const CREDENTIAL_FIELD_NAMES = new Set([
|
|
122
|
-
"accesstoken",
|
|
123
|
-
"accessurl",
|
|
124
|
-
"apikey",
|
|
125
|
-
"apisecret",
|
|
126
|
-
"authorization",
|
|
127
|
-
"cookie",
|
|
128
|
-
"credentials",
|
|
129
|
-
"oauth",
|
|
130
|
-
"passphrase",
|
|
131
|
-
"password",
|
|
132
|
-
"privatekey",
|
|
133
|
-
"refreshtoken",
|
|
134
|
-
"secret",
|
|
135
|
-
"sessiontoken",
|
|
136
|
-
"token",
|
|
137
|
-
]);
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* The proxy receives the desktop session out of band. Keeping this separate
|
|
141
|
-
* from DesktopPaneShotPayload prevents the token from entering the page,
|
|
142
|
-
* semantic evidence, or JSON CLI output.
|
|
143
|
-
*/
|
|
144
|
-
function isCredentialField(key: string): boolean {
|
|
145
|
-
const normalized = key.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
146
|
-
return CREDENTIAL_FIELD_NAMES.has(normalized)
|
|
147
|
-
|| normalized.endsWith("password")
|
|
148
|
-
|| normalized.endsWith("passphrase")
|
|
149
|
-
|| normalized.endsWith("secret")
|
|
150
|
-
|| normalized.endsWith("token")
|
|
151
|
-
|| normalized.endsWith("credential")
|
|
152
|
-
|| normalized.endsWith("cookie")
|
|
153
|
-
|| normalized.endsWith("apikey")
|
|
154
|
-
|| normalized.endsWith("accesskey")
|
|
155
|
-
|| normalized.endsWith("privatekey")
|
|
156
|
-
|| normalized.endsWith("authorization")
|
|
157
|
-
|| normalized.endsWith("accessurl");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Removes credential-shaped object fields before data enters the page or JSON output. */
|
|
161
|
-
export function stripDesktopShotCredentials<T>(value: T): T {
|
|
162
|
-
if (Array.isArray(value)) {
|
|
163
|
-
return value.map((entry) => stripDesktopShotCredentials(entry)) as T;
|
|
164
|
-
}
|
|
165
|
-
if (!value || typeof value !== "object" || value instanceof Date) return value;
|
|
166
|
-
const clean: Record<string, unknown> = {};
|
|
167
|
-
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
168
|
-
if (isCredentialField(key)) continue;
|
|
169
|
-
clean[key] = stripDesktopShotCredentials(entry);
|
|
170
|
-
}
|
|
171
|
-
return clean as T;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function resolveDesktopShotApiProxy(
|
|
175
|
-
context: Pick<MarketContext, "persistence">,
|
|
176
|
-
): DesktopPaneShotApiProxy {
|
|
177
|
-
return {
|
|
178
|
-
baseUrl: getCloudApiBaseUrl(),
|
|
179
|
-
sessionToken: resolvePersistedCloudSessionToken(context),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Data the page may request while it renders. The cloud API is only one of the
|
|
185
|
-
* router's sources, so calling it directly from the page dropped whatever
|
|
186
|
-
* another provider contributes: analyst rating price targets came back empty
|
|
187
|
-
* and corporate actions vanished whenever the cloud leg failed. Quotes are
|
|
188
|
-
* here for board panes that list symbols the payload was never built for.
|
|
189
|
-
* These run on the Bun side through the same router `gloomberb fn` uses.
|
|
190
|
-
*/
|
|
191
|
-
const SHOT_BRIDGE_MARKET_OPERATIONS = new Set([
|
|
192
|
-
"getAnalystResearch",
|
|
193
|
-
"getCorporateActions",
|
|
194
|
-
"getEarningsCalendar",
|
|
195
|
-
"getHolders",
|
|
196
|
-
"getOptionsChain",
|
|
197
|
-
"getCloudTape",
|
|
198
|
-
"getPriceHistory",
|
|
199
|
-
"getPriceHistoryForResolution",
|
|
200
|
-
"getQuote",
|
|
201
|
-
"getQuoteMetadata",
|
|
202
|
-
"getQuotesBatch",
|
|
203
|
-
"getSecFilingContent",
|
|
204
|
-
"getSecFilingDocuments",
|
|
205
|
-
"getSecFilings",
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
const SHOT_BRIDGE_HTTP_TIMEOUT_MS = 20_000;
|
|
209
|
-
|
|
210
|
-
export function createDesktopShotBridge(
|
|
211
|
-
context: Pick<MarketContext, "dataProvider">,
|
|
212
|
-
tapeBootstrap: typeof fetchTape = fetchTape,
|
|
213
|
-
): DesktopPaneShotBridge {
|
|
214
|
-
return {
|
|
215
|
-
async marketData(operation, args) {
|
|
216
|
-
if (!SHOT_BRIDGE_MARKET_OPERATIONS.has(operation)) {
|
|
217
|
-
throw new Error(`Screenshot market bridge does not serve "${operation}".`);
|
|
218
|
-
}
|
|
219
|
-
if (operation === "getCloudTape") {
|
|
220
|
-
const [symbol, exchange] = args;
|
|
221
|
-
if (typeof symbol !== "string" || typeof exchange !== "string") throw new Error("Tape screenshots require a symbol and listing exchange.");
|
|
222
|
-
return tapeBootstrap(symbol, exchange);
|
|
223
|
-
}
|
|
224
|
-
const provider = context.dataProvider as unknown as Record<string, unknown>;
|
|
225
|
-
const handler = provider[operation];
|
|
226
|
-
if (typeof handler !== "function") {
|
|
227
|
-
throw new Error(`No provider available for ${operation}.`);
|
|
228
|
-
}
|
|
229
|
-
return await (handler as (...values: unknown[]) => Promise<unknown>).apply(
|
|
230
|
-
context.dataProvider,
|
|
231
|
-
args,
|
|
232
|
-
);
|
|
233
|
-
},
|
|
234
|
-
httpFetch: (request) => runDesktopShotHttpFetch(request),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Panes such as DVD fetch a third-party API directly. A browser cannot: the
|
|
240
|
-
* request is cross-origin and dies in CORS, so the pane reported that the
|
|
241
|
-
* ticker pays no dividend. The desktop renderer already proxies these through
|
|
242
|
-
* its native half; the screenshot page gets the same treatment.
|
|
243
|
-
*/
|
|
244
|
-
async function runDesktopShotHttpFetch(
|
|
245
|
-
request: HttpProxyRequestEnvelope,
|
|
246
|
-
): Promise<HttpProxyResponseEnvelope> {
|
|
247
|
-
const target = new URL(request.url);
|
|
248
|
-
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
249
|
-
throw new Error(`Screenshot HTTP bridge refuses ${target.protocol} requests.`);
|
|
250
|
-
}
|
|
251
|
-
const { method, headers, body, redirect } = readRequestInit(request.init);
|
|
252
|
-
const response = await fetch(target, {
|
|
253
|
-
method,
|
|
254
|
-
headers,
|
|
255
|
-
body,
|
|
256
|
-
redirect,
|
|
257
|
-
signal: AbortSignal.timeout(SHOT_BRIDGE_HTTP_TIMEOUT_MS),
|
|
258
|
-
});
|
|
259
|
-
return toResponseEnvelope(response);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Same reason as the valuation legs: the renderer cannot fill its own cache. */
|
|
263
|
-
async function collectShotStatSeries(
|
|
264
|
-
resolved: ResolvedPaneFunction,
|
|
265
|
-
): Promise<Array<[string, DatedObservation[]]>> {
|
|
266
|
-
if (resolved.pane.id !== ECON_STATISTICS_PANE_ID) return [];
|
|
267
|
-
const loaded = await Promise.all(STATS.map(async (def) => {
|
|
268
|
-
try {
|
|
269
|
-
const result = await defaultStatLoader(def);
|
|
270
|
-
const observations = Array.isArray(result) ? result : result.observations;
|
|
271
|
-
return [def.seriesId, observations] as [string, DatedObservation[]];
|
|
272
|
-
} catch (error) {
|
|
273
|
-
logFailure("cli.screenshot", "Read screenshot statistics series failed")(error);
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}));
|
|
277
|
-
return loaded.filter((entry): entry is [string, DatedObservation[]] => !!entry);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* The pane reads its legs from a client cache the shot renderer cannot fill itself,
|
|
282
|
-
* so fetch them here on the Bun side and hand them over with the payload.
|
|
283
|
-
*/
|
|
284
|
-
async function collectShotValuationSeries(
|
|
285
|
-
resolved: ResolvedPaneFunction,
|
|
286
|
-
): Promise<Array<[string, DatedObservation[]]>> {
|
|
287
|
-
if (resolved.pane.id !== MARKET_VALUATION_PANE_ID) return [];
|
|
288
|
-
const loaded = await Promise.all(valuationRequiredSeries().map(async (def) => {
|
|
289
|
-
try {
|
|
290
|
-
const series = await defaultValuationSeriesLoader(def);
|
|
291
|
-
return [def.key, series.observations] as [string, DatedObservation[]];
|
|
292
|
-
} catch (error) {
|
|
293
|
-
logFailure("cli.screenshot", "Read screenshot valuation series failed")(error);
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
}));
|
|
297
|
-
return loaded.filter((entry): entry is [string, DatedObservation[]] => !!entry);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export interface PaneScreenshotExpectedSelection {
|
|
301
|
-
control: "metric" | "statement" | "ratio" | "period";
|
|
302
|
-
value?: string;
|
|
303
|
-
label?: string;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
interface PaneScreenshotChartPointEvidence {
|
|
307
|
-
date: string;
|
|
308
|
-
close: number;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export interface PaneScreenshotChartSeriesEvidence {
|
|
312
|
-
symbol: string;
|
|
313
|
-
pointCount: number;
|
|
314
|
-
first: PaneScreenshotChartPointEvidence | null;
|
|
315
|
-
last: PaneScreenshotChartPointEvidence | null;
|
|
316
|
-
projectionBaseValue?: number | null;
|
|
317
|
-
projectionLatestRawValue?: number | null;
|
|
318
|
-
projectionLatestValue?: number | null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export interface PaneScreenshotExpectedChartEvidence {
|
|
322
|
-
kind: "stock-price" | "price-comparison" | "chart-composer";
|
|
323
|
-
symbols: string[];
|
|
324
|
-
rangePreset: string;
|
|
325
|
-
axisMode?: string;
|
|
326
|
-
resolution?: string;
|
|
327
|
-
sourceSeries?: PaneScreenshotChartSeriesEvidence[];
|
|
328
|
-
baseSeries?: Array<{
|
|
329
|
-
id: string;
|
|
330
|
-
sourceKind: "security" | "economic" | "capability";
|
|
331
|
-
symbol?: string;
|
|
332
|
-
fieldId?: string;
|
|
333
|
-
economicSeriesId?: string;
|
|
334
|
-
capabilityId?: string;
|
|
335
|
-
providerSeriesId?: string;
|
|
336
|
-
first?: { date: string; value: number | null } | null;
|
|
337
|
-
last?: { date: string; value: number | null } | null;
|
|
338
|
-
style: string;
|
|
339
|
-
transform: string;
|
|
340
|
-
panelId: string;
|
|
341
|
-
visible: boolean;
|
|
342
|
-
}>;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
interface PaneScreenshotPriceComparisonEvidence {
|
|
346
|
-
kind: "price-comparison";
|
|
347
|
-
symbols: string[];
|
|
348
|
-
range: string;
|
|
349
|
-
series: Array<{
|
|
350
|
-
symbol: string;
|
|
351
|
-
base: { date: string; value: number };
|
|
352
|
-
latest: { date: string; value: number };
|
|
353
|
-
returnPercent: number;
|
|
354
|
-
}>;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface PaneScreenshotPriceSeriesEvidence {
|
|
358
|
-
kind: "price-series";
|
|
359
|
-
symbol: string;
|
|
360
|
-
range: string;
|
|
361
|
-
pointCount: number;
|
|
362
|
-
first: PaneScreenshotChartPointEvidence;
|
|
363
|
-
last: PaneScreenshotChartPointEvidence;
|
|
364
|
-
resolution?: string;
|
|
365
|
-
sessionDates?: string[];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
interface PaneScreenshotFundamentalSeriesEvidence {
|
|
369
|
-
kind: "fundamental-series";
|
|
370
|
-
metric: string;
|
|
371
|
-
period: FundamentalPeriod;
|
|
372
|
-
series: Array<{
|
|
373
|
-
symbol: string;
|
|
374
|
-
rows: Array<{ date: string; value: number }>;
|
|
375
|
-
}>;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
interface PaneScreenshotFinancialStatementEvidence {
|
|
379
|
-
kind: "financial-statement";
|
|
380
|
-
symbol: string;
|
|
381
|
-
statement: string;
|
|
382
|
-
period: FundamentalPeriod;
|
|
383
|
-
latest: {
|
|
384
|
-
date: string;
|
|
385
|
-
metrics: Array<{
|
|
386
|
-
id: string;
|
|
387
|
-
key: string;
|
|
388
|
-
label: string;
|
|
389
|
-
value: number;
|
|
390
|
-
}>;
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export type PaneScreenshotDataEvidence =
|
|
395
|
-
| PaneScreenshotPriceSeriesEvidence
|
|
396
|
-
| PaneScreenshotPriceComparisonEvidence
|
|
397
|
-
| PaneScreenshotFundamentalSeriesEvidence
|
|
398
|
-
| PaneScreenshotFinancialStatementEvidence
|
|
399
|
-
| PaneScreenshotFinancialRatioEvidence
|
|
400
|
-
| PaneScreenshotEvidence;
|
|
401
|
-
|
|
402
|
-
export interface PaneScreenshotReadinessSignals {
|
|
403
|
-
rowCount: number;
|
|
404
|
-
loadingStateDetected: boolean;
|
|
405
|
-
errorStateDetected: boolean;
|
|
406
|
-
emptyStateDetected: boolean;
|
|
407
|
-
complete: boolean;
|
|
408
|
-
semanticMismatch: boolean;
|
|
409
|
-
requiresStructuredDataEvidence: boolean;
|
|
410
|
-
hasStructuredDataEvidence: boolean;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function isPaneScreenshotUsable(signals: PaneScreenshotReadinessSignals): boolean {
|
|
414
|
-
return signals.rowCount > 0
|
|
415
|
-
&& !signals.loadingStateDetected
|
|
416
|
-
&& !signals.errorStateDetected
|
|
417
|
-
&& !signals.emptyStateDetected
|
|
418
|
-
&& signals.complete
|
|
419
|
-
&& !signals.semanticMismatch
|
|
420
|
-
&& (!signals.requiresStructuredDataEvidence || signals.hasStructuredDataEvidence);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export interface PaneScreenshotResult {
|
|
424
|
-
kind: "pane-screenshot";
|
|
425
|
-
target: string;
|
|
426
|
-
capability: {
|
|
427
|
-
id: string;
|
|
428
|
-
botSafe: boolean;
|
|
429
|
-
outputKind: string;
|
|
430
|
-
reportReadiness: ResolvedPaneFunction["capability"]["reportReadiness"];
|
|
431
|
-
screenshotReadiness: ResolvedPaneFunction["capability"]["screenshotReadiness"];
|
|
432
|
-
};
|
|
433
|
-
symbols: string[];
|
|
434
|
-
options: Record<string, string | number | boolean>;
|
|
435
|
-
rowCount: number;
|
|
436
|
-
empty: boolean;
|
|
437
|
-
complete: boolean;
|
|
438
|
-
unavailableSymbols: string[];
|
|
439
|
-
semanticMismatch: boolean;
|
|
440
|
-
usable: boolean;
|
|
441
|
-
unusableReason: string | null;
|
|
442
|
-
dataEvidence: PaneScreenshotDataEvidence | null;
|
|
443
|
-
/** What the pane says its view leaves out, such as OMON's "21 of 145 strikes". */
|
|
444
|
-
notices?: string[];
|
|
445
|
-
/**
|
|
446
|
-
* For a pane that grows to show every row (FUT): how many of the rows its
|
|
447
|
-
* table reports the image shows, section headers left out. `shown` is less
|
|
448
|
-
* than `total` when the height limit or `--height` cut the board.
|
|
449
|
-
*/
|
|
450
|
-
tableRows?: { shown: number; total: number };
|
|
451
|
-
/** What the pane's report leaves out when it is not complete, as `fn` words it: `2 stale`. */
|
|
452
|
-
gaps?: string;
|
|
453
|
-
/** How current the captured data is, as the capture's status line says it; absent with `--no-status`. */
|
|
454
|
-
freshness?: ReportFreshness;
|
|
455
|
-
outputPath: string;
|
|
456
|
-
render: DesktopPaneShotRenderResult & {
|
|
457
|
-
expectedText: string[];
|
|
458
|
-
missingExpectedText: string[];
|
|
459
|
-
expectedSelections: PaneScreenshotExpectedSelection[];
|
|
460
|
-
missingExpectedSelections: PaneScreenshotExpectedSelection[];
|
|
461
|
-
expectedChart: PaneScreenshotExpectedChartEvidence | null;
|
|
462
|
-
chartEvidenceMismatches: string[];
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
export function defaultScreenshotPath(resolved: ResolvedPaneFunction, rawArg: string): string {
|
|
467
|
-
const suffix = slugifyName([resolved.token, rawArg].filter(Boolean).join("-"), "pane");
|
|
468
|
-
return resolve(process.cwd(), `gloomberb-${suffix}.png`);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* The pixels of the PNG a `shot` of this size writes: the layout snapped to
|
|
473
|
-
* whole cells, drawn at the device scale factor (twice the layout size, as
|
|
474
|
-
* `--scale` trades cells for glyph size at the same output size).
|
|
475
|
-
*/
|
|
476
|
-
function shotImagePixels(widthPx: number, heightPx: number, scale = 1): { width: number; height: number } {
|
|
477
|
-
const device = DEFAULT_SHOT_DEVICE_SCALE_FACTOR * scale;
|
|
478
|
-
return {
|
|
479
|
-
width: Math.round(Math.max(1, Math.floor(widthPx / scale / DESKTOP_CELL_WIDTH_PX)) * DESKTOP_CELL_WIDTH_PX * device),
|
|
480
|
-
height: Math.round(Math.max(1, Math.floor(heightPx / scale / DESKTOP_CELL_HEIGHT_PX)) * DESKTOP_CELL_HEIGHT_PX * device),
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/** Whether a capture of this pane, on the tab it opens, grows to show every row (PaneDef.shotFitsRows). */
|
|
485
|
-
function paneFitsRows(resolved: Pick<ResolvedPaneFunction, "pane" | "instance">): boolean {
|
|
486
|
-
const fits = resolved.pane.shotFitsRows;
|
|
487
|
-
return typeof fits === "function" ? fits(resolved.instance) : fits === true;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/** The tallest layout, in cells, a capture may grow to at this scale: the `--height` limit. */
|
|
491
|
-
function shotMaxHeightCells(scale: number): number {
|
|
492
|
-
return Math.max(1, Math.floor(SHOT_SIZE_LIMITS.height.max / scale / DESKTOP_CELL_HEIGHT_PX));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/** What `shot` says when it drew another size than the one asked for, naming the PNG it wrote. */
|
|
496
|
-
export function shotSizeWarnings(
|
|
497
|
-
clamped: readonly ShotSizeClamp[] | undefined,
|
|
498
|
-
size: { width: number; height: number; scale?: number },
|
|
499
|
-
): string[] {
|
|
500
|
-
if (!clamped?.length) return [];
|
|
501
|
-
const pixels = shotImagePixels(size.width, size.height, size.scale);
|
|
502
|
-
return clamped.map(({ dimension, requested, used }) => {
|
|
503
|
-
const { min, max } = SHOT_SIZE_LIMITS[dimension];
|
|
504
|
-
const bound = requested < min ? `below the ${min} minimum` : `above the ${max} maximum`;
|
|
505
|
-
const drawn = dimension === "width" ? `${pixels.width} px wide` : `${pixels.height} px tall`;
|
|
506
|
-
return `--${dimension} ${requested} is ${bound}, so the capture used ${used} and the PNG is ${drawn}.`;
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/** The options typed, by their key: an alias or another spelling (`--min-amount`) reads as the option it names. */
|
|
511
|
-
function givenOptionKeys(capability: ResolvedPaneFunction["capability"], options: Record<string, string | true>): ReadonlySet<string> {
|
|
512
|
-
return new Set(Object.keys(options).flatMap((key) => findCapabilityOption(capability, key)?.key ?? []));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
export async function buildDesktopShotPayload(
|
|
516
|
-
resolved: ResolvedPaneFunction,
|
|
517
|
-
context: MarketContext,
|
|
518
|
-
rawArg: string,
|
|
519
|
-
options: Record<string, string | true>,
|
|
520
|
-
widthPx: number,
|
|
521
|
-
heightPx: number,
|
|
522
|
-
theme: string | null,
|
|
523
|
-
scale: number,
|
|
524
|
-
watermark: string | null,
|
|
525
|
-
loadModel: () => Promise<LoadedHeadlessPaneModel> = () => loadResolvedHeadlessPaneModel(resolved, context, rawArg),
|
|
526
|
-
scannerFeed: Pick<typeof apiClient, "subscribeScanner"> = apiClient,
|
|
527
|
-
): Promise<DesktopPaneShotPayload> {
|
|
528
|
-
// The requested size is the output size. Scale shrinks the CSS viewport and
|
|
529
|
-
// raises the device scale factor by the same factor, so a 1200px wide shot at
|
|
530
|
-
// scale 1.5 lays out 100 cells instead of 150 and every glyph is 1.5x larger.
|
|
531
|
-
// Snap to whole cells: rounding up used to make the pane a few pixels taller
|
|
532
|
-
// than the capture, which clipped the bottom axis of charts.
|
|
533
|
-
const widthCells = Math.max(1, Math.floor(widthPx / scale / DESKTOP_CELL_WIDTH_PX));
|
|
534
|
-
const heightCells = Math.max(1, Math.floor(heightPx / scale / DESKTOP_CELL_HEIGHT_PX));
|
|
535
|
-
widthPx = widthCells * DESKTOP_CELL_WIDTH_PX;
|
|
536
|
-
heightPx = heightCells * DESKTOP_CELL_HEIGHT_PX;
|
|
537
|
-
const deviceScaleFactor = DEFAULT_SHOT_DEVICE_SCALE_FACTOR * scale;
|
|
538
|
-
const initialPaneState = optionPaneState(resolved.options);
|
|
539
|
-
const pluginState = capabilityPluginState(resolved.capability, resolved.options);
|
|
540
|
-
if (Object.keys(pluginState).length > 0) {
|
|
541
|
-
initialPaneState.pluginState = {
|
|
542
|
-
...(initialPaneState.pluginState ?? {}),
|
|
543
|
-
...pluginState,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
if (isFinancialAnalysisFunction(resolved) && !initialPaneState.activeTabId) {
|
|
547
|
-
initialPaneState.activeTabId = "financials";
|
|
548
|
-
}
|
|
549
|
-
// The pane opens on what the report found (13F: the fund a name resolved to, on its view's tab).
|
|
550
|
-
const opened = resolved.headless?.shotState
|
|
551
|
-
? await resolved.headless.shotState(
|
|
552
|
-
buildHeadlessPaneLoadArgs(resolved.headless, resolved.token, rawArg, resolved.options),
|
|
553
|
-
async () => (await loadModel()).result as never,
|
|
554
|
-
givenOptionKeys(resolved.capability, options),
|
|
555
|
-
)
|
|
556
|
-
: null;
|
|
557
|
-
for (const [pluginId, state] of Object.entries(opened?.pluginState ?? {})) {
|
|
558
|
-
initialPaneState.pluginState = {
|
|
559
|
-
...(initialPaneState.pluginState ?? {}),
|
|
560
|
-
[pluginId]: { ...(initialPaneState.pluginState?.[pluginId] ?? {}), ...state },
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
const paneState = stripDesktopShotCredentials<Record<string, PaneRuntimeState>>({
|
|
564
|
-
[resolved.instance.instanceId]: initialPaneState,
|
|
565
|
-
});
|
|
566
|
-
let shotInstance = stripDesktopShotCredentials<typeof resolved.instance>(
|
|
567
|
-
opened?.settings ? { ...resolved.instance, settings: { ...resolved.instance.settings, ...opened.settings } } : resolved.instance,
|
|
568
|
-
);
|
|
569
|
-
const layout = {
|
|
570
|
-
dockRoot: null,
|
|
571
|
-
instances: [shotInstance],
|
|
572
|
-
floating: [{
|
|
573
|
-
instanceId: resolved.instance.instanceId,
|
|
574
|
-
x: 0,
|
|
575
|
-
y: 0,
|
|
576
|
-
width: widthCells,
|
|
577
|
-
height: heightCells,
|
|
578
|
-
zIndex: 1,
|
|
579
|
-
}],
|
|
580
|
-
detached: [],
|
|
581
|
-
};
|
|
582
|
-
const config = stripDesktopShotCredentials<AppConfig>({
|
|
583
|
-
...context.config,
|
|
584
|
-
...(theme ? { theme: requireThemeId(theme) } : {}),
|
|
585
|
-
layout,
|
|
586
|
-
layouts: [{
|
|
587
|
-
name: "CLI Shot",
|
|
588
|
-
layout,
|
|
589
|
-
paneState,
|
|
590
|
-
focusedPaneId: resolved.instance.instanceId,
|
|
591
|
-
}],
|
|
592
|
-
activeLayoutIndex: 0,
|
|
593
|
-
onboardingComplete: true,
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
const tickers: TickerRecord[] = [];
|
|
597
|
-
const financials: Array<[string, TickerFinancials]> = [];
|
|
598
|
-
const intradayHistories: DesktopPaneShotIntradayHistory[] = [];
|
|
599
|
-
let instrumentFinancials: SnapshotMarketData["instrumentFinancials"];
|
|
600
|
-
let historyVariants: SnapshotMarketData["historyVariants"];
|
|
601
|
-
const optionsChains: Array<[string, OptionsChain]> = [];
|
|
602
|
-
const tapeSnapshots: TapeCapture[] = [];
|
|
603
|
-
const [valuationSeries, statSeries] = await Promise.all([
|
|
604
|
-
collectShotValuationSeries(resolved),
|
|
605
|
-
collectShotStatSeries(resolved),
|
|
606
|
-
]);
|
|
607
|
-
const includeOptionsChains = resolved.pane.id === OPTIONS_PANE_ID || resolved.template?.paneId === OPTIONS_PANE_ID;
|
|
608
|
-
let chartModel: ChartPaneModel | undefined;
|
|
609
|
-
const evidenceHook = paneScreenshotEvidenceHook(resolved);
|
|
610
|
-
if (evidenceHook?.prepare) {
|
|
611
|
-
const prepared = await evidenceHook.prepare({ resolved, context, settings: shotInstance.settings ?? {}, loadModel });
|
|
612
|
-
shotInstance = { ...shotInstance, settings: { ...shotInstance.settings, ...prepared.settings } };
|
|
613
|
-
for (const [symbol, data] of prepared.financials ?? []) {
|
|
614
|
-
financials.push([symbol, data]);
|
|
615
|
-
tickers.push(await createFallbackTicker(symbol, data, context));
|
|
616
|
-
}
|
|
617
|
-
} else if (resolved.pane.id === "analytics") {
|
|
618
|
-
const loaded = await loadModel();
|
|
619
|
-
shotInstance = { ...shotInstance, settings: { ...shotInstance.settings, analyticsView: "risk", riskSnapshot: loaded.result.metadata?.model ?? null } };
|
|
620
|
-
} else if (resolved.pane.id === CHART_COMPOSER_PANE_ID) {
|
|
621
|
-
const loaded = await loadModel();
|
|
622
|
-
chartModel = loaded.result as ChartPaneModel;
|
|
623
|
-
shotInstance = { ...shotInstance, settings: { ...shotInstance.settings, chartSpec: chartModel.spec } };
|
|
624
|
-
const authored = parseChartSpec(resolved.instance.settings?.chartSpec);
|
|
625
|
-
const captured = new Map(chartModel.snapshot.financials);
|
|
626
|
-
instrumentFinancials = chartModel.snapshot.instrumentFinancials;
|
|
627
|
-
historyVariants = chartModel.snapshot.historyVariants;
|
|
628
|
-
const identities = new Map(chartModel.spec.series.flatMap((series) => {
|
|
629
|
-
if (series.source.kind !== "security") return [];
|
|
630
|
-
const key = publicTickerKey(series.source.instrument.symbol, series.source.instrument.exchange);
|
|
631
|
-
const original = authored?.series.find(({ id }) => id === series.id)?.source;
|
|
632
|
-
const label = original?.kind === "security" ? publicTickerKey(original.instrument.symbol, original.instrument.exchange) : key;
|
|
633
|
-
return [[snapshotInstrumentKey(series.source.instrument), { key, label, target: series.source.instrument }] as const];
|
|
634
|
-
}));
|
|
635
|
-
for (const [identity, { key, label, target }] of identities) {
|
|
636
|
-
const data = (target.instrument ? instrumentFinancials?.find(entry => snapshotInstrumentKey(entry.instrument) === identity)?.financials : captured.get(key))
|
|
637
|
-
?? { annualStatements: [], quarterlyStatements: [], priceHistory: [] };
|
|
638
|
-
if (!target.instrument) financials.push([label, data]);
|
|
639
|
-
const { symbol } = parsePublicTickerKey(key);
|
|
640
|
-
const ticker = await context.store.loadTicker(key) ?? await context.store.loadTicker(symbol);
|
|
641
|
-
tickers.push(ticker ?? await createFallbackTicker(key, data, context));
|
|
642
|
-
}
|
|
643
|
-
intradayHistories.push(...chartModel.snapshot.intradayHistories.map((history) => ({
|
|
644
|
-
...history, start: history.start?.toISOString() ?? null, end: history.end?.toISOString() ?? null,
|
|
645
|
-
})));
|
|
646
|
-
} else for (const symbol of await collectShotSymbolsWithCollections(resolved, context, rawArg)) {
|
|
647
|
-
let entry: Awaited<ReturnType<typeof fetchTickerFinancials>>;
|
|
648
|
-
try {
|
|
649
|
-
entry = await fetchTickerFinancials(context, symbol);
|
|
650
|
-
} catch (error) {
|
|
651
|
-
// A report that reads its own argument (CDS's issuer name) renders it with no ticker data, as `fn` does.
|
|
652
|
-
if (resolved.headless?.argument.kind === "free-text" && isNotATickerMessage(error instanceof Error ? error.message : null)) continue;
|
|
653
|
-
if (!COLLECTION_PANE_IDS.has(resolved.pane.id)) throw error;
|
|
654
|
-
// The pane values a holding no quote covers, such as an option contract,
|
|
655
|
-
// at the broker's mark. It has no financials to capture.
|
|
656
|
-
const holding = await loadBrokerMarkedHolding(context, symbol);
|
|
657
|
-
if (holding) {
|
|
658
|
-
tickers.push(holding);
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
// Any other holding no source quotes (an over-the-counter name none
|
|
662
|
-
// carries) is still a row of the list: it shows without a price, and the
|
|
663
|
-
// capture names it under "No data for" rather than failing. A read that
|
|
664
|
-
// was refused, rate limited or timed out fails the capture, to be taken again.
|
|
665
|
-
if (!isNoSourceAnswer(error)) throw error;
|
|
666
|
-
const stored = await loadStoredShotTicker(context, symbol);
|
|
667
|
-
if (!stored) throw error;
|
|
668
|
-
tickers.push(stored);
|
|
669
|
-
financials.push([symbol, { annualStatements: [], quarterlyStatements: [], priceHistory: [] }]);
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
const requestedRange = shotPriceHistoryRange(resolved);
|
|
673
|
-
let data = entry.financials;
|
|
674
|
-
const exchange = entry.instrument.exchange
|
|
675
|
-
?? entry.tickerFile?.metadata.exchange
|
|
676
|
-
?? data.quote?.listingExchangeName
|
|
677
|
-
?? data.quote?.exchangeName
|
|
678
|
-
?? "";
|
|
679
|
-
const fiveYearsDaily = resolved.pane.id === "realized-vol" || resolved.pane.id === "iv-history" || resolved.pane.id === "macro-day"
|
|
680
|
-
// KELLY's History tab reads monthly returns off five years of daily closes.
|
|
681
|
-
|| resolved.pane.id === "kelly-sizer";
|
|
682
|
-
if (fiveYearsDaily || resolved.pane.id === "iv-screen" || resolved.pane.id === "short-watch"
|
|
683
|
-
|| resolved.pane.id === "backtest" || resolved.pane.id === OPTIONS_PANE_ID) {
|
|
684
|
-
// Generic 5Y snapshots can contain weekly bars. The snapshot provider
|
|
685
|
-
// treats captured history as authoritative, even for a later 1d request.
|
|
686
|
-
// Preserve the pane's daily buffer and warmup before the renderer clips it.
|
|
687
|
-
if (!context.dataProvider.getPriceHistoryForResolution) {
|
|
688
|
-
throw new Error("Daily-history screenshots require daily history resolution support.");
|
|
689
|
-
}
|
|
690
|
-
const priceHistory = await context.dataProvider.getPriceHistoryForResolution(
|
|
691
|
-
entry.instrument.symbol, exchange,
|
|
692
|
-
fiveYearsDaily ? "5Y" : resolved.pane.id === "backtest" ? "ALL" : "1Y",
|
|
693
|
-
"1d", toMarketDataContext(entry.instrument),
|
|
694
|
-
);
|
|
695
|
-
data = { ...data, priceHistory };
|
|
696
|
-
} else if (shotRatioTab(resolved)?.key === "valuation") {
|
|
697
|
-
data = await withShotPeriodEndHistory(context, entry.instrument, exchange, resolved, data);
|
|
698
|
-
} else if (resolved.pane.id === "seasonality") {
|
|
699
|
-
data = await withShotSeasonalityHistory(context, entry.instrument, resolved, data);
|
|
700
|
-
} else if (resolved.pane.id === "returns") {
|
|
701
|
-
data = await withShotReturnHistory(context, entry.instrument, exchange, resolved, data);
|
|
702
|
-
} else if (requestedRange && readsDailyReturns(resolved)) {
|
|
703
|
-
data = await withShotDailyReturns(context, entry.instrument, exchange, requestedRange, data);
|
|
704
|
-
} else if (requestedRange) {
|
|
705
|
-
try {
|
|
706
|
-
const priceHistory = await context.dataProvider.getPriceHistory(entry.instrument.symbol, exchange, requestedRange);
|
|
707
|
-
data = { ...data, priceHistory: clipPriceHistoryToRange(priceHistory, requestedRange) };
|
|
708
|
-
} catch {
|
|
709
|
-
data = await withShotPriceHistory(context, symbol, entry.tickerFile, data);
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
// The cloud's financials carry no price history. The snapshot provider
|
|
713
|
-
// treats a captured dataset as authoritative, so an empty history here
|
|
714
|
-
// left the overview chart and quote monitor sparklines blank instead of
|
|
715
|
-
// letting the pane fetch them.
|
|
716
|
-
data = await withShotPriceHistory(context, symbol, entry.tickerFile, data);
|
|
717
|
-
}
|
|
718
|
-
tickers.push(entry.tickerFile ?? await createFallbackTicker(symbol, data, context));
|
|
719
|
-
financials.push([symbol, data]);
|
|
720
|
-
if (resolved.pane.id === "time-sales") {
|
|
721
|
-
tapeSnapshots.push([entry.instrument.symbol, exchange, await fetchTape(entry.instrument.symbol, exchange)]);
|
|
722
|
-
}
|
|
723
|
-
if (includeOptionsChains && context.dataProvider.getOptionsChain) {
|
|
724
|
-
// A future with no chain of its own is the pane's to explain (the listed alternative), not a failed shot.
|
|
725
|
-
const chain = await context.dataProvider.getOptionsChain(entry.instrument.symbol, exchange, undefined,
|
|
726
|
-
context.refresh ? { cacheMode: "refresh" } : undefined).catch((error: unknown) => {
|
|
727
|
-
if (isOptionsUnavailableError(error)) return null;
|
|
728
|
-
throw error;
|
|
729
|
-
});
|
|
730
|
-
if (chain) optionsChains.push([symbol, chain]);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
// A scanner pane reads one answer from the capture's own session: the page's socket is refused (see createSnapshotScannerClient).
|
|
734
|
-
const scannerKind = SCANNER_PANE_KINDS[resolved.pane.id];
|
|
735
|
-
const scannerSnapshots: ScannerCapture[] = scannerKind
|
|
736
|
-
? [[scannerKind, await captureScannerEvent(scannerFeed, scannerKind, new AbortController().signal)]]
|
|
737
|
-
: [];
|
|
738
|
-
layout.instances[0] = shotInstance;
|
|
739
|
-
config.layout.instances[0] = shotInstance;
|
|
740
|
-
if (config.layouts[0]) config.layouts[0].layout.instances[0] = shotInstance;
|
|
741
|
-
|
|
742
|
-
const payload: DesktopPaneShotPayload = {
|
|
743
|
-
config,
|
|
744
|
-
marketDataProviderId: context.dataProvider.id,
|
|
745
|
-
paneId: resolved.instance.instanceId,
|
|
746
|
-
widthCells,
|
|
747
|
-
heightCells,
|
|
748
|
-
widthPx,
|
|
749
|
-
heightPx,
|
|
750
|
-
deviceScaleFactor,
|
|
751
|
-
watermark,
|
|
752
|
-
tickers,
|
|
753
|
-
...(COLLECTION_PANE_IDS.has(resolved.pane.id)
|
|
754
|
-
? { brokerAccounts: peekCachedBrokerAccounts(context.config, context.persistence?.resources) }
|
|
755
|
-
: {}),
|
|
756
|
-
financials,
|
|
757
|
-
...(instrumentFinancials?.length ? { instrumentFinancials } : {}),
|
|
758
|
-
...(historyVariants?.length ? { historyVariants } : {}),
|
|
759
|
-
intradayHistories,
|
|
760
|
-
optionsChains,
|
|
761
|
-
...(tapeSnapshots.length ? { tapeSnapshots } : {}),
|
|
762
|
-
...(scannerSnapshots.length ? { scannerSnapshots } : {}),
|
|
763
|
-
valuationSeries,
|
|
764
|
-
statSeries,
|
|
765
|
-
paneState,
|
|
766
|
-
externalPlugins: await collectExternalPluginBundles(),
|
|
767
|
-
};
|
|
768
|
-
if (chartModel) payload.chartModel = chartModel.chart;
|
|
769
|
-
return payload;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/** Panes that render the user's portfolio or watchlist rather than an argument. */
|
|
773
|
-
const COLLECTION_PANE_IDS = new Set(["portfolio-list", "analytics", "kelly-sizer", "earnings-calendar"]);
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* The page only knows the tickers the payload carries, so a collection pane
|
|
777
|
-
* captured with just the argument symbol showed an empty portfolio. Portfolio
|
|
778
|
-
* and watchlist members ride along for the panes that display them.
|
|
779
|
-
*/
|
|
780
|
-
async function collectShotSymbolsWithCollections(
|
|
781
|
-
resolved: ResolvedPaneFunction,
|
|
782
|
-
context: MarketContext,
|
|
783
|
-
rawArg: string,
|
|
784
|
-
): Promise<string[]> {
|
|
785
|
-
if (!COLLECTION_PANE_IDS.has(resolved.pane.id)) return collectShotSymbols(resolved, rawArg);
|
|
786
|
-
// `shot PF Retirement` names a portfolio, not a ticker to fetch.
|
|
787
|
-
const symbols = rawArg.trim() && findCollection(context.config, rawArg) ? [] : collectShotSymbols(resolved, rawArg);
|
|
788
|
-
const members = (await context.store.loadAllTickers())
|
|
789
|
-
.filter(({ metadata }) => metadata.portfolios.length > 0 || metadata.watchlists.length > 0)
|
|
790
|
-
.map(({ metadata }) => metadata.ticker);
|
|
791
|
-
return [...new Set([...symbols, ...members])];
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function shotPriceHistoryRange(resolved: ResolvedPaneFunction): TimeRange | null {
|
|
795
|
-
// Captured history is authoritative, and the generic 5Y capture is weekly.
|
|
796
|
-
if (resolved.pane.id === "quote-monitor") return getQuoteMonitorPaneSettings(resolved.instance.settings).chartPeriod;
|
|
797
|
-
switch (resolved.capability.id) {
|
|
798
|
-
case "return-correlation":
|
|
799
|
-
return (resolved.options.rangePreset ?? "1Y") as TimeRange;
|
|
800
|
-
case "historical-prices":
|
|
801
|
-
return (resolved.options.range ?? HISTORY_DEFAULT_RANGE) as TimeRange;
|
|
802
|
-
case "security-relationship":
|
|
803
|
-
return (resolved.options.range ?? "1Y") as TimeRange;
|
|
804
|
-
default:
|
|
805
|
-
return null;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/** The bridge, keeping every quote it serves the page: what a pane that loads its own quotes (WEI) shows. */
|
|
810
|
-
function recordBridgedQuotes(bridge: DesktopPaneShotBridge): { bridge: DesktopPaneShotBridge; quotes: Quote[] } {
|
|
811
|
-
const quotes: Quote[] = [];
|
|
812
|
-
const keep = (value: unknown) => {
|
|
813
|
-
if (isRecord(value) && typeof value.symbol === "string" && typeof value.price === "number") quotes.push(value as unknown as Quote);
|
|
814
|
-
};
|
|
815
|
-
return {
|
|
816
|
-
quotes,
|
|
817
|
-
bridge: {
|
|
818
|
-
...bridge,
|
|
819
|
-
async marketData(operation, args) {
|
|
820
|
-
const result = await bridge.marketData(operation, args);
|
|
821
|
-
if (operation === "getQuote") keep(result);
|
|
822
|
-
if (operation === "getQuotesBatch" && Array.isArray(result)) {
|
|
823
|
-
for (const entry of result) keep(isRecord(entry) ? entry.quote : null);
|
|
824
|
-
}
|
|
825
|
-
return result;
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* How current a capture's data is (`captureStatusLine`). A pane that dates its
|
|
833
|
-
* view itself (the news wire's delay, FXC's rates) is taken at its word, and so
|
|
834
|
-
* is a report that says what its data is (EE's estimates, MDAY's study): the
|
|
835
|
-
* quotes the page loaded beside them do not date them. A pane that declares a
|
|
836
|
-
* status is never dated by the quotes it shows; otherwise those quotes date it,
|
|
837
|
-
* with their delay and where their markets stand. Then the pane's report, as
|
|
838
|
-
* `fn` ends; then what the rendered view carries. A function that opens one
|
|
839
|
-
* view of a pane hosting several declares it on its template (DDIS in Fixed
|
|
840
|
-
* Income), so its filings, not the issuer's stock quote, date the capture.
|
|
841
|
-
*/
|
|
842
|
-
export function shotFreshness(
|
|
843
|
-
resolved: Pick<ResolvedPaneFunction, "pane" | "template">,
|
|
844
|
-
payload: Pick<DesktopPaneShotPayload, "financials" | "instrumentFinancials">,
|
|
845
|
-
render: Pick<DesktopPaneShotRenderResult, "semanticUi" | "footerText" | "rows">,
|
|
846
|
-
bridgedQuotes: readonly Quote[],
|
|
847
|
-
report: CaptureReport | undefined,
|
|
848
|
-
now = Date.now(),
|
|
849
|
-
): CaptureStatusLine {
|
|
850
|
-
const published = renderedReportFreshness(render.semanticUi ?? []);
|
|
851
|
-
const declared = { ...declaredReportFreshness(resolved), ...published };
|
|
852
|
-
const quotes = [
|
|
853
|
-
...payload.financials.map(([, financials]) => financials.quote),
|
|
854
|
-
...(payload.instrumentFinancials ?? []).map(({ financials }) => financials.quote),
|
|
855
|
-
...bridgedQuotes,
|
|
856
|
-
];
|
|
857
|
-
// A pane that publishes the observations behind its figures (FXC's rates) dates itself.
|
|
858
|
-
const observed = renderedReportObservations(render.semanticUi ?? []);
|
|
859
|
-
const quoted = declared.status || observed.length > 0
|
|
860
|
-
? undefined
|
|
861
|
-
: quotesFreshness(quotes, declared.source ? { source: declared.source } : undefined, now);
|
|
862
|
-
return captureStatusLine({
|
|
863
|
-
report,
|
|
864
|
-
selfDated: Object.keys(published).length > 0 || observed.length > 0,
|
|
865
|
-
quoted,
|
|
866
|
-
rendered: () => deriveRenderedFreshness(declared, {
|
|
867
|
-
footerText: render.footerText ?? "",
|
|
868
|
-
cellTimes: render.rows.flatMap((row) => row.cells.flatMap((cell) => (cell.instant ? [cell.instant] : []))),
|
|
869
|
-
observed,
|
|
870
|
-
}, now),
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* A capture's data dated as its `fn` report dates it, from the same loader:
|
|
876
|
-
* COT's weekly report, CHOKE's transits, GPU's prices. Undefined for a pane
|
|
877
|
-
* without a report, or when it did not load.
|
|
878
|
-
*/
|
|
879
|
-
async function reportShotFreshness(
|
|
880
|
-
resolved: Pick<ResolvedPaneFunction, "headless">,
|
|
881
|
-
loadModel: () => Promise<LoadedHeadlessPaneModel>,
|
|
882
|
-
now = Date.now(),
|
|
883
|
-
): Promise<CaptureReport | undefined> {
|
|
884
|
-
if (!resolved.headless) return undefined;
|
|
885
|
-
try {
|
|
886
|
-
const loaded = await loadModel();
|
|
887
|
-
return deriveCaptureReport(loaded.definition, loaded.result, now);
|
|
888
|
-
} catch (error) {
|
|
889
|
-
logFailure("cli.screenshot", "Read screenshot report freshness failed")(error);
|
|
890
|
-
return undefined;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/** What a capture's report says about its own completeness (`fn`'s `complete`), as `reportCoverage` derives it. */
|
|
895
|
-
export interface ShotReportCoverage {
|
|
896
|
-
complete: boolean;
|
|
897
|
-
unavailableSymbols: string[];
|
|
898
|
-
gaps?: string;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Whether the pane's report covers what was asked, loaded once with the
|
|
903
|
-
* capture's other uses of it. Undefined for a pane without a report, or when
|
|
904
|
-
* it did not load.
|
|
905
|
-
*/
|
|
906
|
-
export async function reportShotCoverage(
|
|
907
|
-
resolved: Pick<ResolvedPaneFunction, "headless">,
|
|
908
|
-
loadModel: () => Promise<LoadedHeadlessPaneModel>,
|
|
909
|
-
): Promise<ShotReportCoverage | undefined> {
|
|
910
|
-
if (!resolved.headless) return undefined;
|
|
911
|
-
try {
|
|
912
|
-
const loaded = await loadModel();
|
|
913
|
-
const { complete, unavailableSymbols } = reportCoverage(loaded);
|
|
914
|
-
const gaps = reportGaps(loaded, deriveHeadlessFreshness(loaded.definition, loaded.result));
|
|
915
|
-
return { complete, unavailableSymbols, ...(gaps ? { gaps } : {}) };
|
|
916
|
-
} catch (error) {
|
|
917
|
-
logFailure("cli.screenshot", "Read screenshot report coverage failed")(error);
|
|
918
|
-
return undefined;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* The capture's result with its report's completeness: a capture of a partial
|
|
924
|
-
* report is partial too, whatever the rendered view shows. Whether the image
|
|
925
|
-
* is fit to use stays what the render says.
|
|
926
|
-
*/
|
|
927
|
-
export function withReportCoverage(result: PaneScreenshotResult, coverage: ShotReportCoverage | undefined): PaneScreenshotResult {
|
|
928
|
-
if (!coverage || coverage.complete) return result;
|
|
929
|
-
return {
|
|
930
|
-
...result,
|
|
931
|
-
complete: false,
|
|
932
|
-
unavailableSymbols: [...new Set([...result.unavailableSymbols, ...coverage.unavailableSymbols])],
|
|
933
|
-
...(coverage.gaps ? { gaps: coverage.gaps } : {}),
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* The words after `Status` in a capture's text output: why it is not
|
|
939
|
-
* complete, empty, matching its data or usable, `partial: 2 stale`. Empty when
|
|
940
|
-
* nothing is wrong.
|
|
941
|
-
*/
|
|
942
|
-
export function shotStatusIssues(
|
|
943
|
-
data: Pick<PaneScreenshotResult, "empty" | "complete" | "gaps" | "semanticMismatch" | "usable"> & {
|
|
944
|
-
render: Pick<PaneScreenshotResult["render"], "accessGate">;
|
|
945
|
-
},
|
|
946
|
-
): string[] {
|
|
947
|
-
const gated = data.render.accessGate && (data.empty || !data.usable) ? data.render.accessGate : null;
|
|
948
|
-
return [
|
|
949
|
-
gated ? accessGateStatus(gated) : data.empty ? "empty" : null,
|
|
950
|
-
// A gate already says why a report is not whole.
|
|
951
|
-
data.complete ? null : data.gaps ? `partial: ${data.gaps}` : gated ? null : "partial",
|
|
952
|
-
data.semanticMismatch ? "does not match the data" : null,
|
|
953
|
-
data.usable ? null : "not usable",
|
|
954
|
-
].filter((issue): issue is string => issue != null);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/** How long a rendered capture waits for its report's freshness before dating itself without it. */
|
|
958
|
-
const SHOT_REPORT_WAIT_MS = 20_000;
|
|
959
|
-
|
|
960
|
-
/**
|
|
961
|
-
* The value of the report's `tab` option for the tab the capture shows, when
|
|
962
|
-
* it was asked for another: `DOE --tab all` draws Crude, so it is dated by
|
|
963
|
-
* Wednesday's petroleum report, not by Thursday's gas storage. Undefined when
|
|
964
|
-
* it shows the tab asked for.
|
|
965
|
-
*/
|
|
966
|
-
export function shownReportTab(
|
|
967
|
-
resolved: Pick<ResolvedPaneFunction, "headless" | "options">,
|
|
968
|
-
semanticUi: readonly RemoteUiNodeSnapshot[],
|
|
969
|
-
): string | undefined {
|
|
970
|
-
const asked = resolved.options.tab;
|
|
971
|
-
const values = resolved.headless?.options?.find(({ key }) => key === "tab")?.values?.map(({ value }) => value) ?? [];
|
|
972
|
-
if (asked == null || values.length === 0) return undefined;
|
|
973
|
-
for (const node of semanticUi) {
|
|
974
|
-
const active = node.role === "tabs" ? node.metadata?.activeValue : undefined;
|
|
975
|
-
if (typeof active === "string" && values.includes(active)) return active === String(asked) ? undefined : active;
|
|
976
|
-
}
|
|
977
|
-
return undefined;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
/** `pending`'s value, or undefined when it has not settled within `ms`; a later result is dropped. */
|
|
981
|
-
function settledWithin<T>(pending: Promise<T>, ms: number): Promise<T | undefined> {
|
|
982
|
-
let cancel = () => {};
|
|
983
|
-
const expired = new Promise<undefined>((resolve) => {
|
|
984
|
-
const timer = setTimeout(() => resolve(undefined), ms);
|
|
985
|
-
cancel = () => clearTimeout(timer);
|
|
986
|
-
});
|
|
987
|
-
return Promise.race([pending, expired]).finally(() => cancel());
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/** One load of the pane's report per capture, shared by the payload and the status line. */
|
|
991
|
-
function onceLoader(load: () => Promise<LoadedHeadlessPaneModel>): () => Promise<LoadedHeadlessPaneModel> {
|
|
992
|
-
let pending: Promise<LoadedHeadlessPaneModel> | undefined;
|
|
993
|
-
return () => (pending ??= load());
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
export async function renderDesktopShot({
|
|
997
|
-
resolved,
|
|
998
|
-
context,
|
|
999
|
-
rawArg,
|
|
1000
|
-
outputPath,
|
|
1001
|
-
width,
|
|
1002
|
-
height,
|
|
1003
|
-
theme,
|
|
1004
|
-
scale,
|
|
1005
|
-
watermark,
|
|
1006
|
-
statusLine = false,
|
|
1007
|
-
fitRows = false,
|
|
1008
|
-
fitReader = false,
|
|
1009
|
-
maxHeightCells,
|
|
1010
|
-
options,
|
|
1011
|
-
captureImage = true,
|
|
1012
|
-
onStalledReport,
|
|
1013
|
-
}: {
|
|
1014
|
-
resolved: ResolvedPaneFunction;
|
|
1015
|
-
context: MarketContext;
|
|
1016
|
-
rawArg: string;
|
|
1017
|
-
outputPath: string;
|
|
1018
|
-
width: number;
|
|
1019
|
-
height: number;
|
|
1020
|
-
theme?: string | null;
|
|
1021
|
-
scale?: number;
|
|
1022
|
-
watermark?: string | null;
|
|
1023
|
-
/** Draw the dated status line in the capture's footer. Reports and `--no-status` leave it out. */
|
|
1024
|
-
statusLine?: boolean;
|
|
1025
|
-
/** No `--height` was asked for: a pane that fits its rows (FUT) grows to show them all. */
|
|
1026
|
-
fitRows?: boolean;
|
|
1027
|
-
/** A pane whose reader has a page open grows to show all of its text (a report's, which saves no image). */
|
|
1028
|
-
fitReader?: boolean;
|
|
1029
|
-
/** How tall a pane that fits its rows or its reader may grow, in cells, where the height limit of a saved image does not apply. */
|
|
1030
|
-
maxHeightCells?: number;
|
|
1031
|
-
options: Record<string, string | true>;
|
|
1032
|
-
captureImage?: boolean;
|
|
1033
|
-
/**
|
|
1034
|
-
* Called when the report behind the status line was still loading at the
|
|
1035
|
-
* wait's end: the load was aborted, but a loader that ignores the signal
|
|
1036
|
-
* may keep the process alive.
|
|
1037
|
-
*/
|
|
1038
|
-
onStalledReport?: () => void;
|
|
1039
|
-
}): Promise<PaneScreenshotResult> {
|
|
1040
|
-
if (captureImage) await mkdir(dirname(outputPath), { recursive: true });
|
|
1041
|
-
const apiProxy = resolveDesktopShotApiProxy(context);
|
|
1042
|
-
const previousSessionToken = apiClient.getSessionToken();
|
|
1043
|
-
let payload: DesktopPaneShotPayload;
|
|
1044
|
-
let render: DesktopPaneShotRenderResult;
|
|
1045
|
-
let freshness: ReportFreshness | undefined;
|
|
1046
|
-
let coverage: Promise<ShotReportCoverage | undefined> | undefined;
|
|
1047
|
-
let covered: ShotReportCoverage | undefined;
|
|
1048
|
-
// A report still loading when the status line stops waiting for it is
|
|
1049
|
-
// aborted, and the caller told: a loader that ignores the signal goes on.
|
|
1050
|
-
const reportLoads = new AbortController();
|
|
1051
|
-
let loading = 0;
|
|
1052
|
-
const loadReport = (target: ResolvedPaneFunction) => {
|
|
1053
|
-
loading += 1;
|
|
1054
|
-
return loadResolvedHeadlessPaneModel(target, context, rawArg, reportLoads.signal).finally(() => { loading -= 1; });
|
|
1055
|
-
};
|
|
1056
|
-
const abandonReports = () => {
|
|
1057
|
-
if (loading === 0 || reportLoads.signal.aborted) return;
|
|
1058
|
-
reportLoads.abort(new Error("The capture stopped waiting for its report."));
|
|
1059
|
-
onStalledReport?.();
|
|
1060
|
-
};
|
|
1061
|
-
apiClient.setSessionToken(apiProxy.sessionToken);
|
|
1062
|
-
try {
|
|
1063
|
-
const loadModel = onceLoader(() => loadReport(resolved));
|
|
1064
|
-
payload = await buildDesktopShotPayload(
|
|
1065
|
-
resolved,
|
|
1066
|
-
context,
|
|
1067
|
-
rawArg,
|
|
1068
|
-
options,
|
|
1069
|
-
width,
|
|
1070
|
-
height,
|
|
1071
|
-
theme ?? null,
|
|
1072
|
-
scale ?? 1,
|
|
1073
|
-
watermark ?? null,
|
|
1074
|
-
loadModel,
|
|
1075
|
-
);
|
|
1076
|
-
if (statusLine && captureImage) payload.statusLine = true;
|
|
1077
|
-
const fitRoles = [
|
|
1078
|
-
...(fitRows && paneFitsRows(resolved) ? ["data-table-body-scroll"] : []),
|
|
1079
|
-
...(fitReader && resolved.pane.shotFitsReader ? ["detail-scroll-body"] : []),
|
|
1080
|
-
];
|
|
1081
|
-
if (fitRoles.length > 0) {
|
|
1082
|
-
payload.maxHeightCells = maxHeightCells ?? shotMaxHeightCells(scale ?? 1);
|
|
1083
|
-
payload.fitRoles = fitRoles;
|
|
1084
|
-
}
|
|
1085
|
-
// Loads while the page renders.
|
|
1086
|
-
const report = payload.statusLine ? reportShotFreshness(resolved, loadModel) : undefined;
|
|
1087
|
-
// The same load says whether the report is complete, with or without a status line.
|
|
1088
|
-
coverage = captureImage ? reportShotCoverage(resolved, loadModel) : undefined;
|
|
1089
|
-
const recorded = recordBridgedQuotes(createDesktopShotBridge(context));
|
|
1090
|
-
const shotPayload = payload;
|
|
1091
|
-
render = await renderDesktopPaneScreenshot(payload, outputPath, apiProxy, {
|
|
1092
|
-
captureImage,
|
|
1093
|
-
bridge: recorded.bridge,
|
|
1094
|
-
statusLine: async (rendered) => {
|
|
1095
|
-
const tab = report && shownReportTab(resolved, rendered.semanticUi);
|
|
1096
|
-
const shown = tab ? reportShotFreshness(resolved, () => loadReport({ ...resolved, options: { ...resolved.options, tab } })) : report;
|
|
1097
|
-
const line = shotFreshness(resolved, shotPayload, rendered, recorded.quotes, shown && await settledWithin(shown, SHOT_REPORT_WAIT_MS));
|
|
1098
|
-
abandonReports();
|
|
1099
|
-
freshness = line.freshness;
|
|
1100
|
-
return { variants: line.variants, dates: line.dates };
|
|
1101
|
-
},
|
|
1102
|
-
});
|
|
1103
|
-
covered = coverage && await settledWithin(coverage, SHOT_REPORT_WAIT_MS);
|
|
1104
|
-
} finally {
|
|
1105
|
-
apiClient.setSessionToken(previousSessionToken);
|
|
1106
|
-
abandonReports();
|
|
1107
|
-
}
|
|
1108
|
-
const renderedInstance = payload.config.layout.instances.find(({ instanceId }) => instanceId === payload.paneId);
|
|
1109
|
-
if (renderedInstance) resolved = { ...resolved, instance: renderedInstance };
|
|
1110
|
-
const assessed = withReportCoverage(assessPaneScreenshot(resolved, payload, render, rawArg, outputPath), covered);
|
|
1111
|
-
return freshness ? { ...assessed, freshness } : assessed;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* For a pane that grows to fit its rows, the rows its table reports (every
|
|
1116
|
-
* one, `reportEveryRow`) and how many of them the image shows, by key;
|
|
1117
|
-
* section headers count in neither. Null for any other pane, or a table that
|
|
1118
|
-
* reports only what is drawn.
|
|
1119
|
-
*/
|
|
1120
|
-
export function shotTableRows(
|
|
1121
|
-
resolved: Pick<ResolvedPaneFunction, "pane" | "instance">,
|
|
1122
|
-
render: Pick<DesktopPaneShotRenderResult, "rows" | "reportRows">,
|
|
1123
|
-
): { shown: number; total: number } | null {
|
|
1124
|
-
if (!paneFitsRows(resolved) || !render.reportRows?.length) return null;
|
|
1125
|
-
const reported = render.reportRows.filter((row) => !row.section && row.key);
|
|
1126
|
-
if (reported.length === 0) return null;
|
|
1127
|
-
const drawn = new Set(render.rows.flatMap((row) => row.key ? [`${row.tableIndex}:${row.key}`] : []));
|
|
1128
|
-
return { shown: reported.filter((row) => drawn.has(`${row.tableIndex}:${row.key}`)).length, total: reported.length };
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/** What a rendered capture shows, and whether it is fit to use. */
|
|
1132
|
-
export function assessPaneScreenshot(
|
|
1133
|
-
resolved: ResolvedPaneFunction,
|
|
1134
|
-
payload: DesktopPaneShotPayload,
|
|
1135
|
-
render: DesktopPaneShotRenderResult,
|
|
1136
|
-
rawArg: string,
|
|
1137
|
-
outputPath: string,
|
|
1138
|
-
): PaneScreenshotResult {
|
|
1139
|
-
const evidenceHook = paneScreenshotEvidenceHook(resolved);
|
|
1140
|
-
// The sizer carries portfolio members to value holdings and the earnings board
|
|
1141
|
-
// to mark them, but each shows only what its argument asks for.
|
|
1142
|
-
const symbols = evidenceHook?.symbols?.({ resolved, payload })
|
|
1143
|
-
?? (resolved.pane.id === "kelly-sizer" || resolved.pane.id === "earnings-calendar" ? collectShotSymbols(resolved, rawArg)
|
|
1144
|
-
: payload.financials.map(([symbol]) => symbol));
|
|
1145
|
-
const usesLiveDomEvidence = resolved.capability.screenshotReadiness === "live-dom" && !evidenceHook;
|
|
1146
|
-
// A live pane without a table, such as a ticker overview, shows its data
|
|
1147
|
-
// as labeled values.
|
|
1148
|
-
const rowCount = usesLiveDomEvidence
|
|
1149
|
-
? render.rows.length || filledKeyValueCount(render.visibleKeyValues)
|
|
1150
|
-
: shotSemanticRowCount(resolved, payload, render.semanticUi);
|
|
1151
|
-
const unavailableSymbols = usesLiveDomEvidence
|
|
1152
|
-
? []
|
|
1153
|
-
: shotUnavailableSymbols(resolved, payload, render.semanticUi);
|
|
1154
|
-
const complete = unavailableSymbols.length === 0;
|
|
1155
|
-
const expectedText = shotExpectedText(resolved, symbols, payload);
|
|
1156
|
-
const normalizedVisibleText = render.visibleText.toLowerCase();
|
|
1157
|
-
const missingExpectedText = expectedText.filter((value) => !normalizedVisibleText.includes(value.toLowerCase()));
|
|
1158
|
-
const expectedSelections = shotExpectedSelections(resolved);
|
|
1159
|
-
const missingExpectedSelections = missingActiveTabSelections(
|
|
1160
|
-
render.semanticUi,
|
|
1161
|
-
expectedSelections,
|
|
1162
|
-
);
|
|
1163
|
-
const expectedChart = shotExpectedChart(resolved, payload);
|
|
1164
|
-
const dataEvidence = shotDataEvidenceFor(resolved, payload, render.semanticUi);
|
|
1165
|
-
const chartEvidenceMismatches = [
|
|
1166
|
-
...(expectedChart ? chartEvidenceMismatchesFor(render.semanticUi, expectedChart) : []),
|
|
1167
|
-
...intradayChartEvidenceMismatchesFor(resolved, payload, render.semanticUi),
|
|
1168
|
-
...paneEvidenceMismatches(resolved, payload, render),
|
|
1169
|
-
...financialRatioRenderMismatches(resolved, payload, render),
|
|
1170
|
-
...surfaceNotDrawnMismatches(render),
|
|
1171
|
-
];
|
|
1172
|
-
const semanticMismatch = missingExpectedText.length > 0
|
|
1173
|
-
|| missingExpectedSelections.length > 0
|
|
1174
|
-
|| chartEvidenceMismatches.length > 0;
|
|
1175
|
-
const empty = render.loadingStateDetected
|
|
1176
|
-
|| render.errorStateDetected
|
|
1177
|
-
|| render.emptyStateDetected
|
|
1178
|
-
|| rowCount === 0
|
|
1179
|
-
|| semanticMismatch;
|
|
1180
|
-
const usable = isPaneScreenshotUsable({
|
|
1181
|
-
rowCount,
|
|
1182
|
-
loadingStateDetected: render.loadingStateDetected,
|
|
1183
|
-
errorStateDetected: render.errorStateDetected,
|
|
1184
|
-
emptyStateDetected: render.emptyStateDetected,
|
|
1185
|
-
complete,
|
|
1186
|
-
semanticMismatch,
|
|
1187
|
-
requiresStructuredDataEvidence: requiresStructuredDataEvidence(resolved),
|
|
1188
|
-
hasStructuredDataEvidence: dataEvidence !== null,
|
|
1189
|
-
});
|
|
1190
|
-
const unusableReason = usable
|
|
1191
|
-
? null
|
|
1192
|
-
: shotUnusableReasonFor(resolved, payload, render, unavailableSymbols, semanticMismatch);
|
|
1193
|
-
const tableRows = shotTableRows(resolved, render);
|
|
1194
|
-
const notices = [
|
|
1195
|
-
...renderedReportNotices(render.semanticUi ?? []).notices,
|
|
1196
|
-
...unquotedHoldingsNotice(resolved, payload),
|
|
1197
|
-
...(render.surfaces?.some((surface) => surface.renderer === "software")
|
|
1198
|
-
? ["The 3D surface was drawn by the software renderer: this browser has no WebGL."] : []),
|
|
1199
|
-
];
|
|
1200
|
-
return {
|
|
1201
|
-
kind: "pane-screenshot",
|
|
1202
|
-
target: resolved.token,
|
|
1203
|
-
capability: {
|
|
1204
|
-
id: resolved.capability.id,
|
|
1205
|
-
botSafe: resolved.capability.botSafe,
|
|
1206
|
-
outputKind: resolved.capability.outputKind,
|
|
1207
|
-
reportReadiness: resolved.capability.reportReadiness,
|
|
1208
|
-
screenshotReadiness: resolved.capability.screenshotReadiness,
|
|
1209
|
-
},
|
|
1210
|
-
symbols,
|
|
1211
|
-
options: stripDesktopShotCredentials(resolved.options),
|
|
1212
|
-
rowCount,
|
|
1213
|
-
empty,
|
|
1214
|
-
complete,
|
|
1215
|
-
unavailableSymbols,
|
|
1216
|
-
semanticMismatch,
|
|
1217
|
-
usable,
|
|
1218
|
-
unusableReason,
|
|
1219
|
-
dataEvidence,
|
|
1220
|
-
...(notices.length > 0 ? { notices } : {}),
|
|
1221
|
-
...(tableRows ? { tableRows } : {}),
|
|
1222
|
-
outputPath,
|
|
1223
|
-
render: {
|
|
1224
|
-
...stripDesktopShotCredentials(render),
|
|
1225
|
-
expectedText,
|
|
1226
|
-
missingExpectedText,
|
|
1227
|
-
expectedSelections,
|
|
1228
|
-
missingExpectedSelections,
|
|
1229
|
-
expectedChart,
|
|
1230
|
-
chartEvidenceMismatches,
|
|
1231
|
-
},
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
/**
|
|
1236
|
-
* The holdings of a portfolio or watchlist capture no source quoted (an
|
|
1237
|
-
* over-the-counter name none carries), whose rows show no price: "No quote
|
|
1238
|
-
* for KISB; its row shows no price."
|
|
1239
|
-
*/
|
|
1240
|
-
function unquotedHoldingsNotice(resolved: ResolvedPaneFunction, payload: DesktopPaneShotPayload): string[] {
|
|
1241
|
-
if (!COLLECTION_PANE_IDS.has(resolved.pane.id)) return [];
|
|
1242
|
-
const unquoted = payload.financials.flatMap(([symbol, financials]) => (
|
|
1243
|
-
!financials.quote && financials.priceHistory.length === 0 && financials.annualStatements.length === 0 ? [symbol] : []
|
|
1244
|
-
));
|
|
1245
|
-
if (unquoted.length === 0) return [];
|
|
1246
|
-
return [`No quote for ${unquoted.join(", ")}; ${unquoted.length === 1 ? "its row shows" : "their rows show"} no price.`];
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function requiresStructuredDataEvidence(resolved: ResolvedPaneFunction): boolean {
|
|
1250
|
-
return paneScreenshotEvidenceHook(resolved) !== null || [
|
|
1251
|
-
"price-chart",
|
|
1252
|
-
"intraday-price-chart",
|
|
1253
|
-
"price-comparison",
|
|
1254
|
-
"fundamental-series",
|
|
1255
|
-
"financial-statements",
|
|
1256
|
-
].includes(resolved.capability.id);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
const PLACEHOLDER_VALUE = /^(?:[-\u2013\u2014]+|n\/?a)?$/i;
|
|
1260
|
-
|
|
1261
|
-
/** Labeled values that hold data. A grid of labels over "-" placeholders,
|
|
1262
|
-
* as for a ticker without fundamentals, is an empty pane. */
|
|
1263
|
-
export function filledKeyValueCount(rows: DesktopPaneShotRenderResult["visibleKeyValues"]): number {
|
|
1264
|
-
return (rows ?? []).filter(({ label, text }) => {
|
|
1265
|
-
const value = text.startsWith(label) ? text.slice(label.length) : text;
|
|
1266
|
-
return !PLACEHOLDER_VALUE.test(value.trim());
|
|
1267
|
-
}).length;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
/**
|
|
1271
|
-
* A 3D surface that is not the real drawing: the frame has no mesh, or the 2D
|
|
1272
|
-
* grid stands in for it. Both are fine for a person using the pane and wrong
|
|
1273
|
-
* for a capture of it.
|
|
1274
|
-
*/
|
|
1275
|
-
export function surfaceNotDrawnMismatches(render: Pick<DesktopPaneShotRenderResult, "surfaces">): string[] {
|
|
1276
|
-
const surfaces = render.surfaces ?? [];
|
|
1277
|
-
return [
|
|
1278
|
-
...(surfaces.some((surface) => surface.state === "blank") ? ["the 3D surface drew no mesh"] : []),
|
|
1279
|
-
...(surfaces.some((surface) => surface.renderer === "grid") ? ["the 3D surface was replaced by a 2D grid"] : []),
|
|
1280
|
-
];
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
export function shotUnusableReasonFor(
|
|
1284
|
-
resolved: ResolvedPaneFunction,
|
|
1285
|
-
payload: DesktopPaneShotPayload,
|
|
1286
|
-
render: Pick<DesktopPaneShotRenderResult, "loadingStateDetected" | "errorStateDetected" | "emptyStateDetected" | "accessGate" | "surfaces">,
|
|
1287
|
-
unavailableSymbols: string[],
|
|
1288
|
-
semanticMismatch: boolean,
|
|
1289
|
-
): string {
|
|
1290
|
-
if (resolved.capability.id === "intraday-price-chart") {
|
|
1291
|
-
const intraday = payload.intradayHistories[0];
|
|
1292
|
-
if (intraday?.unavailableReason) return intraday.unavailableReason;
|
|
1293
|
-
if (intraday && intraday.points.length > 0) {
|
|
1294
|
-
return `Intraday bars were loaded for ${intraday.symbol}, but the chart did not render them.`;
|
|
1295
|
-
}
|
|
1296
|
-
const symbol = payload.financials[0]?.[0] ?? resolved.createOptions?.symbol ?? "the ticker";
|
|
1297
|
-
return `No intraday price history is available for ${symbol} for the requested session window.`;
|
|
1298
|
-
}
|
|
1299
|
-
if (render.loadingStateDetected) return "The pane was still loading when the screenshot was captured.";
|
|
1300
|
-
if (render.accessGate) return accessGateSentence(render.accessGate);
|
|
1301
|
-
if (render.errorStateDetected) return "The pane rendered an error state.";
|
|
1302
|
-
if (render.emptyStateDetected) return "The pane rendered an empty state.";
|
|
1303
|
-
const surfaceGap = surfaceNotDrawnMismatches(render)[0];
|
|
1304
|
-
if (surfaceGap) return `${surfaceGap[0]!.toUpperCase()}${surfaceGap.slice(1)}.`;
|
|
1305
|
-
if (unavailableSymbols.length > 0) return `Data is unavailable for ${unavailableSymbols.join(", ")}.`;
|
|
1306
|
-
const ratioGap = financialRatioShotGap(resolved, payload);
|
|
1307
|
-
if (ratioGap) return ratioGap;
|
|
1308
|
-
if (semanticMismatch) return "The rendered content did not match the requested capability.";
|
|
1309
|
-
return "The pane did not produce verifiable screenshot evidence.";
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
const STATEMENT_EVIDENCE_KEYS: Record<string, ReadonlySet<string>> = {
|
|
1313
|
-
income: new Set([
|
|
1314
|
-
"totalRevenue",
|
|
1315
|
-
"grossProfit",
|
|
1316
|
-
"operatingIncome",
|
|
1317
|
-
"netIncome",
|
|
1318
|
-
"netIncomeCommonStockholders",
|
|
1319
|
-
"eps",
|
|
1320
|
-
"basicEps",
|
|
1321
|
-
]),
|
|
1322
|
-
cashflow: new Set([
|
|
1323
|
-
"operatingCashFlow",
|
|
1324
|
-
"capitalExpenditure",
|
|
1325
|
-
"freeCashFlow",
|
|
1326
|
-
]),
|
|
1327
|
-
balance: new Set([
|
|
1328
|
-
"totalAssets",
|
|
1329
|
-
"totalLiabilities",
|
|
1330
|
-
"cashAndCashEquivalents",
|
|
1331
|
-
"totalDebt",
|
|
1332
|
-
"totalEquity",
|
|
1333
|
-
"commonStockEquity",
|
|
1334
|
-
]),
|
|
1335
|
-
};
|
|
1336
|
-
|
|
1337
|
-
export function shotDataEvidenceFor(
|
|
1338
|
-
resolved: ResolvedPaneFunction,
|
|
1339
|
-
payload: DesktopPaneShotPayload,
|
|
1340
|
-
semanticUi: RemoteUiNodeSnapshot[] = [],
|
|
1341
|
-
): PaneScreenshotDataEvidence | null {
|
|
1342
|
-
const evidenceHook = paneScreenshotEvidenceHook(resolved);
|
|
1343
|
-
if (evidenceHook) return renderedPaneEvidence(evidenceHook, semanticUi);
|
|
1344
|
-
const spec = payload.chartModel ? parseChartSpec(payload.config.layout.instances.find((instance) => instance.instanceId === payload.paneId)?.settings?.chartSpec) : null;
|
|
1345
|
-
const visibleSeries = payload.chartModel && spec ? spec.series.flatMap((entry) => {
|
|
1346
|
-
if (entry.source.kind !== "security" || entry.visible === false) return [];
|
|
1347
|
-
const output = payload.chartModel!.series.find((series) => series.id === entry.id);
|
|
1348
|
-
return [{
|
|
1349
|
-
symbol: publicTickerKey(entry.source.instrument.symbol, entry.source.instrument.exchange),
|
|
1350
|
-
points: (output?.points ?? []).flatMap((point) => {
|
|
1351
|
-
const close = point.rawValue === undefined ? point.value ?? point.close : point.rawValue;
|
|
1352
|
-
return typeof close === "number" && Number.isFinite(close) ? [{ date: point.date, close }] : [];
|
|
1353
|
-
}),
|
|
1354
|
-
}];
|
|
1355
|
-
}) : null;
|
|
1356
|
-
if (resolved.capability.id === "intraday-price-chart") {
|
|
1357
|
-
const intraday = payload.intradayHistories[0];
|
|
1358
|
-
if (!intraday || intraday.points.length === 0) return null;
|
|
1359
|
-
const evidence = chartSeriesEvidence(intraday.symbol, intraday.points);
|
|
1360
|
-
if (!evidence.first || !evidence.last) return null;
|
|
1361
|
-
return {
|
|
1362
|
-
kind: "price-series",
|
|
1363
|
-
symbol: intraday.symbol,
|
|
1364
|
-
range: intraday.rangePreset,
|
|
1365
|
-
pointCount: evidence.pointCount,
|
|
1366
|
-
first: evidence.first,
|
|
1367
|
-
last: evidence.last,
|
|
1368
|
-
resolution: intraday.resolution,
|
|
1369
|
-
sessionDates: intraday.sessionDates,
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
if (resolved.capability.id === "price-chart") {
|
|
1374
|
-
const range = String(resolved.options.rangePreset ?? "5Y") as TimeRange;
|
|
1375
|
-
const [symbol, financials] = payload.financials[0] ?? [];
|
|
1376
|
-
const captured = visibleSeries?.[0];
|
|
1377
|
-
const evidence = captured ? chartSeriesEvidence(captured.symbol, captured.points)
|
|
1378
|
-
: visibleSeries === null && symbol && financials ? normalizeChartSeries(symbol, financials, range).evidence : null;
|
|
1379
|
-
if (!evidence?.first || !evidence.last || evidence.pointCount <= 0) return null;
|
|
1380
|
-
return { kind: "price-series", range, ...evidence, first: evidence.first, last: evidence.last };
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
if (resolved.capability.id === "price-comparison") {
|
|
1384
|
-
const range = String(resolved.options.rangePreset ?? "1Y") as TimeRange;
|
|
1385
|
-
const normalizedSeries = visibleSeries ?? payload.financials.map(([symbol, financials]) => ({
|
|
1386
|
-
symbol,
|
|
1387
|
-
points: normalizeChartSeries(symbol, financials, range).points,
|
|
1388
|
-
}));
|
|
1389
|
-
const latestTimestamp = Math.max(
|
|
1390
|
-
...normalizedSeries.flatMap(({ points }) => points.map(({ date }) => date.getTime())),
|
|
1391
|
-
);
|
|
1392
|
-
if (!Number.isFinite(latestTimestamp)) return null;
|
|
1393
|
-
const projectionStart = subtractTimeRange(new Date(latestTimestamp), range).getTime();
|
|
1394
|
-
const series = normalizedSeries.flatMap(({ symbol, points }) => {
|
|
1395
|
-
const projectionPoints = visibleSeries ? points : points.filter(({ date }) => date.getTime() >= projectionStart);
|
|
1396
|
-
const base = projectionPoints[0];
|
|
1397
|
-
const latest = projectionPoints.at(-1);
|
|
1398
|
-
if (!base || !latest || !Number.isFinite(base.close) || !Number.isFinite(latest.close) || base.close === 0) {
|
|
1399
|
-
return [];
|
|
1400
|
-
}
|
|
1401
|
-
return [{
|
|
1402
|
-
symbol,
|
|
1403
|
-
base: { date: base.date.toISOString(), value: base.close },
|
|
1404
|
-
latest: { date: latest.date.toISOString(), value: latest.close },
|
|
1405
|
-
returnPercent: ((latest.close - base.close) / base.close) * 100,
|
|
1406
|
-
}];
|
|
1407
|
-
});
|
|
1408
|
-
if (series.length !== normalizedSeries.length) return null;
|
|
1409
|
-
return {
|
|
1410
|
-
kind: "price-comparison",
|
|
1411
|
-
symbols: series.map(({ symbol }) => symbol),
|
|
1412
|
-
range,
|
|
1413
|
-
series,
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (resolved.capability.id === "fundamental-series") {
|
|
1418
|
-
const metric = resolved.options.metric as GraphMetricKey;
|
|
1419
|
-
const period = resolved.options.period as FundamentalPeriod;
|
|
1420
|
-
const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
|
|
1421
|
-
const evidencePeriodCount = Math.min(
|
|
1422
|
-
periodCount ?? (period === "annual" ? 6 : 8),
|
|
1423
|
-
period === "annual" ? 6 : 8,
|
|
1424
|
-
);
|
|
1425
|
-
const series = visibleSeries?.map(({ symbol, points }) => ({
|
|
1426
|
-
symbol, rows: points.slice(-evidencePeriodCount).map(({ date, close }) => ({ date: date.toISOString().slice(0, 10), value: close })),
|
|
1427
|
-
})) ?? payload.financials.map(([symbol, financials]) => ({
|
|
1428
|
-
symbol,
|
|
1429
|
-
rows: limitGraphRowsBySymbol(
|
|
1430
|
-
graphRowsForFinancials(financials, "fundamental", metric, period, symbol),
|
|
1431
|
-
evidencePeriodCount,
|
|
1432
|
-
)
|
|
1433
|
-
.sort((left, right) => left.date.localeCompare(right.date))
|
|
1434
|
-
.map(({ date, value }) => ({ date, value })),
|
|
1435
|
-
}));
|
|
1436
|
-
if (series.some(({ rows }) => rows.length === 0)) return null;
|
|
1437
|
-
return { kind: "fundamental-series", metric, period, series };
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (shotRatioTab(resolved)) return financialRatioShotEvidence(resolved, payload);
|
|
1441
|
-
if (resolved.capability.id === "financial-statements") {
|
|
1442
|
-
const [symbol, financials] = payload.financials[0] ?? [];
|
|
1443
|
-
if (!symbol || !financials) return null;
|
|
1444
|
-
const table = buildFinancialTableModel(financials, {
|
|
1445
|
-
period: resolved.options.period as FundamentalPeriod,
|
|
1446
|
-
statement: String(resolved.options.statement),
|
|
1447
|
-
});
|
|
1448
|
-
if (!table || table.statements.length === 0) return null;
|
|
1449
|
-
const allowedKeys = STATEMENT_EVIDENCE_KEYS[table.subTab.key] ?? new Set<string>();
|
|
1450
|
-
const latestPopulated = table.statements.flatMap((statement, statementIndex) => {
|
|
1451
|
-
const seenKeys = new Set<string>();
|
|
1452
|
-
const metrics = table.rows.flatMap((row) => {
|
|
1453
|
-
const key = String(row.key ?? row.summaryKey ?? "");
|
|
1454
|
-
const value = row.cells[statementIndex]?.value;
|
|
1455
|
-
if (!allowedKeys.has(key) || seenKeys.has(key) || typeof value !== "number" || !Number.isFinite(value)) {
|
|
1456
|
-
return [];
|
|
1457
|
-
}
|
|
1458
|
-
seenKeys.add(key);
|
|
1459
|
-
return [{
|
|
1460
|
-
id: row.id,
|
|
1461
|
-
key,
|
|
1462
|
-
label: row.unitLabel,
|
|
1463
|
-
value,
|
|
1464
|
-
}];
|
|
1465
|
-
});
|
|
1466
|
-
return metrics.length > 0 ? [{ statement, metrics }] : [];
|
|
1467
|
-
})[0];
|
|
1468
|
-
if (!latestPopulated) return null;
|
|
1469
|
-
return {
|
|
1470
|
-
kind: "financial-statement",
|
|
1471
|
-
symbol,
|
|
1472
|
-
statement: table.subTab.key,
|
|
1473
|
-
period: table.period,
|
|
1474
|
-
latest: {
|
|
1475
|
-
date: latestPopulated.statement.date,
|
|
1476
|
-
metrics: latestPopulated.metrics,
|
|
1477
|
-
},
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
return null;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
function normalizeChartSeries(
|
|
1485
|
-
symbol: string,
|
|
1486
|
-
financials: TickerFinancials,
|
|
1487
|
-
range: TimeRange,
|
|
1488
|
-
): {
|
|
1489
|
-
evidence: PaneScreenshotChartSeriesEvidence;
|
|
1490
|
-
points: Array<{ date: Date; close: number }>;
|
|
1491
|
-
} {
|
|
1492
|
-
const sorted = clipPriceHistoryToRange(financials.priceHistory, range)
|
|
1493
|
-
.flatMap((point) => {
|
|
1494
|
-
const date = new Date(point.date);
|
|
1495
|
-
return Number.isFinite(date.getTime()) ? [{ ...point, date }] : [];
|
|
1496
|
-
})
|
|
1497
|
-
.sort((left, right) => left.date.getTime() - right.date.getTime());
|
|
1498
|
-
const points = appendLiveQuotePoint(sorted, financials.quote, { assetCategory: financials.quoteMetadata?.instrumentType })
|
|
1499
|
-
.slice()
|
|
1500
|
-
.sort((left, right) => new Date(left.date).getTime() - new Date(right.date).getTime());
|
|
1501
|
-
return {
|
|
1502
|
-
points,
|
|
1503
|
-
evidence: chartSeriesEvidence(symbol, points),
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function chartSeriesEvidence(
|
|
1508
|
-
symbol: string,
|
|
1509
|
-
points: PricePoint[],
|
|
1510
|
-
): PaneScreenshotChartSeriesEvidence {
|
|
1511
|
-
const pointEvidence = (
|
|
1512
|
-
point: typeof points[number] | undefined,
|
|
1513
|
-
): PaneScreenshotChartPointEvidence | null => (
|
|
1514
|
-
point && Number.isFinite(point.date.getTime())
|
|
1515
|
-
? { date: point.date.toISOString(), close: point.close }
|
|
1516
|
-
: null
|
|
1517
|
-
);
|
|
1518
|
-
return {
|
|
1519
|
-
symbol,
|
|
1520
|
-
pointCount: points.length,
|
|
1521
|
-
first: pointEvidence(points[0]),
|
|
1522
|
-
last: pointEvidence(points.at(-1)),
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
export function chartSeriesEvidenceWithinRange(
|
|
1527
|
-
symbol: string,
|
|
1528
|
-
points: PricePoint[],
|
|
1529
|
-
range: TimeRange,
|
|
1530
|
-
): PaneScreenshotChartSeriesEvidence {
|
|
1531
|
-
const dateWindow = buildPresetDateWindow(points.map(({ date }) => date), range);
|
|
1532
|
-
const visiblePoints = getVisibleWindowForDateRange(points, dateWindow, 0).points;
|
|
1533
|
-
return chartSeriesEvidence(symbol, visiblePoints);
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function shotExpectedChart(
|
|
1537
|
-
resolved: ResolvedPaneFunction,
|
|
1538
|
-
payload: DesktopPaneShotPayload,
|
|
1539
|
-
): PaneScreenshotExpectedChartEvidence | null {
|
|
1540
|
-
if (resolved.pane.id === CHART_COMPOSER_PANE_ID) {
|
|
1541
|
-
const spec = parseChartSpec(resolved.instance.settings?.chartSpec);
|
|
1542
|
-
if (!spec) return null;
|
|
1543
|
-
const capabilityPoint = (point: ResolvedSeries["points"][number] | undefined) => point
|
|
1544
|
-
? {
|
|
1545
|
-
date: point.date.toISOString(),
|
|
1546
|
-
value: typeof (point.value ?? point.close) === "number" ? point.value ?? point.close ?? null : null,
|
|
1547
|
-
}
|
|
1548
|
-
: null;
|
|
1549
|
-
return {
|
|
1550
|
-
kind: "chart-composer",
|
|
1551
|
-
symbols: [...new Set(spec.series.flatMap((series) => (
|
|
1552
|
-
series.source.kind === "security"
|
|
1553
|
-
? [publicTickerKey(series.source.instrument.symbol, series.source.instrument.exchange)]
|
|
1554
|
-
: []
|
|
1555
|
-
)))],
|
|
1556
|
-
rangePreset: spec.viewport.range,
|
|
1557
|
-
resolution: spec.viewport.resolution,
|
|
1558
|
-
baseSeries: spec.series.map((series) => ({
|
|
1559
|
-
id: series.id,
|
|
1560
|
-
sourceKind: series.source.kind,
|
|
1561
|
-
...(series.source.kind === "security"
|
|
1562
|
-
? {
|
|
1563
|
-
symbol: publicTickerKey(series.source.instrument.symbol, series.source.instrument.exchange),
|
|
1564
|
-
fieldId: series.source.fieldId,
|
|
1565
|
-
}
|
|
1566
|
-
: series.source.kind === "economic"
|
|
1567
|
-
? { economicSeriesId: series.source.seriesId }
|
|
1568
|
-
: (() => {
|
|
1569
|
-
const resolvedSeries = payload.chartModel?.series.find((entry) => entry.id === series.id);
|
|
1570
|
-
return {
|
|
1571
|
-
capabilityId: series.source.capabilityId,
|
|
1572
|
-
providerSeriesId: series.source.seriesId,
|
|
1573
|
-
first: capabilityPoint(resolvedSeries?.points[0]),
|
|
1574
|
-
last: capabilityPoint(resolvedSeries?.points.at(-1)),
|
|
1575
|
-
};
|
|
1576
|
-
})()),
|
|
1577
|
-
style: series.style,
|
|
1578
|
-
transform: series.transform,
|
|
1579
|
-
panelId: series.panelId,
|
|
1580
|
-
visible: series.visible !== false,
|
|
1581
|
-
})),
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
if (resolved.capability.id === "price-chart") {
|
|
1585
|
-
const range = String(resolved.options.rangePreset ?? "5Y") as TimeRange;
|
|
1586
|
-
const sourceSeries = payload.financials.slice(0, 1).map(([symbol, financials]) => {
|
|
1587
|
-
const { points } = normalizeChartSeries(symbol, financials, range);
|
|
1588
|
-
return chartSeriesEvidenceWithinRange(symbol, points, range);
|
|
1589
|
-
});
|
|
1590
|
-
return {
|
|
1591
|
-
kind: "stock-price",
|
|
1592
|
-
symbols: sourceSeries.map(({ symbol }) => symbol),
|
|
1593
|
-
rangePreset: range,
|
|
1594
|
-
axisMode: "price",
|
|
1595
|
-
sourceSeries,
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
if (resolved.capability.id === "price-comparison") {
|
|
1599
|
-
const range = String(resolved.options.rangePreset ?? "1Y") as TimeRange;
|
|
1600
|
-
const normalizedSeries = payload.financials.map(([symbol, financials]) => (
|
|
1601
|
-
normalizeChartSeries(symbol, financials, range)
|
|
1602
|
-
));
|
|
1603
|
-
const latestTimestamp = Math.max(
|
|
1604
|
-
...normalizedSeries.flatMap(({ points }) => points.map(({ date }) => date.getTime())),
|
|
1605
|
-
);
|
|
1606
|
-
const projectionStart = Number.isFinite(latestTimestamp)
|
|
1607
|
-
? subtractTimeRange(new Date(latestTimestamp), range).getTime()
|
|
1608
|
-
: Number.NaN;
|
|
1609
|
-
const axisMode = String(resolved.options.axisMode ?? "percent");
|
|
1610
|
-
const sourceSeries = normalizedSeries.map(({ evidence, points }) => {
|
|
1611
|
-
const projectionPoints = points.filter(({ date }) => date.getTime() >= projectionStart);
|
|
1612
|
-
const baseValue = projectionPoints[0]?.close ?? null;
|
|
1613
|
-
const latestRawValue = projectionPoints.at(-1)?.close ?? null;
|
|
1614
|
-
const latestValue = baseValue == null || latestRawValue == null
|
|
1615
|
-
? null
|
|
1616
|
-
: axisMode === "percent"
|
|
1617
|
-
? ((latestRawValue - baseValue) / baseValue) * 100
|
|
1618
|
-
: latestRawValue;
|
|
1619
|
-
return {
|
|
1620
|
-
...evidence,
|
|
1621
|
-
projectionBaseValue: baseValue,
|
|
1622
|
-
projectionLatestRawValue: latestRawValue,
|
|
1623
|
-
projectionLatestValue: latestValue,
|
|
1624
|
-
};
|
|
1625
|
-
});
|
|
1626
|
-
return {
|
|
1627
|
-
kind: "price-comparison",
|
|
1628
|
-
symbols: sourceSeries.map(({ symbol }) => symbol),
|
|
1629
|
-
rangePreset: range,
|
|
1630
|
-
axisMode,
|
|
1631
|
-
resolution: String(resolved.options.chartResolution ?? "1d"),
|
|
1632
|
-
sourceSeries,
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
return null;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
function readStringArray(value: unknown): string[] | null {
|
|
1639
|
-
return Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
1640
|
-
? value
|
|
1641
|
-
: null;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
function closeEnough(actual: unknown, expected: number): boolean {
|
|
1645
|
-
return typeof actual === "number"
|
|
1646
|
-
&& Number.isFinite(actual)
|
|
1647
|
-
&& Math.abs(actual - expected) <= Math.max(1e-8, Math.abs(expected) * 1e-8);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
function pointEvidenceMatches(
|
|
1651
|
-
actual: unknown,
|
|
1652
|
-
expected: PaneScreenshotChartPointEvidence | null,
|
|
1653
|
-
): boolean {
|
|
1654
|
-
if (expected === null) return actual === null;
|
|
1655
|
-
return isRecord(actual)
|
|
1656
|
-
&& actual.date === expected.date
|
|
1657
|
-
&& closeEnough(actual.close, expected.close);
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
function sourceSeriesMismatches(
|
|
1661
|
-
actual: unknown,
|
|
1662
|
-
expected: PaneScreenshotChartSeriesEvidence[],
|
|
1663
|
-
): string[] {
|
|
1664
|
-
if (!Array.isArray(actual)) return ["chart source-series evidence is missing"];
|
|
1665
|
-
if (actual.length !== expected.length) return ["chart source-series count does not match"];
|
|
1666
|
-
const mismatches: string[] = [];
|
|
1667
|
-
expected.forEach((expectedSeries, index) => {
|
|
1668
|
-
const actualSeries = actual[index];
|
|
1669
|
-
if (!isRecord(actualSeries) || actualSeries.symbol !== expectedSeries.symbol) {
|
|
1670
|
-
mismatches.push(`chart source symbol ${index + 1} does not match`);
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
if (actualSeries.pointCount !== expectedSeries.pointCount) {
|
|
1674
|
-
mismatches.push(`${expectedSeries.symbol} chart source point count does not match`);
|
|
1675
|
-
}
|
|
1676
|
-
if (!pointEvidenceMatches(actualSeries.first, expectedSeries.first)) {
|
|
1677
|
-
mismatches.push(`${expectedSeries.symbol} chart source first value does not match`);
|
|
1678
|
-
}
|
|
1679
|
-
if (!pointEvidenceMatches(actualSeries.last, expectedSeries.last)) {
|
|
1680
|
-
mismatches.push(`${expectedSeries.symbol} chart source last value does not match`);
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
return mismatches;
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
export function chartEvidenceMismatchesFor(
|
|
1687
|
-
semanticUi: RemoteUiNodeSnapshot[],
|
|
1688
|
-
expected: PaneScreenshotExpectedChartEvidence,
|
|
1689
|
-
): string[] {
|
|
1690
|
-
const node = semanticUi.find((candidate) => (
|
|
1691
|
-
candidate.role === "chart-data"
|
|
1692
|
-
&& candidate.metadata?.kind === expected.kind
|
|
1693
|
-
));
|
|
1694
|
-
const metadata = node?.metadata;
|
|
1695
|
-
if (!metadata) return ["rendered chart-data semantic evidence is missing"];
|
|
1696
|
-
|
|
1697
|
-
const mismatches: string[] = [];
|
|
1698
|
-
const symbols = readStringArray(metadata.symbols);
|
|
1699
|
-
if (!symbols || symbols.join("\0") !== expected.symbols.join("\0")) {
|
|
1700
|
-
mismatches.push("rendered chart symbols do not match");
|
|
1701
|
-
}
|
|
1702
|
-
if (metadata.rangePreset !== expected.rangePreset) {
|
|
1703
|
-
mismatches.push("rendered chart range does not match");
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
if (expected.kind === "chart-composer") {
|
|
1707
|
-
if (expected.resolution != null && metadata.resolution !== expected.resolution) {
|
|
1708
|
-
mismatches.push("rendered chart resolution does not match");
|
|
1709
|
-
}
|
|
1710
|
-
const actualSeries = Array.isArray(metadata.baseSeries) ? metadata.baseSeries : [];
|
|
1711
|
-
const expectedSeries = expected.baseSeries ?? [];
|
|
1712
|
-
if (actualSeries.length !== expectedSeries.length) {
|
|
1713
|
-
mismatches.push("rendered chart base-series count does not match");
|
|
1714
|
-
} else {
|
|
1715
|
-
expectedSeries.forEach((series, index) => {
|
|
1716
|
-
const actual = actualSeries[index];
|
|
1717
|
-
if (!isRecord(actual)
|
|
1718
|
-
|| actual.id !== series.id
|
|
1719
|
-
|| actual.sourceKind !== series.sourceKind
|
|
1720
|
-
|| actual.symbol !== series.symbol
|
|
1721
|
-
|| actual.fieldId !== series.fieldId
|
|
1722
|
-
|| actual.economicSeriesId !== series.economicSeriesId
|
|
1723
|
-
|| actual.capabilityId !== series.capabilityId
|
|
1724
|
-
|| actual.providerSeriesId !== series.providerSeriesId
|
|
1725
|
-
|| (series.sourceKind === "capability" && (
|
|
1726
|
-
JSON.stringify(actual.first) !== JSON.stringify(series.first)
|
|
1727
|
-
|| JSON.stringify(actual.last) !== JSON.stringify(series.last)
|
|
1728
|
-
))
|
|
1729
|
-
|| actual.style !== series.style
|
|
1730
|
-
|| actual.transform !== series.transform
|
|
1731
|
-
|| actual.panelId !== series.panelId
|
|
1732
|
-
|| actual.visible !== series.visible) {
|
|
1733
|
-
mismatches.push(`rendered chart series ${series.id} does not match`);
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
if (series.visible && (typeof actual.pointCount !== "number" || actual.pointCount <= 0)) {
|
|
1737
|
-
mismatches.push(`rendered chart series ${series.id} is empty`);
|
|
1738
|
-
}
|
|
1739
|
-
});
|
|
1740
|
-
}
|
|
1741
|
-
if (typeof metadata.projectedPointCount !== "number" || metadata.projectedPointCount <= 0) {
|
|
1742
|
-
mismatches.push("rendered chart projection is empty");
|
|
1743
|
-
}
|
|
1744
|
-
return mismatches;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
if (expected.kind === "stock-price") {
|
|
1748
|
-
if (metadata.axisMode !== expected.axisMode) {
|
|
1749
|
-
mismatches.push("rendered chart axis mode does not match");
|
|
1750
|
-
}
|
|
1751
|
-
const expectedSeries = expected.sourceSeries?.[0];
|
|
1752
|
-
if (!expectedSeries) return [...mismatches, "expected stock chart source evidence is missing"];
|
|
1753
|
-
mismatches.push(...sourceSeriesMismatches([{
|
|
1754
|
-
symbol: symbols?.[0],
|
|
1755
|
-
pointCount: metadata.sourcePointCount,
|
|
1756
|
-
first: metadata.sourceFirst,
|
|
1757
|
-
last: metadata.sourceLast,
|
|
1758
|
-
}], expected.sourceSeries ?? []));
|
|
1759
|
-
if (typeof metadata.projectedPointCount !== "number" || metadata.projectedPointCount <= 0) {
|
|
1760
|
-
mismatches.push("rendered stock chart projection is empty");
|
|
1761
|
-
}
|
|
1762
|
-
return mismatches;
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
if (metadata.requestedAxisMode !== expected.axisMode || metadata.effectiveAxisMode !== expected.axisMode) {
|
|
1766
|
-
mismatches.push("rendered comparison axis mode does not match");
|
|
1767
|
-
}
|
|
1768
|
-
if (
|
|
1769
|
-
expected.resolution != null
|
|
1770
|
-
&& (
|
|
1771
|
-
metadata.selectedResolution !== expected.resolution
|
|
1772
|
-
|| metadata.effectiveResolution !== expected.resolution
|
|
1773
|
-
)
|
|
1774
|
-
) {
|
|
1775
|
-
mismatches.push("rendered comparison resolution does not match");
|
|
1776
|
-
}
|
|
1777
|
-
const expectedSourceSeries = expected.sourceSeries ?? [];
|
|
1778
|
-
mismatches.push(...sourceSeriesMismatches(metadata.sourceSeries, expectedSourceSeries));
|
|
1779
|
-
|
|
1780
|
-
const projectionSeries = Array.isArray(metadata.projectionSeries)
|
|
1781
|
-
? metadata.projectionSeries
|
|
1782
|
-
: [];
|
|
1783
|
-
if (projectionSeries.length !== expectedSourceSeries.length) {
|
|
1784
|
-
mismatches.push("rendered comparison projection-series count does not match");
|
|
1785
|
-
} else {
|
|
1786
|
-
expectedSourceSeries.forEach((expectedSeries, index) => {
|
|
1787
|
-
const actualSeries = projectionSeries[index];
|
|
1788
|
-
if (!isRecord(actualSeries) || actualSeries.symbol !== expectedSeries.symbol) {
|
|
1789
|
-
mismatches.push(`${expectedSeries.symbol} comparison projection is missing`);
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
const baseValue = expectedSeries.projectionBaseValue;
|
|
1793
|
-
const latestRawValue = expectedSeries.projectionLatestRawValue;
|
|
1794
|
-
if (baseValue == null || latestRawValue == null) {
|
|
1795
|
-
mismatches.push(`${expectedSeries.symbol} expected comparison values are empty`);
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
const latestValue = expectedSeries.projectionLatestValue;
|
|
1799
|
-
if (!closeEnough(actualSeries.baseValue, baseValue)) {
|
|
1800
|
-
mismatches.push(`${expectedSeries.symbol} comparison base value does not match`);
|
|
1801
|
-
}
|
|
1802
|
-
if (!closeEnough(actualSeries.latestRawValue, latestRawValue)) {
|
|
1803
|
-
mismatches.push(`${expectedSeries.symbol} comparison latest value does not match`);
|
|
1804
|
-
}
|
|
1805
|
-
if (latestValue == null || !closeEnough(actualSeries.latestValue, latestValue)) {
|
|
1806
|
-
mismatches.push(`${expectedSeries.symbol} comparison transformed value does not match`);
|
|
1807
|
-
}
|
|
1808
|
-
if (actualSeries.pointCount !== metadata.projectedPointCount) {
|
|
1809
|
-
mismatches.push(`${expectedSeries.symbol} comparison projection point count does not match`);
|
|
1810
|
-
}
|
|
1811
|
-
});
|
|
1812
|
-
}
|
|
1813
|
-
if (typeof metadata.projectedPointCount !== "number" || metadata.projectedPointCount <= 0) {
|
|
1814
|
-
mismatches.push("rendered comparison projection is empty");
|
|
1815
|
-
}
|
|
1816
|
-
return mismatches;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
export function intradayChartEvidenceMismatchesFor(
|
|
1820
|
-
resolved: ResolvedPaneFunction,
|
|
1821
|
-
payload: DesktopPaneShotPayload,
|
|
1822
|
-
semanticUi: RemoteUiNodeSnapshot[],
|
|
1823
|
-
): string[] {
|
|
1824
|
-
if (resolved.capability.id !== "intraday-price-chart") return [];
|
|
1825
|
-
const intraday = payload.intradayHistories[0];
|
|
1826
|
-
if (!intraday || intraday.points.length === 0) return [];
|
|
1827
|
-
const metadata = semanticUi.find((node) => (
|
|
1828
|
-
node.role === "chart-data" && node.metadata?.kind === "chart-composer"
|
|
1829
|
-
))?.metadata;
|
|
1830
|
-
if (!metadata) return ["rendered intraday chart-data semantic evidence is missing"];
|
|
1831
|
-
const baseSeries = Array.isArray(metadata.baseSeries) ? metadata.baseSeries : [];
|
|
1832
|
-
const actual = baseSeries[0];
|
|
1833
|
-
if (!isRecord(actual)) return ["rendered intraday base series is missing"];
|
|
1834
|
-
|
|
1835
|
-
const expected = chartSeriesEvidence(intraday.symbol, intraday.points);
|
|
1836
|
-
const mismatches: string[] = [];
|
|
1837
|
-
if (actual.pointCount !== expected.pointCount) {
|
|
1838
|
-
mismatches.push("rendered intraday point count does not match");
|
|
1839
|
-
}
|
|
1840
|
-
if (metadata.projectedPointCount !== expected.pointCount) {
|
|
1841
|
-
mismatches.push("rendered intraday projection point count does not match");
|
|
1842
|
-
}
|
|
1843
|
-
const pointMatches = (
|
|
1844
|
-
actualPoint: unknown,
|
|
1845
|
-
expectedPoint: PaneScreenshotChartPointEvidence | null,
|
|
1846
|
-
): boolean => {
|
|
1847
|
-
if (!isRecord(actualPoint) || !expectedPoint) return false;
|
|
1848
|
-
return actualPoint.date === expectedPoint.date
|
|
1849
|
-
&& typeof actualPoint.value === "number"
|
|
1850
|
-
&& closeEnough(actualPoint.value, expectedPoint.close);
|
|
1851
|
-
};
|
|
1852
|
-
if (!pointMatches(actual.first, expected.first)) {
|
|
1853
|
-
mismatches.push("rendered intraday first bar does not match");
|
|
1854
|
-
}
|
|
1855
|
-
if (!pointMatches(actual.last, expected.last)) {
|
|
1856
|
-
mismatches.push("rendered intraday last bar does not match");
|
|
1857
|
-
}
|
|
1858
|
-
return mismatches;
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
export function shotUnavailableSymbols(
|
|
1862
|
-
resolved: ResolvedPaneFunction,
|
|
1863
|
-
payload: DesktopPaneShotPayload,
|
|
1864
|
-
semanticUi: RemoteUiNodeSnapshot[] = [],
|
|
1865
|
-
): string[] {
|
|
1866
|
-
const evidenceHook = paneScreenshotEvidenceHook(resolved);
|
|
1867
|
-
if (evidenceHook) return evidenceHook.unavailable(renderedPaneEvidence(evidenceHook, semanticUi), { resolved, payload });
|
|
1868
|
-
if (resolved.capability.id === "chart-composer") {
|
|
1869
|
-
const metadata = semanticUi.find((node) => (
|
|
1870
|
-
node.role === "chart-data" && node.metadata?.kind === "chart-composer"
|
|
1871
|
-
))?.metadata;
|
|
1872
|
-
const baseSeries = Array.isArray(metadata?.baseSeries) ? metadata.baseSeries : [];
|
|
1873
|
-
return baseSeries.flatMap((entry) => {
|
|
1874
|
-
if (!isRecord(entry) || (typeof entry.pointCount === "number" && entry.pointCount > 0)) return [];
|
|
1875
|
-
if (entry.sourceKind === "security" && typeof entry.symbol === "string") return [entry.symbol];
|
|
1876
|
-
if (entry.sourceKind === "economic" && typeof entry.economicSeriesId === "string") {
|
|
1877
|
-
return [`FRED:${entry.economicSeriesId}`];
|
|
1878
|
-
}
|
|
1879
|
-
if (entry.sourceKind === "capability" && typeof entry.capabilityId === "string" && typeof entry.providerSeriesId === "string") {
|
|
1880
|
-
return [`CAP:${entry.capabilityId}:${entry.providerSeriesId}`];
|
|
1881
|
-
}
|
|
1882
|
-
return [];
|
|
1883
|
-
});
|
|
1884
|
-
}
|
|
1885
|
-
if (resolved.capability.id === "intraday-price-chart") {
|
|
1886
|
-
const symbol = payload.intradayHistories[0]?.symbol ?? payload.financials[0]?.[0];
|
|
1887
|
-
const metadata = semanticUi.find((node) => (
|
|
1888
|
-
node.role === "chart-data" && node.metadata?.kind === "chart-composer"
|
|
1889
|
-
))?.metadata;
|
|
1890
|
-
const renderedPoints = typeof metadata?.projectedPointCount === "number"
|
|
1891
|
-
? metadata.projectedPointCount
|
|
1892
|
-
: 0;
|
|
1893
|
-
return symbol && renderedPoints <= 0 ? [symbol] : [];
|
|
1894
|
-
}
|
|
1895
|
-
const graphKind = shotGraphKind(resolved);
|
|
1896
|
-
if (graphKind) {
|
|
1897
|
-
const metric = resolved.options.metric as GraphMetricKey;
|
|
1898
|
-
const period = resolved.options.period as FundamentalPeriod;
|
|
1899
|
-
const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
|
|
1900
|
-
return payload.financials.flatMap(([symbol, financials]) => (
|
|
1901
|
-
limitGraphRowsBySymbol(
|
|
1902
|
-
graphRowsForFinancials(financials, graphKind, metric, period, symbol),
|
|
1903
|
-
periodCount,
|
|
1904
|
-
).length === 0 ? [symbol] : []
|
|
1905
|
-
));
|
|
1906
|
-
}
|
|
1907
|
-
if (["price-chart", "historical-prices"].includes(resolved.capability.id)) {
|
|
1908
|
-
return payload.financials.flatMap(([symbol, financials]) => financials.priceHistory.length > 0 ? [] : [symbol]);
|
|
1909
|
-
}
|
|
1910
|
-
if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
|
|
1911
|
-
return payload.financials.flatMap(([symbol, financials]) => financials.priceHistory.length > 1 ? [] : [symbol]);
|
|
1912
|
-
}
|
|
1913
|
-
if (resolved.capability.id === "financial-statements") {
|
|
1914
|
-
return payload.financials.flatMap(([symbol, financials]) => {
|
|
1915
|
-
const count = resolved.options.period === "quarterly"
|
|
1916
|
-
? financials.quarterlyStatements.length
|
|
1917
|
-
: financials.annualStatements.length;
|
|
1918
|
-
return count > 0 ? [] : [symbol];
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
return payload.financials.flatMap(([symbol, financials]) => financials.quote ? [] : [symbol]);
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
function shotGraphKind(resolved: ResolvedPaneFunction): GraphKind | null {
|
|
1925
|
-
if (resolved.capability.id === "fundamental-series") return "fundamental";
|
|
1926
|
-
if (resolved.capability.id === "valuation-series") return "valuation";
|
|
1927
|
-
return null;
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
export function shotSemanticRowCount(
|
|
1931
|
-
resolved: ResolvedPaneFunction,
|
|
1932
|
-
payload: DesktopPaneShotPayload,
|
|
1933
|
-
semanticUi: RemoteUiNodeSnapshot[] = [],
|
|
1934
|
-
): number {
|
|
1935
|
-
const evidenceHook = paneScreenshotEvidenceHook(resolved);
|
|
1936
|
-
if (evidenceHook) return renderedPaneEvidence(evidenceHook, semanticUi)?.plottedValueCount ?? 0;
|
|
1937
|
-
if (resolved.capability.id === "chart-composer") {
|
|
1938
|
-
const metadata = semanticUi.find((node) => (
|
|
1939
|
-
node.role === "chart-data" && node.metadata?.kind === "chart-composer"
|
|
1940
|
-
))?.metadata;
|
|
1941
|
-
const baseSeries = Array.isArray(metadata?.baseSeries) ? metadata.baseSeries : [];
|
|
1942
|
-
return baseSeries.reduce((count, entry) => (
|
|
1943
|
-
count + (isRecord(entry) && typeof entry.pointCount === "number" ? Math.max(0, entry.pointCount) : 0)
|
|
1944
|
-
), 0);
|
|
1945
|
-
}
|
|
1946
|
-
if (resolved.capability.id === "intraday-price-chart") {
|
|
1947
|
-
const metadata = semanticUi.find((node) => (
|
|
1948
|
-
node.role === "chart-data" && node.metadata?.kind === "chart-composer"
|
|
1949
|
-
))?.metadata;
|
|
1950
|
-
return typeof metadata?.projectedPointCount === "number"
|
|
1951
|
-
? Math.max(0, metadata.projectedPointCount)
|
|
1952
|
-
: 0;
|
|
1953
|
-
}
|
|
1954
|
-
const graphKind = shotGraphKind(resolved);
|
|
1955
|
-
if (graphKind) {
|
|
1956
|
-
const metric = resolved.options.metric as GraphMetricKey;
|
|
1957
|
-
const period = resolved.options.period as FundamentalPeriod;
|
|
1958
|
-
const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
|
|
1959
|
-
return payload.financials.reduce((count, [symbol, financials]) => (
|
|
1960
|
-
count + limitGraphRowsBySymbol(
|
|
1961
|
-
graphRowsForFinancials(financials, graphKind, metric, period, symbol),
|
|
1962
|
-
periodCount,
|
|
1963
|
-
).length
|
|
1964
|
-
), 0);
|
|
1965
|
-
}
|
|
1966
|
-
if (["price-chart", "historical-prices"].includes(resolved.capability.id)) {
|
|
1967
|
-
return payload.financials[0]?.[1].priceHistory.length ?? 0;
|
|
1968
|
-
}
|
|
1969
|
-
if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
|
|
1970
|
-
return payload.financials.filter(([, financials]) => financials.priceHistory.length > 1).length;
|
|
1971
|
-
}
|
|
1972
|
-
if (resolved.capability.id === "financial-statements") {
|
|
1973
|
-
const financials = payload.financials[0]?.[1];
|
|
1974
|
-
const period = resolved.options.period;
|
|
1975
|
-
return period === "quarterly"
|
|
1976
|
-
? financials?.quarterlyStatements.length ?? 0
|
|
1977
|
-
: financials?.annualStatements.length ?? 0;
|
|
1978
|
-
}
|
|
1979
|
-
return payload.financials.filter(([, financials]) => !!financials.quote).length;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
export function shotExpectedText(
|
|
1983
|
-
resolved: ResolvedPaneFunction,
|
|
1984
|
-
symbols: string[],
|
|
1985
|
-
payload: DesktopPaneShotPayload,
|
|
1986
|
-
): string[] {
|
|
1987
|
-
const expected = [...symbols, ...(paneScreenshotEvidenceHook(resolved)?.expectedText?.({ resolved, payload }) ?? [])];
|
|
1988
|
-
const graphKind = shotGraphKind(resolved);
|
|
1989
|
-
if (graphKind) {
|
|
1990
|
-
const metric = resolved.options.metric as GraphMetricKey;
|
|
1991
|
-
const definition = metricDef(graphKind, metric);
|
|
1992
|
-
const period = resolved.options.period as FundamentalPeriod;
|
|
1993
|
-
const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
|
|
1994
|
-
if (resolved.pane.id === CHART_COMPOSER_PANE_ID) {
|
|
1995
|
-
// Chart series are labelled with the field short label ("P/S"), not the
|
|
1996
|
-
// catalog label ("Price / Sales").
|
|
1997
|
-
const field = getTimeSeriesField(`${graphKind}.${metric}`);
|
|
1998
|
-
expected.push(field?.shortLabel ?? definition.label);
|
|
1999
|
-
return expected.filter(Boolean);
|
|
2000
|
-
}
|
|
2001
|
-
expected.push(definition.label);
|
|
2002
|
-
for (const [symbol, financials] of payload.financials) {
|
|
2003
|
-
const latestRow = limitGraphRowsBySymbol(
|
|
2004
|
-
graphRowsForFinancials(financials, graphKind, metric, period, symbol),
|
|
2005
|
-
periodCount,
|
|
2006
|
-
).sort((left, right) => left.date.localeCompare(right.date)).at(-1);
|
|
2007
|
-
if (latestRow) {
|
|
2008
|
-
expected.push(latestRow.date);
|
|
2009
|
-
expected.push(definition.format(latestRow.value));
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
} else if (resolved.capability.id === "financial-statements") {
|
|
2013
|
-
const statementLabels: Record<string, string> = {
|
|
2014
|
-
income: "Income",
|
|
2015
|
-
balance: "Balance",
|
|
2016
|
-
cashflow: "Cash Flow",
|
|
2017
|
-
};
|
|
2018
|
-
// A ratio tab names itself in the query bar and leads with its first ratio.
|
|
2019
|
-
const ratioTab = shotRatioTab(resolved);
|
|
2020
|
-
expected.push(ratioTab?.name ?? statementLabels[String(resolved.options.statement)] ?? "");
|
|
2021
|
-
expected.push(resolved.options.period === "annual" ? "Annual" : "Quarterly");
|
|
2022
|
-
const financials = payload.financials[0]?.[1];
|
|
2023
|
-
if (financials) {
|
|
2024
|
-
const table = buildFinancialTableModel(financials, {
|
|
2025
|
-
period: resolved.options.period as FundamentalPeriod,
|
|
2026
|
-
statement: String(resolved.options.statement),
|
|
2027
|
-
});
|
|
2028
|
-
const latestStatement = table?.statements[0];
|
|
2029
|
-
const firstMetric = table?.rows[0];
|
|
2030
|
-
// The pane prints the compact header: date, currency and an S/P source marker.
|
|
2031
|
-
if (latestStatement) {
|
|
2032
|
-
const currency = latestStatement.currency ?? financialStatementCurrency(
|
|
2033
|
-
financials,
|
|
2034
|
-
[...(financials.annualStatements ?? []), ...(financials.quarterlyStatements ?? [])],
|
|
2035
|
-
);
|
|
2036
|
-
expected.push(formatFinancialHeader(
|
|
2037
|
-
latestStatement.date,
|
|
2038
|
-
currency,
|
|
2039
|
-
latestStatement.dateSource,
|
|
2040
|
-
true,
|
|
2041
|
-
latestStatement.aggregation?.periodEnd,
|
|
2042
|
-
).trim());
|
|
2043
|
-
}
|
|
2044
|
-
const firstLabel = ratioTab ? ratioTab.ratios[0]?.label : firstMetric?.unitLabel;
|
|
2045
|
-
if (firstLabel) expected.push(firstLabel);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
return expected.filter(Boolean);
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function shotExpectedSelections(
|
|
2052
|
-
resolved: ResolvedPaneFunction,
|
|
2053
|
-
): PaneScreenshotExpectedSelection[] {
|
|
2054
|
-
if (resolved.pane.id === CHART_COMPOSER_PANE_ID) return [];
|
|
2055
|
-
if (shotGraphKind(resolved)) {
|
|
2056
|
-
return [{
|
|
2057
|
-
control: "metric",
|
|
2058
|
-
value: String(resolved.options.metric),
|
|
2059
|
-
}];
|
|
2060
|
-
}
|
|
2061
|
-
if (resolved.capability.id !== "financial-statements") return [];
|
|
2062
|
-
const ratioTab = shotRatioTab(resolved);
|
|
2063
|
-
if (ratioTab) {
|
|
2064
|
-
return [
|
|
2065
|
-
{ control: "ratio", value: ratioTab.key, label: ratioTab.name },
|
|
2066
|
-
{ control: "period", value: String(resolved.options.period) },
|
|
2067
|
-
];
|
|
2068
|
-
}
|
|
2069
|
-
const labels: Record<string, string> = {
|
|
2070
|
-
income: "Income",
|
|
2071
|
-
cashflow: "Cash Flow",
|
|
2072
|
-
balance: "Balance Sheet",
|
|
2073
|
-
};
|
|
2074
|
-
return [
|
|
2075
|
-
{
|
|
2076
|
-
control: "statement",
|
|
2077
|
-
label: labels[String(resolved.options.statement)] ?? String(resolved.options.statement),
|
|
2078
|
-
},
|
|
2079
|
-
{
|
|
2080
|
-
control: "period",
|
|
2081
|
-
value: String(resolved.options.period),
|
|
2082
|
-
},
|
|
2083
|
-
];
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
export function missingActiveTabSelections(
|
|
2087
|
-
semanticUi: RemoteUiNodeSnapshot[],
|
|
2088
|
-
expected: PaneScreenshotExpectedSelection[],
|
|
2089
|
-
): PaneScreenshotExpectedSelection[] {
|
|
2090
|
-
const activeTabs = semanticUi.flatMap((node): { value: string | null; label: string | null }[] => {
|
|
2091
|
-
if (!node.metadata) return [];
|
|
2092
|
-
// Query bars carry the active choice of each inline filter and the view.
|
|
2093
|
-
if (node.role === "query-bar") {
|
|
2094
|
-
const selections = Array.isArray(node.metadata.selections) ? node.metadata.selections : [];
|
|
2095
|
-
return selections.flatMap((selection) => (
|
|
2096
|
-
isRecord(selection) ? [{
|
|
2097
|
-
value: typeof selection.value === "string" ? selection.value : null,
|
|
2098
|
-
label: typeof selection.label === "string" ? selection.label : null,
|
|
2099
|
-
}] : []
|
|
2100
|
-
));
|
|
2101
|
-
}
|
|
2102
|
-
if (node.role !== "tabs") return [];
|
|
2103
|
-
const activeValue = typeof node.metadata.activeValue === "string"
|
|
2104
|
-
? node.metadata.activeValue
|
|
2105
|
-
: null;
|
|
2106
|
-
const tabs = Array.isArray(node.metadata.tabs)
|
|
2107
|
-
? node.metadata.tabs.filter((tab): tab is Record<string, unknown> => (
|
|
2108
|
-
!!tab && typeof tab === "object" && !Array.isArray(tab)
|
|
2109
|
-
))
|
|
2110
|
-
: [];
|
|
2111
|
-
const activeTab = tabs.find((tab) => String(tab.value) === activeValue);
|
|
2112
|
-
return [{
|
|
2113
|
-
value: activeValue,
|
|
2114
|
-
label: typeof activeTab?.label === "string" ? activeTab.label : null,
|
|
2115
|
-
}];
|
|
2116
|
-
});
|
|
2117
|
-
|
|
2118
|
-
return expected.filter((selection) => !activeTabs.some((active) => (
|
|
2119
|
-
(selection.value == null || active.value === selection.value)
|
|
2120
|
-
&& (selection.label == null || active.label?.toLowerCase() === selection.label.toLowerCase())
|
|
2121
|
-
)));
|
|
2122
|
-
}
|