gloomberb 0.18.1 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -224,6 +224,15 @@ export function resolvePortfolioAccountState(
224
224
  };
225
225
  }
226
226
 
227
+ /**
228
+ * What the day's move is measured from, after it, as every change line says
229
+ * it ("vs Fri 16 Oct close", "vs mixed closes"), so a narrow row never shows
230
+ * the move without it.
231
+ */
232
+ function dayBasisPart(totals: Pick<PortfolioSummaryTotals, "dayChangeBasis">): PortfolioSummarySegment["parts"] {
233
+ return totals.dayChangeBasis ? [{ text: totals.dayChangeBasis, tone: "muted" }] : [];
234
+ }
235
+
227
236
  /** Headline numbers in priority order, so the narrowest header row keeps the most important ones. */
228
237
  export function buildPortfolioSummarySegments({
229
238
  totals,
@@ -258,6 +267,7 @@ export function buildPortfolioSummarySegments({
258
267
  ? [createSummarySegment("avg-day", [
259
268
  { text: "Avg Day", tone: "label" },
260
269
  { text: formatPercentRaw(totals.avgWatchlistChange), tone: "value", color: priceColor(totals.avgWatchlistChange), bold: true },
270
+ ...dayBasisPart(totals),
261
271
  ])]
262
272
  : [];
263
273
  }
@@ -332,6 +342,8 @@ export function buildPortfolioSummarySegments({
332
342
  { text: "Day", tone: "label" },
333
343
  { text: money(accountMetrics.dailyPnl, true), tone: "value", color: priceColor(accountMetrics.dailyPnl), bold: true },
334
344
  { text: `(${formatPercentRaw(accountMetrics.dailyPnlPct)})`, tone: "muted", color: priceColor(accountMetrics.dailyPnlPct) },
345
+ // The broker's own day is from its account's close, not the quotes': it names none.
346
+ ...(accountMetrics.brokerDay || !Number.isFinite(accountMetrics.dailyPnl) ? [] : dayBasisPart(totals)),
335
347
  ]));
336
348
  candidates.push(createSummarySegment("pnl", [
337
349
  { text: !Number.isFinite(account?.unrealizedPnl)
@@ -3,6 +3,7 @@ import type { Portfolio, TickerRecord } from "../../../../types/ticker";
3
3
  import { convertCurrency, formatCompactAmount } from "../../../../utils/format";
4
4
  import { getCurrencySymbol } from "../../../../market-data/market/format";
5
5
  import { getActiveQuoteDisplay } from "../../../../market-data/market/status";
6
+ import { combinedChangeBasisLabel, quoteChangeBasis, type QuoteChangeBasis } from "../../../../market-data/market/change-basis";
6
7
  import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
7
8
  import { headlineQuoteDisplay } from "../../../../portfolio/column-values";
8
9
  import { isManualPortfolio } from "../../../../portfolio/mutations";
@@ -45,6 +46,13 @@ export interface PortfolioSummaryTotals {
45
46
  unrealizedPnlBasis?: PortfolioPositionPnl["basis"];
46
47
  dailyPnl: number;
47
48
  dailyPnlPct: number;
49
+ /**
50
+ * What Day (or a watchlist's Avg Day) is measured from, as each holding's
51
+ * change line says it: "vs Fri 16 Oct close" when they agree, "vs mixed
52
+ * closes" when they do not (an HKEX listing beside a US one). Absent
53
+ * without a move.
54
+ */
55
+ dayChangeBasis?: string;
48
56
  totalCostBasis: number;
49
57
  hasPositions: boolean;
50
58
  unrealizedPnl: number;
@@ -131,6 +139,7 @@ export function calculatePortfolioSummaryTotals(
131
139
  let hasPositions = false;
132
140
  let watchlistChangeSum = 0;
133
141
  let watchlistCount = 0;
142
+ const dayBases: Array<QuoteChangeBasis | null> = [];
134
143
  const unavailableConversions = new Set<string>();
135
144
  let livePriced = true;
136
145
  const pricedLots: PricedPortfolioLot[] = [];
@@ -150,11 +159,12 @@ export function calculatePortfolioSummaryTotals(
150
159
  const quoteCurrency = quote?.currency || ticker.metadata.currency || "USD";
151
160
 
152
161
  if (!isPortfolio) {
153
- // The CHG% column's value, so Avg Day is the mean of that column.
154
- const changePercent = headlineQuoteDisplay(activeQuote, quote)?.changePercent;
162
+ // The CHG% column's value, so Avg Day is the mean of that column; a listing that no longer trades has none.
163
+ const changePercent = quote?.delisting ? undefined : headlineQuoteDisplay(activeQuote, quote)?.changePercent;
155
164
  if (changePercent != null) {
156
165
  watchlistChangeSum += changePercent;
157
166
  watchlistCount++;
167
+ dayBases.push(quoteChangeBasis(quote));
158
168
  }
159
169
  continue;
160
170
  }
@@ -220,9 +230,18 @@ export function calculatePortfolioSummaryTotals(
220
230
  totalMktValue += marketValue.gross;
221
231
  netMktValue += marketValue.net;
222
232
  if (activeQuote && Number.isFinite(grossPriceUnits)) {
223
- const previousClose = activeQuote.change != null ? activeQuote.price - activeQuote.change : Number.NaN;
233
+ // A listing that no longer trades does not move: its last session's change is not today's.
234
+ const previousClose = quote?.delisting ? activeQuote.price
235
+ : activeQuote.change != null ? activeQuote.price - activeQuote.change : Number.NaN;
224
236
  totalPrevValue += toBaseQuote(grossPriceUnits * previousClose);
225
237
  signedDailyPnl += toBaseQuote(totalPriceUnits * (activeQuote.price - previousClose));
238
+ // The move valued: the latest print's from the daily reference, the regular close's from the session before it.
239
+ if (!quote?.delisting) {
240
+ dayBases.push(quoteChangeBasis(quote, {
241
+ session: valuationBasis === "regular-close" ? "regular" : "active",
242
+ terms: futuresTerms?.get(ticker.metadata.ticker),
243
+ }));
244
+ }
226
245
  } else {
227
246
  // A broker mark is not evidence of an unchanged day.
228
247
  signedDailyPnl = Number.NaN;
@@ -242,6 +261,7 @@ export function calculatePortfolioSummaryTotals(
242
261
  const unrealizedPnl = signedUnrealizedPnl;
243
262
  const unrealizedPnlPct = portfolioPnlPercent(unrealizedPnl, totalCostBasis) ?? Number.NaN;
244
263
  const avgWatchlistChange = watchlistCount > 0 ? watchlistChangeSum / watchlistCount : 0;
264
+ const dayChangeBasis = combinedChangeBasisLabel(dayBases, now);
245
265
 
246
266
  return {
247
267
  totalMktValue,
@@ -258,6 +278,7 @@ export function calculatePortfolioSummaryTotals(
258
278
  : pnlBases.size > 1 ? "mixed" : [...pnlBases][0],
259
279
  dailyPnl,
260
280
  dailyPnlPct,
281
+ ...(dayChangeBasis ? { dayChangeBasis } : {}),
261
282
  totalCostBasis,
262
283
  hasPositions,
263
284
  unrealizedPnl,
@@ -106,11 +106,11 @@ export async function fetchPowerTickerScopes(symbol: string, tab: PowerTab, cont
106
106
  }
107
107
  const key = (query: unknown, scope: string) => `${scope}:${JSON.stringify(query)}`;
108
108
  export const loadPowerBoard = (filter: PowerFilter, scope: string, force = false, signal?: AbortSignal) =>
109
- loadCloudResource(powerBoardCache, key(filter, scope), () => fetchPowerBoard(filter, apiClient, signal), { force, validate: validatePowerBoard });
109
+ loadCloudResource(powerBoardCache, key(filter, scope), () => fetchPowerBoard(filter, apiClient, signal), { force, validate: validatePowerBoard, signal });
110
110
  export const loadPowerHistory = (filter: PowerFilter, scope: string, force = false) =>
111
111
  loadCloudResource(powerHistoryCache, key(filter, scope), () => fetchPowerHistory(filter), { force, validate: validatePowerHistory });
112
112
  export const loadPowerDetail = (id: string, scope: string, force = false, offset = 0, signal?: AbortSignal) => loadCloudResource(powerDetailCache, key({ id, offset }, scope),
113
113
  async () => { const value = validateDetail(await apiClient.getCloudPowerProject(id, { offset, limit: 100 }, signal));
114
114
  if (value.hasMore && value.nextOffset! <= offset) throw new Error("Project revision pagination did not advance.");
115
115
  if (value.project && value.project.id !== id || value.revisions.some((r) => r.id !== id)) throw new Error("Project evidence belongs to a different project.");
116
- return value; }, { force, validate: validateDetail });
116
+ return value; }, { force, validate: validateDetail, signal });
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
  import type { PowerBoard, PowerFilter, PowerHistory } from "../../../api-client/power";
3
3
  import { isAccessDenied } from "../../../api-client/errors";
4
4
  import { usePlanAccess } from "../../../api-client/plan-access";
5
- import { Button, ChartTableHeader, DataTableStackView, EmptyState, PaneStatusBody, QueryBar, useChartTableSelection, usePagedRows, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableCell, type PaneHint, type QueryBarFilter } from "../../../components";
5
+ import { Button, ChartTableHeader, DataTableStackView, EmptyState, QueryBar, useChartTableSelection, usePagedRows, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableCell, type PaneHint, type QueryBarFilter } from "../../../components";
6
6
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
7
7
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
8
8
  import { useThemeColors } from "../../../theme/theme-context";
@@ -97,6 +97,10 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
97
97
  }, [filter, scope, projectTab, snapshot]);
98
98
  const pages = usePagedRows(loader, { getId: (row) => row.id });
99
99
  const board = isAccessDenied(pages.error) || isAccessDenied(pages.moreError) ? undefined : pages.pages[0]?.payload;
100
+ // A tab or filter change starts its list over; the filter menus keep the last board's choices until the new one answers.
101
+ const lastFilters = useRef(board?.filters);
102
+ if (board) lastFilters.current = board.filters;
103
+ const filterChoices = board?.filters ?? lastFilters.current;
100
104
  const historyLoader = useCallback(async ({ offset, force }: { offset: number; force: boolean }) => {
101
105
  const result = snapshot?.history ? { payload: snapshot.history, stale: false, refreshError: null }
102
106
  : await loadPowerHistory({ ...filter, offset, limit: 500 }, scope, force);
@@ -157,7 +161,8 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
157
161
  const notices = [pages.moreError?.message, historyPages.moreError?.message, ...pages.pages.map((p) => p.refreshError), historyData?.refreshError,
158
162
  board && tab !== "capacity" ? powerCoverageNote(board.coverage) : null].filter((v): v is string => !!v);
159
163
  usePaneNoticeFooter({ registrationId: "power:notices", focused, notices: [...new Set(notices)], enabled: !openRow });
160
- const { strip, rows: tabRows } = usePaneTabs(board ? { tabs: [...POWER_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
164
+ const signInWall = !board && isCloudSessionRequired(error);
165
+ const { strip, rows: tabRows } = usePaneTabs(!signInWall ? { tabs: [...POWER_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
161
166
  const baseColumns = tab === "capacity" && context === "generation" ? GENERATION_COLUMNS : tab === "capacity" && context === "utility" ? UTILITY_COLUMNS : projectTab ? PROJECT_COLUMNS : tab === "history" ? HISTORY_COLUMNS : tab === "outcomes" ? RATE_COLUMNS : tab === "utilities" ? EXPOSURE_COLUMNS : COVERAGE_COLUMNS;
162
167
  const records = useMemo(() => rows.filter(isRecord), [rows]);
163
168
  const columns = useMemo(() => withoutQuietColumns(baseColumns, records, (r, id) => r.cells[id] ?? "", QUIET_COLUMNS), [baseColumns, records]);
@@ -167,10 +172,10 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
167
172
  const filters: QueryBarFilter[] = [
168
173
  ...(historical && ["queue", "history", "outcomes"].includes(tab) ? [{ id: "source", label: "History", value: sourceId, defaultValue: "", options: [{ value: "", label: "All benchmarks" }, { value: "us-lbnl-annual", label: "Annual benchmark" }, { value: "us-lbnl", label: "Project benchmark" }], onChange: setSourceId }] : []),
169
174
  ...(["loads", "utilities"].includes(tab) ? [{ id: "loadClass", label: "Loads", value: loadClass, defaultValue: "", options: [{ value: "", label: "All loads" }, { value: "datacenter", label: "Datacenters" }], onChange: setLoadClass }] : []),
170
- { id: "country", label: "Country", value: country, defaultValue: "", options: choices(board?.filters.countries), onChange: setCountry },
171
- { id: "region", label: "Region", value: region, defaultValue: "", options: choices(board?.filters.regions), onChange: setRegion },
172
- ...(!["loads", "utilities", "coverage"].includes(tab) ? [{ id: "fuel", label: "Fuel", value: fuel, defaultValue: "", options: choices(board?.filters.fuels), onChange: setFuel }] : []),
173
- ...(tab !== "coverage" ? [{ id: "status", label: "Status", value: status, defaultValue: "", options: choices(board?.filters.statuses), onChange: setStatus }] : []),
175
+ { id: "country", label: "Country", value: country, defaultValue: "", options: choices(filterChoices?.countries), onChange: setCountry },
176
+ { id: "region", label: "Region", value: region, defaultValue: "", options: choices(filterChoices?.regions), onChange: setRegion },
177
+ ...(!["loads", "utilities", "coverage"].includes(tab) ? [{ id: "fuel", label: "Fuel", value: fuel, defaultValue: "", options: choices(filterChoices?.fuels), onChange: setFuel }] : []),
178
+ ...(tab !== "coverage" ? [{ id: "status", label: "Status", value: status, defaultValue: "", options: choices(filterChoices?.statuses), onChange: setStatus }] : []),
174
179
  ...(["queue", "history", "outcomes"].includes(tab) ? [{ id: "historical", label: "Benchmark", kind: "toggle" as const, value: historical, defaultValue: false, onChange: setHistorical }] : []),
175
180
  ...(tab === "capacity" ? [{ id: "context", label: "Context", value: context, options: [{ value: "capacity", label: "Capacity" }, { value: "generation", label: "Generation" }, { value: "utility", label: "Utilities" }], onChange: setContext }] : []),
176
181
  ];
@@ -206,31 +211,31 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
206
211
  color: column.id === "name" || column.id === "utility" || column.id === "capacityMw" || column.id === "requestedMw" ? colors.textBright
207
212
  : ["location", "asOf", "observedAt", "scope", "proposedDate", "region", "country", "kind", "role", "period"].includes(column.id) ? colors.textDim : colors.text };
208
213
  };
209
- if (!board && isCloudSessionRequired(error)) return <SignInWall placement="power-signin" action="view power and interconnection data" needsVerification={needsVerification} />;
214
+ if (signInWall) return <SignInWall placement="power-signin" action="view power and interconnection data" needsVerification={needsVerification} />;
215
+ // The strip and the query bar stay through every load: a tab or filter change waits in the table's body.
210
216
  return <Box width={width} height={height} flexDirection="column">{strip}
211
- <PaneStatusBody loading={!board && pages.loading} error={!board ? error : null} subject="power and interconnection data">
212
- {board ? <DataTableStackView<PowerRow> columns={columns} items={items} getItemKey={rowId} focused={focused && !searchFocus.active}
213
- rootWidth={width} rootHeight={bodyHeight} scrollRef={scrollRef} onBodyScrollActivity={loadMore} resetScrollKey={`${tab}:${queryKey}`}
214
- selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelectedId }}
215
- onActivate={(row) => row.locked ? upgrade() : setOpenId(row.id)} detailOpen={!!openRow} onBack={() => setOpenId(null)} detailTitle={openRow?.label}
216
- detailScrollRef={detailScrollRef} detailContent={openRow ? <PowerDetailView row={openRow} scope={scope} scrollRef={detailScrollRef} snapshot={!!snapshot} focused={focused} width={width} height={bodyHeight - 1} /> : null}
217
- renderCell={cell} freezeFirstColumn showHorizontalScrollbar selectedTextOverridesCellColor
218
- isNavigable={isRecord} renderSectionHeader={(row) => row.section ? { text: `${row.label} (${row.section.count})` } : null}
219
- sortColumnId={projectTab ? sort.column ?? null : null} sortDirection={sort.direction}
220
- isColumnSortable={(column) => projectTab && POWER_SORT_COLUMNS.includes(column.id)}
221
- onHeaderClick={projectTab ? (column) => { if (POWER_SORT_COLUMNS.includes(column)) setSort((old) => ({ column: column as PowerFilter["sort"], direction: old.column === column && old.direction === "desc" ? "asc" : "desc" })); } : undefined}
222
- rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={query}
223
- figures={projectTab && tab === "queue" && board.access === "full" ? queueFigures : undefined}
224
- chart={tab === "history" ? { series, ...chartLink, formatValue: (n) => `${powerNumber(n)} MW`, loading: historyPages.loading || historyPages.hasMore && !historyPages.moreError,
225
- empty: series.length ? undefined : historyPages.moreError ? "Full history could not load. Refresh to retry." : historyData?.payload.access === "preview" ? "Full history is available with Pro." : "History is accumulating from recorded snapshots.", remoteKind: "power-history" } : null} />}
226
- emptyStateTitle={tab === "history" ? historyPages.error?.message ?? "No recorded history for these filters." : "No reported records match these filters."}
227
- emptyContent={tickerMessage ? <Box paddingX={1} paddingY={1}>
228
- <EmptyState title={tickerMessage.title} hint={tickerMessage.hint || undefined}
229
- actions={tickerMessage.found.length ? tickerMessage.found.map((scope) => <Button key={`${scope.tab}:${scope.context}`} label={`Open ${scope.view}`}
230
- onPress={() => { if (scope.tab === "capacity") setContext(scope.context); setTab(scope.tab); }} />) : undefined} />
231
- </Box> : undefined}
232
- bodyAfter={locked && desktop ? <LockedOverlay rows={Math.min(3, locked)} text="Unlock full power data with Pro" onPress={upgrade} role="power-upgrade" /> : undefined}
233
- /> : null}
234
- </PaneStatusBody>
217
+ <DataTableStackView<PowerRow> columns={columns} items={board ? items : []} getItemKey={rowId} focused={focused && !searchFocus.active}
218
+ status={board ? undefined : { loading: pages.loading, error, subject: "power and interconnection data" }}
219
+ rootWidth={width} rootHeight={bodyHeight} scrollRef={scrollRef} onBodyScrollActivity={loadMore} resetScrollKey={`${tab}:${queryKey}`}
220
+ selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelectedId }}
221
+ onActivate={(row) => row.locked ? upgrade() : setOpenId(row.id)} detailOpen={!!openRow} onBack={() => setOpenId(null)} detailTitle={openRow?.label}
222
+ detailScrollRef={detailScrollRef} detailContent={openRow ? <PowerDetailView row={openRow} scope={scope} scrollRef={detailScrollRef} snapshot={!!snapshot} focused={focused} width={width} height={bodyHeight - 1} /> : null}
223
+ renderCell={cell} freezeFirstColumn showHorizontalScrollbar selectedTextOverridesCellColor
224
+ isNavigable={isRecord} renderSectionHeader={(row) => row.section ? { text: `${row.label} (${row.section.count})` } : null}
225
+ sortColumnId={projectTab ? sort.column ?? null : null} sortDirection={sort.direction}
226
+ isColumnSortable={(column) => projectTab && POWER_SORT_COLUMNS.includes(column.id)}
227
+ onHeaderClick={projectTab ? (column) => { if (POWER_SORT_COLUMNS.includes(column)) setSort((old) => ({ column: column as PowerFilter["sort"], direction: old.column === column && old.direction === "desc" ? "asc" : "desc" })); } : undefined}
228
+ rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={query}
229
+ figures={projectTab && tab === "queue" && board?.access === "full" ? queueFigures : undefined}
230
+ chart={board && tab === "history" ? { series, ...chartLink, formatValue: (n) => `${powerNumber(n)} MW`, loading: historyPages.loading || historyPages.hasMore && !historyPages.moreError,
231
+ empty: series.length ? undefined : historyPages.moreError ? "Full history could not load. Refresh to retry." : historyData?.payload.access === "preview" ? "Full history is available with Pro." : "History is accumulating from recorded snapshots.", remoteKind: "power-history" } : null} />}
232
+ emptyStateTitle={tab === "history" ? historyPages.error?.message ?? "No recorded history for these filters." : "No reported records match these filters."}
233
+ emptyContent={tickerMessage ? <Box paddingX={1} paddingY={1}>
234
+ <EmptyState title={tickerMessage.title} hint={tickerMessage.hint || undefined}
235
+ actions={tickerMessage.found.length ? tickerMessage.found.map((scope) => <Button key={`${scope.tab}:${scope.context}`} label={`Open ${scope.view}`}
236
+ onPress={() => { if (scope.tab === "capacity") setContext(scope.context); setTab(scope.tab); }} />) : undefined} />
237
+ </Box> : undefined}
238
+ bodyAfter={locked && desktop ? <LockedOverlay rows={Math.min(3, locked)} text="Unlock full power data with Pro" onPress={upgrade} role="power-upgrade" /> : undefined}
239
+ />
235
240
  </Box>;
236
241
  }
@@ -2,26 +2,17 @@ import { httpFetch } from "../../../utils/http-transport";
2
2
  import { isoDateOrNull, numericOrNull, recordOrNull, textOrNull } from "../../../utils/guards";
3
3
  import {
4
4
  boardAsOf,
5
- selectSeries,
6
- toDealerRow,
7
- type CatalogEntry,
5
+ buildDealerRows,
6
+ newestSeriesBreak,
7
+ sumSeries,
8
8
  type DealerRow,
9
- type DealerTab,
9
+ type SeriesPoint,
10
10
  } from "./model";
11
11
 
12
- const LIST_URL = "https://markets.newyorkfed.org/api/pd/list/timeseries.json";
12
+ const API = "https://markets.newyorkfed.org/api/pd";
13
13
  const FETCH_TIMEOUT_MS = 15_000;
14
-
15
- interface SeriesPoint {
16
- asOf: string;
17
- value: number;
18
- }
19
-
20
- interface ParsedSeries {
21
- keyid: string;
22
- latest: SeriesPoint | null;
23
- previous: SeriesPoint | null;
24
- }
14
+ /** The release is weekly; a series read is kept this long so moving through the board never asks twice. */
15
+ const HISTORY_TTL_MS = 30 * 60_000;
25
16
 
26
17
  export interface DealerBoard {
27
18
  seriesBreak: string;
@@ -30,52 +21,57 @@ export interface DealerBoard {
30
21
  asOf: string | null;
31
22
  }
32
23
 
33
- function parseCatalog(payload: unknown): CatalogEntry[] {
24
+ /** A row's weekly history: the sum of the series behind it, oldest first. */
25
+ export interface DealerHistory {
26
+ rowId: string;
27
+ points: SeriesPoint[];
28
+ }
29
+
30
+ function timeseriesRows(payload: unknown): unknown[] {
34
31
  const root = recordOrNull(payload);
35
32
  const pd = recordOrNull(root?.pd);
36
- const rows = Array.isArray(pd?.timeseries) ? pd.timeseries : Array.isArray(root?.timeseries) ? root.timeseries : [];
37
- const entries: CatalogEntry[] = [];
38
- for (const raw of rows) {
39
- const row = recordOrNull(raw);
40
- if (!row) continue;
41
- const seriesBreak = textOrNull(row.seriesbreak) ?? textOrNull(row.seriesBreak);
42
- const keyid = textOrNull(row.keyid) ?? textOrNull(row.keyId);
43
- if (!seriesBreak || !keyid) continue;
44
- entries.push({ seriesBreak, keyid, description: textOrNull(row.description) ?? "" });
45
- }
46
- return entries;
33
+ return Array.isArray(pd?.timeseries) ? pd.timeseries : Array.isArray(root?.timeseries) ? root.timeseries : [];
47
34
  }
48
35
 
49
- /** Latest two observations. Points may arrive oldest-last or newest-last. */
50
- function parseSeriesObservations(payload: unknown, fallbackKeyid = ""): ParsedSeries {
36
+ function parseSeriesBreaks(payload: unknown): Array<{ seriesBreak: string; startDate: string }> {
51
37
  const root = recordOrNull(payload);
52
38
  const pd = recordOrNull(root?.pd);
53
- const rows = Array.isArray(pd?.timeseries) ? pd.timeseries : Array.isArray(root?.timeseries) ? root.timeseries : [];
54
- const points: Array<SeriesPoint & { keyid: string }> = [];
39
+ const rows = Array.isArray(pd?.seriesbreaks) ? pd.seriesbreaks : [];
40
+ const breaks: Array<{ seriesBreak: string; startDate: string }> = [];
55
41
  for (const raw of rows) {
56
42
  const row = recordOrNull(raw);
57
- if (!row) continue;
58
- const asOf = isoDateOrNull(row.asofdate) ?? isoDateOrNull(row.asOfDate) ?? isoDateOrNull(row.date);
59
- const value = numericOrNull(row.value);
60
- if (!asOf || value == null) continue;
61
- points.push({
62
- asOf,
63
- value,
64
- keyid: textOrNull(row.keyid) ?? fallbackKeyid,
65
- });
43
+ const seriesBreak = textOrNull(row?.seriesbreak) ?? textOrNull(row?.seriesBreak);
44
+ const startDate = isoDateOrNull(row?.startdate) ?? isoDateOrNull(row?.startDate);
45
+ if (seriesBreak && startDate) breaks.push({ seriesBreak, startDate });
66
46
  }
67
- points.sort((a, b) => a.asOf < b.asOf ? -1 : a.asOf > b.asOf ? 1 : 0);
68
- const latest = points.length > 0 ? points[points.length - 1] ?? null : null;
69
- const previous = points.length >= 2 ? points[points.length - 2] ?? null : null;
70
- return {
71
- keyid: latest?.keyid || fallbackKeyid,
72
- latest: latest ? { asOf: latest.asOf, value: latest.value } : null,
73
- previous: previous ? { asOf: previous.asOf, value: previous.value } : null,
74
- };
47
+ return breaks;
48
+ }
49
+
50
+ /** The newest observation of every series that has a figure (the release writes `*` for a suppressed one). */
51
+ function parseLatest(payload: unknown): Map<string, SeriesPoint> {
52
+ const latest = new Map<string, SeriesPoint>();
53
+ for (const raw of timeseriesRows(payload)) {
54
+ const row = recordOrNull(raw);
55
+ const keyid = textOrNull(row?.keyid) ?? textOrNull(row?.keyId);
56
+ const asOf = isoDateOrNull(row?.asofdate) ?? isoDateOrNull(row?.asOfDate);
57
+ const value = numericOrNull(row?.value);
58
+ if (!keyid || !asOf || value == null) continue;
59
+ const known = latest.get(keyid);
60
+ if (!known || asOf > known.asOf) latest.set(keyid, { asOf, value });
61
+ }
62
+ return latest;
75
63
  }
76
64
 
77
- function seriesUrl(seriesBreak: string, keyid: string): string {
78
- return `https://markets.newyorkfed.org/api/pd/get/${encodeURIComponent(seriesBreak)}/timeseries/${encodeURIComponent(keyid)}.json`;
65
+ /** Every observation of one series, oldest first. */
66
+ function parseSeriesPoints(payload: unknown): SeriesPoint[] {
67
+ const points: SeriesPoint[] = [];
68
+ for (const raw of timeseriesRows(payload)) {
69
+ const row = recordOrNull(raw);
70
+ const asOf = isoDateOrNull(row?.asofdate) ?? isoDateOrNull(row?.asOfDate);
71
+ const value = numericOrNull(row?.value);
72
+ if (asOf && value != null) points.push({ asOf, value });
73
+ }
74
+ return points.sort((a, b) => (a.asOf < b.asOf ? -1 : a.asOf > b.asOf ? 1 : 0));
79
75
  }
80
76
 
81
77
  async function readJson(url: string): Promise<unknown> {
@@ -87,22 +83,101 @@ async function readJson(url: string): Promise<unknown> {
87
83
  return response.json() as Promise<unknown>;
88
84
  }
89
85
 
86
+ /** The series break changes about every two years; one read serves the session. */
87
+ let seriesBreakRead: Promise<string> | null = null;
88
+
89
+ function currentSeriesBreak(force: boolean): Promise<string> {
90
+ if (!seriesBreakRead || force) {
91
+ const read = readJson(`${API}/list/seriesbreaks.json`).then((payload) => {
92
+ const seriesBreak = newestSeriesBreak(parseSeriesBreaks(payload));
93
+ if (!seriesBreak) throw new Error("No series break was listed.");
94
+ return seriesBreak;
95
+ });
96
+ seriesBreakRead = read;
97
+ // A failed read is not kept, so the next one asks again.
98
+ read.catch(() => { if (seriesBreakRead === read) seriesBreakRead = null; });
99
+ }
100
+ return seriesBreakRead;
101
+ }
102
+
103
+ /**
104
+ * The board in two reads: the break the release is in now, and the latest
105
+ * figure of every series in it together with each one's change from the week
106
+ * before. History is not read here; see `loadDealerHistory`.
107
+ */
90
108
  export async function fetchPrimaryDealers(): Promise<DealerBoard> {
91
- const selected = selectSeries(parseCatalog(await readJson(LIST_URL)));
92
- const jobs: Array<{ entry: CatalogEntry; tab: DealerTab }> = [
93
- ...selected.positions.map((entry) => ({ entry, tab: "positions" as const })),
94
- ...selected.fails.map((entry) => ({ entry, tab: "fails" as const })),
95
- ];
96
- if (jobs.length === 0) throw new Error("No position series were listed.");
97
- const rows = (await Promise.all(jobs.map(async ({ entry, tab }) => {
98
- const parsed = parseSeriesObservations(await readJson(seriesUrl(entry.seriesBreak, entry.keyid)), entry.keyid);
99
- return toDealerRow(entry, parsed, tab);
100
- }))).filter((row) => row.latest != null);
109
+ const seriesBreak = await currentSeriesBreak(false);
110
+ const rows = buildDealerRows(parseLatest(await readJson(`${API}/latest/${encodeURIComponent(seriesBreak)}.json`)));
101
111
  if (rows.length === 0) throw new Error("No positions were recognized.");
102
112
  return {
103
- seriesBreak: selected.seriesBreak,
113
+ seriesBreak,
104
114
  positions: rows.filter((row) => row.tab === "positions"),
105
115
  fails: rows.filter((row) => row.tab === "fails"),
106
116
  asOf: boardAsOf(rows),
107
117
  };
108
118
  }
119
+
120
+ interface SeriesRead {
121
+ at: number;
122
+ read: Promise<SeriesPoint[]>;
123
+ points: SeriesPoint[] | null;
124
+ }
125
+
126
+ const seriesReads = new Map<string, SeriesRead>();
127
+
128
+ function seriesRead(seriesBreak: string, key: string, force: boolean): Promise<SeriesPoint[]> {
129
+ const id = `${seriesBreak}:${key}`;
130
+ const known = seriesReads.get(id);
131
+ if (known && !force && Date.now() - known.at < HISTORY_TTL_MS) return known.read;
132
+ const url = `${API}/get/${encodeURIComponent(seriesBreak)}/timeseries/${encodeURIComponent(key)}.json`;
133
+ const entry: SeriesRead = {
134
+ at: Date.now(),
135
+ points: null,
136
+ read: readJson(url).then((payload) => {
137
+ entry.points = parseSeriesPoints(payload);
138
+ return entry.points;
139
+ }),
140
+ };
141
+ seriesReads.set(id, entry);
142
+ // A failed read is not kept, so the next one asks again.
143
+ entry.read.catch(() => { if (seriesReads.get(id) === entry) seriesReads.delete(id); });
144
+ return entry.read;
145
+ }
146
+
147
+ /** A row's history, one read per series behind it, each kept for the session. */
148
+ export async function loadDealerHistory(
149
+ seriesBreak: string,
150
+ row: Pick<DealerRow, "id" | "keys">,
151
+ force = false,
152
+ ): Promise<DealerHistory> {
153
+ const series = await Promise.all(row.keys.map((key) => seriesRead(seriesBreak, key, force)));
154
+ return { rowId: row.id, points: sumSeries(series) };
155
+ }
156
+
157
+ /** The points of every series behind the row when they are all in hand and fresh. */
158
+ function heldSeries(seriesBreak: string, row: Pick<DealerRow, "keys">): SeriesPoint[][] | null {
159
+ const series: SeriesPoint[][] = [];
160
+ for (const key of row.keys) {
161
+ const known = seriesReads.get(`${seriesBreak}:${key}`);
162
+ if (!known?.points || Date.now() - known.at >= HISTORY_TTL_MS) return null;
163
+ series.push(known.points);
164
+ }
165
+ return series;
166
+ }
167
+
168
+ /** Whether the row's history is in hand, so a chart can follow the selection at once. */
169
+ export function hasDealerHistory(seriesBreak: string, row: Pick<DealerRow, "keys">): boolean {
170
+ return heldSeries(seriesBreak, row) != null;
171
+ }
172
+
173
+ /** The history when every series behind the row is already in hand, so a chart can draw before any read. */
174
+ export function cachedDealerHistory(seriesBreak: string, row: Pick<DealerRow, "id" | "keys">): DealerHistory | null {
175
+ const series = heldSeries(seriesBreak, row);
176
+ return series ? { rowId: row.id, points: sumSeries(series) } : null;
177
+ }
178
+
179
+ /** Forgets what was read, for a test or a refresh that must see the release again. */
180
+ export function resetPrimaryDealersCache(): void {
181
+ seriesBreakRead = null;
182
+ seriesReads.clear();
183
+ }
@@ -5,19 +5,26 @@ import { formatChange, formatLevel, type DealerRow } from "./model";
5
5
  const level = (value: unknown) => typeof value === "number" ? formatLevel(value) : "—";
6
6
  const change = (value: unknown) => typeof value === "number" ? formatChange(value) : "—";
7
7
  const text = (value: unknown) => typeof value === "string" && value ? value : "—";
8
+ const group = (value: unknown) => typeof value === "string" ? value : "";
8
9
 
9
10
  const COLUMNS: HeadlessPaneColumn[] = [
10
11
  { key: "position", header: "Position" },
12
+ { key: "group", header: "Group", format: group },
11
13
  { key: "asOf", header: "As of", format: text },
12
14
  { key: "latest", header: "Latest $mn", align: "right", format: level },
13
15
  { key: "previous", header: "Previous $mn", align: "right", format: level },
14
16
  { key: "change", header: "Change $mn", align: "right", format: change },
15
17
  ];
16
18
 
19
+ /** The report's table sets a breakdown in under its total, as the pane does; the export names the total in `group` instead. */
20
+ const TEXT_COLUMNS: HeadlessPaneColumn[] = COLUMNS.filter((column) => column.key !== "group");
21
+
17
22
  function reportRow(row: DealerRow) {
18
23
  return {
19
- id: row.keyid,
24
+ id: row.id,
20
25
  position: row.label,
26
+ group: row.parent,
27
+ series: row.keys.join("+"),
21
28
  asOf: row.asOf,
22
29
  latest: row.latest,
23
30
  previous: row.previous,
@@ -25,6 +32,24 @@ function reportRow(row: DealerRow) {
25
32
  };
26
33
  }
27
34
 
35
+ function textRow(row: DealerRow) {
36
+ return { ...reportRow(row), position: `${" ".repeat(row.depth)}${row.label}` };
37
+ }
38
+
39
+ function section(title: string, rows: readonly DealerRow[]) {
40
+ return {
41
+ title,
42
+ columns: COLUMNS,
43
+ rows: rows.map(reportRow),
44
+ textColumns: TEXT_COLUMNS,
45
+ textRows: rows.map(textRow),
46
+ unit: "USD millions",
47
+ rowSymbolKey: "series",
48
+ rowAsOfKey: "asOf",
49
+ interval: "weekly",
50
+ };
51
+ }
52
+
28
53
  export const primaryDealersHeadless: HeadlessPaneDefinition<"bundle"> = {
29
54
  shape: "bundle",
30
55
  argument: { kind: "none" },
@@ -35,8 +60,8 @@ export const primaryDealersHeadless: HeadlessPaneDefinition<"bundle"> = {
35
60
  aliases: ["PDLR"],
36
61
  dataRequirements: ["Weekly primary dealer positions and settlement fails"],
37
62
  limitations: [
38
- "Treasury and mortgage-backed aggregate totals, not tenor or CUSIP buckets",
39
- "Latest observation and the one before it",
63
+ "Net positions in Treasuries (bills, coupons by tenor, TIPS, FRN), agencies, MBS, corporates, munis and ABS; settlement fails in Treasuries, TIPS and MBS",
64
+ "Latest observation and the change from the week before; the weekly history is in the pane",
40
65
  ],
41
66
  },
42
67
  async load() {
@@ -44,8 +69,8 @@ export const primaryDealersHeadless: HeadlessPaneDefinition<"bundle"> = {
44
69
  return {
45
70
  complete: board.positions.length > 0,
46
71
  sections: [
47
- { title: "Positions", columns: COLUMNS, rows: board.positions.map(reportRow) },
48
- { title: "Fails", columns: COLUMNS, rows: board.fails.map(reportRow) },
72
+ section("Positions", board.positions),
73
+ section("Fails", board.fails),
49
74
  ],
50
75
  metadata: { asOf: board.asOf, seriesBreak: board.seriesBreak },
51
76
  };
@@ -8,8 +8,9 @@ export const primaryDealersModule: PluginModule = {
8
8
  id: "primary-dealers-pane",
9
9
  paneId: PRIMARY_DEALERS_PANE_ID,
10
10
  label: "Primary Dealer Positions",
11
- description: "Weekly net positions and settlement fails in Treasuries and mortgage-backed securities.",
12
- keywords: ["primary", "dealer", "dealers", "positions", "fails", "treasury", "tips", "mbs", "pdlr"],
11
+ description: "Weekly net positions with history, by sector and Treasury tenor, and settlement fails.",
12
+ // No "muni": a muni search lands on DVD's muni funds alone (known-gaps.test.ts), not on a table of two functions.
13
+ keywords: ["primary", "dealer", "dealers", "positions", "fails", "treasury", "bills", "coupons", "tips", "frn", "mbs", "pdlr"],
13
14
  shortcut: { prefix: "PDLR" },
14
15
  headless: primaryDealersHeadless,
15
16
  createInstance: () => ({ placement: "floating" }),