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.
Files changed (886) hide show
  1. package/README.md +8 -8
  2. package/README.zh-CN.md +11 -11
  3. package/bin/{gloomberb → gloom} +3 -1
  4. package/package.json +6 -2
  5. package/src/account/checkout-handoff.tsx +225 -0
  6. package/src/account/device-signin-dialog.tsx +2 -2
  7. package/src/account/device-signin.ts +2 -2
  8. package/src/account/upgrade-dialog.tsx +1 -1
  9. package/src/api-client/askg.ts +2 -2
  10. package/src/api-client/auth.ts +2 -2
  11. package/src/api-client/central-bank-rates.ts +6 -0
  12. package/src/api-client/collections.ts +5 -1
  13. package/src/api-client/cot.ts +2 -0
  14. package/src/api-client/crypto-etf-flows.ts +37 -0
  15. package/src/api-client/crypto-markets.ts +60 -0
  16. package/src/api-client/data.ts +84 -5
  17. package/src/api-client/doe.ts +15 -0
  18. package/src/api-client/equity-screener.ts +28 -0
  19. package/src/api-client/exchange-announcements.ts +38 -0
  20. package/src/api-client/fund-facts.ts +39 -0
  21. package/src/api-client/futures-curve.ts +9 -1
  22. package/src/api-client/hiring.ts +301 -0
  23. package/src/api-client/index.ts +22 -5
  24. package/src/api-client/market-discovery.ts +14 -0
  25. package/src/api-client/members.ts +6 -0
  26. package/src/api-client/mna.ts +6 -0
  27. package/src/api-client/paths.ts +18 -1
  28. package/src/api-client/request.ts +21 -4
  29. package/src/api-client/short-side-publications.ts +37 -0
  30. package/src/api-client/socket.ts +64 -4
  31. package/src/api-client/swap-curve.ts +35 -0
  32. package/src/api-client/themes.ts +7 -1
  33. package/src/api-client/types.ts +90 -2
  34. package/src/app/pane-runtime/plugin-bindings.ts +9 -2
  35. package/src/app/runtime/desktop-deeplink.ts +5 -5
  36. package/src/app/runtime/plugin-bindings.ts +2 -2
  37. package/src/app/runtime/startup.ts +13 -0
  38. package/src/app/star-prompt/status-line.tsx +2 -2
  39. package/src/architecture/core-builtin-allowlist.json +5 -2
  40. package/src/architecture/ui-literals-allowlist.json +3 -3
  41. package/src/architecture/ui-tokens-allowlist.json +1 -1
  42. package/src/brokers/broker-portfolio-sync.ts +37 -0
  43. package/src/brokers/directory.ts +11 -0
  44. package/src/brokers/logos.ts +3 -1
  45. package/src/brokers/signed-in/adapter.ts +304 -26
  46. package/src/brokers/signed-in/client.ts +14 -2
  47. package/src/brokers/signed-in/connect.ts +10 -2
  48. package/src/brokers/sync-broker-instance.ts +9 -1
  49. package/src/capabilities/chart-series.ts +22 -6
  50. package/src/cli/bank-section.ts +1 -1
  51. package/src/cli/base-converter.ts +23 -14
  52. package/src/cli/central-bank-events.ts +1 -1
  53. package/src/cli/command-options.ts +59 -4
  54. package/src/cli/commands/api.ts +9 -6
  55. package/src/cli/commands/broker.ts +2 -2
  56. package/src/cli/commands/command-utils.ts +2 -2
  57. package/src/cli/commands/crypto-hints.ts +7 -20
  58. package/src/cli/commands/fred-series.ts +4 -2
  59. package/src/cli/commands/keybindings.ts +2 -2
  60. package/src/cli/commands/market.ts +1117 -264
  61. package/src/cli/commands/news-hints.ts +2 -2
  62. package/src/cli/commands/news-search.ts +59 -6
  63. package/src/cli/commands/options-leaps.ts +41 -26
  64. package/src/cli/commands/overview.ts +121 -48
  65. package/src/cli/commands/plugins.ts +9 -9
  66. package/src/cli/commands/remote.ts +13 -10
  67. package/src/cli/commands/search.ts +64 -44
  68. package/src/cli/commands/system.ts +27 -19
  69. package/src/cli/commands/themes.ts +1 -1
  70. package/src/cli/commands/ticker.ts +192 -47
  71. package/src/cli/context.ts +3 -2
  72. package/src/cli/desktop-pane-shot-view.ts +2 -2
  73. package/src/cli/desktop-pane-shot.ts +1 -1
  74. package/src/cli/earnings-calendar.ts +4 -4
  75. package/src/cli/entry.ts +18 -1
  76. package/src/cli/errors.ts +20 -2
  77. package/src/cli/exit-codes.ts +1 -1
  78. package/src/cli/export-source.ts +10 -4
  79. package/src/cli/freshness.ts +2 -8
  80. package/src/cli/fx-forward.ts +130 -0
  81. package/src/cli/fx-pair.ts +10 -9
  82. package/src/cli/glossary.ts +64 -20
  83. package/src/cli/help.ts +19 -13
  84. package/src/cli/history-export.ts +126 -0
  85. package/src/cli/history-rows.ts +196 -43
  86. package/src/cli/index-roots.ts +5 -45
  87. package/src/cli/index.ts +24 -18
  88. package/src/cli/known-gaps.ts +118 -25
  89. package/src/cli/listing-arg.ts +300 -41
  90. package/src/cli/name-entries.ts +30 -18
  91. package/src/cli/native-smoke.ts +2 -0
  92. package/src/cli/not-a-ticker.ts +19 -9
  93. package/src/cli/opentui-libc.ts +5 -0
  94. package/src/cli/options.ts +12 -0
  95. package/src/cli/pane-functions/capabilities.ts +75 -20
  96. package/src/cli/pane-functions/catalog.ts +24 -23
  97. package/src/cli/pane-functions/discovery.ts +1 -1
  98. package/src/cli/pane-functions/dom.ts +10 -7
  99. package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
  100. package/src/cli/pane-functions/freshness.ts +174 -23
  101. package/src/cli/pane-functions/function-help.ts +66 -27
  102. package/src/cli/pane-functions/headless.ts +24 -11
  103. package/src/cli/pane-functions/index.ts +18 -16
  104. package/src/cli/pane-functions/options.ts +22 -2
  105. package/src/cli/pane-functions/report-notices.ts +12 -4
  106. package/src/cli/pane-functions/report.ts +5 -5
  107. package/src/cli/pane-functions/resolver.ts +47 -10
  108. package/src/cli/pane-functions/screenshot-assessment.ts +322 -0
  109. package/src/cli/pane-functions/screenshot-bridge.ts +152 -0
  110. package/src/cli/pane-functions/screenshot-chart-evidence.ts +396 -0
  111. package/src/cli/pane-functions/screenshot-data-evidence.ts +383 -0
  112. package/src/cli/pane-functions/screenshot-evidence.ts +1 -1
  113. package/src/cli/pane-functions/screenshot-payload.ts +453 -0
  114. package/src/cli/pane-functions/screenshot-render.ts +316 -0
  115. package/src/cli/pane-functions/screenshot-types.ts +162 -0
  116. package/src/cli/pane-functions/shot-options.ts +8 -3
  117. package/src/cli/peso-dollar-rates.ts +79 -0
  118. package/src/cli/quote-answer.ts +38 -3
  119. package/src/cli/report-tables.ts +61 -16
  120. package/src/cli/restore-plugins.ts +29 -14
  121. package/src/cli/result.ts +111 -22
  122. package/src/cli/series-search.ts +5 -5
  123. package/src/cli/statement-filings.ts +19 -0
  124. package/src/cli/suffixed-listing-rows.ts +49 -0
  125. package/src/cli/symbol-args.ts +50 -15
  126. package/src/cli/symbol-cells.ts +120 -0
  127. package/src/cli/symbol-list.ts +198 -68
  128. package/src/cli/symbol-rows.ts +131 -25
  129. package/src/cli/ticker-resolution.ts +108 -11
  130. package/src/cli/treasury-yields.ts +5 -5
  131. package/src/cli/types.ts +2 -0
  132. package/src/cli/uncovered-instrument.ts +3 -3
  133. package/src/cli/unknown-command.ts +6 -6
  134. package/src/components/chart/composite/chart-layout.ts +131 -0
  135. package/src/components/chart/composite/composite-chart.tsx +373 -16
  136. package/src/components/chart/composite/interactions.ts +7 -2
  137. package/src/components/chart/composite/legend.tsx +75 -3
  138. package/src/components/chart/composite/levels.ts +5 -0
  139. package/src/components/chart/composite/panel-gestures.ts +41 -0
  140. package/src/components/chart/composite/panel-labels.ts +2 -1
  141. package/src/components/chart/composite/panel-surface.tsx +305 -11
  142. package/src/components/chart/composite/price-axis-labels.tsx +27 -3
  143. package/src/components/chart/composite/rasterizer.ts +44 -19
  144. package/src/components/chart/composite/scene.ts +16 -4
  145. package/src/components/chart/composite/text-renderer.ts +48 -21
  146. package/src/components/chart/composite/toolbar.tsx +52 -5
  147. package/src/components/chart/composite/types.ts +13 -0
  148. package/src/components/chart/composite/use-composite-chart-keyboard.ts +120 -13
  149. package/src/components/chart/composite/use-composite-value-scales.ts +44 -0
  150. package/src/components/chart/composite/use-composite-viewport.ts +12 -3
  151. package/src/components/chart/composite/value-scale.ts +149 -0
  152. package/src/components/chart/composite/volume-profile-paint.ts +6 -3
  153. package/src/components/chart/core/pointer.ts +2 -0
  154. package/src/components/chart-table/index.ts +2 -0
  155. package/src/components/chart-table/read.tsx +74 -0
  156. package/src/components/command-bar/commands/collection-list.ts +41 -19
  157. package/src/components/command-bar/commands/collection.ts +9 -3
  158. package/src/components/command-bar/commands/direct/index.ts +1 -1
  159. package/src/components/command-bar/commands/direct/runtime.ts +3 -2
  160. package/src/components/command-bar/pane-templates/items.ts +59 -6
  161. package/src/components/command-bar/pane-templates/workflow.ts +10 -1
  162. package/src/components/command-bar/routes/root/plugin-install.ts +1 -1
  163. package/src/components/command-bar/routes/root/provider-search.ts +42 -11
  164. package/src/components/command-bar/routes/root/results.ts +18 -4
  165. package/src/components/command-bar/routes/root/runtime.ts +15 -3
  166. package/src/components/command-bar/routes/root/search-providers.ts +25 -2
  167. package/src/components/command-bar/routes/root/selection.ts +6 -1
  168. package/src/components/command-bar/routes/root/shortcut-feedback.ts +4 -1
  169. package/src/components/command-bar/routes/root/shortcuts.ts +30 -3
  170. package/src/components/command-bar/routes/ticker-search/results.ts +73 -4
  171. package/src/components/command-bar/selection-runtime.ts +50 -10
  172. package/src/components/command-bar/surface/index.tsx +24 -2
  173. package/src/components/command-bar/view-model.ts +7 -0
  174. package/src/components/command-bar/workflow/broker.ts +2 -2
  175. package/src/components/command-bar/workflow/collection-actions.ts +46 -7
  176. package/src/components/command-bar/workflow/collection-ticker.ts +32 -4
  177. package/src/components/command-bar/workflow/fields.ts +8 -2
  178. package/src/components/command-bar/workflow/ops.ts +18 -4
  179. package/src/components/command-bar/workflow/portfolio-position.ts +53 -4
  180. package/src/components/command-bar/workflow/submit.ts +3 -2
  181. package/src/components/command-bar/workflow/tickers.ts +5 -1
  182. package/src/components/company-logo.tsx +39 -0
  183. package/src/components/data-table/sections.ts +4 -2
  184. package/src/components/form-modal/confirm.tsx +2 -1
  185. package/src/components/form-modal/request.ts +5 -0
  186. package/src/components/form-modal/routes.ts +4 -1
  187. package/src/components/index.ts +12 -6
  188. package/src/components/layout/header.tsx +7 -6
  189. package/src/components/layout/market-summary.ts +9 -6
  190. package/src/components/layout/pane/capture-status.ts +1 -1
  191. package/src/components/layout/pane/footer/model.ts +11 -5
  192. package/src/components/layout/shell/chrome.ts +19 -6
  193. package/src/components/layout/shell/index.tsx +2 -1
  194. package/src/components/layout/shell/pane/layers.tsx +19 -1
  195. package/src/components/layout/status-bar.tsx +2 -2
  196. package/src/components/market-board/index.tsx +11 -3
  197. package/src/components/onboarding/onboarding-wizard.tsx +1 -0
  198. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +2 -2
  199. package/src/components/onboarding/wizard-broker-sync.ts +4 -1
  200. package/src/components/pane-settings-dialog/desktop.tsx +20 -7
  201. package/src/components/pane-settings-dialog/tui.tsx +17 -9
  202. package/src/components/pane-settings-dialog/value.ts +8 -1
  203. package/src/components/pane-settings-dialog.tsx +44 -13
  204. package/src/components/sign-in-code-panel.tsx +27 -19
  205. package/src/components/ui/column-grid.tsx +48 -4
  206. package/src/components/ui/country-flag-art.tsx +180 -0
  207. package/src/components/ui/country-flag.tsx +118 -12
  208. package/src/components/ui/data-table/opentui/index.tsx +61 -2
  209. package/src/components/ui/data-table/opentui/model.ts +109 -2
  210. package/src/components/ui/data-table/types.ts +1 -1
  211. package/src/components/ui/display.tsx +5 -1
  212. package/src/components/ui/index.ts +3 -1
  213. package/src/components/ui/logo-mark.tsx +11 -2
  214. package/src/components/ui/query-bar.tsx +1 -1
  215. package/src/components/ui/row-notes.ts +79 -0
  216. package/src/core/state/app/layout.ts +4 -4
  217. package/src/data/cached-query.ts +8 -1
  218. package/src/data/config/chart-settings.ts +3 -3
  219. package/src/data/config/home.ts +10 -2
  220. package/src/data/config/layout.ts +4 -1
  221. package/src/data/config/store/node.ts +4 -0
  222. package/src/data/config/store/normalize.ts +13 -3
  223. package/src/data/config/store/portfolio-as-of-column.ts +2 -2
  224. package/src/data/json-storage.ts +2 -0
  225. package/src/data/saved-copies.ts +84 -0
  226. package/src/docs/snapshot.json +65 -32
  227. package/src/i18n/es.ts +29 -12
  228. package/src/i18n/ja.ts +29 -12
  229. package/src/i18n/ko.ts +29 -12
  230. package/src/i18n/zh-cn.ts +29 -12
  231. package/src/i18n/zh-tw.ts +29 -12
  232. package/src/layout/desks.ts +1 -1
  233. package/src/layout/launch-layout.ts +1 -1
  234. package/src/layout/pane-manager/floating-actions.ts +5 -3
  235. package/src/layout/pane-manager/floating.ts +43 -1
  236. package/src/layout/pane-manager/index.ts +2 -0
  237. package/src/layout/selected-ticker.ts +74 -0
  238. package/src/market-data/depositary-pair.ts +251 -0
  239. package/src/market-data/futures-board.ts +1 -1
  240. package/src/market-data/futures-contract.ts +124 -40
  241. package/src/market-data/fx-closes.ts +17 -2
  242. package/src/market-data/hooks.tsx +19 -11
  243. package/src/market-data/market/change-basis.ts +49 -7
  244. package/src/market-data/market/freshness.ts +34 -5
  245. package/src/market-data/market/period-return.ts +163 -0
  246. package/src/market-data/market/trading-sessions.ts +13 -0
  247. package/src/market-data/market/year-to-date.ts +8 -13
  248. package/src/market-data/performance.ts +20 -14
  249. package/src/market-data/perpetual-premium.ts +32 -0
  250. package/src/market-data/published-apac-sessions.ts +2 -1
  251. package/src/market-data/published-futures-sessions.ts +69 -0
  252. package/src/market-data/quotes/contributions.ts +5 -1
  253. package/src/market-data/quotes/freshness.ts +42 -0
  254. package/src/market-data/quotes/intraday-bar.ts +37 -0
  255. package/src/market-data/quotes/regular-session-valuation.ts +13 -8
  256. package/src/market-data/quotes/resolution.ts +4 -2
  257. package/src/market-data/tax-equivalent.ts +1 -1
  258. package/src/market-data/ticker-price-returns.ts +3 -1
  259. package/src/news/aggregator.ts +5 -1
  260. package/src/news/news-model.ts +4 -0
  261. package/src/news/types.ts +11 -0
  262. package/src/notifications/app-notifier.ts +1 -1
  263. package/src/plugins/auto-update.ts +2 -2
  264. package/src/plugins/builtin/account-management/index.tsx +1 -0
  265. package/src/plugins/builtin/account-management/pane.tsx +9 -8
  266. package/src/plugins/builtin/alerts/alert-engine.ts +29 -19
  267. package/src/plugins/builtin/alerts/calendar-events.ts +378 -0
  268. package/src/plugins/builtin/alerts/cli.ts +485 -76
  269. package/src/plugins/builtin/alerts/command.ts +5 -10
  270. package/src/plugins/builtin/alerts/event-rows.ts +9 -5
  271. package/src/plugins/builtin/alerts/events-pane.tsx +71 -56
  272. package/src/plugins/builtin/alerts/events.ts +107 -5
  273. package/src/plugins/builtin/alerts/fired-log.ts +78 -0
  274. package/src/plugins/builtin/alerts/format.ts +2 -1
  275. package/src/plugins/builtin/alerts/headless.ts +100 -39
  276. package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
  277. package/src/plugins/builtin/alerts/history-rows.ts +89 -0
  278. package/src/plugins/builtin/alerts/index.tsx +45 -18
  279. package/src/plugins/builtin/alerts/iv-level.ts +365 -0
  280. package/src/plugins/builtin/alerts/levels.ts +3 -3
  281. package/src/plugins/builtin/alerts/pane.tsx +97 -29
  282. package/src/plugins/builtin/alerts/price.ts +35 -0
  283. package/src/plugins/builtin/alerts/quotes.ts +24 -0
  284. package/src/plugins/builtin/alerts/research-builder.ts +84 -11
  285. package/src/plugins/builtin/alerts/research-rules.ts +76 -3
  286. package/src/plugins/builtin/alerts/scheduled-check.ts +206 -32
  287. package/src/plugins/builtin/alerts/scheduled-sources.ts +26 -3
  288. package/src/plugins/builtin/alerts/scheduled.ts +188 -29
  289. package/src/plugins/builtin/alerts/storage.ts +13 -10
  290. package/src/plugins/builtin/alerts/targets.ts +12 -3
  291. package/src/plugins/builtin/analytics/index.tsx +8 -2
  292. package/src/plugins/builtin/analytics/metrics.ts +1 -1
  293. package/src/plugins/builtin/analytics/risk-book.ts +215 -0
  294. package/src/plugins/builtin/analytics/risk-client.ts +13 -6
  295. package/src/plugins/builtin/analytics/risk-headless.ts +55 -21
  296. package/src/plugins/builtin/analytics/risk-math.ts +6 -3
  297. package/src/plugins/builtin/analytics/risk-model.ts +297 -104
  298. package/src/plugins/builtin/analytics/risk-options.ts +17 -2
  299. package/src/plugins/builtin/analytics/risk-pane.tsx +97 -36
  300. package/src/plugins/builtin/attention/pane.tsx +6 -2
  301. package/src/plugins/builtin/awards/headless.ts +7 -3
  302. package/src/plugins/builtin/awards/model.ts +18 -0
  303. package/src/plugins/builtin/awards/pane.tsx +6 -3
  304. package/src/plugins/builtin/backtest/headless.ts +3 -3
  305. package/src/plugins/builtin/backtest/rules.ts +41 -5
  306. package/src/plugins/builtin/broker-manager/add-flow.tsx +12 -7
  307. package/src/plugins/builtin/broker-manager/detail.tsx +47 -19
  308. package/src/plugins/builtin/broker-manager/footer.ts +27 -11
  309. package/src/plugins/builtin/broker-manager/index.tsx +21 -12
  310. package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
  311. package/src/plugins/builtin/broker-manager/model.ts +12 -2
  312. package/src/plugins/builtin/broker-manager/pane-actions.tsx +183 -57
  313. package/src/plugins/builtin/broker-manager/table.ts +28 -12
  314. package/src/plugins/builtin/broker-trading/account-picker.tsx +2 -1
  315. package/src/plugins/builtin/broker-trading/chart-levels.ts +82 -13
  316. package/src/plugins/builtin/broker-trading/controller.ts +45 -13
  317. package/src/plugins/builtin/broker-trading/dock.tsx +22 -4
  318. package/src/plugins/builtin/broker-trading/headless.ts +2 -1
  319. package/src/plugins/builtin/broker-trading/index.tsx +7 -1
  320. package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
  321. package/src/plugins/builtin/broker-trading/order-feed.ts +75 -7
  322. package/src/plugins/builtin/broker-trading/orders.tsx +109 -31
  323. package/src/plugins/builtin/broker-trading/overview-orders.tsx +9 -3
  324. package/src/plugins/builtin/broker-trading/pane-model.ts +30 -2
  325. package/src/plugins/builtin/broker-trading/pane.tsx +110 -23
  326. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
  327. package/src/plugins/builtin/broker-trading/ticket.tsx +112 -31
  328. package/src/plugins/builtin/buildout/cells.tsx +4 -0
  329. package/src/plugins/builtin/buildout/detail/index.tsx +5 -1
  330. package/src/plugins/builtin/buildout/detail/ui.tsx +7 -4
  331. package/src/plugins/builtin/buildout/pane/index.tsx +25 -12
  332. package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
  333. package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
  334. package/src/plugins/builtin/catalysts/detail.tsx +3 -2
  335. package/src/plugins/builtin/catalysts/index.tsx +1 -1
  336. package/src/plugins/builtin/catalysts/model.ts +49 -4
  337. package/src/plugins/builtin/catalysts/pane.tsx +6 -8
  338. package/src/plugins/builtin/central-bank-rates/client.ts +5 -0
  339. package/src/plugins/builtin/central-bank-rates/headless.ts +36 -10
  340. package/src/plugins/builtin/central-bank-rates/model.ts +33 -8
  341. package/src/plugins/builtin/changelog/index.tsx +1 -0
  342. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +1 -0
  343. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +2 -1
  344. package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
  345. package/src/plugins/builtin/chart-composer/cli-options.ts +2 -5
  346. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  347. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
  348. package/src/plugins/builtin/chart-composer/editor.tsx +4 -1
  349. package/src/plugins/builtin/chart-composer/headless-schema.ts +28 -26
  350. package/src/plugins/builtin/chart-composer/headless.ts +253 -35
  351. package/src/plugins/builtin/chart-composer/index.tsx +12 -2
  352. package/src/plugins/builtin/chart-composer/leverage-overlay.ts +142 -0
  353. package/src/plugins/builtin/chart-composer/pane.tsx +127 -28
  354. package/src/plugins/builtin/chart-composer/series-catalog.ts +1 -0
  355. package/src/plugins/builtin/chart-composer/series-expression.ts +65 -6
  356. package/src/plugins/builtin/chart-composer/session-history.ts +15 -21
  357. package/src/plugins/builtin/chart-composer/session-range.ts +4 -2
  358. package/src/plugins/builtin/chart-composer/settings.ts +31 -19
  359. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +5 -6
  360. package/src/plugins/builtin/chart-composer/viewport-labels.ts +18 -8
  361. package/src/plugins/builtin/chat/attachments/model.ts +3 -1
  362. package/src/plugins/builtin/chat/content/index.tsx +1 -1
  363. package/src/plugins/builtin/chat/pane-links/open.ts +1 -1
  364. package/src/plugins/builtin/chat/people.ts +1 -1
  365. package/src/plugins/builtin/clinical-trials/client.ts +5 -2
  366. package/src/plugins/builtin/clinical-trials/model.ts +13 -4
  367. package/src/plugins/builtin/clinical-trials/pane.tsx +32 -12
  368. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +17 -0
  369. package/src/plugins/builtin/cloud/askg/controller.ts +1 -1
  370. package/src/plugins/builtin/cloud/askg/lock-note.ts +1 -1
  371. package/src/plugins/builtin/cloud/askg/tool-display.ts +1 -1
  372. package/src/plugins/builtin/cloud/auth-commands.ts +3 -3
  373. package/src/plugins/builtin/cloud/team/pane-sections.tsx +105 -47
  374. package/src/plugins/builtin/cloud/team/pane-ui.tsx +42 -4
  375. package/src/plugins/builtin/cloud/team/pane.tsx +204 -64
  376. package/src/plugins/builtin/cloud/thesis/store.ts +45 -22
  377. package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
  378. package/src/plugins/builtin/commodities-energy/index.tsx +29 -2
  379. package/src/plugins/builtin/commodities-energy/view-state.ts +2 -0
  380. package/src/plugins/builtin/commodity-balances/pane.tsx +11 -4
  381. package/src/plugins/builtin/company-attention/app-rank.tsx +7 -5
  382. package/src/plugins/builtin/company-attention/building-model.ts +222 -0
  383. package/src/plugins/builtin/company-attention/building.tsx +172 -0
  384. package/src/plugins/builtin/company-attention/client.ts +117 -1
  385. package/src/plugins/builtin/company-attention/evidence.ts +16 -3
  386. package/src/plugins/builtin/company-attention/headless.ts +101 -5
  387. package/src/plugins/builtin/company-attention/index.tsx +6 -3
  388. package/src/plugins/builtin/company-attention/model.ts +33 -7
  389. package/src/plugins/builtin/company-attention/moves-model.ts +253 -0
  390. package/src/plugins/builtin/company-attention/moves.tsx +159 -0
  391. package/src/plugins/builtin/company-attention/pane.tsx +79 -23
  392. package/src/plugins/builtin/company-attention/signals-model.ts +229 -0
  393. package/src/plugins/builtin/company-attention/signals.tsx +160 -0
  394. package/src/plugins/builtin/company-kpis/headless.ts +3 -2
  395. package/src/plugins/builtin/company-kpis/model.ts +2 -2
  396. package/src/plugins/builtin/company-kpis/pane.tsx +1 -1
  397. package/src/plugins/builtin/composite-plugins.ts +2 -1
  398. package/src/plugins/builtin/congress-trades/headless.ts +1 -1
  399. package/src/plugins/builtin/connections/index.ts +1 -0
  400. package/src/plugins/builtin/correlation/headless-schema.ts +3 -2
  401. package/src/plugins/builtin/correlation/headless.ts +5 -3
  402. package/src/plugins/builtin/cot/headless.ts +28 -6
  403. package/src/plugins/builtin/cot/model.ts +12 -0
  404. package/src/plugins/builtin/cot/pane.tsx +3 -2
  405. package/src/plugins/builtin/cpi/headless.ts +5 -1
  406. package/src/plugins/builtin/cpi/pane.tsx +11 -9
  407. package/src/plugins/builtin/credit-boards/model.ts +2 -0
  408. package/src/plugins/builtin/credit-documents/headless.ts +1 -1
  409. package/src/plugins/builtin/crypto-board/client.ts +86 -0
  410. package/src/plugins/builtin/crypto-board/defi-view.tsx +218 -0
  411. package/src/plugins/builtin/crypto-board/defi.ts +239 -0
  412. package/src/plugins/builtin/crypto-board/headless.ts +117 -6
  413. package/src/plugins/builtin/crypto-board/index.tsx +8 -3
  414. package/src/plugins/builtin/crypto-board/model.ts +40 -8
  415. package/src/plugins/builtin/crypto-board/pane.tsx +55 -21
  416. package/src/plugins/builtin/crypto-board/series.ts +19 -1
  417. package/src/plugins/builtin/crypto-etf-flows/client.ts +60 -0
  418. package/src/plugins/builtin/crypto-etf-flows/headless.ts +132 -0
  419. package/src/plugins/builtin/crypto-etf-flows/index.tsx +44 -0
  420. package/src/plugins/builtin/crypto-etf-flows/model.ts +121 -0
  421. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +193 -0
  422. package/src/plugins/builtin/debug/index.tsx +1 -0
  423. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  424. package/src/plugins/builtin/docs/index.tsx +1 -1
  425. package/src/plugins/builtin/docs/pane.tsx +2 -2
  426. package/src/plugins/builtin/docs/targets.ts +1 -1
  427. package/src/plugins/builtin/doe/client.ts +5 -1
  428. package/src/plugins/builtin/doe/headless.ts +12 -6
  429. package/src/plugins/builtin/doe/model.ts +43 -6
  430. package/src/plugins/builtin/doe/pane.tsx +18 -11
  431. package/src/plugins/builtin/earnings/board-model.ts +13 -6
  432. package/src/plugins/builtin/earnings/board-table.ts +4 -1
  433. package/src/plugins/builtin/earnings/board.tsx +253 -50
  434. package/src/plugins/builtin/earnings/calendar-model.ts +188 -0
  435. package/src/plugins/builtin/earnings/calendar.tsx +338 -0
  436. package/src/plugins/builtin/earnings/client.ts +4 -3
  437. package/src/plugins/builtin/earnings/headless.ts +53 -15
  438. package/src/plugins/builtin/earnings/history-model.ts +53 -36
  439. package/src/plugins/builtin/earnings/index.tsx +15 -22
  440. package/src/plugins/builtin/earnings/model.ts +17 -12
  441. package/src/plugins/builtin/earnings/scope.ts +89 -0
  442. package/src/plugins/builtin/earnings-calls/calendar.tsx +310 -0
  443. package/src/plugins/builtin/earnings-calls/data.ts +146 -2
  444. package/src/plugins/builtin/earnings-calls/headless.ts +151 -9
  445. package/src/plugins/builtin/earnings-calls/index.tsx +3 -3
  446. package/src/plugins/builtin/earnings-calls/lock-note.ts +1 -1
  447. package/src/plugins/builtin/earnings-calls/pane.tsx +149 -31
  448. package/src/plugins/builtin/earnings-calls/upcoming.ts +195 -0
  449. package/src/plugins/builtin/earnings-ripple/pane.tsx +10 -5
  450. package/src/plugins/builtin/econ/calendar-model.ts +132 -20
  451. package/src/plugins/builtin/econ/detail-view.tsx +48 -10
  452. package/src/plugins/builtin/econ/fred-catalog.ts +23 -6
  453. package/src/plugins/builtin/econ/fred-series-map.ts +4 -0
  454. package/src/plugins/builtin/econ/index.tsx +112 -25
  455. package/src/plugins/builtin/econ/print-watch.ts +2 -1
  456. package/src/plugins/builtin/econ/types.ts +32 -1
  457. package/src/plugins/builtin/econ-statistics/client.ts +16 -3
  458. package/src/plugins/builtin/econ-statistics/defs.ts +4 -0
  459. package/src/plugins/builtin/econ-statistics/detail.tsx +2 -2
  460. package/src/plugins/builtin/econ-statistics/headless.ts +13 -2
  461. package/src/plugins/builtin/econ-statistics/index.tsx +3 -3
  462. package/src/plugins/builtin/econ-statistics/pane.tsx +2 -1
  463. package/src/plugins/builtin/econ-statistics/stats.ts +103 -0
  464. package/src/plugins/builtin/econ-statistics/view.ts +16 -1
  465. package/src/plugins/builtin/equity-screener/client.ts +21 -3
  466. package/src/plugins/builtin/equity-screener/headless.ts +68 -6
  467. package/src/plugins/builtin/equity-screener/live.ts +1 -1
  468. package/src/plugins/builtin/equity-screener/model.ts +6 -0
  469. package/src/plugins/builtin/equity-screener/pane.tsx +45 -10
  470. package/src/plugins/builtin/equity-screener/screen-check.ts +90 -0
  471. package/src/plugins/builtin/equity-screener/ticker-check.tsx +148 -0
  472. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -1
  473. package/src/plugins/builtin/estimate-revisions/lock-note.ts +3 -3
  474. package/src/plugins/builtin/estimate-revisions/model.ts +8 -10
  475. package/src/plugins/builtin/exposure/headless.ts +51 -6
  476. package/src/plugins/builtin/exposure/index.tsx +28 -2
  477. package/src/plugins/builtin/exposure/model.ts +7 -0
  478. package/src/plugins/builtin/exposure/pane.tsx +6 -1
  479. package/src/plugins/builtin/fear-greed/index.tsx +1 -1
  480. package/src/plugins/builtin/filing-events/index.tsx +1 -1
  481. package/src/plugins/builtin/futures/model.ts +10 -9
  482. package/src/plugins/builtin/futures/returns.ts +8 -36
  483. package/src/plugins/builtin/futures/table.tsx +1 -1
  484. package/src/plugins/builtin/futures-curve/client.ts +40 -9
  485. package/src/plugins/builtin/futures-curve/headless.ts +39 -17
  486. package/src/plugins/builtin/futures-curve/model.ts +258 -69
  487. package/src/plugins/builtin/futures-curve/pane.tsx +46 -17
  488. package/src/plugins/builtin/fx-matrix/client.ts +15 -8
  489. package/src/plugins/builtin/fx-matrix/headless.ts +32 -14
  490. package/src/plugins/builtin/fx-matrix/index.tsx +13 -11
  491. package/src/plugins/builtin/fx-matrix/live-legs.ts +22 -0
  492. package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
  493. package/src/plugins/builtin/gpu/headless.ts +9 -4
  494. package/src/plugins/builtin/help/index.tsx +13 -3
  495. package/src/plugins/builtin/holders/beneficial-model.ts +8 -11
  496. package/src/plugins/builtin/holders/beneficial-report.ts +6 -5
  497. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  498. package/src/plugins/builtin/holders/client.ts +13 -1
  499. package/src/plugins/builtin/holders/format.ts +28 -17
  500. package/src/plugins/builtin/holders/headless.ts +46 -7
  501. package/src/plugins/builtin/holders/index-holders.ts +264 -0
  502. package/src/plugins/builtin/holders/pane.tsx +79 -15
  503. package/src/plugins/builtin/holders/report-rows.ts +35 -7
  504. package/src/plugins/builtin/holders/table-model.ts +2 -1
  505. package/src/plugins/builtin/holders/treemap.tsx +2 -2
  506. package/src/plugins/builtin/insider/headless.ts +1 -1
  507. package/src/plugins/builtin/insider/signals-client.ts +1 -1
  508. package/src/plugins/builtin/insider/signals.tsx +1 -1
  509. package/src/plugins/builtin/ipo-calendar/pane.tsx +7 -4
  510. package/src/plugins/builtin/iv-history/client.ts +20 -0
  511. package/src/plugins/builtin/iv-history/headless.ts +4 -4
  512. package/src/plugins/builtin/iv-history/index.tsx +1 -1
  513. package/src/plugins/builtin/iv-history/model.ts +13 -4
  514. package/src/plugins/builtin/iv-history/series.ts +3 -1
  515. package/src/plugins/builtin/jobs/lock-note.ts +1 -1
  516. package/src/plugins/builtin/jobs/model.ts +4 -15
  517. package/src/plugins/builtin/jobs/pane.tsx +7 -5
  518. package/src/plugins/builtin/jobs/share-bars.tsx +11 -5
  519. package/src/plugins/builtin/jodi/pane.tsx +10 -4
  520. package/src/plugins/builtin/layout-manager/index.tsx +1 -0
  521. package/src/plugins/builtin/macro-day/headless.ts +20 -7
  522. package/src/plugins/builtin/macro-day/index.tsx +3 -2
  523. package/src/plugins/builtin/macro-day/model.ts +29 -8
  524. package/src/plugins/builtin/macro-day/pane.tsx +46 -10
  525. package/src/plugins/builtin/macro-day/releases.ts +55 -13
  526. package/src/plugins/builtin/market-heatmap/data.ts +9 -2
  527. package/src/plugins/builtin/market-heatmap/evidence.ts +1 -1
  528. package/src/plugins/builtin/market-heatmap/headless.ts +9 -6
  529. package/src/plugins/builtin/market-heatmap/index.tsx +3 -3
  530. package/src/plugins/builtin/market-heatmap/model.ts +10 -7
  531. package/src/plugins/builtin/market-movers/client.ts +33 -19
  532. package/src/plugins/builtin/market-movers/headless.ts +8 -2
  533. package/src/plugins/builtin/market-movers/index.tsx +16 -10
  534. package/src/plugins/builtin/market-movers/screener.ts +29 -8
  535. package/src/plugins/builtin/market-movers/session-body.tsx +4 -2
  536. package/src/plugins/builtin/market-movers/session-table.ts +4 -2
  537. package/src/plugins/builtin/market-movers/table.ts +4 -1
  538. package/src/plugins/builtin/market-valuation/headless.ts +2 -0
  539. package/src/plugins/builtin/market-valuation/view.ts +5 -0
  540. package/src/plugins/builtin/members/headless.ts +6 -5
  541. package/src/plugins/builtin/members/pane.tsx +11 -4
  542. package/src/plugins/builtin/mna/client.ts +1 -0
  543. package/src/plugins/builtin/mna/detail.tsx +20 -32
  544. package/src/plugins/builtin/mna/distress/detail.tsx +16 -4
  545. package/src/plugins/builtin/mna/distress/model.ts +3 -2
  546. package/src/plugins/builtin/mna/distress/pane.tsx +7 -4
  547. package/src/plugins/builtin/mna/headless.ts +6 -13
  548. package/src/plugins/builtin/mna/model.ts +93 -15
  549. package/src/plugins/builtin/mna/pane.tsx +5 -4
  550. package/src/plugins/builtin/news/headless.ts +12 -6
  551. package/src/plugins/builtin/news/index.tsx +10 -1
  552. package/src/plugins/builtin/news/wire/index.tsx +3 -2
  553. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +26 -10
  554. package/src/plugins/builtin/news/wire/news/query-presets.ts +6 -2
  555. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -3
  556. package/src/plugins/builtin/notes/cli.ts +49 -14
  557. package/src/plugins/builtin/openfda/pane.tsx +1 -1
  558. package/src/plugins/builtin/options/carry.ts +2 -2
  559. package/src/plugins/builtin/options/index.tsx +1 -1
  560. package/src/plugins/builtin/options/view.tsx +10 -10
  561. package/src/plugins/builtin/options-calculator/headless.ts +46 -7
  562. package/src/plugins/builtin/options-scenario/client.ts +123 -28
  563. package/src/plugins/builtin/options-scenario/collar.ts +8 -6
  564. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +127 -0
  565. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +165 -0
  566. package/src/plugins/builtin/options-scenario/csp-screen.ts +258 -0
  567. package/src/plugins/builtin/options-scenario/csp.ts +101 -21
  568. package/src/plugins/builtin/options-scenario/headless.ts +36 -15
  569. package/src/plugins/builtin/options-scenario/hedge.ts +20 -14
  570. package/src/plugins/builtin/options-scenario/index.tsx +28 -3
  571. package/src/plugins/builtin/options-scenario/model.ts +13 -0
  572. package/src/plugins/builtin/options-scenario/pane.tsx +9 -5
  573. package/src/plugins/builtin/perps/equity-row.tsx +11 -3
  574. package/src/plugins/builtin/perps/freshness.ts +8 -5
  575. package/src/plugins/builtin/perps/headless.ts +3 -1
  576. package/src/plugins/builtin/perps/list-perpetuals.ts +25 -4
  577. package/src/plugins/builtin/perps/live-rows.ts +161 -0
  578. package/src/plugins/builtin/perps/markets-section.tsx +154 -0
  579. package/src/plugins/builtin/perps/pane.tsx +36 -17
  580. package/src/plugins/builtin/perps/venue-markets.ts +96 -0
  581. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -1
  582. package/src/plugins/builtin/plugin-marketplace/pane.tsx +143 -59
  583. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +72 -24
  584. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
  585. package/src/plugins/builtin/portfolio-list/cli/render.ts +49 -20
  586. package/src/plugins/builtin/portfolio-list/cli/shared.ts +3 -3
  587. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +29 -24
  588. package/src/plugins/builtin/portfolio-list/headless.ts +6 -3
  589. package/src/plugins/builtin/portfolio-list/income.ts +13 -2
  590. package/src/plugins/builtin/portfolio-list/index.tsx +9 -2
  591. package/src/plugins/builtin/portfolio-list/pane/index.tsx +21 -8
  592. package/src/plugins/builtin/portfolio-list/pane/purchase-date-fx.ts +46 -0
  593. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +16 -9
  594. package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
  595. package/src/plugins/builtin/power/client.ts +4 -1
  596. package/src/plugins/builtin/power/evidence.ts +22 -7
  597. package/src/plugins/builtin/power/gas.tsx +5 -1
  598. package/src/plugins/builtin/power/pane.tsx +8 -1
  599. package/src/plugins/builtin/power/prices.tsx +10 -3
  600. package/src/plugins/builtin/research/analyst-headless.ts +25 -8
  601. package/src/plugins/builtin/research/analyst-model.ts +68 -3
  602. package/src/plugins/builtin/research/corporate-actions-pane.tsx +63 -14
  603. package/src/plugins/builtin/research/event-model.ts +3 -1
  604. package/src/plugins/builtin/research/event-sources.ts +12 -4
  605. package/src/plugins/builtin/research/index.tsx +2 -1
  606. package/src/plugins/builtin/research/relative-valuation-basis.ts +58 -0
  607. package/src/plugins/builtin/research/relative-valuation-columns.ts +31 -2
  608. package/src/plugins/builtin/research/relative-valuation-currency.ts +12 -5
  609. package/src/plugins/builtin/research/relative-valuation-headless.ts +84 -26
  610. package/src/plugins/builtin/research/relative-valuation-model.ts +88 -19
  611. package/src/plugins/builtin/research/relative-valuation-pane.tsx +76 -9
  612. package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
  613. package/src/plugins/builtin/reverse-dcf/model.ts +6 -2
  614. package/src/plugins/builtin/risk-factors/index.tsx +2 -2
  615. package/src/plugins/builtin/scanner/lock-note.ts +1 -1
  616. package/src/plugins/builtin/sec/forms.ts +13 -0
  617. package/src/plugins/builtin/sec/fund-abroad.ts +32 -0
  618. package/src/plugins/builtin/sec/fund-facts.ts +68 -8
  619. package/src/plugins/builtin/sec/headless.ts +85 -8
  620. package/src/plugins/builtin/sec/index.tsx +58 -12
  621. package/src/plugins/builtin/sectors/client.ts +2 -1
  622. package/src/plugins/builtin/sectors/sector-model.ts +11 -15
  623. package/src/plugins/builtin/series-inventory.ts +2 -0
  624. package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
  625. package/src/plugins/builtin/shared/cloud-upgrade.ts +34 -6
  626. package/src/plugins/builtin/shared/earnings-calendar.ts +375 -0
  627. package/src/plugins/builtin/{earnings/estimate-basis.ts → shared/earnings-estimate-basis.ts} +1 -1
  628. package/src/plugins/builtin/shared/earnings-report.ts +3 -3
  629. package/src/plugins/builtin/shared/fx-rate-freshness.ts +23 -1
  630. package/src/plugins/builtin/shared/job-posting.ts +129 -0
  631. package/src/plugins/builtin/shared/pro-lock-note.ts +3 -3
  632. package/src/plugins/builtin/shared/report-freshness.ts +3 -0
  633. package/src/plugins/builtin/shared/research-trading.tsx +21 -0
  634. package/src/plugins/builtin/shared/series-cache.ts +2 -0
  635. package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
  636. package/src/plugins/builtin/short-interest/client.ts +2 -0
  637. package/src/plugins/builtin/short-interest/headless.ts +41 -12
  638. package/src/plugins/builtin/short-interest/index.tsx +23 -1
  639. package/src/plugins/builtin/short-interest/model.ts +70 -19
  640. package/src/plugins/builtin/short-interest/pane.tsx +19 -2
  641. package/src/plugins/builtin/short-interest/publications-client.ts +82 -0
  642. package/src/plugins/builtin/short-interest/publications-headless.ts +29 -0
  643. package/src/plugins/builtin/short-interest/publications-model.ts +50 -0
  644. package/src/plugins/builtin/short-interest/publications-pane.tsx +66 -0
  645. package/src/plugins/builtin/short-interest/schedule.ts +35 -2
  646. package/src/plugins/builtin/short-interest/surface.tsx +11 -4
  647. package/src/plugins/builtin/short-interest/types.ts +9 -0
  648. package/src/plugins/builtin/short-interest/watch-client.ts +14 -8
  649. package/src/plugins/builtin/short-interest/watch-model.ts +12 -25
  650. package/src/plugins/builtin/short-interest/watch-pane.tsx +5 -1
  651. package/src/plugins/builtin/supply-chain/flow.tsx +33 -9
  652. package/src/plugins/builtin/supply-chain/model.ts +51 -3
  653. package/src/plugins/builtin/supply-chain/pane.tsx +78 -39
  654. package/src/plugins/builtin/supply-chain/row-evidence.tsx +1 -1
  655. package/src/plugins/builtin/themes/headless.ts +8 -5
  656. package/src/plugins/builtin/themes/model.ts +14 -4
  657. package/src/plugins/builtin/themes/pane.tsx +24 -5
  658. package/src/plugins/builtin/thirteenf/headless.ts +5 -5
  659. package/src/plugins/builtin/thirteenf/signals.ts +3 -1
  660. package/src/plugins/builtin/ticker-detail/adr-pair/headless.ts +123 -0
  661. package/src/plugins/builtin/ticker-detail/adr-pair/model.ts +132 -0
  662. package/src/plugins/builtin/ticker-detail/adr-pair/tab.tsx +87 -0
  663. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +40 -26
  664. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +8 -4
  665. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +67 -3
  666. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +112 -5
  667. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +17 -2
  668. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +21 -7
  669. package/src/plugins/builtin/ticker-detail/financials/model.ts +36 -1
  670. package/src/plugins/builtin/ticker-detail/financials/offerings.ts +56 -0
  671. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +62 -13
  672. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +30 -4
  673. package/src/plugins/builtin/ticker-detail/headless-schema.ts +14 -2
  674. package/src/plugins/builtin/ticker-detail/headless.ts +148 -35
  675. package/src/plugins/builtin/ticker-detail/index.tsx +37 -5
  676. package/src/plugins/builtin/ticker-detail/overview/model.ts +65 -10
  677. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +9 -1
  678. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +68 -11
  679. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -0
  680. package/src/plugins/builtin/ticker-detail/quote-footer.ts +8 -3
  681. package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
  682. package/src/plugins/builtin/vol-surface/client.ts +5 -4
  683. package/src/plugins/builtin/vol-surface/headless.ts +12 -3
  684. package/src/plugins/builtin/vol-surface/model.ts +4 -2
  685. package/src/plugins/builtin/volatility/model.ts +5 -10
  686. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +4 -3
  687. package/src/plugins/builtin/world-venue-map/basemap.ts +3 -1
  688. package/src/plugins/builtin/world-venue-map/chokepoint-chart.ts +40 -0
  689. package/src/plugins/builtin/world-venue-map/evidence.ts +1 -1
  690. package/src/plugins/builtin/world-venue-map/geo-series.ts +90 -11
  691. package/src/plugins/builtin/world-venue-map/headless.ts +1 -1
  692. package/src/plugins/builtin/world-venue-map/index.ts +3 -26
  693. package/src/plugins/builtin/world-venue-map/layers.ts +1 -1
  694. package/src/plugins/builtin/world-venue-map/map.tsx +2 -1
  695. package/src/plugins/builtin/world-venue-map/pane.tsx +4 -2
  696. package/src/plugins/builtin/world-venue-map/venue-header.tsx +15 -4
  697. package/src/plugins/builtin/yield-curve/client.ts +25 -0
  698. package/src/plugins/builtin/yield-curve/curves.ts +7 -4
  699. package/src/plugins/builtin/yield-curve/headless.ts +38 -4
  700. package/src/plugins/builtin/yield-curve/history.ts +4 -3
  701. package/src/plugins/builtin/yield-curve/index.tsx +12 -6
  702. package/src/plugins/builtin/yield-curve/swaps-model.ts +93 -0
  703. package/src/plugins/builtin/yield-curve/swaps.tsx +99 -0
  704. package/src/plugins/bundle.ts +1 -1
  705. package/src/plugins/catalog-browser.ts +2 -1
  706. package/src/plugins/compat.ts +3 -3
  707. package/src/plugins/dependencies.ts +75 -14
  708. package/src/plugins/extracted-plugins.ts +132 -34
  709. package/src/plugins/host-link.ts +149 -34
  710. package/src/plugins/installer.ts +35 -10
  711. package/src/plugins/loader.ts +206 -38
  712. package/src/plugins/manager-node.ts +2 -2
  713. package/src/plugins/marketplace/deactivation.ts +22 -0
  714. package/src/plugins/marketplace/model.ts +2 -2
  715. package/src/plugins/marketplace/packs.ts +1 -1
  716. package/src/plugins/ownership.ts +1 -1
  717. package/src/plugins/peer-retry.ts +155 -0
  718. package/src/plugins/registry/contributions.ts +3 -1
  719. package/src/plugins/registry/index.ts +4 -3
  720. package/src/plugins/registry/plugin-state.ts +3 -0
  721. package/src/plugins/seed-notice.ts +20 -0
  722. package/src/plugins/shortcut-argument.ts +39 -0
  723. package/src/portfolio/allocation.ts +1 -1
  724. package/src/portfolio/collection-name.ts +31 -0
  725. package/src/portfolio/column-values.ts +10 -8
  726. package/src/portfolio/cost-fx.ts +296 -19
  727. package/src/portfolio/manual-lots.ts +75 -0
  728. package/src/portfolio/mutations.ts +60 -22
  729. package/src/portfolio/suffixed-listings.ts +209 -0
  730. package/src/portfolio/totals-currency.ts +24 -0
  731. package/src/public/__snapshots__/public-api.test.ts.snap +14 -0
  732. package/src/public/time-series.ts +11 -8
  733. package/src/remote/schema.ts +1 -1
  734. package/src/renderers/browser/config-host.ts +3 -0
  735. package/src/renderers/browser/main.tsx +23 -3
  736. package/src/renderers/dom/boot-frame-page.ts +187 -0
  737. package/src/renderers/dom/boot-frame.ts +237 -0
  738. package/src/renderers/dom/build-assets.ts +46 -8
  739. package/src/renderers/dom/dom-ui-host.tsx +4 -29
  740. package/src/renderers/dom/host/ascii-text.ts +16 -2
  741. package/src/renderers/dom/host/box.tsx +43 -2
  742. package/src/renderers/dom/host/chart-surface.tsx +2 -0
  743. package/src/renderers/dom/host/hls.chunk.ts +3 -0
  744. package/src/renderers/dom/host/image-surface.tsx +49 -0
  745. package/src/renderers/dom/host/media-surface.tsx +24 -18
  746. package/src/renderers/dom/host/mouse.ts +30 -0
  747. package/src/renderers/dom/host/size-observer.ts +1 -1
  748. package/src/renderers/dom/host/style.ts +1 -1
  749. package/src/renderers/dom/styles.css +76 -0
  750. package/src/renderers/dom/test-utils.tsx +2 -1
  751. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -3
  752. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
  753. package/src/renderers/electrobun/bun/desktop/backend.ts +46 -3
  754. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +1 -1
  755. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -3
  756. package/src/renderers/electrobun/bun/desktop/initialization.ts +81 -18
  757. package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
  758. package/src/renderers/electrobun/bun/index.ts +11 -1
  759. package/src/renderers/electrobun/bun/window/boot-frame.ts +67 -0
  760. package/src/renderers/electrobun/bun/window/create.ts +4 -1
  761. package/src/renderers/electrobun/shared/boot-frame.ts +7 -0
  762. package/src/renderers/electrobun/shared/protocol.ts +6 -1
  763. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  764. package/src/renderers/electrobun/view/config-host.ts +3 -0
  765. package/src/renderers/electrobun/view/fatal-screen.tsx +1 -1
  766. package/src/renderers/electrobun/view/main.tsx +30 -4
  767. package/src/renderers/electrobun/view/news-source.ts +5 -5
  768. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -1
  769. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  770. package/src/renderers/opentui/browser-open.ts +88 -0
  771. package/src/renderers/opentui/host.tsx +5 -14
  772. package/src/renderers/opentui/image/loader.ts +3 -2
  773. package/src/renderers/opentui/native-smoke.ts +10 -1
  774. package/src/renderers/opentui/pane-image/index.ts +2 -1
  775. package/src/renderers/opentui/render-lib.ts +253 -0
  776. package/src/renderers/opentui/render-timing-smoke.ts +1 -1
  777. package/src/renderers/opentui/start.tsx +1 -1
  778. package/src/renderers/share/layout-view.tsx +2 -2
  779. package/src/renderers/share/main.tsx +4 -4
  780. package/src/renderers/share/view.tsx +1 -1
  781. package/src/series/resolve.ts +22 -6
  782. package/src/series/standalone.ts +30 -2
  783. package/src/series/types.ts +8 -1
  784. package/src/shares/api.ts +4 -1
  785. package/src/shares/pane.ts +1 -1
  786. package/src/sources/gloomberb-cloud/fred-series.ts +46 -1
  787. package/src/sources/gloomberb-cloud/index.ts +32 -16
  788. package/src/sources/gloomberb-cloud/news.ts +39 -0
  789. package/src/sources/gloomberb-cloud/normalizers.ts +14 -1
  790. package/src/sources/gloomberb-cloud/perpetuals.ts +49 -1
  791. package/src/sources/listing-symbols.ts +37 -0
  792. package/src/sources/provider-errors.ts +16 -2
  793. package/src/sources/provider-router/financial-routes.ts +21 -3
  794. package/src/sources/provider-router/financials.ts +15 -2
  795. package/src/sources/provider-router/history.ts +30 -8
  796. package/src/sources/provider-router/primary.ts +1 -1
  797. package/src/state/app/bootstrap.ts +23 -0
  798. package/src/state/hooks/inline-tickers.ts +30 -5
  799. package/src/sync/controller.ts +1 -1
  800. package/src/sync/core-contributors.ts +46 -14
  801. package/src/team/collections-sync.ts +2 -1
  802. package/src/team/collections.ts +178 -17
  803. package/src/telemetry/crash-reports.ts +17 -4
  804. package/src/test-support/builtin-panes.ts +1 -1
  805. package/src/theme/colors.ts +3 -30
  806. package/src/theme/ui-tokens.ts +37 -1
  807. package/src/tickers/company-name-listing.ts +210 -33
  808. package/src/tickers/index-roots.ts +47 -0
  809. package/src/tickers/instrument-label.ts +5 -1
  810. package/src/tickers/metadata.ts +10 -0
  811. package/src/tickers/perpetual-listing.ts +5 -2
  812. package/src/tickers/search/home-listing.ts +75 -0
  813. package/src/tickers/search/index.ts +76 -27
  814. package/src/tickers/search/ranking.ts +54 -4
  815. package/src/tickers/search/upsert.ts +29 -0
  816. package/src/tickers/terminal-codes.ts +416 -40
  817. package/src/time-series/bar-aggregation.ts +243 -0
  818. package/src/time-series/base-series.ts +307 -0
  819. package/src/time-series/chart-bounds.ts +349 -0
  820. package/src/time-series/chart-seed.ts +142 -0
  821. package/src/time-series/economic.ts +5 -0
  822. package/src/time-series/fundamentals.ts +18 -3
  823. package/src/time-series/history-acquisition.ts +499 -0
  824. package/src/time-series/leverage.ts +100 -0
  825. package/src/time-series/market-resolution.ts +4 -2
  826. package/src/time-series/market.ts +2 -1
  827. package/src/time-series/price-comparison.ts +175 -30
  828. package/src/time-series/range.ts +9 -1
  829. package/src/time-series/resolution.ts +174 -16
  830. package/src/time-series/resolve-context.ts +157 -0
  831. package/src/time-series/resolve.ts +126 -1577
  832. package/src/time-series/series-name.ts +38 -0
  833. package/src/time-series/series-presentation.ts +208 -0
  834. package/src/time-series/spec.ts +11 -6
  835. package/src/time-series/studies.ts +109 -12
  836. package/src/time-series/tail-reconcile.ts +121 -0
  837. package/src/time-series/types.ts +29 -2
  838. package/src/time-series/use-chart-resolution.ts +4 -5
  839. package/src/types/broker.ts +10 -0
  840. package/src/types/config.ts +14 -0
  841. package/src/types/data-provider.ts +14 -0
  842. package/src/types/exchange-rate.ts +6 -0
  843. package/src/types/financials.ts +104 -7
  844. package/src/types/headless.ts +76 -6
  845. package/src/types/instrument.ts +3 -0
  846. package/src/types/plugin.ts +65 -8
  847. package/src/types/price-history.ts +2 -0
  848. package/src/types/ticker.ts +13 -0
  849. package/src/types/trading.ts +59 -1
  850. package/src/ui/ascii-font.ts +13 -1
  851. package/src/ui/host.tsx +6 -1
  852. package/src/updater/command-names.ts +175 -0
  853. package/src/updater/index.ts +10 -5
  854. package/src/utils/calendar-date.ts +32 -15
  855. package/src/utils/cash-runway.ts +241 -0
  856. package/src/utils/central-banks.ts +42 -0
  857. package/src/utils/cli-output.ts +259 -80
  858. package/src/utils/contract-month.ts +16 -0
  859. package/src/utils/crypto-pair.ts +61 -3
  860. package/src/utils/currency-units.ts +29 -0
  861. package/src/utils/datetime-format.ts +10 -0
  862. package/src/utils/debug-log.ts +1 -1
  863. package/src/utils/decimal-comma.ts +39 -6
  864. package/src/utils/exchange-announcements.ts +71 -0
  865. package/src/utils/exchange-rate-snapshot.ts +10 -0
  866. package/src/utils/financial-statements.ts +83 -4
  867. package/src/utils/format.ts +34 -16
  868. package/src/utils/fundamentals.ts +150 -34
  869. package/src/utils/fx-market-hours.ts +10 -1
  870. package/src/utils/latest-financial-period.ts +8 -0
  871. package/src/utils/linux-libc.ts +54 -0
  872. package/src/utils/market-capitalization.ts +111 -15
  873. package/src/utils/max-drawdown.ts +64 -0
  874. package/src/utils/network-failure.ts +83 -0
  875. package/src/utils/option-expiry.ts +139 -7
  876. package/src/utils/price-earnings.ts +11 -5
  877. package/src/utils/price-history.ts +18 -8
  878. package/src/utils/price-window.ts +131 -0
  879. package/src/utils/release-date.ts +47 -0
  880. package/src/utils/sec.ts +3 -0
  881. package/src/utils/throttled-fetch.ts +11 -4
  882. package/src/utils/trailing-eps.ts +67 -0
  883. package/src/utils/typed-text.ts +21 -0
  884. package/src/utils/utc-time.ts +24 -1
  885. package/src/version.ts +6 -0
  886. 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
- }