gloomberb 0.11.0 → 0.11.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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { Quote } from "../../../types/financials";
3
+ import { getActiveQuoteDisplay } from "../../../market-data/market/status";
3
4
  import type { AlertRule } from "./types";
4
5
 
5
6
  export function normalizeAlertSymbol(value: string | null | undefined): string {
@@ -17,12 +18,19 @@ export function createQuoteErrorMessage(
17
18
  export async function resolveAlertQuote(
18
19
  marketData: Pick<DataProvider, "getQuote">,
19
20
  symbol: string,
21
+ exchange = "",
20
22
  ): Promise<Quote> {
21
- const quote = await marketData.getQuote(symbol, "");
22
- if (!quote || typeof quote.price !== "number" || !Number.isFinite(quote.price)) {
23
+ const quote = await marketData.getQuote(symbol, exchange);
24
+ const display = getActiveQuoteDisplay(quote);
25
+ if (!display || !Number.isFinite(display.price)) {
23
26
  throw new Error(`No quote found for "${symbol}".`);
24
27
  }
25
- return quote;
28
+ return {
29
+ ...quote,
30
+ price: display.price,
31
+ change: display.change,
32
+ changePercent: display.changePercent,
33
+ };
26
34
  }
27
35
 
28
36
  export function quoteAlertFields(
@@ -6,6 +6,7 @@ export type AlertStatus = "active" | "triggered" | "expired";
6
6
  export interface AlertRule {
7
7
  id: string;
8
8
  symbol: string;
9
+ exchange?: string;
9
10
  condition: AlertCondition;
10
11
  targetPrice: number;
11
12
  createdAt: number;
@@ -5,6 +5,7 @@ import { EmptyState, Tabs } from "../../../components";
5
5
  import type { PaneProps } from "../../../types/plugin";
6
6
  import type { PluginModule } from "../plugin-module";
7
7
  import { colors } from "../../../theme/colors";
8
+ import { convertCurrency } from "../../../utils/format";
8
9
  import {
9
10
  getFocusedCollectionId,
10
11
  useAppSelector,
@@ -212,8 +213,14 @@ function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
212
213
  activePortfolio,
213
214
  brokerPerformance: brokerPerformance.performance,
214
215
  portfolioStats,
216
+ convertAccountValue: (value) => convertCurrency(
217
+ value,
218
+ accountState?.account.currency || baseCurrency,
219
+ baseCurrency,
220
+ effectiveExchangeRates,
221
+ ),
215
222
  }),
216
- [accountState, activePortfolio, brokerPerformance.performance, portfolioStats],
223
+ [accountState, activePortfolio, baseCurrency, brokerPerformance.performance, effectiveExchangeRates, portfolioStats],
217
224
  );
218
225
 
219
226
  const riskRows = useMemo(
@@ -340,6 +347,9 @@ export const portfolioAnalyticsModule: PluginModule = {
340
347
  defaultPosition: "right",
341
348
  defaultMode: "floating",
342
349
  defaultFloatingSize: { width: 80, height: 30 },
350
+ portableShare: {
351
+ private: { params: true, settings: true, state: true },
352
+ },
343
353
  },
344
354
  ],
345
355
 
@@ -142,23 +142,25 @@ export function buildAnalyticsSummaryRows({
142
142
  accountState,
143
143
  brokerPerformance,
144
144
  portfolioStats,
145
+ convertAccountValue = (value) => value,
145
146
  }: {
146
147
  accountState: ResolvedPortfolioAccountState | null;
147
148
  activePortfolio: Portfolio | null;
148
149
  brokerPerformance: BrokerPortfolioPerformance | null;
149
150
  portfolioStats: PortfolioSummaryTotals;
151
+ convertAccountValue?: (value: number) => number;
150
152
  }): AnalyticsMetricRow[] {
151
153
  const rows: AnalyticsMetricRow[] = [];
152
154
  const account = accountState?.account;
153
- const accountMetrics = resolvePortfolioAccountMetrics(portfolioStats, account);
155
+ const accountMetrics = resolvePortfolioAccountMetrics(portfolioStats, account, convertAccountValue);
154
156
  const accountFreshness = formatAccountFreshness(account);
155
- const totalMarketValue = resolvePortfolioMarketValue(portfolioStats, account);
157
+ const totalMarketValue = resolvePortfolioMarketValue(portfolioStats, account, convertAccountValue);
156
158
 
157
159
  if (account?.netLiquidation != null) {
158
160
  rows.push({
159
161
  id: "net-liquidation",
160
162
  label: "Net Liq",
161
- value: formatCompact(account.netLiquidation),
163
+ value: formatCompact(convertAccountValue(account.netLiquidation)),
162
164
  color: colors.text,
163
165
  });
164
166
  }
@@ -184,7 +186,7 @@ export function buildAnalyticsSummaryRows({
184
186
  rows.push({
185
187
  id: "cash",
186
188
  label: "Cash",
187
- value: formatCompact(account.totalCashValue),
189
+ value: formatCompact(convertAccountValue(account.totalCashValue)),
188
190
  color: colors.text,
189
191
  });
190
192
  }
@@ -307,6 +307,9 @@ export const brokerManagerModule: PluginModule = {
307
307
  defaultPosition: "right",
308
308
  defaultMode: "floating",
309
309
  defaultFloatingSize: { width: 92, height: 24 },
310
+ portableShare: {
311
+ private: { title: true, params: true, settings: true, state: true },
312
+ },
310
313
  },
311
314
  ],
312
315
  paneTemplates: [
@@ -122,7 +122,7 @@ export async function loadBuildoutData(token: string | null) {
122
122
  }
123
123
 
124
124
  export async function getBuildoutProToken() {
125
- if (!apiClient.getSessionToken()) return null;
125
+ if (!apiClient.isSignedIn()) return null;
126
126
 
127
127
  const session = await apiClient.getSession().catch(() => null);
128
128
  if (!session) return null;
@@ -4,6 +4,7 @@ import {
4
4
  DataTableStackView,
5
5
  EmptyState,
6
6
  Spinner,
7
+ useTableLoadMore,
7
8
  usePaneFooter,
8
9
  type DataTableKeyEvent,
9
10
  type PaneHint,
@@ -245,24 +246,22 @@ export function BuildoutPane({ focused, width, height }: PaneProps) {
245
246
  return true;
246
247
  }, [canFavorite, toggleFavorite]);
247
248
 
248
- const loadMoreActiveRows = useCallback(() => {
249
- if (state.status !== "ready") return;
250
- const scrollBox = tableScrollRef.current;
251
- if (!scrollBox?.viewport) return;
252
- const page = activeBuildoutPage(state, activeTab, selectedList);
253
- if (!page || page.loadingMore || !page.hasMore || page.error) return;
254
- const visibleBottom = scrollBox.scrollTop + scrollBox.viewport.height;
255
- const remaining = page.items.length - visibleBottom;
256
- if (remaining > LOAD_MORE_THRESHOLD) return;
257
-
258
- if (activeTab === "companies" && selectedList) {
259
- void loadCompanies(selectedList, page.offset, true);
260
- } else if (activeTab === "sites") {
261
- void loadSites(page.offset, true);
262
- } else if (activeTab === "intel") {
263
- void loadIntel(page.offset, true);
264
- }
265
- }, [activeTab, loadCompanies, loadIntel, loadSites, selectedList, state]);
249
+ const activePage = state.status === "ready" ? activeBuildoutPage(state, activeTab, selectedList) : null;
250
+ const loadMoreActiveRows = useTableLoadMore(
251
+ tableScrollRef,
252
+ !!activePage && !activePage.loadingMore && !!activePage.hasMore && !activePage.error,
253
+ () => {
254
+ if (!activePage) return;
255
+ if (activeTab === "companies" && selectedList) {
256
+ void loadCompanies(selectedList, activePage.offset, true);
257
+ } else if (activeTab === "sites") {
258
+ void loadSites(activePage.offset, true);
259
+ } else if (activeTab === "intel") {
260
+ void loadIntel(activePage.offset, true);
261
+ }
262
+ },
263
+ LOAD_MORE_THRESHOLD,
264
+ );
266
265
 
267
266
  const handleRootKeyDown = useCallback((event: DataTableKeyEvent) => {
268
267
  if (event.name === "u" && state.status === "ready" && state.access !== "pro") {
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CloudCdsResponse } from "../../../api-client";
3
+ import type { InstrumentSearchResult } from "../../../types/instrument";
4
+ import { loadCdsActivity } from "./client";
5
+
6
+ function result(symbol: string, name: string): InstrumentSearchResult {
7
+ return { providerId: "gloomberb-cloud", symbol, name, exchange: "NYSE", type: "STK" };
8
+ }
9
+
10
+ const EMPTY_CDS: CloudCdsResponse = { source: "DTCC PPD", asOf: null, trades: [] };
11
+
12
+ /** Records what the loader searched for and what it then asked the backend. */
13
+ function spy(searchResults: InstrumentSearchResult[] | Error = []) {
14
+ const searched: string[] = [];
15
+ const requested: Array<string | undefined> = [];
16
+ return {
17
+ searched,
18
+ requested,
19
+ fetchCds: async (params: { issuer?: string }) => {
20
+ requested.push(params.issuer);
21
+ return EMPTY_CDS;
22
+ },
23
+ searchInstruments: async (query: string) => {
24
+ searched.push(query);
25
+ if (searchResults instanceof Error) throw searchResults;
26
+ return searchResults;
27
+ },
28
+ };
29
+ }
30
+
31
+ describe("loadCdsActivity", () => {
32
+ test("expands a bare ticker through search before requesting CDS", async () => {
33
+ // A near-miss is returned first, so first-result-wins would send the wrong name.
34
+ const calls = spy([result("ORCL.MX", "Oracle de Mexico"), result("ORCL", "Oracle Corporation")]);
35
+
36
+ const activity = await loadCdsActivity("ORCL", calls.fetchCds, calls.searchInstruments);
37
+
38
+ expect(calls.searched).toEqual(["ORCL"]);
39
+ expect(calls.requested).toEqual(["Oracle Corporation"]);
40
+ // The pane body reads this back, so ORCL must not survive resolution.
41
+ expect(activity.issuer).toBe("Oracle Corporation");
42
+ });
43
+
44
+ test("falls back to the first result when no symbol matches exactly", async () => {
45
+ const calls = spy([result("AVGO", "Broadcom Inc."), result("AVGOP", "Broadcom Preferred")]);
46
+
47
+ await loadCdsActivity("BRCM", calls.fetchCds, calls.searchInstruments);
48
+
49
+ expect(calls.requested).toEqual(["Broadcom Inc."]);
50
+ });
51
+
52
+ test("sends a company name straight through without searching", async () => {
53
+ const calls = spy([result("ORCL", "Oracle Corporation")]);
54
+
55
+ const activity = await loadCdsActivity("Tencent Holdings Limited", calls.fetchCds, calls.searchInstruments);
56
+
57
+ expect(calls.searched).toEqual([]);
58
+ expect(calls.requested).toEqual(["Tencent Holdings Limited"]);
59
+ expect(activity.issuer).toBe("Tencent Holdings Limited");
60
+ });
61
+
62
+ test("keeps the raw ticker when search is empty or fails", async () => {
63
+ const empty = spy([]);
64
+ await loadCdsActivity("ORCL", empty.fetchCds, empty.searchInstruments);
65
+ expect(empty.requested).toEqual(["ORCL"]);
66
+
67
+ // A search outage must not also take out the CDS request.
68
+ const broken = spy(new Error("search unavailable"));
69
+ const activity = await loadCdsActivity("ORCL", broken.fetchCds, broken.searchInstruments);
70
+ expect(broken.requested).toEqual(["ORCL"]);
71
+ expect(activity.issuer).toBe("ORCL");
72
+ });
73
+
74
+ test("market-wide load searches nothing and sends no issuer", async () => {
75
+ const calls = spy([result("ORCL", "Oracle Corporation")]);
76
+
77
+ const activity = await loadCdsActivity(null, calls.fetchCds, calls.searchInstruments);
78
+
79
+ expect(calls.searched).toEqual([]);
80
+ expect(calls.requested).toEqual([undefined]);
81
+ expect(activity.issuer).toBeNull();
82
+ });
83
+ });
@@ -0,0 +1,67 @@
1
+ import { apiClient, type CloudCdsResponse } from "../../../api-client";
2
+ import type { InstrumentSearchResult } from "../../../types/instrument";
3
+ import { normalizeCdsTrades, type CdsTrade } from "./model";
4
+
5
+ /** One trading week of public dissemination is what "recent activity" means here. */
6
+ const CDS_HISTORY_DAYS = 5;
7
+ const CDS_TRADE_LIMIT = 500;
8
+
9
+ export interface CdsActivity {
10
+ source: string;
11
+ asOf: string | null;
12
+ /** The issuer name actually sent, after any symbol lookup. */
13
+ issuer: string | null;
14
+ trades: CdsTrade[];
15
+ }
16
+
17
+ export type CdsActivityLoader = (issuer: string | null) => Promise<CdsActivity>;
18
+
19
+ type CdsFetch = (params: { issuer?: string; days?: number; limit?: number }) => Promise<CloudCdsResponse>;
20
+ type InstrumentSearch = (query: string, limit?: number) => Promise<InstrumentSearchResult[]>;
21
+
22
+ /**
23
+ * A bare ticker with normal symbol punctuation and no spaces. Anything longer or
24
+ * containing a space is already a company name and is sent to the backend as-is.
25
+ */
26
+ const TICKER_LIKE = /^[A-Za-z0-9][A-Za-z0-9.^:-]{0,11}$/;
27
+
28
+ /**
29
+ * The backend matches DTCC issuer names, so a ticker has to become a company
30
+ * name first. An untracked symbol has no local metadata to expand it, and its
31
+ * quote name never arrives for a fixed binding, so the shared instrument search
32
+ * is the only reliable expansion. A search outage falls back to the raw input
33
+ * rather than taking the CDS request down with it.
34
+ */
35
+ export async function resolveIssuerName(
36
+ issuer: string,
37
+ searchInstruments: InstrumentSearch,
38
+ ): Promise<string> {
39
+ if (!TICKER_LIKE.test(issuer)) return issuer;
40
+ try {
41
+ const results = await searchInstruments(issuer);
42
+ const symbol = issuer.toUpperCase();
43
+ const match = results.find((result) => result.symbol.toUpperCase() === symbol) ?? results[0];
44
+ return match?.name?.trim() || issuer;
45
+ } catch {
46
+ return issuer;
47
+ }
48
+ }
49
+
50
+ export async function loadCdsActivity(
51
+ issuer: string | null,
52
+ fetchCds: CdsFetch = (params) => apiClient.getCloudCds(params),
53
+ searchInstruments: InstrumentSearch = (query, limit) => apiClient.searchInstruments(query, limit),
54
+ ): Promise<CdsActivity> {
55
+ const resolved = issuer ? await resolveIssuerName(issuer, searchInstruments) : null;
56
+ const response = await fetchCds({
57
+ ...(resolved ? { issuer: resolved } : {}),
58
+ days: CDS_HISTORY_DAYS,
59
+ limit: CDS_TRADE_LIMIT,
60
+ });
61
+ return {
62
+ source: response.source,
63
+ asOf: response.asOf,
64
+ issuer: resolved,
65
+ trades: normalizeCdsTrades(response.trades ?? []),
66
+ };
67
+ }
@@ -0,0 +1,41 @@
1
+ import type { PaneTemplateCreateOptions } from "../../../types/plugin";
2
+ import type { PluginModule } from "../plugin-module";
3
+ import { CDS_PANE_ID } from "./model";
4
+ import { CdsPane } from "./pane";
5
+
6
+ /** Only an explicit argument binds a ticker; bare `CDS` stays market-wide. */
7
+ function explicitSymbol(options?: PaneTemplateCreateOptions): string | null {
8
+ const raw = options?.symbol ?? options?.ticker?.metadata.ticker ?? options?.arg;
9
+ return raw?.trim().toUpperCase() || null;
10
+ }
11
+
12
+ export const cdsModule: PluginModule = {
13
+ panes: [{
14
+ id: CDS_PANE_ID,
15
+ name: "Single-Name CDS",
16
+ icon: "D",
17
+ component: CdsPane,
18
+ defaultPosition: "right",
19
+ defaultMode: "floating",
20
+ defaultFloatingSize: { width: 92, height: 22 },
21
+ }],
22
+ paneTemplates: [{
23
+ id: "cds-pane",
24
+ paneId: CDS_PANE_ID,
25
+ label: "Single-Name CDS",
26
+ description: "Single-name corporate CDS trade activity from DTCC public dissemination.",
27
+ keywords: ["cds", "credit", "default", "swap", "single name", "issuer", "dtcc", "protection"],
28
+ shortcut: { prefix: "CDS", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
29
+ createInstance: (_context, options) => {
30
+ const symbol = explicitSymbol(options);
31
+ return symbol
32
+ ? {
33
+ instanceId: `${CDS_PANE_ID}:${symbol}`,
34
+ title: `CDS ${symbol}`,
35
+ binding: { kind: "fixed", symbol },
36
+ placement: "floating",
37
+ }
38
+ : { instanceId: `${CDS_PANE_ID}:market`, title: "CDS", placement: "floating" };
39
+ },
40
+ }],
41
+ };
@@ -0,0 +1,232 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CloudCdsTradePayload } from "../../../api-client";
3
+ import {
4
+ formatNotional,
5
+ issuerGroupKey,
6
+ normalizeCdsTrades,
7
+ resolveIssuerQuery,
8
+ spreadToBasisPoints,
9
+ summarizeIssuers,
10
+ tradesForIssuer,
11
+ } from "./model";
12
+
13
+ function payload(overrides: Partial<CloudCdsTradePayload> = {}): CloudCdsTradePayload {
14
+ return {
15
+ disseminationId: "1",
16
+ originalDisseminationId: null,
17
+ actionType: "NEWT",
18
+ eventTimestamp: "2026-08-25T14:30:00Z",
19
+ executionTimestamp: "2026-08-25T14:30:00Z",
20
+ effectiveDate: null,
21
+ expirationDate: null,
22
+ maturityDate: "2031-06-20",
23
+ issuerName: "Oracle Corporation",
24
+ underlierId: null,
25
+ underlierIdSource: null,
26
+ upi: null,
27
+ upiFisn: null,
28
+ upiUnderlierName: null,
29
+ notionalAmount: 5_000_000,
30
+ notionalCapped: false,
31
+ notionalCurrency: "USD",
32
+ // Raw DTCC decimals: 0.01 is a 100bp coupon.
33
+ fixedRate: 0.01,
34
+ reportedSpread: null,
35
+ spreadNotation: null,
36
+ upfrontAmount: null,
37
+ upfrontCurrency: null,
38
+ ...overrides,
39
+ };
40
+ }
41
+
42
+ describe("CDS spread units", () => {
43
+ test("converts reported spreads to basis points by notation", () => {
44
+ // Notation code "3" is what the raw DTCC feed carries for a decimal spread.
45
+ expect(spreadToBasisPoints(0.00256, "3")).toBeCloseTo(25.6, 6);
46
+ expect(spreadToBasisPoints(0.00256, "Decimal")).toBeCloseTo(25.6, 6);
47
+ // Unlabelled raw values are decimals too, not percent.
48
+ expect(spreadToBasisPoints(0.00256, null)).toBeCloseTo(25.6, 6);
49
+ expect(spreadToBasisPoints(25.6, "BPS")).toBe(25.6);
50
+ expect(spreadToBasisPoints(25.6, "Basis points")).toBe(25.6);
51
+ expect(spreadToBasisPoints(0.256, "Percentage")).toBeCloseTo(25.6, 6);
52
+ expect(spreadToBasisPoints(null, "3")).toBeNull();
53
+ });
54
+ });
55
+
56
+ describe("normalizeCdsTrades", () => {
57
+ test("keeps the reported spread only, and falls back through issuer names", () => {
58
+ const [withSpread, withoutName] = normalizeCdsTrades([
59
+ payload({ reportedSpread: 0.00256, spreadNotation: "3", fixedRate: 0.05 }),
60
+ payload({ disseminationId: "2", issuerName: null, upiUnderlierName: "ACME INC" }),
61
+ ]);
62
+ expect(withSpread!.spreadBp).toBeCloseTo(25.6, 6);
63
+ expect(withSpread!.couponBp).toBeCloseTo(500, 6);
64
+ // A missing spread is never back-solved from coupon or upfront.
65
+ expect(withoutName!.spreadBp).toBeNull();
66
+ expect(withoutName!.couponBp).toBeCloseTo(100, 6);
67
+ expect(withoutName!.issuer).toBe("ACME INC");
68
+ });
69
+
70
+ test("dates a trade by execution time so a late correction is not a new print", () => {
71
+ const [corrected, noExecution, unusableExecution] = normalizeCdsTrades([
72
+ payload({
73
+ actionType: "CORR",
74
+ originalDisseminationId: "9",
75
+ executionTimestamp: "2026-08-18T09:15:00Z",
76
+ eventTimestamp: "2026-08-25T14:30:00Z",
77
+ }),
78
+ payload({ disseminationId: "2", executionTimestamp: null }),
79
+ payload({ disseminationId: "3", executionTimestamp: "not-a-date" }),
80
+ ]);
81
+ expect(corrected!.eventAt).toBe(Date.parse("2026-08-18T09:15:00Z"));
82
+ expect(noExecution!.eventAt).toBe(Date.parse("2026-08-25T14:30:00Z"));
83
+ expect(unusableExecution!.eventAt).toBe(Date.parse("2026-08-25T14:30:00Z"));
84
+ });
85
+
86
+ test("drops reports with an unusable timestamp instead of dating them to 1970", () => {
87
+ expect(normalizeCdsTrades([
88
+ payload({ executionTimestamp: null, eventTimestamp: "not-a-date" }),
89
+ ])).toHaveLength(0);
90
+ });
91
+
92
+ test("marks capped notionals so a floor is not read as the trade size", () => {
93
+ expect(formatNotional({ notional: 5_000_000, notionalCapped: true })).toBe("5M+");
94
+ expect(formatNotional({ notional: 5_000_000, notionalCapped: false })).toBe("5M");
95
+ expect(formatNotional({ notional: null, notionalCapped: false })).toBe("--");
96
+ });
97
+ });
98
+
99
+ describe("summarizeIssuers", () => {
100
+ const trades = normalizeCdsTrades([
101
+ payload({ disseminationId: "a", executionTimestamp: "2026-08-25T10:00:00Z", reportedSpread: 0.009, spreadNotation: "3" }),
102
+ payload({ disseminationId: "b", executionTimestamp: "2026-08-25T14:00:00Z", reportedSpread: null }),
103
+ payload({ disseminationId: "c", executionTimestamp: "2026-08-25T12:00:00Z", reportedSpread: 0.011, spreadNotation: "3" }),
104
+ payload({ disseminationId: "d", executionTimestamp: "2026-08-25T09:00:00Z", issuerName: "Ford Motor Company" }),
105
+ ]);
106
+
107
+ test("groups by issuer with count, last trade, and the newest available spread", () => {
108
+ const summaries = summarizeIssuers(trades);
109
+ const oracle = summaries.find((row) => row.issuer === "Oracle Corporation")!;
110
+ expect(oracle.trades).toBe(3);
111
+ expect(oracle.lastTradeAt).toBe(Date.parse("2026-08-25T14:00:00Z"));
112
+ // The newest print carried no spread, so the last quoted level survives.
113
+ expect(oracle.latestSpreadBp).toBeCloseTo(110, 6);
114
+ expect(summaries.find((row) => row.issuer === "Ford Motor Company")!.latestSpreadBp).toBeNull();
115
+ });
116
+ });
117
+
118
+ describe("generic obligation names", () => {
119
+ // Verbatim `UPI Underlier Name` values seen dominating the live market view.
120
+ const GENERIC = [
121
+ "SR NT",
122
+ "SR NT 144A",
123
+ "NT",
124
+ "SR GTD NT 144A",
125
+ "No name obtainable",
126
+ "MEDIUM TERM NOTES EUR 2.3750 S.10/CALL",
127
+ "MEDIUM TERM NOTES EUR 0.6250 S.001STLA/CALL",
128
+ "GLOBAL BD",
129
+ ];
130
+
131
+ function unnamed(id: string, upiUnderlierName: string | null) {
132
+ return payload({ disseminationId: id, issuerName: null, upiUnderlierName });
133
+ }
134
+
135
+ test("drops records whose only name is a generic obligation label", () => {
136
+ const rows = normalizeCdsTrades([
137
+ ...GENERIC.map((name, index) => unnamed(`g${index}`, name)),
138
+ unnamed("empty", ""),
139
+ unnamed("missing", null),
140
+ ]);
141
+ expect(rows).toEqual([]);
142
+ });
143
+
144
+ test("accepts UPI underlier names that identify an issuer", () => {
145
+ const rows = normalizeCdsTrades([
146
+ unnamed("t", "Tencent Holdings Limited"),
147
+ unnamed("d", "DT.BANK MTN 17/20"),
148
+ ]);
149
+ expect(rows.map((row) => row.issuer)).toEqual(["Tencent Holdings Limited", "DT.BANK MTN 17/20"]);
150
+ });
151
+
152
+ test("keeps a reported issuer name and never falls back to an underlier id", () => {
153
+ const rows = normalizeCdsTrades([
154
+ payload({ disseminationId: "b", issuerName: "Broadcom Inc", upiUnderlierName: "SR NT 144A" }),
155
+ payload({ disseminationId: "x", issuerName: null, upiUnderlierName: null, underlierId: "US11135FAX50" }),
156
+ ]);
157
+ expect(rows.map((row) => row.issuer)).toEqual(["Broadcom Inc"]);
158
+ });
159
+
160
+ test("leaves the summary to real issuers when fake ones outnumber them", () => {
161
+ const summaries = summarizeIssuers(normalizeCdsTrades([
162
+ ...GENERIC.map((name, index) => unnamed(`g${index}`, name)),
163
+ payload({ disseminationId: "b", issuerName: "Broadcom Inc", upiUnderlierName: "SR GTD NT 144A" }),
164
+ payload({ disseminationId: "o", issuerName: "Oracle Corporation", upiUnderlierName: "SR NT" }),
165
+ ]));
166
+ expect(summaries.map((row) => row.issuer).sort()).toEqual(["Broadcom Inc", "Oracle Corporation"]);
167
+ });
168
+ });
169
+
170
+ describe("issuer aliases", () => {
171
+ // Spellings the deployed backend actually returns for one reference entity,
172
+ // deliberately in an order where the best one is not the first seen.
173
+ const ORACLE = ["ORACLE CORPORATION", "Oracle Cop", "Oracle Corporation"];
174
+ const BOEING = ["THE BOEING COMPANY", "Boeing Co/The", "The Boeing Company"];
175
+
176
+ const aliasTrades = normalizeCdsTrades([
177
+ ...ORACLE.map((issuerName, index) => payload({
178
+ disseminationId: `o${index}`,
179
+ issuerName,
180
+ executionTimestamp: `2026-08-25T1${index}:00:00Z`,
181
+ reportedSpread: 0.009,
182
+ spreadNotation: "3",
183
+ })),
184
+ ...BOEING.map((issuerName, index) => payload({
185
+ disseminationId: `b${index}`,
186
+ issuerName,
187
+ executionTimestamp: `2026-08-25T0${index}:00:00Z`,
188
+ })),
189
+ ]);
190
+
191
+ test("collapses case, punctuation, and legal-suffix aliases onto one key", () => {
192
+ expect(new Set(ORACLE.map(issuerGroupKey))).toEqual(new Set(["oracle"]));
193
+ expect(new Set(BOEING.map(issuerGroupKey))).toEqual(new Set(["boeing"]));
194
+ });
195
+
196
+ test("keeps meaningful words that distinguish real entities", () => {
197
+ expect(issuerGroupKey("Oracle Holdings Corp")).not.toBe(issuerGroupKey("Oracle Corporation"));
198
+ expect(issuerGroupKey("Boeing Capital Group")).not.toBe(issuerGroupKey("The Boeing Company"));
199
+ expect(issuerGroupKey("Acme 2031 Ltd")).not.toBe(issuerGroupKey("Acme 2026 Ltd"));
200
+ // A name that is only noise keeps its own row instead of an empty key.
201
+ expect(issuerGroupKey("The Company")).toBe("the company");
202
+ });
203
+
204
+ test("counts every alias in one row and displays the best spelling", () => {
205
+ const summaries = summarizeIssuers(aliasTrades);
206
+ expect(summaries).toHaveLength(2);
207
+
208
+ const oracle = summaries.find((row) => row.key === "oracle")!;
209
+ expect(oracle.trades).toBe(3);
210
+ expect(oracle.issuer).toBe("Oracle Corporation");
211
+
212
+ const boeing = summaries.find((row) => row.key === "boeing")!;
213
+ expect(boeing.trades).toBe(3);
214
+ expect(boeing.issuer).toBe("The Boeing Company");
215
+ });
216
+
217
+ test("drills down on the key so aliases open together", () => {
218
+ expect(tradesForIssuer(aliasTrades, "oracle").map((row) => row.issuer)).toEqual(ORACLE);
219
+ expect(tradesForIssuer(aliasTrades, "boeing").map((row) => row.issuer)).toEqual(BOEING);
220
+ });
221
+ });
222
+
223
+ describe("resolveIssuerQuery", () => {
224
+ const ticker = { metadata: { ticker: "ORCL", name: "Oracle Corporation" } } as never;
225
+
226
+ test("prefers tracked metadata and otherwise hands the loader a bare symbol", () => {
227
+ expect(resolveIssuerQuery("ORCL", ticker)).toBe("Oracle Corporation");
228
+ // Untracked: the loader expands this through instrument search.
229
+ expect(resolveIssuerQuery("orcl", null)).toBe("ORCL");
230
+ expect(resolveIssuerQuery(null, null)).toBeNull();
231
+ });
232
+ });