gloomberb 0.18.0 → 0.18.2

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 (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -39,6 +39,7 @@ import { ResearchTabKeysProvider, useResearchTabKeysHost } from "./research-tab-
39
39
  import { ResearchTabNavigationProvider } from "./research-tab-navigation";
40
40
  import { usePluginAppActions } from "../../../public/react";
41
41
  import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
42
+ import { ResearchTradeWorkspace } from "../broker-trading/dock";
42
43
 
43
44
  const TICKER_RESEARCH_TAB_COMMIT_DELAY_MS = 120;
44
45
  /** A tab counts as viewed once it stays open this long, not when h/l passes over it. */
@@ -207,7 +208,9 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
207
208
  const quoteFooterActive = resolvedTabId === "overview" || resolvedTabId === "chart";
208
209
  const cloudAccess = useCloudAccessFooter({
209
210
  delayLabel: tf("{count}m", { count: CLOUD_QUOTE_DELAY_MINUTES }),
210
- degraded: financials?.quote?.priceObservation !== "nav" && financials?.quote?.dataSource !== "live",
211
+ // A listing that no longer trades has no delay to speak of: its quote is its last session's close.
212
+ degraded: !financials?.quote?.delisting && financials?.quote?.priceObservation !== "nav"
213
+ && financials?.quote?.dataSource !== "live",
211
214
  focused: focused && quoteFooterActive,
212
215
  segmentId: "ticker-research-access",
213
216
  shortcutScope: "ticker-research:upgrade",
@@ -359,16 +362,14 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
359
362
  );
360
363
  }
361
364
 
362
- return (
363
- <Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
364
- {tabStrip}
365
-
366
- <Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
367
- <ResearchTabKeysProvider value={showTabs ? researchTabKeys.value : null}>
365
+ const tradeAvailable = visibleTabIds.has("broker-trade");
366
+ // Trade shows Chart beside its ticket: the workspace names the tab it shows.
367
+ const renderTabs = (contentWidth: number, contentFocused: boolean, shownTabId = resolvedTabId) => (
368
+ <ResearchTabKeysProvider value={showTabs ? researchTabKeys.value : null}>
368
369
  {tickerResearchTabs.map((tab) => {
369
- if (!renderedTabIds.has(tab.id) || !visibleTabIds.has(tab.id)) return null;
370
+ if (tradeAvailable && tab.id === "broker-trade" || !(renderedTabIds.has(tab.id) || tab.id === shownTabId) || !visibleTabIds.has(tab.id)) return null;
370
371
  const TickerResearchTab = tab.component;
371
- const isActive = resolvedTabId === tab.id;
372
+ const isActive = shownTabId === tab.id;
372
373
  return (
373
374
  <Box
374
375
  key={tab.id}
@@ -383,9 +384,9 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
383
384
  <NestedPaneTabs>
384
385
  <ResearchTabNavigationProvider value={showTabs ? openResearchTab : null}>
385
386
  <TickerResearchTab
386
- width={width}
387
+ width={contentWidth}
387
388
  height={contentHeight}
388
- focused={focused && isActive}
389
+ focused={contentFocused && isActive}
389
390
  onCapture={isActive ? handlePluginCapture : ignorePluginCapture}
390
391
  />
391
392
  </ResearchTabNavigationProvider>
@@ -394,8 +395,14 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
394
395
  </Box>
395
396
  );
396
397
  })}
397
- </ResearchTabKeysProvider>
398
- </Box>
399
- </Box>
398
+ </ResearchTabKeysProvider>
400
399
  );
400
+ return <Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
401
+ {tabStrip}
402
+ <Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
403
+ {tradeAvailable ? <ResearchTradeWorkspace width={width} height={contentHeight} focused={focused} symbol={ticker.metadata.ticker}
404
+ tabId={resolvedTabId} selectTab={selectResearchTab} onCapture={handlePluginCapture} locked={!!paneSettings.hideTabs}>{renderTabs}</ResearchTradeWorkspace>
405
+ : renderTabs(width, focused)}
406
+ </Box>
407
+ </Box>;
401
408
  }
@@ -2,6 +2,7 @@ import type { PaneFooterSegment } from "../../../components/layout/pane/footer";
2
2
  import { t } from "../../../i18n";
3
3
  import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
4
4
  import { formatQuoteNavAsOf } from "../../../market-data/quotes/time";
5
+ import { delistingLabel } from "../../../market-data/quotes/delisting";
5
6
  import type { Quote } from "../../../types/financials";
6
7
  import { displayWidth } from "../../../utils/format";
7
8
 
@@ -12,7 +13,10 @@ export function tickerQuoteFooterInfo(
12
13
  ): PaneFooterSegment[] {
13
14
  const info: PaneFooterSegment[] = [];
14
15
  const navAsOf = formatQuoteNavAsOf(quote);
15
- if (isQuoteStaleForCurrentSession(quote)) {
16
+ if (quote?.delisting) {
17
+ // A listing that no longer trades: its quote is its last trade, said once with its date.
18
+ info.push({ id: "ticker-research-stale", parts: [{ text: delistingLabel(quote.delisting), tone: "warning" }] });
19
+ } else if (isQuoteStaleForCurrentSession(quote)) {
16
20
  const status: PaneFooterSegment = { id: "ticker-research-stale", parts: [{ text: t("Stale quote"), tone: "warning" }] };
17
21
  const timestamp = new Date(quote!.lastUpdated);
18
22
  if (navAsOf) {
@@ -20,6 +20,7 @@ import {
20
20
  } from "../../../../market-data/market/status";
21
21
  import { t } from "../../../../i18n";
22
22
  import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
23
+ import { delistingLabel, delistingStateLabel } from "../../../../market-data/quotes/delisting";
23
24
  import { formatQuoteNavAsOf } from "../../../../market-data/quotes/time";
24
25
  import { useQuoteFlashDirection } from "../../../../components/quote-flash";
25
26
  import { followLiveSparklinePrice } from "../../../../components/price-sparkline/model";
@@ -89,6 +90,8 @@ interface QuoteStatus {
89
90
  failed: boolean;
90
91
  text: string;
91
92
  stale?: boolean;
93
+ /** DELISTED or INACTIVE: the listing no longer trades, and `text` says since when. */
94
+ ended?: string;
92
95
  }
93
96
 
94
97
  function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quote: Quote | null | undefined): QuoteStatus {
@@ -98,6 +101,7 @@ function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quo
98
101
  ? { failed: true, text: `${symbol} not recognized` }
99
102
  : { failed: true, text: error.message || "Quote unavailable" };
100
103
  }
104
+ if (quote?.delisting) return { failed: true, text: delistingLabel(quote.delisting), stale: true, ended: delistingStateLabel(quote.delisting) };
101
105
  if (isQuoteStaleForCurrentSession(quote)) return { failed: true, text: "Stale quote", stale: true };
102
106
  if (entry?.phase === "ready") return { failed: false, text: "No quote data" };
103
107
  return { failed: false, text: "Loading quote..." };
@@ -175,8 +179,9 @@ export function QuoteMonitorCard({
175
179
  // One decimal count per instrument, so streamed ticks never narrow or widen the price column.
176
180
  const priceOptions = liveQuoteFormatOptions(quote, currency, assetCategory, cachedFinancials?.quoteMetadata?.instrumentType);
177
181
  const priceText = display ? formatMarketPriceWithCurrency(display.price, currency, priceOptions) : "";
178
- const changePercentText = display ? formatPercentRaw(display.changePercent) : "";
179
- const changeValueText = display ? formatSignedMarketPrice(display.change, priceOptions) : "";
182
+ // A listing that no longer trades shows its last close alone: no move, not even a dash for one.
183
+ const changePercentText = display && !quote?.delisting ? formatPercentRaw(display.changePercent) : "";
184
+ const changeValueText = display && !quote?.delisting ? formatSignedMarketPrice(display.change, priceOptions) : "";
180
185
  const priceColumnWidth = Math.max(priceText.length, changePercentText.length + changeValueText.length + 1);
181
186
  const nameMaxWidth = Math.max(10, width - priceColumnWidth - (nativePaneChrome ? 5 : 3));
182
187
  // The terminal sets the line beside the change, under the symbol (or after it in a
@@ -187,6 +192,8 @@ export function QuoteMonitorCard({
187
192
  const extendedParts = display && extended
188
193
  ? extendedLineParts(extended, formatMarketPriceWithCurrency(extended.price, currency, priceOptions), (text) => displayWidth(text) <= extendedRoom)
189
194
  : null;
195
+ // A desktop card says a listing no longer trades on a row of its own.
196
+ const endedRow = nativePaneChrome && quoteFailed && !!quoteStatus.ended && !extendedParts;
190
197
  const extendedColor = quoteFailed ? colors.textDim : priceColor(extended?.change ?? 0);
191
198
  const extendedDim = !!flashDirection && headlineHolds;
192
199
  const sparklineRange = resolvePriceSparklineRange(priceHistory, chartPeriod);
@@ -254,7 +261,7 @@ export function QuoteMonitorCard({
254
261
  <Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
255
262
  {symbol}
256
263
  </Text>
257
- <Text fg={quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
264
+ <Text fg={quoteStatus.ended ? colors.warning : quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
258
265
  </Box>
259
266
  ) : nativePaneChrome ? (
260
267
  <Box
@@ -276,7 +283,7 @@ export function QuoteMonitorCard({
276
283
  minWidth={0}
277
284
  style={{
278
285
  gridColumn: "1",
279
- gridRow: extendedParts ? "1" : "1 / span 2",
286
+ gridRow: extendedParts || endedRow ? "1" : "1 / span 2",
280
287
  minWidth: 0,
281
288
  overflow: "hidden",
282
289
  }}
@@ -284,8 +291,8 @@ export function QuoteMonitorCard({
284
291
  <Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
285
292
  {symbol}
286
293
  </Text>
287
- {quoteFailed && (
288
- <Text fg={colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
294
+ {quoteFailed && !endedRow && (
295
+ <Text fg={quoteStatus.ended ? colors.warning : colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
289
296
  {quoteStatus.text}
290
297
  </Text>
291
298
  )}
@@ -332,6 +339,28 @@ export function QuoteMonitorCard({
332
339
  </Box>
333
340
  </Box>
334
341
 
342
+ {endedRow && (
343
+ // Under the price, across the card, so the date is never cut on a narrow card;
344
+ // backed like the price so the sparkline does not run through it.
345
+ <Text
346
+ fg={colors.warning}
347
+ style={{
348
+ gridColumn: "1 / -1",
349
+ gridRow: "2",
350
+ justifySelf: "start",
351
+ alignSelf: "start",
352
+ fontSize: "var(--gloom-type-ui)",
353
+ lineHeight: "14px",
354
+ whiteSpace: "nowrap",
355
+ backgroundColor: colors.bg,
356
+ borderRadius: "var(--gloom-radius-sm)",
357
+ boxShadow: `0 0 0 2px ${colors.bg}`,
358
+ }}
359
+ >
360
+ {quoteStatus.text}
361
+ </Text>
362
+ )}
363
+
335
364
  {extendedParts && (
336
365
  <ExtendedSessionLine
337
366
  parts={extendedParts}
@@ -385,7 +414,7 @@ export function QuoteMonitorCard({
385
414
  </Box>
386
415
  <Box flexDirection="column">
387
416
  <FigureText fg={changeColor} dim={flashing} style={figureHalo}>
388
- {compactQuoteFailure ? `${priceText} · ${quoteStatus.stale ? "STALE" : "ERROR"}` : priceText}
417
+ {compactQuoteFailure ? `${priceText} · ${quoteStatus.ended ?? (quoteStatus.stale ? "STALE" : "ERROR")}` : priceText}
389
418
  </FigureText>
390
419
  <Box flexDirection="row" gap={1}>
391
420
  <FigureText part="sub" fg={changeColor} dim={flashing} style={figureHalo}>{changePercentText}</FigureText>
@@ -432,7 +461,7 @@ export function QuoteMonitorCard({
432
461
  )}
433
462
 
434
463
  {quoteFailed && !compactQuoteFailure && (
435
- <Box height={1} overflow="hidden"><Text fg={colors.negative}>{quoteStatus.text}</Text></Box>
464
+ <Box height={1} overflow="hidden"><Text fg={quoteStatus.ended ? colors.warning : colors.negative}>{quoteStatus.text}</Text></Box>
436
465
  )}
437
466
  {navAsOf && !compactNavAsOf && (
438
467
  <Box height={1} overflow="hidden"><Text fg={colors.textDim}>{navAsOf}</Text></Box>
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box } from "../../../ui";
3
3
  import { usePaneSettingValue, useShortcut } from "../../../public/react";
4
- import { DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
4
+ import { splitDelayedStatus, DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
5
5
  import { useThemeColors } from "../../../theme/theme-context";
6
6
  import type { PaneProps } from "../../../types/plugin";
7
7
  import type { TapeQuote, TapeSnapshot, TapeTrade } from "../../../api-client/tape";
@@ -109,7 +109,7 @@ function TimeSalesView({ width, height, focused, symbol, exchange }: PaneProps &
109
109
  usePaneNoticeFooter({ registrationId: "time-sales:notices", focused, notices: [...(data?.gaps ?? []), ...(resource.transport ? [resource.transport] : []),
110
110
  ...(pinnedBeforeTape ? [`The pinned moment is before this tape's first ${tab === "quotes" ? "quote" : "print"}, ${tapeStampNewYork(rowTime(rows.at(-1)!))}.`] : [])] });
111
111
  usePaneStatusFooter({ registrationId: "time-sales:status", loading: resource.loading, error: resource.error,
112
- info: data ? [{ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] },
112
+ info: data ? [...splitDelayedStatus({ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] }),
113
113
  ...(frozen ? [{ id: "paused", parts: [{ text: "paused", tone: "warning" as const }] }] : []),
114
114
  ...(!data.connected || resource.snapshotOnly ? [{ id: "snapshot", parts: [{ text: "snapshot", tone: "warning" as const }] }] : [])] : [] });
115
115
  const selectTab = (value: string) => { setTab(value); setDetail(null); setSelected(null); };
@@ -4,6 +4,7 @@ import { signalWithTimeout } from "../../../utils/async-deadline";
4
4
  import { parsePublicTickerKey } from "../../../utils/exchanges";
5
5
  import { httpFetch } from "../../../utils/http-transport";
6
6
  import { isoDateOrNull, numericOrNull, recordOrNull, textOrNull } from "../../../utils/guards";
7
+ import { logFailure } from "../../../utils/log-failure";
7
8
  import { zonedDateKey } from "../../../utils/zoned-date-time";
8
9
 
9
10
  const ENDPOINT = "https://services-dynarep.ddwa.finra.org/public/reporting/v2/data/group/FixedIncomeMarket/name/corporateAndAgencySecurities";
@@ -226,6 +227,7 @@ function dataRows(data: unknown): unknown[] | null {
226
227
  const parsed = JSON.parse(data) as unknown;
227
228
  return Array.isArray(parsed) ? parsed : null;
228
229
  } catch {
230
+ // Intentional ignore: malformed nested rows make postBondPage reject the response shape.
229
231
  return null;
230
232
  }
231
233
  }
@@ -340,7 +342,9 @@ async function readPayload(response: Response): Promise<unknown> {
340
342
  async function openSession(signal: AbortSignal): Promise<string> {
341
343
  const session = await httpFetch(ENDPOINT, { headers: REQUEST_HEADERS, signal });
342
344
  const token = xsrfToken(session);
343
- await session.body?.cancel().catch(() => {});
345
+ await session.body?.cancel().catch(() => {
346
+ // Intentional ignore: only session headers are needed; discarding its unused body is best effort.
347
+ });
344
348
  if (!token) throw new Error(failureText(session.status, "session"));
345
349
  return token;
346
350
  }
@@ -477,7 +481,14 @@ export async function fetchBondTape(input: string, options: BondTapeOptions = {}
477
481
  let answer = first;
478
482
  let issuer: BondIssuer | null = null;
479
483
  if (first.bonds.length === 0 && query?.kind === "issuer" && query.ticker && options.companyName) {
480
- const company = await options.companyName(query.ticker).catch(() => null);
484
+ const company = await options.companyName(query.ticker).catch((error: unknown) => {
485
+ options.signal?.throwIfAborted();
486
+ if (error instanceof Error && error.name === "AbortError") throw error;
487
+ logFailure("trace-bonds.client", "Resolve bond issuer company failed")(error);
488
+ answer = { ...first, failures: [...first.failures, "Issuer-name lookup failed."] };
489
+ return null;
490
+ });
491
+ options.signal?.throwIfAborted();
481
492
  const name = company ? tapeIssuerName(company) : null;
482
493
  if (company && name && name !== query.name) {
483
494
  issuer = { company, name };
@@ -492,7 +503,7 @@ export async function fetchBondTape(input: string, options: BondTapeOptions = {}
492
503
  // No prints for the query: date the answer by the market's newest sale.
493
504
  const market = await postBondPage(token, [TRADED], 1, deadline)
494
505
  .then((payload) => latestTradeDate(mergeBondAnswers([{ payload, accepts: anyBond, limit: 1 }]).bonds))
495
- .catch(() => null);
506
+ .catch(logFailure("trace-bonds.client", "Load bond market date failed", null));
496
507
  return { ...result, tradeDate: market };
497
508
  }
498
509
 
@@ -89,7 +89,7 @@ export const traceBondsHeadless: HeadlessPaneDefinition<"bundle"> = {
89
89
  rows: bonds.map(reportRow),
90
90
  ...(query ? { empty: noPrintsText(query, tradeDate, issuer) } : {}),
91
91
  }],
92
- complete: true,
92
+ complete: failures.length === 0,
93
93
  ...(failures.length > 0 ? { errors: failures } : {}),
94
94
  ...(truncated ? { notes: [TRUNCATED_TEXT] } : {}),
95
95
  ...(shown ? { warnings: [issuerFallbackText(query!, issuer)] } : {}),
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { DataTableView, EmptyState, PaneStatusBody, QueryBar, StatGrid, statGridRows, usePaneFooter, usePaneMenuItems,
2
+ import { splitDelayedStatus, DataTableView, EmptyState, PaneStatusBody, QueryBar, StatGrid, statGridRows, usePaneFooter, usePaneMenuItems,
3
3
  usePaneNoticeFooter, usePaneTabs, usePaneTicker, type DataTableColumn, type DataTableKeyEvent, type StatItem } from "../../../components";
4
4
  import { useTableLoadMore } from "../../../components/table-view-shared";
5
5
  import { useStaticChartBitmapSize } from "../../../components/chart/composite/bitmap";
@@ -360,7 +360,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
360
360
  info: [
361
361
  ...(active.loading ? [{ id: "loading", parts: [{ text: snapshot && snapshot.loaded < snapshot.requested
362
362
  ? `loading ${snapshot.loaded}/${snapshot.requested} expiries` : "loading", tone: "muted" as const }] }] : []),
363
- ...(freshness ? [{ id: "source", parts: [{ text: freshness, tone: "muted" as const }] }] : []),
363
+ ...(freshness ? splitDelayedStatus({ id: "source", parts: [{ text: freshness, tone: "muted" }] }) : []),
364
364
  ...(snapshot?.expiries.some((entry) => entry.stale) ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
365
365
  ...(selectedExpiry?.fit && activeTab === "smile" ? [{ id: "fit", parts: [{ text: `${selectedExpiry.fit.method} · RMSE ${(selectedExpiry.fit.residual * 100).toFixed(3)} vol pts`, tone: "muted" as const }] }] : []),
366
366
  ...(selectionStatus ? [{ id: "selection", parts: [{ text: selectionStatus, tone: "muted" as const }] }] : []),
@@ -31,7 +31,7 @@ export const BUILTIN_EDITORIAL: Readonly<Record<string, BuiltinEditorial>> = {
31
31
  tagline: "Account, chat, Ask Gloom and Cloud data.",
32
32
  icon: "plugin-icons/gloomberb-cloud.webp",
33
33
  },
34
- alerts: { categories: ["alerts"], tagline: "Price, market and filing alerts." },
34
+ alerts: { categories: ["alerts"], tagline: "Price, earnings, filing, news and flow alerts." },
35
35
  "alt-data": {
36
36
  categories: ["research", "data"],
37
37
  tagline: "Supply chains, hiring, apps, GPUs and power.",
@@ -68,6 +68,8 @@ export interface PluginManifest {
68
68
  id: string | null;
69
69
  /** The oldest Gloomberb the checkout says it runs on. */
70
70
  minGloomberb: string | null;
71
+ /** gloom.json `icon`: the plugin's logo, a path inside the checkout such as `icon.webp`. */
72
+ icon: string | null;
71
73
  }
72
74
 
73
75
  function readJson(path: string): Record<string, unknown> | null {
@@ -106,6 +108,7 @@ export function readPluginManifest(pluginDir: string): PluginManifest {
106
108
  minGloomberb: versionField(gloom?.minGloom)
107
109
  ?? versionField(gloom?.minGloomberb)
108
110
  ?? peerRangeMinimum(peers?.gloomberb),
111
+ icon: typeof gloom?.icon === "string" && gloom.icon.trim() ? gloom.icon.trim() : null,
109
112
  };
110
113
  }
111
114
 
@@ -24,6 +24,11 @@ export function listExternalPlugins(): readonly LoadedExternalPlugin[] {
24
24
  return entries;
25
25
  }
26
26
 
27
+ /** The logo of a plugin loaded here, as a `data:` URL; null for a built-in or a plugin without one. */
28
+ export function externalPluginIcon(pluginId: string): string | null {
29
+ return entries.find((entry) => entry.plugin.id === pluginId)?.icon ?? null;
30
+ }
31
+
27
32
  /** Replaces the entry for the same plugin id or directory, or appends. */
28
33
  export function upsertExternalPlugin(entry: LoadedExternalPlugin): void {
29
34
  const index = entries.findIndex((existing) => (
@@ -33,6 +33,12 @@ export interface LoadedExternalPlugin {
33
33
  repo?: string;
34
34
  /** A symlink to a local checkout (`gloomberb plugin link`) rather than a clone. */
35
35
  linked?: boolean;
36
+ /**
37
+ * The plugin's logo as a `data:` URL (plugin-icon.ts), for the desktop and
38
+ * the web to draw beside what it contributes, such as its broker. The
39
+ * terminal draws no images and leaves it unread.
40
+ */
41
+ icon?: string;
36
42
  error?: string;
37
43
  /**
38
44
  * The Gloomberb the checkout declares it needs, when this one is older. The
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "fs";
2
+ import { basename, extname, isAbsolute, relative, resolve, sep } from "path";
3
+ import { logFailure } from "../utils/log-failure";
4
+ import { readPluginManifest } from "./compat";
5
+
6
+ /**
7
+ * A plugin's logo as the desktop and the web draw it: the file its gloom.json
8
+ * names as `icon` (PLUGINS.md), or the `icon.svg`, `icon.webp` or `icon.png`
9
+ * a checkout without one ships beside its entry, read into a `data:` URL. The
10
+ * view has no filesystem, so the Bun process that reads the plugins folder
11
+ * hands the image over with the plugin's bundle.
12
+ */
13
+
14
+ const ICON_TYPES: Readonly<Record<string, string>> = {
15
+ ".svg": "image/svg+xml",
16
+ ".webp": "image/webp",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".jpeg": "image/jpeg",
20
+ };
21
+
22
+ const CONVENTIONAL_ICONS = ["icon.svg", "icon.webp", "icon.png"];
23
+
24
+ /** A logo is drawn at most a few dozen pixels wide; anything larger is not one. */
25
+ const MAX_ICON_BYTES = 256 * 1024;
26
+
27
+ /**
28
+ * The icon `name` names inside `pluginDir`, typed by the name the plugin gave
29
+ * it, or null for a path that leaves the checkout or is not a small image.
30
+ */
31
+ function iconFile(pluginDir: string, name: string): { path: string; type: string } | null {
32
+ const type = ICON_TYPES[extname(name).toLowerCase()];
33
+ if (!type) return null;
34
+ const root = realpathSync(pluginDir);
35
+ const path = resolve(root, name);
36
+ if (!existsSync(path)) return null;
37
+ // A link that points out of the checkout is not the plugin's to show.
38
+ const real = realpathSync(path);
39
+ const inside = relative(root, real);
40
+ if (!inside || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside)) return null;
41
+ const info = statSync(real);
42
+ return info.isFile() && info.size > 0 && info.size <= MAX_ICON_BYTES ? { path: real, type } : null;
43
+ }
44
+
45
+ /**
46
+ * The plugin's logo as a `data:` URL. A declared icon that is missing,
47
+ * unusable or unreadable falls back to the conventional names, each tried on
48
+ * its own, so one file the process may not read does not hide the next.
49
+ */
50
+ export function readPluginIcon(pluginDir: string): string | null {
51
+ const declared = readPluginManifest(pluginDir).icon;
52
+ for (const name of declared ? [declared, ...CONVENTIONAL_ICONS] : CONVENTIONAL_ICONS) {
53
+ try {
54
+ const icon = iconFile(pluginDir, name);
55
+ if (icon) return `data:${icon.type};base64,${readFileSync(icon.path).toString("base64")}`;
56
+ } catch (error) {
57
+ logFailure("plugin-icon", `Reading ${name} for ${basename(pluginDir)} failed`)(error);
58
+ }
59
+ }
60
+ return null;
61
+ }
@@ -9,6 +9,7 @@ import type { NewsQuery, NewsQueryState } from "../../types/news-source";
9
9
  import type {
10
10
  AppNotificationDelivery,
11
11
  AppNotificationRequest,
12
+ BrokerInstanceCreateOptions,
12
13
  BrokerInstanceUpdateOptions,
13
14
  PaneSettingField,
14
15
  PaneTemplateCreateOptions,
@@ -27,7 +28,7 @@ export interface PluginHostActions {
27
28
  getTicker(symbol: string): TickerRecord | null;
28
29
  getData(symbol: string): TickerFinancials | null;
29
30
  getConfig(): AppConfig;
30
- createBrokerInstance(brokerType: string, label: string, values: Record<string, unknown>): Promise<BrokerInstanceConfig>;
31
+ createBrokerInstance(brokerType: string, label: string, values: Record<string, unknown>, options?: BrokerInstanceCreateOptions): Promise<BrokerInstanceConfig>;
31
32
  connectBrokerInstance(instanceId: string): Promise<void>;
32
33
  updateBrokerInstance(instanceId: string, values: Record<string, unknown>, options?: BrokerInstanceUpdateOptions): Promise<void>;
33
34
  syncBrokerInstance(instanceId: string): Promise<void>;
@@ -45,6 +45,7 @@ import type {
45
45
  import { debugLog } from "../../utils/debug-log";
46
46
  import { logFailure } from "../../utils/log-failure";
47
47
  import { EventBus, type HostEvents } from "../event-bus";
48
+ import { externalPluginIcon } from "../external-runtime";
48
49
  import { isReservedBuiltinPluginId } from "../ownership";
49
50
  import { createPluginPersistence } from "../plugin-persistence";
50
51
  import { createPluginTeamState } from "../team-state";
@@ -532,6 +533,12 @@ export class PluginRegistry implements PluginRuntimeAccess {
532
533
  return this.contributions.brokersMap.owners.get(brokerType);
533
534
  }
534
535
 
536
+ /** The logo of the plugin that registered `brokerType` (its gloom.json `icon`), for lists of brokers. */
537
+ getBrokerLogo = (brokerType: string): string | null => {
538
+ const owner = this.getBrokerPluginId(brokerType);
539
+ return owner ? externalPluginIcon(owner) : null;
540
+ };
541
+
535
542
  getShortcutPluginId(shortcutId: string): string | undefined {
536
543
  return this.contributions.shortcutsMap.owners.get(shortcutId);
537
544
  }
@@ -619,6 +626,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
619
626
  getData: (ticker) => this.getData(ticker),
620
627
  getTicker: (symbol) => this.getTicker(symbol),
621
628
  getConfig: () => this.getConfig(),
629
+ getBrokerAdapter: this.getBrokerAdapter,
622
630
  getPaneDef: (paneId) => contributions.panesMap.get(paneId),
623
631
  marketData: this.marketData,
624
632
  connectionHealth: this.connectionHealth,
@@ -630,7 +638,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
630
638
  updateLayout: (layout) => this.updateLayout(layout),
631
639
  resolvePaneTarget: (paneId) => this.resolvePaneTarget(paneId),
632
640
  }),
633
- createBrokerInstance: (brokerType, label, values) => this.createBrokerInstance(brokerType, label, values),
641
+ createBrokerInstance: (brokerType, label, values, options) => this.createBrokerInstance(brokerType, label, values, options),
634
642
  updateBrokerInstance: this.updateBrokerInstance,
635
643
  syncBrokerInstance: this.syncBrokerInstance,
636
644
  removeBrokerInstance: this.removeBrokerInstance,
@@ -18,6 +18,11 @@ export interface PluginRuntimeAccess extends CapabilityInvoker, PluginRuntimeHos
18
18
  getBrokerAdapter(brokerType: string): BrokerAdapter | null;
19
19
  /** Every broker adapter installed now, read when called: plugins come and go. */
20
20
  listBrokerAdapters(): BrokerAdapter[];
21
+ /**
22
+ * The logo of the plugin that registered a broker adapter, as an image URL
23
+ * for the desktop and the web; null when it has none.
24
+ */
25
+ getBrokerLogo?(brokerType: string): string | null;
21
26
  subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void;
22
27
  getResumeState<T = unknown>(pluginId: string, key: string, schemaVersion?: number): T | null;
23
28
  setResumeState(pluginId: string, key: string, value: unknown, schemaVersion?: number): void;
@@ -78,11 +78,14 @@ export function useCapabilityInvoker(): CapabilityInvoker {
78
78
  return usePluginRenderContext().runtime;
79
79
  }
80
80
 
81
+ const noBrokerLogo = (): string | null => null;
82
+
81
83
  export function usePluginBrokerActions() {
82
84
  const { runtime } = usePluginRenderContext();
83
85
  return {
84
86
  getBrokerAdapter: runtime.getBrokerAdapter,
85
87
  listBrokerAdapters: runtime.listBrokerAdapters,
88
+ getBrokerLogo: runtime.getBrokerLogo ?? noBrokerLogo,
86
89
  createBrokerInstance: runtime.createBrokerInstance,
87
90
  connectBrokerInstance: runtime.connectBrokerInstance,
88
91
  updateBrokerInstance: runtime.updateBrokerInstance,
@@ -8,6 +8,7 @@ import type { EarningsEvent } from "../types/data-provider";
8
8
  import type { TickerRecord } from "../types/ticker";
9
9
  import { colors, priceColor } from "../theme/colors";
10
10
  import { formatQuoteAgeWithSource, resolveQuoteAgeTimestamp } from "../market-data/quotes/time";
11
+ import { delistingStateLabel } from "../market-data/quotes/delisting";
11
12
  import { formatFeedTime, formatShortDate, parseDisplayDate, type ShortDateOptions } from "../utils/datetime-format";
12
13
  import { convertCurrency, formatCompact, formatCompactAmount, formatNumber, formatPercentRaw } from "../utils/format";
13
14
  import {
@@ -40,6 +41,7 @@ import { currentListingName } from "../tickers/listing-name";
40
41
  import { PRICE_SPARKLINE_COLUMN_ID } from "../components/price-sparkline/view";
41
42
  import { followLiveSparklinePrice, resolveSparklineHistory, sparklineValues } from "../components/price-sparkline/model";
42
43
  import {
44
+ analystTargetDisplayCurrency,
43
45
  listingDividendYield,
44
46
  liveDividendYield,
45
47
  liveFiftyTwoWeekRange,
@@ -67,6 +69,7 @@ import {
67
69
  } from "./valuation-basis";
68
70
  import { futuresQuantityText, futuresSettlementDisplay, type PortfolioFuturesTerms } from "./futures-positions";
69
71
  import { isFiniteNumber } from "../utils/guards";
72
+ import { currencyCodeLabel } from "../utils/currency-units";
70
73
  import {
71
74
  allocationFigures,
72
75
  formatAllocationDrift,
@@ -453,7 +456,7 @@ export function getColumnValue(
453
456
  ),
454
457
  };
455
458
  case "currency":
456
- return { text: compactText(listedCurrency(positionCurrency, quoteCurrency, ticker, quote)) };
459
+ return { text: compactText(currencyCodeLabel(listedCurrency(positionCurrency, quoteCurrency, ticker, quote))) };
457
460
  case "sector":
458
461
  return { text: compactText(ticker.metadata.sector || financials?.profile?.sector) };
459
462
  case "industry":
@@ -464,6 +467,8 @@ export function getColumnValue(
464
467
  return resolvePortfolioPriceValue(headlineQuote, brokerMarkPrice, headlineQuote ? currentQuoteOptions : markOptions, col.width, quote?.marketState);
465
468
  case "change":
466
469
  if (!headlineQuote) return { text: "—" };
470
+ // A listing that no longer trades has no day's move: its last session's is years old.
471
+ if (quote?.delisting) return { text: delistingStateLabel(quote.delisting), color: colors.warning };
467
472
  return {
468
473
  text: formatSignedMarketPrice(headlineQuote.change, { ...currentQuoteOptions, maxWidth: col.width }),
469
474
  color: marketChangeColor(headlineQuote.change, quote?.marketState),
@@ -489,6 +494,7 @@ export function getColumnValue(
489
494
  return { text: `${formatCompact(quote?.bidSize)}/${formatCompact(quote?.askSize)}` };
490
495
  }
491
496
  case "change_pct": {
497
+ if (quote?.delisting) return { text: delistingStateLabel(quote.delisting), color: colors.warning };
492
498
  const changePercent = headlineChangePercent(headlineQuote, quote);
493
499
  return quote
494
500
  ? { text: formatPercentRaw(changePercent), color: marketChangeColor(changePercent, quote.marketState) }
@@ -573,6 +579,7 @@ export function getColumnValue(
573
579
  return { text, color: /[1-9]/.test(text) ? priceColor(allocation.tradeValue!) : undefined };
574
580
  }
575
581
  case "day_pnl":
582
+ if (quote?.delisting) return { text: delistingStateLabel(quote.delisting), color: colors.warning };
576
583
  if (activeQuote && isFiniteNumber(activeQuote.change) && Number.isFinite(positionMetrics.grossPriceUnits) && positionMetrics.grossPriceUnits !== 0) {
577
584
  const dayPnl = toBaseQuote(totalPriceUnits * activeQuote.change);
578
585
  return { text: formatCompactAmount(dayPnl, { signed: true }), color: priceColor(dayPnl) };
@@ -603,7 +610,8 @@ export function getColumnValue(
603
610
  const analyst = mapData(ctx.analystResearch, ticker.metadata.ticker);
604
611
  if (analyst.pending) return { text: "…" };
605
612
  const value = targetValue(analyst.data);
606
- const currency = analyst.data?.priceTarget?.currency ?? analyst.data?.currency ?? quoteCurrency;
613
+ // The target's own currency: one in pounds under a quote in pence reads £, not p.
614
+ const currency = analystTargetDisplayCurrency(analyst.data, quoteCurrency, displayQuote?.price);
607
615
  return {
608
616
  text: value == null
609
617
  ? "—"
@@ -730,9 +738,9 @@ export function getSortValue(
730
738
  ? (quote?.bidSize ?? 0) + (quote?.askSize ?? 0)
731
739
  : null;
732
740
  case "change":
733
- return headlineQuote?.change ?? null;
741
+ return quote?.delisting ? null : headlineQuote?.change ?? null;
734
742
  case "change_pct":
735
- return headlineChangePercent(headlineQuote, quote) ?? null;
743
+ return quote?.delisting ? null : headlineChangePercent(headlineQuote, quote) ?? null;
736
744
  case "volume":
737
745
  return reportedQuoteVolume(quote);
738
746
  case "dollar_volume": {
@@ -774,6 +782,7 @@ export function getSortValue(
774
782
  case "trade_value":
775
783
  return rowAllocation(ticker, activeQuote, baseMetrics, toBaseQuote, ctx).tradeValue;
776
784
  case "day_pnl":
785
+ if (quote?.delisting) return null;
777
786
  if (activeQuote && isFiniteNumber(activeQuote.change) && Number.isFinite(positionMetrics.grossPriceUnits) && positionMetrics.grossPriceUnits !== 0) {
778
787
  return toBaseQuote(totalPriceUnits * activeQuote.change);
779
788
  }