gloomberb 0.18.1 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -1,10 +1,12 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneContext } from "../../../types/headless";
3
3
  import {
4
+ KPI_FIELDS,
4
5
  NUMERIC_FIELDS,
5
6
  type NumericField,
6
7
  type ScreenRow,
7
8
  } from "../../../api-client/equity-screener";
9
+ import { KPI_COVERAGE_NOTE } from "../company-kpis/model";
8
10
  import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
9
11
  import { buildQuoteKey } from "../../../market-data/selectors";
10
12
  import type { Quote } from "../../../types/financials";
@@ -17,8 +19,12 @@ import {
17
19
  formatScreenValue,
18
20
  metricDate,
19
21
  parseScreenDefinition,
22
+ screenColumnLabel,
20
23
  screenDefinitionSyntax,
21
- SHORT_LABELS,
24
+ screenCurrencyEmpty,
25
+ screenLabel,
26
+ screenYtdBase,
27
+ withheldMetricNote,
22
28
  } from "./model";
23
29
  import type { ScreenDefinition } from "../../../api-client/equity-screener";
24
30
 
@@ -128,15 +134,33 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
128
134
  // A sort on one of them is redone on those figures.
129
135
  const rows = sortLiveScreenRows(overlayScreenQuotes(data.rows, await loadRowQuotes(data.rows, ctx)), data.definition.sort);
130
136
  const extendedSessions = screenExtendedSessions(rows);
137
+ // A Pro metric the server held back from Free says so once, as KPIS says it; a KPI
138
+ // with gaps says what a gap is, as KPIS does.
139
+ const withheld = rows.some((row) => row.metrics[metric].access === "pro");
140
+ const errors = withheld ? [...data.warnings, withheldMetricNote(metric)] : data.warnings;
141
+ const missing = rows.filter((row) => row.metrics[metric].value === null).length;
142
+ const kpiGaps = !withheld && missing > 0 && (KPI_FIELDS as readonly string[]).includes(metric)
143
+ ? [`${missing} of ${rows.length} ${rows.length === 1 ? "row has" : "rows have"} no ${screenLabel(metric)}. ${KPI_COVERAGE_NOTE}`]
144
+ : [];
145
+ // A CAD screen of listings quoted in USD is empty for its currency, not for want of companies: say so, and give the screen in USD.
146
+ const currencyEmpty = rows.length === 0 ? screenCurrencyEmpty(data.definition, data.universe) : null;
147
+ const empty = currencyEmpty
148
+ ? [
149
+ `${currencyEmpty.title} ${currencyEmpty.detail}`,
150
+ `In the base currency, ${currencyEmpty.base.currency}: gloomberb fn EQS --definition '${spacedJson(currencyEmpty.base)}'${metric === "marketCap" ? "" : ` --metric ${metric}`}`,
151
+ ].join("\n")
152
+ : null;
131
153
  // Payload status also counts ordinary per-field sparsity; only generation
132
154
  // warnings are failures, and more pages are not an error.
133
155
  return {
134
156
  freshness: { asOf: data.snapshot?.sourceNewestAt ?? null },
135
- complete: !data.nextCursor && data.warnings.length === 0,
136
- errors: data.warnings,
157
+ complete: !data.nextCursor && errors.length === 0,
158
+ errors,
159
+ ...(kpiGaps.length ? { notes: kpiGaps } : {}),
137
160
  sections: [
138
161
  {
139
162
  title: "Matching equities",
163
+ ...(empty ? { empty } : {}),
140
164
  columns: [
141
165
  { key: "symbol", header: "Symbol" },
142
166
  { key: "exchange", header: "Exchange" },
@@ -145,7 +169,8 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
145
169
  ...(metric === "price" ? [] : [{ key: "price", header: "Last", align: "right" as const, format: price }]),
146
170
  ...(metric === "changePercent" ? [] : [{ key: "changePercent", header: "Chg%", align: "right" as const, format: percent }]),
147
171
  ...extendedSessions.map(extendedColumn),
148
- { key: "value", header: SHORT_LABELS[metric], align: "right" },
172
+ // YTD names the close it is measured from: `YTD% vs 31 Dec close`.
173
+ { key: "value", header: screenColumnLabel(metric, screenYtdBase(data.rows, data.snapshot?.assembledAt), "report"), align: "right" },
149
174
  { key: "unit", header: "Unit" },
150
175
  { key: "percentile", header: "Covered pctl", align: "right" },
151
176
  { key: "asOf", header: "As of" },
@@ -172,7 +197,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
172
197
  ? null
173
198
  : Math.round(observation.percentile.value),
174
199
  asOf: stamp.collected ? `${stamp.text} (collected)` : stamp.text,
175
- state: observation.state,
200
+ state: observation.access === "pro" ? "locked" : observation.state,
176
201
  };
177
202
  }),
178
203
  },
@@ -17,9 +17,11 @@ import {
17
17
  type ScreenPayload,
18
18
  type ScreenRow,
19
19
  } from "../../../api-client/equity-screener";
20
+ import { yearToDateBaseLabel } from "../../../market-data/market/year-to-date";
20
21
  import { canonicalExchange } from "../../../utils/exchanges";
21
22
  import { LATEST_DISTRIBUTION, TRAILING_DIVIDEND_BASIS } from "../../../utils/forward-dividend";
22
23
  import { formatCompact, formatNumber } from "../../../utils/format";
24
+ import { kpiFreeLimitNote } from "../company-kpis/model";
23
25
 
24
26
  /**
25
27
  * Column headers. Currency sits in the footer; percentage fields carry %.
@@ -160,8 +162,29 @@ export function resultFields(definition: ScreenDefinition, metric: NumericField)
160
162
  return [...new Set(fields)].filter((field) => definition.currency || !MONETARY.has(field) || field === metric);
161
163
  }
162
164
 
165
+ /**
166
+ * The close the screen's YTD returns are measured from, `31 Dec`: NYSE's last
167
+ * session of the year before the newest session a row's YTD ends on, the
168
+ * snapshot's day without one.
169
+ */
170
+ export function screenYtdBase(rows: readonly ScreenRow[], assembledAt?: string | null, now = Date.now()): string | null {
171
+ const day = rows.map((row) => row.metrics.returnYtdPercent?.asOf?.slice(0, 10) ?? "").filter(Boolean).sort().at(-1)
172
+ ?? assembledAt?.slice(0, 10);
173
+ return day ? yearToDateBaseLabel(day, "NYSE", now) : null;
174
+ }
175
+
176
+ /**
177
+ * A column's header. YTD names the close it is measured from: `YTD% vs 31 Dec
178
+ * close` in a report, `YTD% vs 31 Dec` in the pane and `YTD% 31 Dec` where
179
+ * the pane is short of room.
180
+ */
181
+ export function screenColumnLabel(field: NumericField, ytdBase: string | null, form: "report" | "pane" | "compact" = "pane"): string {
182
+ if (field !== "returnYtdPercent" || !ytdBase) return SHORT_LABELS[field];
183
+ return form === "compact" ? `YTD% ${ytdBase}` : `YTD% vs ${ytdBase}${form === "report" ? " close" : ""}`;
184
+ }
185
+
163
186
  /** Prices get room for a separated six-figure quote (757,398.00). */
164
- export const columnWidth = (field: NumericField) => Math.max(field === "price" ? 10 : 8, SHORT_LABELS[field].length);
187
+ export const columnWidth = (field: NumericField, label = SHORT_LABELS[field]) => Math.max(field === "price" ? 10 : 8, label.length);
165
188
 
166
189
  export const DEFAULT_SCREEN: ScreenDefinition = {
167
190
  version: 1,
@@ -169,6 +192,34 @@ export const DEFAULT_SCREEN: ScreenDefinition = {
169
192
  criteria: [{ field: "marketCap", op: "gte", value: 10_000_000_000 }],
170
193
  sort: { field: "marketCap", direction: "desc" },
171
194
  };
195
+
196
+ /**
197
+ * Why a screen with a currency found nothing, when the currency may be why:
198
+ * `currency` keeps the listings quoted in it and converts nothing, so a CAD
199
+ * screen of listings quoted in USD reads as no company at all. `title` says
200
+ * which listings it kept, `detail` that it converts nothing and what the
201
+ * covered listings are quoted in, and `base` is the same screen in the base
202
+ * currency the default screen uses (USD), to run instead. Null for a screen
203
+ * in that currency or with none.
204
+ */
205
+ export function screenCurrencyEmpty(
206
+ definition: ScreenDefinition,
207
+ universe: Pick<ScreenPayload["universe"], "covered" | "currencies">,
208
+ ): { title: string; detail: string; base: ScreenDefinition } | null {
209
+ const currency = definition.currency;
210
+ const baseCurrency = DEFAULT_SCREEN.currency!;
211
+ if (!currency || currency === baseCurrency) return null;
212
+ const quoted = universe.currencies.filter(Boolean);
213
+ const none = quoted.length > 0 && !quoted.includes(currency);
214
+ return {
215
+ title: none
216
+ ? `None of the ${universe.covered.toLocaleString("en-US")} covered listings is quoted in ${currency}.`
217
+ : `No listing quoted in ${currency} matches.`,
218
+ detail: `Currency ${currency} keeps only the listings quoted in ${currency} and converts nothing${none ? `; the covered listings are quoted in ${quoted.join(", ")}` : ""}.`,
219
+ base: { ...definition, currency: baseCurrency },
220
+ };
221
+ }
222
+
172
223
  export const screenRowId = (row: Pick<ScreenRow, "symbol" | "exchange">) =>
173
224
  `${canonicalExchange(row.exchange)}:${row.symbol}`;
174
225
  export const screenLabel = (field: string) =>
@@ -481,6 +532,20 @@ const unavailableMetric = (): ScreenMetric => ({
481
532
  state: "unavailable", scope: "reported", reason: "Not provided by this server", sourceUrl: null,
482
533
  percentile: { value: null, sampleCount: 0, scope: "covered-universe" },
483
534
  });
535
+ /** Company KPIs, guidance, hiring and app attention: the server leaves them out of a screen for a caller without Gloom Pro. */
536
+ const PRO_FIELDS: ReadonlySet<string> = new Set([...KPI_FIELDS, ...HIRING_APP_FIELDS]);
537
+ const withheldMetric = (): ScreenMetric => ({ ...unavailableMetric(), reason: "Needs Gloom Pro", access: "pro" });
538
+
539
+ /**
540
+ * What a screen says once of a metric the server held back from Free, a
541
+ * KPI's in the words `fn KPIS` uses ("The free screener shows no KPI values;
542
+ * Gloom Pro shows each company's Backlog.").
543
+ */
544
+ export function withheldMetricNote(field: NumericField): string {
545
+ return (KPI_FIELDS as readonly string[]).includes(field)
546
+ ? kpiFreeLimitNote({ metric: screenLabel(field), shown: 0, total: null, guidance: field.startsWith("guide"), where: "The free screener" })
547
+ : `${screenLabel(field)} needs Gloom Pro.`;
548
+ }
484
549
  export function validateScreenPayload(value: unknown): ScreenPayload {
485
550
  const data = value as ScreenPayload;
486
551
  if (
@@ -523,8 +588,9 @@ export function validateScreenPayload(value: unknown): ScreenPayload {
523
588
  ids.add(screenRowId(row));
524
589
  for (const field of NUMERIC_FIELDS) {
525
590
  // A server from before social, research, volatility or dividend fields omits them; they read as unavailable.
591
+ // A Pro field it left out was held back from this caller.
526
592
  if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS, ...VOLATILITY_FIELDS, ...ATTENTION_FIELDS, ...HIRING_APP_FIELDS, ...KPI_FIELDS, ...DIVIDEND_FIELDS] as readonly string[]).includes(field))
527
- row.metrics[field] = unavailableMetric();
593
+ row.metrics[field] = PRO_FIELDS.has(field) ? withheldMetric() : unavailableMetric();
528
594
  const metric = row.metrics[field];
529
595
  if (
530
596
  !metric ||
@@ -67,10 +67,12 @@ import {
67
67
  metricDate,
68
68
  parseScreenDefinition,
69
69
  resultFields,
70
- SHORT_LABELS,
71
70
  SIGN_COLORED,
71
+ screenColumnLabel,
72
+ screenCurrencyEmpty,
72
73
  screenLabel,
73
74
  screenRowId,
75
+ screenYtdBase,
74
76
  } from "./model";
75
77
  import { useScreenResults } from "./results";
76
78
  import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, sortLiveScreenRows, type LiveScreenRow } from "./live";
@@ -101,20 +103,19 @@ const EXTENDED_COLUMN_PREFIX = "ext:";
101
103
  * CHG% for each session in `extendedSessions`, and is added only whole, as
102
104
  * the metrics are.
103
105
  */
104
- function resultColumns(
106
+ export function resultColumns(
105
107
  width: number,
106
108
  definition: ScreenDefinition,
107
109
  metric: NumericField,
108
110
  asOf: boolean,
109
111
  extendedSessions: readonly ExtendedSession[] = [],
112
+ ytdBase: string | null = null,
110
113
  ): DataTableColumn[] {
111
114
  const [focus, ...others] = resultFields(definition, metric);
112
- const metricColumn = (field: NumericField): DataTableColumn => ({
113
- id: `m:${field}`,
114
- label: SHORT_LABELS[field],
115
- width: columnWidth(field),
116
- align: "right",
117
- });
115
+ const metricColumn = (field: NumericField, form: "pane" | "compact"): DataTableColumn => {
116
+ const label = screenColumnLabel(field, ytdBase, form);
117
+ return { id: `m:${field}`, label, width: columnWidth(field, label), align: "right" };
118
+ };
118
119
  const extendedColumns = extendedSessions.map((session): DataTableColumn => ({
119
120
  id: `${EXTENDED_COLUMN_PREFIX}${session}`,
120
121
  label: extendedSessionStateLabel(session),
@@ -122,20 +123,21 @@ function resultColumns(
122
123
  align: "right",
123
124
  }));
124
125
  // The extended move reads beside CHG%, or first after the focus metric when that is CHG%.
125
- const extras = [
126
+ const extras = (form: "pane" | "compact") => [
126
127
  ...(focus === "changePercent" ? extendedColumns : []),
127
- ...others.flatMap((field) => field === "changePercent" ? [metricColumn(field), ...extendedColumns] : [metricColumn(field)]),
128
+ ...others.flatMap((field) => field === "changePercent" ? [metricColumn(field, form), ...extendedColumns] : [metricColumn(field, form)]),
128
129
  ];
129
130
  const build = (
130
131
  extra: DataTableColumn[],
131
132
  exchange: boolean,
132
133
  sector: boolean,
134
+ form: "pane" | "compact",
133
135
  ): DataTableColumn[] => [
134
136
  { id: "symbol", label: "SYMBOL", width: 8, align: "left" },
135
137
  { id: "name", label: "NAME", width: 18, flexGrow: 1, align: "left" },
136
138
  ...(exchange ? [{ id: "exchange", label: "EXCH", width: 7, align: "left" as const }] : []),
137
139
  ...(sector ? [{ id: "sector", label: "SECTOR", width: 22, align: "left" as const }] : []),
138
- metricColumn(focus!),
140
+ metricColumn(focus!, form),
139
141
  { id: "percentile", label: "PCTL", width: 4, align: "right" },
140
142
  ...(asOf ? [{ id: "date", label: "AS OF", width: 10, align: "left" as const }] : []),
141
143
  ...extra,
@@ -143,15 +145,15 @@ function resultColumns(
143
145
  // Measured the way the table draws them (header floor, gaps, lead gaps), so a
144
146
  // column is only added when it fits whole.
145
147
  const fits = (columns: DataTableColumn[]) => getTableWidth(columns) <= width;
146
- let shown: DataTableColumn[] = [];
147
- for (const column of extras) {
148
- const next = [...shown, column];
149
- if (!fits(build(next, false, false))) break;
150
- shown = next;
151
- }
152
- const exchange = fits(build(shown, true, false));
153
- const sector = exchange && fits(build(shown, true, true));
154
- return build(shown, exchange, sector);
148
+ // Columns are chosen with YTD's short header (`YTD% 31 Dec`), which reads
149
+ // `YTD% vs 31 Dec` when that still fits: naming the close never costs a column.
150
+ const compact = extras("compact");
151
+ let shown = 0;
152
+ while (shown < compact.length && fits(build(compact.slice(0, shown + 1), false, false, "compact"))) shown++;
153
+ const exchange = fits(build(compact.slice(0, shown), true, false, "compact"));
154
+ const sector = exchange && fits(build(compact.slice(0, shown), true, true, "compact"));
155
+ const named = build(extras("pane").slice(0, shown), exchange, sector, "pane");
156
+ return fits(named) ? named : build(compact.slice(0, shown), exchange, sector, "compact");
155
157
  }
156
158
  const loadFields = () => fetchScreenFields();
157
159
  function ScreenDetail({
@@ -351,7 +353,11 @@ function EquityScreenView({
351
353
  const staleFocus = snapshotRows.some((row) => row.metrics[metric]?.state === "stale");
352
354
  // A pre-market, after-hours or overnight column only while a row has such a print.
353
355
  const extendedSessions = screenExtendedSessions(rows);
354
- const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions);
356
+ // YTD names the close it is measured from, `31 Dec`.
357
+ const ytdBase = screenYtdBase(snapshotRows, data?.snapshot?.assembledAt);
358
+ // A screen its currency emptied says so, not that no company matches.
359
+ const currencyEmpty = data && data.rows.length === 0 ? screenCurrencyEmpty(data.definition, data.universe) : null;
360
+ const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions, ytdBase);
355
361
  const apply = (next: ScreenDefinition) => {
356
362
  try {
357
363
  setDefinition(parseScreenDefinition(next));
@@ -886,11 +892,8 @@ function EquityScreenView({
886
892
  </PaneStatusBody>
887
893
  )
888
894
  ) : (
889
- <PaneStatusBody
890
- loading={results.loading && !data}
891
- error={!data ? results.error : null}
892
- subject="equity screen"
893
- >
895
+ // The metric stays above the results while a new sort loads.
896
+ <>
894
897
  {!opened ? (
895
898
  <QueryBar
896
899
  width={width}
@@ -915,101 +918,108 @@ function EquityScreenView({
915
918
  }]}
916
919
  />
917
920
  ) : null}
918
- <DataTableStackView
919
- columns={columns}
920
- items={rows}
921
- focused={focused && !saveForm}
922
- rootWidth={width}
923
- rootHeight={bodyHeight - 1}
924
- scrollRef={tableScroll}
925
- onBodyScrollActivity={loadMore}
926
- resetScrollKey={JSON.stringify(definition)}
927
- visibleRangeKey={JSON.stringify(definition)}
928
- onVisibleRangeChange={setVisibleRange}
929
- getItemKey={screenRowId}
930
- selection={{
931
- kind: "id",
932
- selectedId: selectedId ?? (rows[0] ? screenRowId(rows[0]) : null),
933
- getId: screenRowId,
934
- onChange: setSelectedId,
935
- }}
936
- onActivate={(row) => setOpenId(screenRowId(row))}
937
- detailOpen={!!opened}
938
- onBack={() => setOpenId(null)}
939
- detailTitle={opened?.name ?? opened?.symbol}
940
- detailContent={
941
- opened ? (
942
- <ScreenDetail
943
- row={opened}
944
- width={width}
945
- height={bodyHeight - 2}
946
- />
947
- ) : null
948
- }
949
- renderCell={(row, column) => {
950
- if (column.id.startsWith(EXTENDED_COLUMN_PREFIX)) {
951
- // The extended print's move from the close in PRICE and CHG%.
952
- const move = screenExtendedMove(row, column.id.slice(EXTENDED_COLUMN_PREFIX.length) as ExtendedSession);
953
- return move == null
954
- ? { text: "--", color: colors.textDim }
955
- : { text: formatPercentRaw(move), color: priceColor(move, colors) };
921
+ <PaneStatusBody
922
+ loading={results.loading && !data}
923
+ error={!data ? results.error : null}
924
+ subject="equity screen"
925
+ >
926
+ <DataTableStackView
927
+ columns={columns}
928
+ items={rows}
929
+ focused={focused && !saveForm}
930
+ rootWidth={width}
931
+ rootHeight={bodyHeight - 1}
932
+ scrollRef={tableScroll}
933
+ onBodyScrollActivity={loadMore}
934
+ resetScrollKey={JSON.stringify(definition)}
935
+ visibleRangeKey={JSON.stringify(definition)}
936
+ onVisibleRangeChange={setVisibleRange}
937
+ getItemKey={screenRowId}
938
+ selection={{
939
+ kind: "id",
940
+ selectedId: selectedId ?? (rows[0] ? screenRowId(rows[0]) : null),
941
+ getId: screenRowId,
942
+ onChange: setSelectedId,
943
+ }}
944
+ onActivate={(row) => setOpenId(screenRowId(row))}
945
+ detailOpen={!!opened}
946
+ onBack={() => setOpenId(null)}
947
+ detailTitle={opened?.name ?? opened?.symbol}
948
+ detailContent={
949
+ opened ? (
950
+ <ScreenDetail
951
+ row={opened}
952
+ width={width}
953
+ height={bodyHeight - 2}
954
+ />
955
+ ) : null
956
956
  }
957
- const field = column.id.startsWith("m:")
958
- ? (column.id.slice(2) as NumericField)
959
- : null;
960
- if (field) {
961
- const observation = row.metrics[field];
962
- const change = SIGN_COLORED.has(field) && observation.value != null
963
- // A move that prints 0.0 stays neutral.
964
- ? priceColor(Math.abs(observation.value) < 0.05 ? 0 : observation.value, colors)
965
- : undefined;
966
- return {
967
- text: formatScreenValue(field, observation.value),
968
- color: observation.state === "stale" ? colors.warning : change,
969
- };
957
+ renderCell={(row, column) => {
958
+ if (column.id.startsWith(EXTENDED_COLUMN_PREFIX)) {
959
+ // The extended print's move from the close in PRICE and CHG%.
960
+ const move = screenExtendedMove(row, column.id.slice(EXTENDED_COLUMN_PREFIX.length) as ExtendedSession);
961
+ return move == null
962
+ ? { text: "--", color: colors.textDim }
963
+ : { text: formatPercentRaw(move), color: priceColor(move, colors) };
964
+ }
965
+ const field = column.id.startsWith("m:")
966
+ ? (column.id.slice(2) as NumericField)
967
+ : null;
968
+ if (field) {
969
+ const observation = row.metrics[field];
970
+ const change = SIGN_COLORED.has(field) && observation.value != null
971
+ // A move that prints 0.0 stays neutral.
972
+ ? priceColor(Math.abs(observation.value) < 0.05 ? 0 : observation.value, colors)
973
+ : undefined;
974
+ return {
975
+ text: formatScreenValue(field, observation.value),
976
+ color: observation.state === "stale" ? colors.warning : change,
977
+ };
978
+ }
979
+ const focus = row.metrics[metric];
980
+ if (column.id === "percentile")
981
+ return { text: rank(focus.percentile.value) };
982
+ if (column.id === "date")
983
+ return focus.state === "stale"
984
+ ? { text: metricDate(focus).text, color: colors.warning }
985
+ : { text: "" };
986
+ if (column.id === "name") return { text: row.name ?? "--" };
987
+ return { text: String(row[column.id as "symbol"] ?? "--") };
988
+ }}
989
+ sortColumnId={
990
+ (NUMERIC_FIELDS as readonly string[]).includes(definition.sort.field)
991
+ ? `m:${definition.sort.field}`
992
+ : definition.sort.field
970
993
  }
971
- const focus = row.metrics[metric];
972
- if (column.id === "percentile")
973
- return { text: rank(focus.percentile.value) };
974
- if (column.id === "date")
975
- return focus.state === "stale"
976
- ? { text: metricDate(focus).text, color: colors.warning }
977
- : { text: "" };
978
- if (column.id === "name") return { text: row.name ?? "--" };
979
- return { text: String(row[column.id as "symbol"] ?? "--") };
980
- }}
981
- sortColumnId={
982
- (NUMERIC_FIELDS as readonly string[]).includes(definition.sort.field)
983
- ? `m:${definition.sort.field}`
984
- : definition.sort.field
985
- }
986
- sortDirection={definition.sort.direction}
987
- onHeaderClick={(id) => {
988
- const field = id.startsWith("m:")
989
- ? (id.slice(2) as NumericField)
990
- : id === "percentile" || id === "date"
991
- ? metric
992
- : ["symbol", "sector", "exchange"].includes(id)
993
- ? (id as ScreenField)
994
- : null;
995
- if (!field) return;
996
- if ((NUMERIC_FIELDS as readonly string[]).includes(field))
997
- setMetric(field as NumericField);
998
- apply({
999
- ...definition,
1000
- sort: {
1001
- field,
1002
- direction:
1003
- definition.sort.field === field &&
1004
- definition.sort.direction === "desc"
1005
- ? "asc"
1006
- : "desc",
1007
- },
1008
- });
1009
- }}
1010
- emptyStateTitle="No equities match this screen."
1011
- />
1012
- </PaneStatusBody>
994
+ sortDirection={definition.sort.direction}
995
+ onHeaderClick={(id) => {
996
+ const field = id.startsWith("m:")
997
+ ? (id.slice(2) as NumericField)
998
+ : id === "percentile" || id === "date"
999
+ ? metric
1000
+ : ["symbol", "sector", "exchange"].includes(id)
1001
+ ? (id as ScreenField)
1002
+ : null;
1003
+ if (!field) return;
1004
+ if ((NUMERIC_FIELDS as readonly string[]).includes(field))
1005
+ setMetric(field as NumericField);
1006
+ apply({
1007
+ ...definition,
1008
+ sort: {
1009
+ field,
1010
+ direction:
1011
+ definition.sort.field === field &&
1012
+ definition.sort.direction === "desc"
1013
+ ? "asc"
1014
+ : "desc",
1015
+ },
1016
+ });
1017
+ }}
1018
+ emptyStateTitle={currencyEmpty?.title ?? "No equities match this screen."}
1019
+ emptyStateHint={currencyEmpty ? `${currencyEmpty.detail} Choose ${currencyEmpty.base.currency} to screen in the base currency.` : undefined}
1020
+ />
1021
+ </PaneStatusBody>
1022
+ </>
1013
1023
  );
1014
1024
  return (
1015
1025
  <Box width={width} height={height} flexDirection="column">
@@ -45,10 +45,11 @@ export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
45
45
  instrument?.exchange ?? "",
46
46
  ctx.apiClient,
47
47
  ctx.marketData,
48
- ).catch((error: unknown) => {
49
- throw explainRefusal(error, estimateRevisionsLockNote, {
48
+ ).catch(async (error: unknown) => {
49
+ throw await explainRefusal(error, estimateRevisionsLockNote, {
50
50
  symbol: args.symbols[0] ?? null,
51
51
  token: ctx.settings?.tab === "guidance" ? "GUID" : "EM",
52
+ context: ctx,
52
53
  });
53
54
  });
54
55
  const periods = pinnedEstimatePeriods(
@@ -1,13 +1,37 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import { fetchCompanyData } from "../company-kpis/client";
3
+ import { filesForm } from "../shared/free-routes";
4
+ import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
5
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
6
+
7
+ /** GUIDE's free preview, where it holds guidance for the company: Apple issues none, so GUIDE has no AAPL rows. */
8
+ const GUIDE_ROUTE: FreeRoute = {
9
+ text: (ticker, company) => `gloomberb fn GUIDE ${ticker} lists ${company}'s latest guidance ranges, with the date each was issued.`,
10
+ answers: async (symbol, context) => {
11
+ const data = await fetchCompanyData("guidance", symbol, {}, context.apiClient, await resolveHeadlessIssuerListing(context, symbol), context.signal);
12
+ return "guidance" in data && data.guidance.length + data.history.length > 0;
13
+ },
14
+ };
15
+
16
+ /** The company's 8-Ks, where results and any guidance it gives are filed, for a company that files them. */
17
+ const EIGHT_K_ROUTE: FreeRoute = {
18
+ text: (ticker, company) => `gloomberb filings ${ticker} --form 8-K lists ${company}'s 8-Ks, where it files its results and any guidance, each with its items and a link to the filing.`,
19
+ answers: (symbol, context) => filesForm(symbol, "8-K", context),
20
+ };
21
+
22
+ const EARNINGS_ROUTE: FreeRoute = {
23
+ text: (ticker, company) => `gloomberb earnings ${ticker} lists ${company}'s next report date with its consensus EPS estimate.`,
24
+ answers: async (symbol, context) => (await context.marketData.getEarningsCalendar?.([symbol]) ?? []).length > 0,
25
+ };
3
26
 
4
27
  /**
5
28
  * EM, EEO and GUID read one Gloom Cloud route that asks for a signed-in account
6
29
  * with a verified email, and no plan. The report names the function it was run
7
- * for. GUIDE lists a company's latest guidance ranges, as a free preview;
8
- * `earnings` lists the next report date with its consensus EPS estimate.
30
+ * for. GUIDE lists a company's latest guidance ranges, as a free preview, and
31
+ * its 8-Ks hold what GUIDE does not cover; `earnings` lists the next report
32
+ * date with its consensus EPS estimate.
9
33
  */
10
- export function estimateRevisionsLockNote(lock: PaneLock): string {
34
+ export function estimateRevisionsLockNote(lock: PaneLock): Promise<string> {
11
35
  const guidance = lock.token === "GUID";
12
36
  return proLockNote(lock, {
13
37
  token: guidance ? "GUID" : "EM",
@@ -16,8 +40,6 @@ export function estimateRevisionsLockNote(lock: PaneLock): string {
16
40
  ? "it reads the EPS guidance a company issued, cited from its filings and calls, against consensus"
17
41
  : "it reads consensus EPS estimates by fiscal period, how they were revised, the surprises against reported EPS and the guidance",
18
42
  shows: "it",
19
- free: guidance
20
- ? (ticker, company) => `gloomberb fn GUIDE ${ticker} lists ${company}'s latest guidance ranges, with the date each was issued.`
21
- : (ticker, company) => `gloomberb earnings ${ticker} lists ${company}'s next report date with its consensus EPS estimate.`,
43
+ free: guidance ? [GUIDE_ROUTE, EIGHT_K_ROUTE] : [EARNINGS_ROUTE],
22
44
  });
23
45
  }
@@ -21,7 +21,10 @@ export function validateExposure(value: ExposurePayload): ExposurePayload {
21
21
  && typeof data.evaluatedAt === "string" && Number.isFinite(Date.parse(data.evaluatedAt))
22
22
  && Number.isInteger(data.requestedHoldings) && Number.isInteger(data.lockedHoldings) && data.lockedHoldings >= 0
23
23
  && Array.isArray(data.holdings) && strings(data.unknowns) && strings(data.methodology);
24
- try { parseCustomScenario(JSON.stringify(data.scenario)); } catch { valid = false; }
24
+ try { parseCustomScenario(JSON.stringify(data.scenario)); } catch {
25
+ // Intentional ignore: invalid scenarios use the payload validation error below.
26
+ valid = false;
27
+ }
25
28
  valid &&= data.holdings.every(h => typeof h.symbol === "string" && finite(h.weight) && Array.isArray(h.unknowns)
26
29
  && ["quantified", "partial", "unknown"].includes(h.status) && !!h.coverage
27
30
  && Array.isArray(h.measures) && h.measures.every(m => validRange(m.exposurePct) && nullableRange(m.impactPct) && strings(m.componentIds))
@@ -7,13 +7,13 @@ import type { ExposureScenario } from "../../../api-client/exposure";
7
7
  import { parseCustomScenario } from "./model";
8
8
 
9
9
  export interface ExposureInputs { input: string; source: string; nav: string; cash: string; depth: number; scenario: ExposureScenario; }
10
- export function ExposureEditor({ initial, sources, focused, width, onSave, onCancel }: {
11
- initial: ExposureInputs; sources: { value: string; label: string }[]; focused: boolean; width: number;
10
+ export function ExposureEditor({ initial, initialCustom, sources, focused, width, onSave, onCancel }: {
11
+ initial: ExposureInputs; initialCustom?: string; sources: { value: string; label: string }[]; focused: boolean; width: number;
12
12
  onSave: (value: ExposureInputs) => void; onCancel: () => void;
13
13
  }) {
14
14
  const [draft, setDraft] = useState(initial);
15
15
  const first = initial.scenario.shocks[0]!;
16
- const [custom, setCustom] = useState(initial.scenario.shocks.length > 1 || !!first.transmission ? JSON.stringify(initial.scenario) : "");
16
+ const [custom, setCustom] = useState(initialCustom ?? (initial.scenario.shocks.length > 1 || !!first.transmission ? JSON.stringify(initial.scenario) : ""));
17
17
  const [label, setLabel] = useState(initial.scenario.label);
18
18
  const [kind, setKind] = useState(first.kind);
19
19
  const [target, setTarget] = useState(first.target);
@@ -26,7 +26,10 @@ export const exposureScreenshotEvidence: PaneScreenshotEvidenceHook<ExposureCapt
26
26
  const rows = metadata.tab === "drivers" ? driverRows(payload) : metadata.tab === "paths" ? pathRows(payload) : metadata.tab === "portfolio" ? portfolioRows(payload, String(metadata.view)) : tableRows(payload);
27
27
  if (metadata.plottedValueCount !== rows.length || metadata.rowIds.length !== rows.length || metadata.rowIds.some(id => !rows.some(r => r.id === id))) return null;
28
28
  return metadata as unknown as ExposureCapture;
29
- } catch { return null; }
29
+ } catch {
30
+ // Intentional ignore: malformed capture metadata cannot prove the screenshot's exposure data.
31
+ return null;
32
+ }
30
33
  },
31
34
  mismatches(evidence, { resolved, payload }) {
32
35
  const snapshot = payload.config.layout.instances.find(i => i.instanceId === payload.paneId)?.settings?.exposureSnapshot;
@@ -4,6 +4,7 @@ import type { Portfolio, TickerRecord } from "../../../types/ticker";
4
4
  import { publicTickerKey } from "../../../utils/exchanges";
5
5
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
6
6
  import { convertCurrency } from "../../../utils/format";
7
+ import { logFailure } from "../../../utils/log-failure";
7
8
  import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, resolvePortfolioMarketValue } from "../../../portfolio/position-metrics";
8
9
 
9
10
  /** Explicit NAV preserves cash, shorts and leverage; net holdings are never used as NAV. */
@@ -35,7 +36,8 @@ export async function portfolioHoldings(portfolio: Portfolio, tickers: TickerRec
35
36
  for (const currency of currencies) await ensureRate(currency);
36
37
  const holdings: ExposureHolding[] = [];
37
38
  for (const ticker of positions) {
38
- const quote = await market.getQuote(ticker.metadata.ticker, ticker.metadata.exchange).catch(() => null);
39
+ const quote = await market.getQuote(ticker.metadata.ticker, ticker.metadata.exchange)
40
+ .catch(logFailure("exposure.holdings", "Load portfolio holding quote failed", null));
39
41
  const currency = quote?.currency || ticker.metadata.currency;
40
42
  if (!rates.has(currency)) await ensureRate(currency);
41
43
  const metrics = getPortfolioPositionMetrics(ticker, portfolio.id, currency, {