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
@@ -10,6 +10,7 @@ import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActi
10
10
  import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
11
11
  import { useThemeColors } from "../../../theme/theme-context";
12
12
  import { quoteBlockStyle } from "../../../components/ui/quote";
13
+ import { ClippedText } from "../../../components/ui/display";
13
14
  import type { PaneProps } from "../../../types/plugin";
14
15
  import { Box, Text, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
15
16
  import { SignInWall } from "../cloud/auth-actions";
@@ -20,8 +21,8 @@ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/resea
20
21
  import { cachedCompanyData, loadCompanyData, KPI_UNAVAILABLE, validateCompanyData, type CompanyDataset, type CompanyMode } from "./client";
21
22
  import { useCompanyEvidence } from "./screenshot-evidence";
22
23
  import { allGuidance, allObservations, basisLabel, changeText, changeTone, companyCellText, companyColumns, dimensionLabel, directionLabel, directionTone, guideGroupKey, guidanceSeries, fiscalLabel,
23
- isGuidance, exactObservation, observationValue, observationChange, observationChart, periodOrder, outcomeLabel, QUIET_COLUMNS, rangeText, seriesLabel, unitLabel, valueText, companyPreviewNote,
24
- type CompanyRow } from "./model";
24
+ isGuidance, exactObservation, observationValue, observationChange, observationChart, periodOrder, outcomeLabel, QUIET_COLUMNS, rangeText, scopeWords, seriesDetail, seriesLabel, seriesLabels, seriesName,
25
+ seriesOrder, unitLabel, valueText, companyPreviewNote, type CompanyRow } from "./model";
25
26
  import { PriceSparkline } from "../../../components/price-sparkline/view";
26
27
  import type { PricePoint } from "../../../types/financials";
27
28
  import { toneColor, withoutQuietColumns } from "../shared/research-cells";
@@ -41,7 +42,7 @@ function DisclosureEvidence({ row, sourceId, onSource, width }: { row: CompanyRo
41
42
  <KeyValueRow label="Value" value={isGuidance(row) ? rangeText(row) : observationValue(row)} detail={row.currency || row.unit === "volume" || row.unit === "count" ? unitLabel(row) : undefined} labelWidth={18} />
42
43
  <KeyValueRow label="Fiscal period" value={row.period.label} detail={row.period.end ? row.period.start ? `${row.period.start} to ${row.period.end}` : `${row.period.kind === "instant" ? "As of" : "Ended"} ${row.period.end}` : "Calendar dates unavailable"} labelWidth={18} />
43
44
  <KeyValueRow label="Basis" value={basisLabel(row.basis)} labelWidth={18} />
44
- <KeyValueRow label="Scope" value={dimensionLabel(row.dimensions) || "Consolidated"} labelWidth={18} />
45
+ <KeyValueRow label="Scope" value={(desktop ? scopeWords(seriesLabels(row)) : dimensionLabel(row.dimensions)) || "Consolidated"} labelWidth={18} />
45
46
  <KeyValueRow label="Definition" value={row.metric.definition} labelWidth={18} />
46
47
  <KeyValueRow label="Revision" value={`${row.revision}${row.current ? " · current" : " · superseded"}${!isGuidance(row) && (row.conflict || row.contested) ? " · conflicting disclosures" : ""}`} labelWidth={18} />
47
48
  {row.revisionReason ? <KeyValueRow label="Correction" value={row.revisionReason} labelWidth={18} /> : null}
@@ -99,22 +100,24 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
99
100
  const guidance = data && "guidance" in data ? data : null;
100
101
  const [openingTab] = usePaneSettingValue("tab", "table");
101
102
  const [tabValue, setTab] = usePluginPaneState<string>("company:tab", openingTab);
102
- const tab = ["table", "chart", "evidence", ...(guidance ? ["history"] : [])].includes(tabValue) ? tabValue : "table";
103
+ const tab = ["table", "chart", "evidence", ...(mode === "guidance" ? ["history"] : [])].includes(tabValue) ? tabValue : "table";
103
104
  const [openingMetric] = usePaneSettingValue("metric", "all");
104
105
  const [metric, setMetric] = usePluginPaneState<string>("company:metric", openingMetric);
105
106
  const [plotKey, setPlotKey] = usePluginPaneState<string>("company:plot", "");
106
107
  const [selectedId, setSelected] = usePluginPaneState<string | null>(`company:selected:${tab}`, null);
107
108
  const [openId, setOpen] = usePluginPaneState<string | null>("company:open", null);
108
109
  const [sourceId, setSource] = usePluginPaneState<string | null>("company:source", null);
109
- const [sort, setSort] = usePluginPaneState<{ column: string; direction: "asc" | "desc" }>("company:sort", { column: "period", direction: "desc" });
110
+ // The desktop lists KPIs by metric, each series named in plain words (GLO-1046); the terminal keeps its frames.
111
+ const [sort, setSort] = usePluginPaneState<{ column: string; direction: "asc" | "desc" }>("company:sort",
112
+ desktop && mode === "kpis" ? { column: "metric", direction: "asc" } : { column: "period", direction: "desc" });
110
113
  const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
111
114
  const observations = useMemo(() => kpis ? allObservations(kpis) : [], [kpis]);
112
115
  const guides = useMemo(() => guidance ? allGuidance(guidance) : [], [guidance]);
113
116
  const allRows = useMemo<CompanyRow[]>(() => [...observations, ...guides], [observations, guides]);
114
117
  const eligible = useMemo(() => allRows.filter((row) => metric === "all" || row.metricId === metric), [allRows, metric]);
115
118
  const plotOptions = useMemo(() => mode === "kpis"
116
- ? [...new Map(eligible.filter((row) => !isGuidance(row)).map((row) => [`${row.seriesKey}:${row.period.kind}`, { value: `${row.seriesKey}:${row.period.kind}`, label: `${seriesLabel(row)} · ${row.period.kind}` }])).values()]
117
- : [...new Map(eligible.filter(isGuidance).map((row) => [guideGroupKey(row), { value: guideGroupKey(row), label: `${seriesLabel(row)} · ${row.period.label}` }])).values()], [eligible, mode]);
119
+ ? [...new Map(eligible.filter((row) => !isGuidance(row)).map((row) => [`${row.seriesKey}:${row.period.kind}`, { value: `${row.seriesKey}:${row.period.kind}`, label: `${desktop ? seriesName(row) : seriesLabel(row)} · ${row.period.kind}` }])).values()]
120
+ : [...new Map(eligible.filter(isGuidance).map((row) => [guideGroupKey(row), { value: guideGroupKey(row), label: `${desktop ? seriesName(row) : seriesLabel(row)} · ${row.period.label}` }])).values()], [eligible, mode, desktop]);
118
121
  const activePlot = plotOptions.some((option) => option.value === plotKey) ? plotKey : plotOptions[0]?.value ?? "";
119
122
  const rows = useMemo(() => {
120
123
  const values = eligible.filter((row) => tab === "chart" ? (isGuidance(row) ? guideGroupKey(row) : `${row.seriesKey}:${row.period.kind}`) === activePlot
@@ -130,11 +133,13 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
130
133
  : sort.column === "quote" ? row.evidence[0]!.quote : sort.column === "change" ? guide ? guide.direction : observationChange(row as KpiObservation, observations).value
131
134
  : periodOrder(row);
132
135
  };
136
+ const direction = sort.direction === "asc" ? 1 : -1;
137
+ if (desktop && sort.column === "metric") return [...values].sort((a, b) => (seriesOrder(a, b) || periodOrder(b).localeCompare(periodOrder(a)) || a.id.localeCompare(b.id)) * direction);
133
138
  return [...values].sort((a, b) => { const x = key(a), y = key(b);
134
139
  return x === null || y === null ? x === y ? a.id.localeCompare(b.id) : x === null ? 1 : -1
135
- : (x < y ? -1 : x > y ? 1 : a.metric.name.localeCompare(b.metric.name) || dimensionLabel(a.dimensions).localeCompare(dimensionLabel(b.dimensions)) || a.id.localeCompare(b.id)) * (sort.direction === "asc" ? 1 : -1);
140
+ : (x < y ? -1 : x > y ? 1 : a.metric.name.localeCompare(b.metric.name) || dimensionLabel(a.dimensions).localeCompare(dimensionLabel(b.dimensions)) || a.id.localeCompare(b.id)) * direction;
136
141
  });
137
- }, [eligible, tab, activePlot, guidance, kpis, sort, observations]);
142
+ }, [eligible, tab, activePlot, guidance, kpis, sort, observations, desktop]);
138
143
  const selected = rows.find((row) => row.id === selectedId) ?? rows[0];
139
144
  const openRow = allRows.find((row) => row.id === openId);
140
145
  const activeRow = openRow ?? selected;
@@ -153,15 +158,16 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
153
158
  }
154
159
  return map;
155
160
  }, [kpis]);
156
- const columns = useMemo(() => withoutQuietColumns(companyColumns(mode === "guidance", tab === "evidence", tab === "history", mode === "kpis" && tab === "table" && trends.size > 0 && width >= 100),
157
- rows, (row, id) => id === "basis" ? basisLabel(row.basis) : id === "scope" ? dimensionLabel(row.dimensions) || "Consolidated" : "", QUIET_COLUMNS), [mode, tab, trends, rows, width]);
161
+ const columns = useMemo(() => withoutQuietColumns(companyColumns(mode === "guidance", tab === "evidence", tab === "history", mode === "kpis" && tab === "table" && trends.size > 0 && width >= 100, desktop),
162
+ rows, (row, id) => id === "basis" ? basisLabel(row.basis) : id === "scope" ? dimensionLabel(row.dimensions) || "Consolidated" : "", QUIET_COLUMNS), [mode, tab, trends, rows, width, desktop]);
158
163
  const metricOptions = useMemo(() => [{ value: "all", label: "All metrics" }, ...[...new Map(allRows.map((row) => [row.metricId, { value: row.metricId, label: row.metric.name }])).values()]], [allRows]);
159
164
  const kpiChart = useMemo(() => observationChart(rows.filter((row): row is KpiObservation => !isGuidance(row)), colors.warning), [rows, colors.warning]);
160
165
  const series = useMemo(() => tab !== "chart" ? [] : mode === "kpis" ? kpiChart.series
161
166
  : guidanceSeries(guides, selected && isGuidance(selected) ? selected : undefined, { low: colors.textMuted, high: colors.warning, actual: colors.positive }), [tab, mode, kpiChart, guides, selected, colors]);
162
167
  const link = useChartTableSelection({ rows, getId: (row) => row.id, getDate: (row) => isGuidance(row) ? new Date(row.issuedDate) : kpiChart.dates.get(row.id) ?? null,
163
168
  selectedId: selected?.id ?? null, onSelect: setSelected, focused: focused && !openRow, enabled: tab === "chart" });
164
- const { strip, rows: tabRows } = usePaneTabs(data ? { tabs: [{ value: "table", label: mode === "guidance" ? "Tracker" : "Table" }, { value: "chart", label: "Chart" },
169
+ const signInWall = !data && isCloudSessionRequired(resource.error);
170
+ const { strip, rows: tabRows } = usePaneTabs(symbol && !signInWall ? { tabs: [{ value: "table", label: mode === "guidance" ? "Tracker" : "Table" }, { value: "chart", label: "Chart" },
165
171
  ...(mode === "guidance" ? [{ value: "history", label: "History" }] : []), { value: "evidence", label: "Evidence" }], activeValue: tab,
166
172
  onSelect: (value) => { setTab(value); setOpen(null); }, focused, dense: true } : null);
167
173
  useAutoRefresh(resource.updatedAt, resource.load);
@@ -185,12 +191,12 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
185
191
  data?.truncated && data.access === "full" ? "The response reached its stored-record limit. Narrow the metric or date range in a CLI or REST query." : null,
186
192
  data?.coverage.conflicts ? "Conflicting disclosures are retained in Evidence and excluded from comparable trends." : null].filter((value): value is string => !!value) });
187
193
  if (!symbol) return <EmptyState title="Select a ticker." />;
188
- if (!data && isCloudSessionRequired(resource.error)) return <SignInWall placement="company-kpis-signin" action={mode === "guidance" ? "view company guidance" : "view company KPIs"} needsVerification={session.needsVerification} />;
194
+ if (signInWall) return <SignInWall placement="company-kpis-signin" action={mode === "guidance" ? "view company guidance" : "view company KPIs"} needsVerification={session.needsVerification} />;
189
195
  const bodyHeight = Math.max(3, height - tabRows);
190
196
  const query = <QueryBar width={width} filters={[{ id: "metric", label: "Metric", value: metric, defaultValue: "all", options: metricOptions,
191
197
  onChange: (value: string) => { setMetric(value); setSelected(null); setOpen(null); } }, ...(tab === "chart" && plotOptions.length ? [{ id: "series", label: "Series", value: activePlot,
192
198
  options: plotOptions, onChange: (value: string) => { setPlotKey(value); setSelected(null); } }] : [])]} />;
193
- const renderCell = (item: Item, column: (typeof columns)[number]): DataTableCell => {
199
+ const renderCell = (item: Item, column: (typeof columns)[number], _index: number, state: { selected: boolean }): DataTableCell => {
194
200
  const row = item.row;
195
201
  if (!row) {
196
202
  if (isNote(item)) return { text: "" };
@@ -210,6 +216,15 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
210
216
  const change = observationChange(row as KpiObservation, observations);
211
217
  return { text: changeText(change), value: change.value, color: toneColor(changeTone(row as KpiObservation, change.value), colors) };
212
218
  }
219
+ if (desktop && column.id === "metric") {
220
+ // The series in plain words: the metric bright, what tells it apart dim. The detail starts from no width and takes
221
+ // what the name leaves, so it gives way first and the name is cut only when it alone does not fit.
222
+ const detail = seriesDetail(row);
223
+ return { text: detail ? `${row.metric.name} ${detail}` : row.metric.name, content: <Box flexDirection="row" height={1} overflow="hidden" flexShrink={1} minWidth={0}>
224
+ <ClippedText fg={state.selected ? colors.selectedText : colors.textBright} text={row.metric.name} width={column.width} />
225
+ {detail ? <ClippedText fg={state.selected ? colors.selectedText : colors.textDim} lead=" " text={detail} width={column.width} flex="1 1 0%" /> : null}
226
+ </Box> };
227
+ }
213
228
  const text = companyCellText(row, column.id, observations);
214
229
  return { text, ...(column.id === "value" && !guide ? { value: exactObservation(row as KpiObservation) ? (row as KpiObservation).value : observationValue(row as KpiObservation) } : column.id === "actual" ? { value: guide?.actual?.value } : {}),
215
230
  color: column.id === "metric" ? colors.textBright : column.id === "value" || column.id === "actual" ? colors.textBright
@@ -225,7 +240,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
225
240
  selection={{ kind: "id", selectedId: selected?.id ?? null, getId, onChange: setSelected }}
226
241
  onActivate={(item) => item.row ? setOpen(item.row.id) : isNote(item) ? undefined : openUpgrade()} renderCell={renderCell}
227
242
  isNavigable={(item) => !isNote(item)} renderSectionHeader={(item) => "note" in item && item.note ? { text: item.note } : null} freezeFirstColumn selectedTextOverridesCellColor showHorizontalScrollbar
228
- detailOpen={!!openRow} onBack={() => setOpen(null)} detailTitle={openRow ? `${openRow.metric.name} · ${fiscalLabel(openRow)}` : undefined}
243
+ detailOpen={!!openRow} onBack={() => setOpen(null)} detailTitle={openRow ? `${desktop ? seriesName(openRow) : openRow.metric.name} · ${fiscalLabel(openRow)}` : undefined}
229
244
  detailScrollRef={detailScrollRef} detailContent={openRow ? <DetailScrollBody ref={detailScrollRef} resetScrollKey={openRow.id}><DisclosureEvidence row={openRow} sourceId={sourceId} onSource={setSource} width={width} /></DetailScrollBody> : null}
230
245
  sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((previous) => ({ column, direction: previous.column === column && previous.direction === "desc" ? "asc" : "desc" }))}
231
246
  resetScrollKey={`${mode}:${symbol}:${tab}:${metric}:${activePlot}`}
@@ -10,6 +10,7 @@ import { supplyChainModule } from "./supply-chain";
10
10
  import { awardsModule } from "./awards";
11
11
  import { cryptoBoardModule } from "./crypto-board";
12
12
  import { portfolioAnalyticsModule } from "./analytics";
13
+ import { brokerTradingModule } from "./broker-trading";
13
14
  import { brokerManagerModule } from "./broker-manager";
14
15
  import { changelogModule } from "./changelog";
15
16
  import { connectionsModule } from "./connections";
@@ -189,9 +190,9 @@ export const brokerPlugin = composeBuiltinPlugin({
189
190
  id: "broker",
190
191
  name: "Broker",
191
192
  version: "1.0.0",
192
- description: "Broker profiles, account sync, and connection status.",
193
+ description: "Broker profiles, account sync, order review, and trading status.",
193
194
  toggleable: true,
194
- modules: [brokerManagerModule],
195
+ modules: [brokerManagerModule, brokerTradingModule],
195
196
  });
196
197
 
197
198
  export const globalMarketsPlugin = composeBuiltinPlugin({
@@ -1,6 +1,7 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/headless";
2
2
  import type { TimeRange } from "../../../time-series/range";
3
3
  import { formatNumber, formatSignificant } from "../../../utils/format";
4
+ import { logFailure } from "../../../utils/log-failure";
4
5
  import { mixedSessionCloseNote } from "../../../market-data/market/session-close-note";
5
6
  import { resolveHeadlessInstrument, loadHeadlessSymbols } from "../shared/headless-market-data";
6
7
  import { buildCorrelationMatrix, buildCorrelationSeries, buildGeoCorrelationSeries, pairKey, type CorrelationSeries } from "./matrix/model";
@@ -27,7 +28,7 @@ async function loadListedHistory(ctx: HeadlessPaneContext, key: string, range: T
27
28
  const { symbol, exchange } = await resolveHeadlessInstrument(ctx, key);
28
29
  const [points, quote] = await Promise.all([
29
30
  loadHistory(ctx, key, range),
30
- exchange ? null : (async () => ctx.marketData.getQuote(symbol, "", ctx.refresh ? { cacheMode: "refresh" } : undefined))().catch(() => null),
31
+ exchange ? null : (async () => ctx.marketData.getQuote(symbol, "", ctx.refresh ? { cacheMode: "refresh" } : undefined))().catch(logFailure("correlation", "Listing exchange quote failed", null)),
31
32
  ]);
32
33
  return { exchange: exchange || quote?.listingExchangeName || quote?.exchangeName || "", points };
33
34
  }
@@ -242,6 +242,7 @@ export function relationshipTemplateSymbols(
242
242
  try {
243
243
  return normalizeRelationshipSymbols(parseTickerListInput(raw));
244
244
  } catch {
245
+ // Intentional ignore: invalid template input disables creation through canCreate and creates no pane.
245
246
  return null;
246
247
  }
247
248
  }
@@ -34,6 +34,7 @@ function coerceStoredSymbols(value: unknown): string[] {
34
34
  try {
35
35
  return parseCorrelationSymbolsInput(text, MAX_CORRELATION_TICKERS);
36
36
  } catch {
37
+ // Intentional ignore: malformed legacy symbols fall back to saved text or the default preset.
37
38
  return [];
38
39
  }
39
40
  }
@@ -29,7 +29,7 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
29
29
  shape: "bundle",
30
30
  // Tuesday positions published the Friday after; two weeks without a new report means one was missed.
31
31
  freshness: { source: "CFTC", status: "not-a-feed", basis: "weekly report", maxAgeMinutes: 14 * 24 * 60 },
32
- argument: { kind: "free-text", optional: true, placeholder: "code or root", description: "CFTC market code or verified futures root; omit for the extremes board." },
32
+ argument: { kind: "free-text", optional: true, placeholder: "code or name", description: "Futures root (KC), commodity name (coffee) or CFTC market code; omit for the extremes board." },
33
33
  options: [{ key: "report", type: "enum", values: [{ value: "legacy" }, { value: "disaggregated" }], defaultValue: "legacy", description: "CFTC report family." },
34
34
  { key: "traderClass", type: "enum", values: ["noncommercial", "commercial", "producer", "swap", "managed-money", "other-reportable", "nonreportable"].map((value) => ({ value })), description: "Trader class for the extremes board; defaults by report family.",
35
35
  pluginState: { pluginId: COMMODITIES_VIEW_STATE.cot, key: "class" } },
@@ -13,7 +13,7 @@ export const cotModule: PluginModule = {
13
13
  paneTemplates: [{ id: "cot-cot", paneId: "cot", label: "CFTC Positioning",
14
14
  description: "Weekly futures positioning, changes, historical percentiles and cross-market extremes.",
15
15
  keywords: ["cot", "cftc", "positioning", "commitments", "managed money", "net spec", "crude oil", "natural gas"],
16
- shortcut: { prefix: "COT", aliases: ["CFTC"], argKind: "text" as const, argPlaceholder: "code or root", argOptional: true,
16
+ shortcut: { prefix: "COT", aliases: ["CFTC"], argKind: "text" as const, argPlaceholder: "code or name", argOptional: true,
17
17
  argOptions: () => COT_ROOT_OPTIONS }, headless: cotHeadless,
18
18
  createInstance: (context, options) => {
19
19
  const input = options?.arg?.trim();
@@ -5,7 +5,7 @@ import type { PricePoint } from "../../../types/financials";
5
5
  import { formatPriceObservation } from "../../../market-data/market/format";
6
6
  import { cleanFloat32Price } from "../../../cli/history-rows";
7
7
  import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
8
- import { FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, type FuturesContract } from "../../../market-data/futures-board";
8
+ import { bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByName, type FuturesContract } from "../../../market-data/futures-board";
9
9
 
10
10
  export const COT_CLASSES: Record<CotFamily, Array<{ value: CotClass; label: string }>> = {
11
11
  legacy: [{ value: "noncommercial", label: "Noncommercial" }, { value: "commercial", label: "Commercial" }, { value: "nonreportable", label: "Nonreportable" }],
@@ -124,25 +124,48 @@ export function cotScope(value: unknown): CotScope { return value === "all" ? "a
124
124
  const rootAlias = (input: string) => ROOTS[input === "VIX" ? "VX" : input]?.code ?? null;
125
125
  const marketCode = (input: string) => /^[0-9A-Z]{5}[0-9A-Z+]$/.test(input) ? input : null;
126
126
  /**
127
- * A CFTC code or root typed as an argument or stored in a pane: every alias,
128
- * crypto included, and every FUT board root the CFTC reports. A typed root is
129
- * always the future, so ZM here is soybean meal, never Zoom.
127
+ * The FUT board row a typed argument names, by code, symbol or commodity name
128
+ * as FUT reads them (CC, CC=F, cocoa), or a verified root named the same way
129
+ * (bitcoin, VIX, SOFR, fed funds).
130
+ */
131
+ function typedMarket(value: string): { board: FuturesContract } | { root: string } | null {
132
+ const board = futuresBoardContract(value) ?? futuresContractByName(value);
133
+ if (board) return { board };
134
+ const root = bestNameMatch(Object.keys(ROOTS), (key) => [ROOTS[key]!.name], value);
135
+ return root ? { root } : null;
136
+ }
137
+ /**
138
+ * A CFTC code, root or commodity name typed as an argument or stored in a
139
+ * pane: every alias, crypto included, every FUT board root the CFTC reports,
140
+ * and the names FUT finds them by (cocoa, coffee, sugar, gold, crude). A typed
141
+ * root is always the future, so ZM here is soybean meal, never Zoom. A CFTC
142
+ * code is tried after the names, as one could be spelled with six letters
143
+ * (COFFEE, COPPER).
130
144
  */
131
145
  export function cotContractCode(value: unknown): string | null {
132
146
  if (typeof value !== "string") return null;
133
147
  const input = value.trim().toUpperCase().replace(/=F$/, "");
134
- const board = futuresBoardContract(value);
135
- return rootAlias(input) ?? (board ? boardMarketCode(board) : null) ?? marketCode(input);
148
+ const alias = rootAlias(input);
149
+ if (alias) return alias;
150
+ const market = typedMarket(value);
151
+ if (market && "root" in market) return ROOTS[market.root]!.code;
152
+ return (market ? boardMarketCode(market.board) : null) ?? (market ? null : marketCode(input));
136
153
  }
154
+ /** The markets the unknown-market hint names, by the code and the word a newcomer knows them by. */
155
+ const SUGGESTED_MARKETS: ReadonlyArray<readonly [string, string]> = [
156
+ ["ES", "S&P 500"], ["CL", "crude oil"], ["NG", "natural gas"], ["GC", "gold"], ["HG", "copper"],
157
+ ["ZC", "corn"], ["CC", "cocoa"], ["KC", "coffee"], ["SB", "sugar"], ["ZN", "10-year note"],
158
+ ];
137
159
  /**
138
- * What a market `cotContractCode` does not know is told: a FUT board root the
139
- * CFTC does not report says so; anything else gets roots and a code that work.
160
+ * What a market `cotContractCode` does not know is told: a FUT board row the
161
+ * CFTC does not report says so; anything else gets codes and names that work,
162
+ * softs and metals among them, and a CFTC code.
140
163
  */
141
164
  export function cotUnknownMarketMessage(value: unknown): string {
142
- const board = futuresBoardContract(value);
143
- if (board) return `${board.code} (${board.name}) is not reported in the CFTC COT.`;
144
- const roots = ["ES", "CL", "GC", "ZN"].filter((root) => ROOTS[root]);
145
- return `Unknown market "${String(value)}". Try ${roots.join(", ")} or a CFTC market code such as ${ROOTS.GC!.code}.`;
165
+ const market = typeof value === "string" ? typedMarket(value) : null;
166
+ if (market && "board" in market) return `${market.board.code} (${market.board.name}) is not reported in the CFTC COT.`;
167
+ const known = SUGGESTED_MARKETS.map(([code, name]) => `${code} ${name}`).join(", ");
168
+ return `Unknown market "${String(value)}". Try a code or name such as ${known}, or a CFTC market code such as ${ROOTS.GC!.code}.`;
146
169
  }
147
170
  /**
148
171
  * The code for the ticker under the cursor, when COT is opened without an
@@ -82,8 +82,8 @@ function CotBoard({ width, height, focused, family, initialCode }: PaneProps & {
82
82
  }, [data, query, scope, sort]);
83
83
  useAutoRefresh(resource.updatedAt, resource.load);
84
84
  // The class, scope and search controls live in the board's query bar, so
85
- // their keys only work once the board is on screen.
86
- const boardShown = !!data && !open;
85
+ // their keys work while it is on screen: over the board and while a class loads.
86
+ const boardShown = !open;
87
87
  useShortcut((event) => {
88
88
  if (!focused || open || event.targetEditable || event.ctrl || event.meta || event.alt || event.super) return;
89
89
  if (event.name === "r") { event.preventDefault(); void resource.reload(); }
@@ -99,8 +99,9 @@ function CotBoard({ width, height, focused, family, initialCode }: PaneProps & {
99
99
  usePaneNoticeFooter({ registrationId: "cot:board-notices", focused, enabled: !open, notices: data?.gaps ?? [] });
100
100
  usePaneStatusFooter({ registrationId: "cot:board", enabled: !open, loading: resource.loading, error: resource.error,
101
101
  info: data ? [{ id: "as-of", parts: [{ text: `CFTC futures only · ${data.asOf ?? "--"} · contracts`, tone: "muted" }] }] : [] });
102
- if (!data && !open) return <PaneStatusBody loading={resource.loading} error={resource.error} empty={!resource.loading && !resource.error} subject="COT history" />;
102
+ // A class change reloads under the query bar, which stays.
103
103
  return <DataTableStackView<CotBoardRow> columns={BOARD_COLUMNS} items={rows} focused={focused && !searching}
104
+ status={data ? undefined : { loading: resource.loading, error: resource.error, empty: !resource.loading && !resource.error, subject: "COT history" }}
104
105
  rootWidth={width} rootHeight={height} selection={{ kind: "id", selectedId: selected, getId: (row) => row.contractCode, onChange: setSelected }}
105
106
  getItemKey={(row) => row.contractCode} onActivate={(row) => setOpen(row.contractCode)} freezeFirstColumn
106
107
  sortable sortColumnId={sort.columnId} sortDirection={sort.direction} onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id, { firstDirection: "desc" }))}
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  MarketBoardStack,
@@ -107,7 +108,7 @@ export function CdxPane({ focused, width, height }: PaneProps) {
107
108
  const asOf = formatAsOf(payload?.asOf ?? null);
108
109
  const info = useMemo<PaneFooterSegment[]>(() => [
109
110
  ...(asOf ? [{ id: "as-of", parts: [{ text: `as of ${asOf}`, tone: "muted" as const }] }] : []),
110
- ...(payload ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
111
+ ...(payload ? [DELAYED_SEGMENT] : []),
111
112
  ...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
112
113
  ], [asOf, payload, resource.data?.stale]);
113
114
  usePaneStatusFooter({
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  formatBpAxis,
@@ -112,7 +113,7 @@ export function SovrPane({ focused, width, height }: PaneProps) {
112
113
  const asOf = formatAsOf(payload?.asOf ?? null);
113
114
  const info = useMemo<PaneFooterSegment[]>(() => [
114
115
  ...(asOf ? [{ id: "as-of", parts: [{ text: `as of ${asOf}`, tone: "muted" as const }] }] : []),
115
- ...(payload ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
116
+ ...(payload ? [DELAYED_SEGMENT] : []),
116
117
  ...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
117
118
  ], [asOf, payload, resource.data?.stale]);
118
119
  usePaneStatusFooter({
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  formatBpAxis,
@@ -115,7 +116,7 @@ export function CreditConditionsPane({ paneId, focused, width, height }: PanePro
115
116
  notices: mixedDates ? ["The indexes carry different observation dates, so the spreads are not one session."] : [],
116
117
  });
117
118
  const footerInfo = useMemo<PaneFooterSegment[]>(() => [
118
- ...(rows.length > 0 ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
119
+ ...(rows.length > 0 ? [DELAYED_SEGMENT] : []),
119
120
  ...(partial ? [{ id: "partial", parts: [{ text: `PARTIAL ${rows.length}/${series.length}`, tone: "warning" as const, bold: true }] }] : []),
120
121
  ...(stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
121
122
  ...loadingErrorFooterInfo(loading, error),
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react";
2
2
  import { isAccessDenied } from "../../../api-client/errors";
3
3
  import { usePlanAccess } from "../../../api-client/plan-access";
4
4
  import type { CreditDocumentsPayload, CreditHeadroom, CreditInstrument, CreditScreenPayload, CreditScreenRow } from "../../../api-client/credit-documents";
5
- import { ChartTableHeader, DataTableView, EmptyState, PageStackView, PaneStatusBody, QueryBar, useChartTableSelection, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint } from "../../../components";
5
+ import { ChartTableHeader, DataTableView, EmptyState, PageStackView, QueryBar, useChartTableSelection, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint } from "../../../components";
6
6
  import { formatCompactAxis } from "../../../components/chart-table";
7
7
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
8
8
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
@@ -170,18 +170,19 @@ function CreditView({ symbol, width, height, focused, accessKey, needsVerificati
170
170
  return { text, value, color: column.id === columns[0]!.id ? colors.textBright : colors.text };
171
171
  };
172
172
  return <Box width={width} height={height} flexDirection="column">{strip}
173
- <PaneStatusBody loading={!available && loading} error={!available && currentError !== CREDIT_UNAVAILABLE ? currentError : null}
174
- empty={!available && currentError === CREDIT_UNAVAILABLE} emptyTitle={CREDIT_UNAVAILABLE} subject="credit documents">
175
- {available ? <PageStackView focused={focused && !openFactId} detailOpen={!!openInstrument} onBack={() => setOpen(null)} detailTitle={openInstrument?.name}
173
+ <PageStackView focused={focused && !openFactId} detailOpen={!!openInstrument} onBack={() => setOpen(null)} detailTitle={openInstrument?.name}
176
174
  detailContent={openInstrument && activeSymbol ? <CreditInstrumentDetail key={`${activeSymbol}:${openInstrument.id}`} symbol={activeSymbol} instrument={openInstrument} width={width} height={bodyHeight - 1} focused={focused} accessKey={accessKey} view={view} setView={setView} covenant={activeCovenant} openId={openFactId} setOpen={setOpenFact} captured={instrumentSnapshot?.id === openInstrument.id ? instrumentSnapshot : null} pro={data?.access === "full"} onUpgrade={openUpgrade} /> : null}
177
- rootContent={<DataTableView<Row> columns={columns} items={items} sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((current) => ({column,direction:current.column === column && current.direction === "asc" ? "desc" : "asc"}))} focused={focused && !openInstrument} rootWidth={width} rootHeight={bodyHeight} rootBefore={rootBefore}
175
+ rootContent={<DataTableView<Row> columns={columns} items={available ? items : []}
176
+ // The Screen tab and its thresholds load under the query bar, which stays.
177
+ status={available ? undefined : { loading, error: currentError !== CREDIT_UNAVAILABLE ? currentError : null,
178
+ empty: currentError === CREDIT_UNAVAILABLE, emptyTitle: CREDIT_UNAVAILABLE, subject: "credit documents" }}
179
+ sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((current) => ({column,direction:current.column === column && current.direction === "asc" ? "desc" : "asc"}))} focused={focused && !openInstrument} rootWidth={width} rootHeight={bodyHeight} rootBefore={rootBefore}
178
180
  getItemKey={rowId} selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelected }}
179
181
  onActivate={(row) => row.locked ? openUpgrade() : setOpen(row.screen ? row.id : row.instrumentId ?? null)}
180
182
  renderCell={renderCell}
181
183
  emptyStateTitle={tab === "screen" ? "No supported credit signals match these thresholds." : tab === "covenants" ? "No financial maintenance covenants documented." : tab === "maturities" ? "No supported instrument maturity amounts." : CREDIT_UNAVAILABLE}
182
184
  emptyStateHint={tab === "screen" && (headroom < 100 || months < 36) ? "Raise either threshold to widen the screen." : undefined}
183
185
  selectedTextOverridesCellColor showHorizontalScrollbar freezeFirstColumn resetScrollKey={`${symbol}:${tab}:${selectedCurrency}`}
184
- bodyAfter={locked > 0 && desktop ? <LockedOverlay rows={Math.min(3, locked)} text={lockedText} onPress={openUpgrade} /> : undefined} />} /> : null}
185
- </PaneStatusBody>
186
+ bodyAfter={locked > 0 && desktop ? <LockedOverlay rows={Math.min(3, locked)} text={lockedText} onPress={openUpgrade} /> : undefined} />} />
186
187
  </Box>;
187
188
  }
@@ -84,7 +84,34 @@ function withValidStablecoinFields(asset: CryptoMarketAsset): CryptoMarketAsset
84
84
  return { ...asset, ...(peg ? { peg: null } : {}), ...(range ? { range24h: null } : {}) };
85
85
  }
86
86
 
87
- export function validateCryptoMarkets(data: CryptoMarketsPayload): CryptoMarketsPayload {
87
+ const DAY_MS = 86_400_000;
88
+ /** How many UTC days old a coin's latest daily close may be for its previous close to stand, as Gloom Cloud judges a coin quote's. */
89
+ const PREVIOUS_CLOSE_MAX_AGE_DAYS = 2;
90
+
91
+ /**
92
+ * A coin of a board from before Gloom Cloud measured its moves since 00:00
93
+ * UTC (a server not yet updated, a copy cached from one), whose change runs
94
+ * over the screener's 24 hours: moved from its previous close instead, the
95
+ * close of its last UTC day while its daily closes reach the last two days.
96
+ * Otherwise that close may be weeks old (Toncoin on 18 Oct 2026), and the
97
+ * coin has no move.
98
+ */
99
+ function onUtcDayBasis(asset: CryptoMarketAsset, now: number): CryptoMarketAsset {
100
+ const history = asset.history;
101
+ const last = history?.closes.findLastIndex((close) => close != null) ?? -1;
102
+ const lastDay = history && last >= 0 ? Date.parse(`${history.start}T00:00:00Z`) / DAY_MS + last : null;
103
+ const holds = lastDay != null && asset.previousClose != null && asset.previousClose > 0
104
+ && Math.floor(now / DAY_MS) - lastDay <= PREVIOUS_CLOSE_MAX_AGE_DAYS;
105
+ const change = holds ? asset.price - asset.previousClose! : null;
106
+ return {
107
+ ...asset,
108
+ previousClose: holds ? asset.previousClose : null,
109
+ change,
110
+ changePercent: change != null ? (change / asset.previousClose!) * 100 : null,
111
+ };
112
+ }
113
+
114
+ export function validateCryptoMarkets(data: CryptoMarketsPayload, now = Date.now()): CryptoMarketsPayload {
88
115
  if (
89
116
  !data
90
117
  || data.version !== 1
@@ -103,7 +130,11 @@ export function validateCryptoMarkets(data: CryptoMarketsPayload): CryptoMarkets
103
130
  ) {
104
131
  throw new Error("The server returned an invalid crypto board");
105
132
  }
106
- const assets = data.assets.map(withValidStablecoinFields);
133
+ const legacy = data.dayChangeBasis !== "utc-day";
134
+ const assets = data.assets.map((asset) => {
135
+ const valid = withValidStablecoinFields(asset);
136
+ return legacy ? onUtcDayBasis(valid, now) : valid;
137
+ });
107
138
  return assets.every((asset, index) => asset === data.assets[index]) ? data : { ...data, assets };
108
139
  }
109
140
 
@@ -7,6 +7,7 @@ import { fetchCryptoMarkets, fetchStablecoinSupply } from "./client";
7
7
  import {
8
8
  buildCryptoRows,
9
9
  CRYPTO_BOARD_FRESHNESS,
10
+ CRYPTO_CHANGE_BASIS_LABEL,
10
11
  formatCryptoPercent,
11
12
  formatDayRangeTime,
12
13
  formatOffPeg,
@@ -30,7 +31,9 @@ const COIN_COLUMNS: HeadlessPaneColumn[] = [
30
31
  { key: "code", header: "Coin" },
31
32
  { key: "name", header: "Name" },
32
33
  { key: "price", header: "Price", align: "right", format: (_value, row) => String(row.priceText ?? "—") },
33
- { key: "changePercent", header: "Chg %", align: "right", format: percent },
34
+ // Since 00:00 UTC, the day `quote` gives a coin (CRYPTO_CHANGE_BASIS_LABEL): the text says so, as the pane's
35
+ // CHG% UTC does, and an export keeps the header it has always had.
36
+ { key: "changePercent", header: "Chg %", textHeader: "Chg % UTC", align: "right", format: percent },
34
37
  { key: "return7d", header: "7D %", align: "right", format: percent },
35
38
  { key: "return30d", header: "30D %", align: "right", format: percent },
36
39
  { key: "return1y", header: "1Y %", align: "right", format: percent },
@@ -167,7 +170,7 @@ export const cryptoBoardHeadless: HeadlessPaneDefinition<"rows"> = {
167
170
  pegType: row.pegType,
168
171
  } : {}),
169
172
  }));
170
- const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt };
173
+ const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt, changeBasis: CRYPTO_CHANGE_BASIS_LABEL };
171
174
  if (kind !== "stablecoin") return { rows, errors: data.warnings, metadata };
172
175
  const summary = supply ? supplySummaryLine(supply) : null;
173
176
  const history = supply ? supplyHistoryTable(supply) : null;
@@ -57,6 +57,11 @@ export interface CryptoRow extends CryptoRowSupply {
57
57
  price: number;
58
58
  /** The price as the board prints it. */
59
59
  priceText: string;
60
+ /**
61
+ * The move since 00:00 UTC, from the previous UTC day's close, as `quote`
62
+ * measures a coin (CRYPTO_CHANGE_BASIS_LABEL); null when the coin has no
63
+ * such close to measure from.
64
+ */
60
65
  changePercent: number | null;
61
66
  return7d: number | null;
62
67
  return30d: number | null;
@@ -100,9 +105,28 @@ function closeDaysAgo(asset: CryptoMarketAsset, days: number, now: number): numb
100
105
  return isFiniteNumber(close) && close > 0 ? close : null;
101
106
  }
102
107
 
103
- const percentChange = (price: number, reference: number | null) =>
108
+ const percentChange = (price: number, reference: number | null | undefined) =>
104
109
  reference != null && reference > 0 ? (price / reference - 1) * 100 : null;
105
110
 
111
+ /** What the board's day change is measured from, as `quote` labels a coin's: its header says it (CHG% UTC). */
112
+ export const CRYPTO_CHANGE_BASIS_LABEL = "since 00:00 UTC";
113
+
114
+ /**
115
+ * A coin's move since 00:00 UTC, the basis `quote` gives a coin: its live
116
+ * quote's while that quote is measured from the previous close, otherwise the
117
+ * board's, which Gloom Cloud measures the same way (with a live price, from
118
+ * the board's previous close). A coin either says moves over 24 hours, or not
119
+ * at all, has none here, so the column never mixes the two.
120
+ */
121
+ function utcDayChangePercent(asset: CryptoMarketAsset, quote: Quote | null, price: number): number | null {
122
+ if (quote && quote.changeBasis == null) {
123
+ if (isFiniteNumber(quote.changePercent)) return quote.changePercent;
124
+ if (positive(quote.previousClose)) return percentChange(price, quote.previousClose);
125
+ }
126
+ if (asset.changeBasis != null) return null;
127
+ return quote ? percentChange(price, asset.previousClose) : asset.changePercent;
128
+ }
129
+
106
130
  /** A live quote replaces the board's snapshot once it is at least as new. */
107
131
  function liveQuote(
108
132
  asset: CryptoMarketAsset,
@@ -134,11 +158,7 @@ function buildCryptoRow(
134
158
  now = Date.now(),
135
159
  ): CryptoRow {
136
160
  const price = quote?.price ?? asset.price;
137
- const changePercent = quote && isFiniteNumber(quote.changePercent)
138
- ? quote.changePercent
139
- : quote
140
- ? percentChange(price, asset.previousClose)
141
- : asset.changePercent;
161
+ const changePercent = utcDayChangePercent(asset, quote, price);
142
162
  // Supply moves slowly; the price is what makes market cap live. Scaling the
143
163
  // reported cap keeps the source's supply basis and the board's ranking.
144
164
  const marketCap = isFiniteNumber(asset.marketCap)
@@ -389,7 +409,7 @@ const COLUMN_SPECS: CryptoColumnSpec[] = [
389
409
  { id: "code", label: "COIN", width: 7, align: "left" },
390
410
  { id: "name", label: "NAME", width: 10, align: "left", optional: 1 },
391
411
  { id: "price", label: "PRICE", width: 13, align: "right" },
392
- { id: "changePercent", label: "CHG%", width: 8, align: "right" },
412
+ { id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right" },
393
413
  { id: "return7d", label: "7D%", width: 8, align: "right" },
394
414
  { id: "return30d", label: "30D%", width: 8, align: "right", optional: 3 },
395
415
  { id: "return1y", label: "1Y%", width: 10, align: "right", optional: 5 },
@@ -415,7 +435,7 @@ const STABLECOIN_COLUMN_SPECS: CryptoColumnSpec[] = [
415
435
  { id: "low24hAt", label: "LOW UTC", width: 9, align: "left" },
416
436
  { id: "high24h", label: "24H HIGH", width: 8, align: "right", optional: 1 },
417
437
  { id: "high24hAt", label: "HIGH UTC", width: 9, align: "left", optional: 2 },
418
- { id: "changePercent", label: "CHG%", width: 8, align: "right", optional: 5 },
438
+ { id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right", optional: 5 },
419
439
  { id: "return7d", label: "7D%", width: 8, align: "right", optional: 6 },
420
440
  { id: "return30d", label: "30D%", width: 8, align: "right", optional: 8 },
421
441
  { id: "return1y", label: "1Y%", width: 10, align: "right", optional: 9 },
@@ -410,8 +410,9 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
410
410
  const detailOpen =
411
411
  tab === "maturities" ? !!openBucketRow : tab === "history" && !!openHistoryRow;
412
412
  const tabsFocused = focused && !detailOpen;
413
+ const signInWall = !data && isCloudSessionRequired(resource.error);
413
414
  const { strip: tabStrip, rows: tabRows } = usePaneTabs(
414
- latest
415
+ symbol && !signInWall
415
416
  ? { tabs: TABS, activeValue: tab, onSelect: setTab, focused: tabsFocused, dense: true }
416
417
  : null,
417
418
  );
@@ -522,7 +523,7 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
522
523
  message="Select a ticker to view debt maturities."
523
524
  />
524
525
  );
525
- if (!data && isCloudSessionRequired(resource.error))
526
+ if (signInWall)
526
527
  return (
527
528
  <SignInWall
528
529
  placement="debt-maturities-signin"
@@ -532,6 +533,7 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
532
533
  );
533
534
  return (
534
535
  <Box width={width} height={height} flexDirection="column">
536
+ {tabStrip}
535
537
  <PaneStatusBody
536
538
  loading={resource.loading && !data}
537
539
  error={!data ? resource.error : null}
@@ -542,7 +544,6 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
542
544
  >
543
545
  {latest ? (
544
546
  <>
545
- {tabStrip}
546
547
  {tab === "filing" ? (
547
548
  <FilingDetail latest={latest} quarter={quarter} width={width} height={bodyHeight} />
548
549
  ) : tab === "maturities" ? (