gloomberb 0.16.4 → 0.16.5

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 (554) hide show
  1. package/README.md +14 -91
  2. package/package.json +4 -1
  3. package/src/{plugins/builtin/cloud → account}/auth-form.tsx +9 -9
  4. package/src/{plugins/builtin/cloud → account}/auth-model.ts +4 -4
  5. package/src/account/billing.ts +57 -0
  6. package/src/{plugins/builtin/cloud → account}/device-signin-dialog.tsx +18 -12
  7. package/src/{plugins/builtin/cloud → account}/device-signin.ts +2 -2
  8. package/src/{plugins/builtin/cloud → account}/email-auth-attempt.ts +2 -2
  9. package/src/account/session.ts +16 -0
  10. package/src/{plugins/builtin/cloud → account}/upgrade-dialog.tsx +16 -15
  11. package/src/{plugins/builtin/cloud → account}/upgrade-personalization.ts +7 -7
  12. package/src/{plugins/builtin/cloud → account}/upgrade-tickers.ts +6 -6
  13. package/src/{plugins/builtin/cloud → account}/verification-panel.tsx +6 -6
  14. package/src/api-client/data.ts +24 -2
  15. package/src/api-client/earnings.ts +6 -0
  16. package/src/api-client/futures-curve.ts +56 -0
  17. package/src/api-client/index.ts +4 -1
  18. package/src/api-client/market-discovery.ts +10 -1
  19. package/src/api-client/paths.ts +7 -0
  20. package/src/api-client/research-activity.ts +15 -10
  21. package/src/api-client/types.ts +17 -1
  22. package/src/api-client/yield-curves.ts +8 -0
  23. package/src/app/global-shortcuts.ts +15 -1
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +7 -3
  25. package/src/architecture/core-builtin-allowlist.json +4 -13
  26. package/src/brokers/signed-in/sign-in-dialog.tsx +11 -5
  27. package/src/capabilities/chart-series.ts +11 -1
  28. package/src/cli/commands/crypto-hints.ts +7 -0
  29. package/src/cli/commands/fred-series.ts +61 -0
  30. package/src/cli/commands/market.ts +407 -104
  31. package/src/cli/commands/news-hints.ts +62 -0
  32. package/src/cli/commands/news-search.ts +53 -0
  33. package/src/cli/commands/options-leaps.ts +5 -2
  34. package/src/cli/commands/overview.ts +93 -20
  35. package/src/cli/commands/remote.ts +2 -1
  36. package/src/cli/commands/search.ts +82 -9
  37. package/src/cli/commands/system.ts +55 -8
  38. package/src/cli/commands/ticker.ts +88 -11
  39. package/src/cli/context.ts +45 -14
  40. package/src/cli/desktop-pane-shot.ts +59 -7
  41. package/src/cli/earnings-calendar.ts +71 -0
  42. package/src/cli/freshness.ts +18 -7
  43. package/src/cli/glossary.ts +429 -0
  44. package/src/cli/history-rows.ts +48 -7
  45. package/src/cli/index-roots.ts +7 -0
  46. package/src/cli/index.ts +21 -7
  47. package/src/cli/known-gaps.ts +27 -0
  48. package/src/cli/listing-arg.ts +417 -56
  49. package/src/cli/not-a-ticker.ts +12 -5
  50. package/src/cli/pane-functions/access-gate.ts +5 -0
  51. package/src/cli/pane-functions/capabilities.ts +3 -2
  52. package/src/cli/pane-functions/catalog.ts +35 -5
  53. package/src/cli/pane-functions/data.ts +12 -8
  54. package/src/cli/pane-functions/discovery.ts +1 -0
  55. package/src/cli/pane-functions/dom.ts +20 -6
  56. package/src/cli/pane-functions/freshness.ts +363 -43
  57. package/src/cli/pane-functions/function-help.ts +12 -5
  58. package/src/cli/pane-functions/headless.ts +45 -17
  59. package/src/cli/pane-functions/index.ts +63 -10
  60. package/src/cli/pane-functions/market-session.ts +22 -8
  61. package/src/cli/pane-functions/options.ts +63 -22
  62. package/src/cli/pane-functions/report-notices.ts +25 -7
  63. package/src/cli/pane-functions/report.ts +15 -8
  64. package/src/cli/pane-functions/resolver.ts +49 -12
  65. package/src/cli/pane-functions/screenshot.ts +110 -23
  66. package/src/cli/report-tables.ts +3 -0
  67. package/src/cli/result.ts +4 -1
  68. package/src/cli/staged-tickers.ts +52 -0
  69. package/src/cli/symbol-args.ts +9 -0
  70. package/src/cli/symbol-list.ts +172 -0
  71. package/src/cli/ticker-resolution.ts +223 -30
  72. package/src/cli/types.ts +5 -0
  73. package/src/components/chart/composite/axis-overlays.tsx +6 -2
  74. package/src/components/chart/composite/composite-chart.tsx +40 -6
  75. package/src/components/chart/composite/legend.tsx +49 -2
  76. package/src/components/chart/composite/panel-surface.tsx +44 -17
  77. package/src/components/chart/composite/price-axis-labels.tsx +3 -1
  78. package/src/components/command-bar/panel/index.tsx +11 -3
  79. package/src/components/command-bar/panel/runtime.ts +3 -0
  80. package/src/components/command-bar/panel/types.ts +2 -0
  81. package/src/components/command-bar/routes/root/provider-search.ts +4 -3
  82. package/src/components/command-bar/routes/ticker-search/results.ts +12 -11
  83. package/src/components/command-bar/routes/ticker-search/route.ts +4 -3
  84. package/src/components/command-bar/workflow/tickers.ts +23 -4
  85. package/src/components/layout/detached-pane-shell.tsx +9 -0
  86. package/src/components/layout/floating-pane.tsx +3 -0
  87. package/src/components/layout/header.tsx +20 -5
  88. package/src/components/layout/pane/content.tsx +7 -13
  89. package/src/components/layout/pane/footer/index.tsx +6 -2
  90. package/src/components/layout/pane/footer/model.ts +35 -1
  91. package/src/components/layout/pane/header.tsx +24 -1
  92. package/src/components/layout/pane/index.tsx +12 -2
  93. package/src/components/layout/pane/sidebar.tsx +78 -10
  94. package/src/components/layout/shell/active-drag.ts +21 -5
  95. package/src/components/layout/shell/drag/index.ts +39 -9
  96. package/src/components/layout/shell/drag/live.ts +61 -51
  97. package/src/components/layout/shell/drag/runtime.ts +54 -12
  98. package/src/components/layout/shell/index.tsx +25 -7
  99. package/src/components/layout/shell/menu.ts +5 -0
  100. package/src/components/layout/shell/native/pointer-runtime.ts +3 -1
  101. package/src/components/layout/shell/pane/layers.tsx +192 -45
  102. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -0
  103. package/src/components/markdown-text.tsx +62 -20
  104. package/src/components/metric-treemap/heat-surface.tsx +10 -1
  105. package/src/components/onboarding/account-step/form-panel.tsx +1 -1
  106. package/src/components/onboarding/account-step/index.tsx +1 -1
  107. package/src/components/onboarding/account-step/qr-panel.tsx +1 -1
  108. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  109. package/src/components/onboarding/onboarding-keyboard.ts +1 -1
  110. package/src/components/onboarding/onboarding-wizard.tsx +2 -2
  111. package/src/components/onboarding/use-onboarding-upgrade.ts +3 -3
  112. package/src/components/onboarding/wizard-account.ts +2 -2
  113. package/src/components/pane-template-wizard.tsx +9 -1
  114. package/src/components/price-selector-dialog.tsx +11 -2
  115. package/src/components/sign-in-gate.tsx +3 -3
  116. package/src/components/ticker/list-table-view.tsx +24 -1
  117. package/src/components/ui/action-row.tsx +2 -1
  118. package/src/components/ui/button.tsx +7 -1
  119. package/src/components/ui/choice-dialog.tsx +6 -4
  120. package/src/components/ui/confirm-dialog.tsx +28 -18
  121. package/src/components/ui/data-table/index.tsx +17 -6
  122. package/src/components/ui/data-table/opentui/index.tsx +37 -6
  123. package/src/components/ui/data-table/types.ts +12 -0
  124. package/src/components/ui/dialog-actions.tsx +64 -0
  125. package/src/components/ui/display.tsx +17 -3
  126. package/src/components/ui/frame.tsx +35 -2
  127. package/src/components/ui/icon.tsx +2 -0
  128. package/src/components/ui/text-prompt-dialog.tsx +11 -9
  129. package/src/data/cached-query.ts +2 -0
  130. package/src/data/config/store/normalize.ts +7 -2
  131. package/src/data/ticker-repository.ts +5 -0
  132. package/src/data/ticker-store.ts +15 -0
  133. package/src/docs/content.ts +215 -0
  134. package/src/docs/load.ts +139 -0
  135. package/src/docs/requests.ts +39 -0
  136. package/src/docs/snapshot.chunk.ts +8 -0
  137. package/src/docs/snapshot.json +7416 -0
  138. package/src/docs/topics.ts +216 -0
  139. package/src/i18n/es.ts +61 -4
  140. package/src/i18n/ja.ts +61 -4
  141. package/src/i18n/ko.ts +61 -4
  142. package/src/i18n/zh-cn.ts +61 -4
  143. package/src/i18n/zh-tw.ts +61 -4
  144. package/src/layout/desks.ts +1 -1
  145. package/src/layout/pane-manager/dock-tree.ts +18 -0
  146. package/src/layout/pane-manager/index.ts +1 -0
  147. package/src/market-data/coordinator/chart.ts +10 -4
  148. package/src/market-data/coordinator/index.ts +6 -3
  149. package/src/{plugins/builtin/futures/contracts.ts → market-data/futures-board.ts} +81 -28
  150. package/src/market-data/futures-contract.ts +151 -0
  151. package/src/market-data/hooks.tsx +10 -2
  152. package/src/market-data/market/format.ts +39 -21
  153. package/src/market-data/market/freshness.ts +81 -18
  154. package/src/market-data/market/status.ts +19 -23
  155. package/src/market-data/published-apac-sessions.ts +23 -1
  156. package/src/market-data/quotes/contributions.ts +3 -0
  157. package/src/market-data/quotes/metadata.ts +7 -24
  158. package/src/market-data/quotes/resolution.ts +2 -0
  159. package/src/market-data/quotes/screener-live-quotes.ts +5 -4
  160. package/src/market-data/result-types.ts +2 -0
  161. package/src/plugins/builtin/account-management/model.ts +1 -56
  162. package/src/plugins/builtin/account-management/pane.tsx +6 -4
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  164. package/src/plugins/builtin/alerts/cli.ts +313 -47
  165. package/src/plugins/builtin/alerts/events-pane.tsx +45 -7
  166. package/src/plugins/builtin/alerts/events.ts +23 -2
  167. package/src/plugins/builtin/alerts/history-pane.tsx +7 -2
  168. package/src/plugins/builtin/alerts/history.ts +2 -0
  169. package/src/plugins/builtin/alerts/index.tsx +41 -9
  170. package/src/plugins/builtin/alerts/pane.tsx +8 -0
  171. package/src/plugins/builtin/alerts/research-builder.ts +8 -3
  172. package/src/plugins/builtin/alerts/research-rules.ts +18 -2
  173. package/src/plugins/builtin/alerts/scheduled-check.ts +122 -0
  174. package/src/plugins/builtin/alerts/scheduled-sources.ts +25 -0
  175. package/src/plugins/builtin/alerts/scheduled.ts +254 -0
  176. package/src/plugins/builtin/alerts/targets.ts +92 -0
  177. package/src/plugins/builtin/analytics/index.tsx +4 -2
  178. package/src/plugins/builtin/analytics/risk-headless.ts +30 -12
  179. package/src/plugins/builtin/analytics/risk-math.ts +91 -27
  180. package/src/plugins/builtin/analytics/risk-model.ts +35 -5
  181. package/src/plugins/builtin/analytics/risk-pane.tsx +15 -8
  182. package/src/plugins/builtin/attention/index.tsx +2 -0
  183. package/src/plugins/builtin/attention/pane.tsx +3 -1
  184. package/src/plugins/builtin/attention/series.ts +26 -0
  185. package/src/plugins/builtin/awards/index.tsx +2 -0
  186. package/src/plugins/builtin/awards/pane.tsx +3 -1
  187. package/src/plugins/builtin/awards/series.ts +29 -0
  188. package/src/plugins/builtin/bond-calculator/model.ts +2 -1
  189. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  190. package/src/plugins/builtin/cds/client.ts +70 -13
  191. package/src/plugins/builtin/cds/headless.ts +116 -19
  192. package/src/plugins/builtin/cds/index.tsx +3 -1
  193. package/src/plugins/builtin/cds/model.ts +193 -31
  194. package/src/plugins/builtin/cds/pane.tsx +56 -14
  195. package/src/plugins/builtin/cds/series.ts +19 -0
  196. package/src/plugins/builtin/central-bank-rates/index.tsx +2 -0
  197. package/src/plugins/builtin/central-bank-rates/series.ts +42 -0
  198. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +192 -255
  199. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +84 -46
  200. package/src/plugins/builtin/chart-composer/editor.tsx +5 -4
  201. package/src/plugins/builtin/chart-composer/field-series.ts +40 -0
  202. package/src/plugins/builtin/chart-composer/headless.ts +8 -3
  203. package/src/plugins/builtin/chart-composer/index.tsx +9 -2
  204. package/src/plugins/builtin/chart-composer/series-catalog.ts +58 -50
  205. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -10
  206. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +59 -3
  207. package/src/plugins/builtin/chart-composer/user-series.ts +84 -0
  208. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +10 -2
  209. package/src/plugins/builtin/chat/controller/index.ts +2 -0
  210. package/src/plugins/builtin/cloud/askg/controller.ts +111 -19
  211. package/src/plugins/builtin/cloud/askg/executor.ts +57 -3
  212. package/src/plugins/builtin/cloud/askg/manifest.ts +32 -7
  213. package/src/plugins/builtin/cloud/askg/model.ts +27 -8
  214. package/src/plugins/builtin/cloud/askg/protocol.ts +10 -1
  215. package/src/plugins/builtin/cloud/askg/tool-display.ts +3 -0
  216. package/src/plugins/builtin/cloud/askg/undo.ts +2 -0
  217. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  218. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  219. package/src/plugins/builtin/cloud/auth-dialog.tsx +3 -3
  220. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  221. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +3 -0
  222. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +77 -9
  223. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +6 -1
  224. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +9 -0
  225. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +35 -11
  226. package/src/plugins/builtin/cloud/thesis/exposure.ts +1 -1
  227. package/src/plugins/builtin/cloud/thesis/prompts.tsx +2 -1
  228. package/src/plugins/builtin/company-attention/app-rank.tsx +1 -1
  229. package/src/plugins/builtin/company-attention/headless.ts +19 -5
  230. package/src/plugins/builtin/company-attention/index.tsx +2 -0
  231. package/src/plugins/builtin/company-attention/model.ts +26 -7
  232. package/src/plugins/builtin/company-attention/pane.tsx +12 -9
  233. package/src/plugins/builtin/company-attention/series.ts +52 -0
  234. package/src/plugins/builtin/company-kpis/pane.tsx +4 -2
  235. package/src/plugins/builtin/composite-plugins.ts +19 -13
  236. package/src/plugins/builtin/cot/index.tsx +3 -1
  237. package/src/plugins/builtin/cot/model.ts +40 -11
  238. package/src/plugins/builtin/cot/series.ts +27 -0
  239. package/src/plugins/builtin/credit-boards/index.tsx +2 -0
  240. package/src/plugins/builtin/credit-boards/series.ts +66 -0
  241. package/src/plugins/builtin/credit-conditions/index.tsx +2 -0
  242. package/src/plugins/builtin/credit-conditions/series.ts +26 -0
  243. package/src/plugins/builtin/credit-documents/headless.ts +31 -1
  244. package/src/plugins/builtin/credit-documents/pane.tsx +3 -1
  245. package/src/plugins/builtin/crypto-board/headless.ts +2 -3
  246. package/src/plugins/builtin/crypto-board/index.tsx +2 -0
  247. package/src/plugins/builtin/crypto-board/model.ts +12 -0
  248. package/src/plugins/builtin/crypto-board/pane.tsx +19 -12
  249. package/src/plugins/builtin/crypto-board/series.ts +48 -0
  250. package/src/plugins/builtin/custom-view/live-quotes.ts +2 -2
  251. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  252. package/src/plugins/builtin/debt-maturities/series.ts +45 -0
  253. package/src/plugins/builtin/dividend-yield/headless.ts +2 -2
  254. package/src/plugins/builtin/dividend-yield/index.tsx +2 -0
  255. package/src/plugins/builtin/dividend-yield/series.ts +20 -0
  256. package/src/plugins/builtin/docs/blocks.tsx +181 -0
  257. package/src/plugins/builtin/docs/index.tsx +68 -0
  258. package/src/plugins/builtin/docs/pane.tsx +426 -0
  259. package/src/plugins/builtin/docs/targets.ts +72 -0
  260. package/src/plugins/builtin/docs/use-docs.ts +60 -0
  261. package/src/plugins/builtin/doe/index.tsx +2 -0
  262. package/src/plugins/builtin/doe/series.ts +62 -0
  263. package/src/plugins/builtin/earnings/client.ts +3 -2
  264. package/src/plugins/builtin/earnings/format.ts +1 -5
  265. package/src/plugins/builtin/earnings/headless.ts +58 -6
  266. package/src/plugins/builtin/earnings/history-model.ts +15 -1
  267. package/src/plugins/builtin/earnings/history-table.ts +13 -2
  268. package/src/plugins/builtin/earnings/history.tsx +7 -3
  269. package/src/plugins/builtin/econ/fred-catalog.ts +189 -0
  270. package/src/plugins/builtin/econ/fred-series-map.ts +0 -46
  271. package/src/plugins/builtin/econ/index.tsx +2 -0
  272. package/src/plugins/builtin/econ-statistics/defs.ts +5 -0
  273. package/src/plugins/builtin/econ-statistics/headless.ts +19 -4
  274. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/pane.tsx +4 -11
  276. package/src/plugins/builtin/exposure/holdings.ts +1 -1
  277. package/src/plugins/builtin/exposure/pane.tsx +3 -1
  278. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  279. package/src/plugins/builtin/fear-greed/series.ts +52 -0
  280. package/src/plugins/builtin/futures/index.tsx +43 -3
  281. package/src/plugins/builtin/futures/model.ts +1 -1
  282. package/src/plugins/builtin/futures/returns.ts +1 -1
  283. package/src/plugins/builtin/futures/series.ts +13 -0
  284. package/src/plugins/builtin/futures/table.tsx +1 -1
  285. package/src/plugins/builtin/futures/use-front-returns.ts +1 -1
  286. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  287. package/src/plugins/builtin/futures-curve/headless.ts +70 -9
  288. package/src/plugins/builtin/futures-curve/index.tsx +1 -1
  289. package/src/plugins/builtin/futures-curve/model.ts +66 -11
  290. package/src/plugins/builtin/futures-curve/pane.tsx +52 -18
  291. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  292. package/src/plugins/builtin/gpu/board.tsx +13 -10
  293. package/src/plugins/builtin/gpu/changes.tsx +4 -4
  294. package/src/plugins/builtin/gpu/client.ts +19 -1
  295. package/src/plugins/builtin/gpu/equities.tsx +14 -10
  296. package/src/plugins/builtin/gpu/headless.ts +56 -25
  297. package/src/plugins/builtin/gpu/history.tsx +3 -3
  298. package/src/plugins/builtin/gpu/index.tsx +2 -0
  299. package/src/plugins/builtin/gpu/model.ts +101 -16
  300. package/src/plugins/builtin/gpu/pane.tsx +28 -12
  301. package/src/plugins/builtin/gpu/series.ts +38 -0
  302. package/src/plugins/builtin/help/function-card.tsx +25 -6
  303. package/src/plugins/builtin/help/index.tsx +17 -8
  304. package/src/plugins/builtin/holders/beneficial-model.ts +90 -3
  305. package/src/plugins/builtin/holders/beneficial-report.ts +57 -5
  306. package/src/plugins/builtin/holders/beneficial-view.tsx +55 -11
  307. package/src/plugins/builtin/holders/client.ts +22 -3
  308. package/src/plugins/builtin/holders/format.ts +18 -3
  309. package/src/plugins/builtin/holders/headless.ts +36 -133
  310. package/src/plugins/builtin/holders/pane.tsx +11 -4
  311. package/src/plugins/builtin/holders/report-header.ts +40 -5
  312. package/src/plugins/builtin/holders/report-rows.ts +240 -0
  313. package/src/plugins/builtin/insider/client.ts +35 -20
  314. package/src/plugins/builtin/insider/headless.ts +41 -11
  315. package/src/plugins/builtin/insider/index.tsx +26 -9
  316. package/src/plugins/builtin/insider/insider-data.ts +10 -0
  317. package/src/plugins/builtin/insider/issuer.ts +49 -0
  318. package/src/plugins/builtin/insider/model.ts +3 -0
  319. package/src/plugins/builtin/iv-history/client.ts +14 -4
  320. package/src/plugins/builtin/iv-history/headless.ts +7 -4
  321. package/src/plugins/builtin/iv-history/index.tsx +2 -0
  322. package/src/plugins/builtin/iv-history/model.ts +111 -7
  323. package/src/plugins/builtin/iv-history/screen-pane.tsx +5 -9
  324. package/src/plugins/builtin/iv-history/series.ts +29 -0
  325. package/src/plugins/builtin/jobs/headless.ts +28 -3
  326. package/src/plugins/builtin/jobs/index.tsx +3 -1
  327. package/src/plugins/builtin/jobs/series.ts +19 -0
  328. package/src/plugins/builtin/kelly-sizer/pane.tsx +2 -1
  329. package/src/plugins/builtin/kelly-sizer/portfolio.ts +0 -39
  330. package/src/plugins/builtin/macro-day/client.ts +44 -3
  331. package/src/plugins/builtin/macro-day/headless.ts +13 -10
  332. package/src/plugins/builtin/macro-day/model.ts +56 -10
  333. package/src/plugins/builtin/macro-day/pane.tsx +30 -20
  334. package/src/plugins/builtin/market-heatmap/model.ts +19 -11
  335. package/src/plugins/builtin/market-heatmap/portfolio.ts +2 -2
  336. package/src/plugins/builtin/market-movers/client.ts +7 -5
  337. package/src/plugins/builtin/market-movers/index.tsx +3 -2
  338. package/src/plugins/builtin/market-movers/model.ts +30 -0
  339. package/src/plugins/builtin/market-movers/screener.ts +31 -0
  340. package/src/plugins/builtin/market-valuation/chart-series.ts +29 -69
  341. package/src/plugins/builtin/market-valuation/index.tsx +3 -3
  342. package/src/plugins/builtin/market-valuation/indicators.ts +0 -5
  343. package/src/plugins/builtin/members/headless.ts +7 -2
  344. package/src/plugins/builtin/members/model.ts +31 -3
  345. package/src/plugins/builtin/members/pane.tsx +3 -6
  346. package/src/plugins/builtin/mna/headless.ts +15 -3
  347. package/src/plugins/builtin/money-markets/index.tsx +2 -0
  348. package/src/plugins/builtin/money-markets/series.ts +41 -0
  349. package/src/plugins/builtin/notes/cli.ts +12 -7
  350. package/src/plugins/builtin/options/view.tsx +1 -1
  351. package/src/plugins/builtin/options-calculator/inputs.ts +2 -1
  352. package/src/plugins/builtin/options-positioning/client.ts +7 -3
  353. package/src/plugins/builtin/options-positioning/headless.ts +1 -0
  354. package/src/plugins/builtin/options-positioning/pane.tsx +2 -2
  355. package/src/plugins/builtin/options-scenario/client.ts +2 -1
  356. package/src/plugins/builtin/options-scenario/headless.ts +1 -0
  357. package/src/plugins/builtin/options-scenario/hedge.ts +2 -1
  358. package/src/plugins/builtin/perps/client.ts +40 -6
  359. package/src/plugins/builtin/perps/equity-row.tsx +33 -11
  360. package/src/plugins/builtin/perps/freshness.ts +66 -0
  361. package/src/plugins/builtin/perps/headless.ts +45 -30
  362. package/src/plugins/builtin/perps/index.tsx +18 -1
  363. package/src/plugins/builtin/perps/model.ts +20 -5
  364. package/src/plugins/builtin/perps/pane.tsx +76 -20
  365. package/src/plugins/builtin/perps/series.ts +76 -0
  366. package/src/plugins/builtin/plugin-marketplace/pane.tsx +67 -61
  367. package/src/plugins/builtin/plugin-module.ts +3 -0
  368. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  369. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +273 -0
  370. package/src/plugins/builtin/portfolio-list/cli/import-13f.ts +109 -0
  371. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +331 -65
  372. package/src/plugins/builtin/portfolio-list/cli/render.ts +200 -31
  373. package/src/plugins/builtin/portfolio-list/cli/shared.ts +24 -0
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -47
  375. package/src/plugins/builtin/portfolio-list/column-values.ts +1 -1
  376. package/src/plugins/builtin/portfolio-list/grid.tsx +1 -1
  377. package/src/plugins/builtin/portfolio-list/headless.ts +34 -4
  378. package/src/plugins/builtin/portfolio-list/imported-from.ts +8 -0
  379. package/src/plugins/builtin/portfolio-list/live-valuation.ts +6 -2
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -1
  381. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +17 -1
  382. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  383. package/src/plugins/builtin/portfolio-list/table.tsx +7 -1
  384. package/src/plugins/builtin/power/client.ts +16 -0
  385. package/src/plugins/builtin/power/evidence.ts +6 -2
  386. package/src/plugins/builtin/power/headless.ts +71 -7
  387. package/src/plugins/builtin/power/index.tsx +4 -1
  388. package/src/plugins/builtin/power/model.ts +72 -1
  389. package/src/plugins/builtin/power/pane.tsx +32 -9
  390. package/src/plugins/builtin/rate-path/headless.ts +11 -3
  391. package/src/plugins/builtin/realized-vol/headless.ts +5 -0
  392. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -2
  393. package/src/plugins/builtin/research/event-model.ts +68 -24
  394. package/src/plugins/builtin/research/events-headless.ts +2 -1
  395. package/src/plugins/builtin/research/headless.ts +2 -1
  396. package/src/plugins/builtin/seasonality/client.ts +15 -7
  397. package/src/plugins/builtin/seasonality/headless.ts +36 -12
  398. package/src/plugins/builtin/seasonality/index.tsx +2 -2
  399. package/src/plugins/builtin/seasonality/model.ts +119 -0
  400. package/src/plugins/builtin/seasonality/pane.tsx +108 -37
  401. package/src/plugins/builtin/series-inventory.ts +91 -0
  402. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -1
  403. package/src/plugins/builtin/shared/command-tab.ts +30 -12
  404. package/src/plugins/builtin/shared/earnings-report.ts +55 -0
  405. package/src/plugins/builtin/shared/fred-points.ts +20 -0
  406. package/src/plugins/builtin/shared/fx-rate-freshness.ts +101 -0
  407. package/src/plugins/builtin/shared/open-description.ts +29 -0
  408. package/src/plugins/builtin/shared/report-freshness.ts +9 -0
  409. package/src/plugins/builtin/shared/series-warning.ts +5 -0
  410. package/src/plugins/builtin/short-interest/headless.ts +1 -0
  411. package/src/plugins/builtin/short-interest/index.tsx +2 -0
  412. package/src/plugins/builtin/short-interest/series.ts +36 -0
  413. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  414. package/src/plugins/builtin/short-interest/watch-model.ts +9 -5
  415. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  416. package/src/plugins/builtin/short-volume/index.tsx +2 -0
  417. package/src/plugins/builtin/short-volume/series.ts +23 -0
  418. package/src/plugins/builtin/social-mentions/index.tsx +2 -0
  419. package/src/plugins/builtin/social-mentions/series.ts +23 -0
  420. package/src/plugins/builtin/supply-chain/graph-pane.tsx +3 -2
  421. package/src/plugins/builtin/supply-chain/pane.tsx +3 -1
  422. package/src/plugins/builtin/thirteenf/api.ts +1 -0
  423. package/src/plugins/builtin/thirteenf/data.ts +6 -4
  424. package/src/plugins/builtin/thirteenf/headless.ts +217 -36
  425. package/src/plugins/builtin/thirteenf/model.ts +39 -1
  426. package/src/plugins/builtin/thirteenf/overlap.ts +75 -0
  427. package/src/plugins/builtin/thirteenf/pane.tsx +10 -1
  428. package/src/plugins/builtin/thirteenf/report-notes.ts +103 -1
  429. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  430. package/src/plugins/builtin/ticker-detail/index.tsx +3 -0
  431. package/src/plugins/builtin/ticker-detail/overview/components.tsx +1 -1
  432. package/src/plugins/builtin/ticker-detail/overview/futures-contract.ts +31 -0
  433. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -3
  434. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -1
  435. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +48 -4
  436. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +13 -3
  437. package/src/plugins/builtin/time-sales/headless.ts +1 -0
  438. package/src/plugins/builtin/treasury-auctions/headless.ts +16 -2
  439. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  440. package/src/plugins/builtin/treasury-auctions/model.ts +8 -0
  441. package/src/plugins/builtin/vol-surface/headless.ts +1 -0
  442. package/src/plugins/builtin/volatility/index.tsx +2 -0
  443. package/src/plugins/builtin/volatility/series.ts +84 -0
  444. package/src/plugins/builtin/world-venue-map/geo-series.ts +26 -1
  445. package/src/plugins/builtin/world-venue-map/headless.ts +4 -1
  446. package/src/plugins/builtin/world-venue-map/index.ts +2 -1
  447. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +8 -4
  448. package/src/plugins/builtin/world-venue-map/layers.ts +29 -0
  449. package/src/plugins/builtin/yield-curve/chart.ts +2 -2
  450. package/src/plugins/builtin/yield-curve/curves.ts +21 -8
  451. package/src/plugins/builtin/yield-curve/headless.ts +71 -18
  452. package/src/plugins/builtin/yield-curve/index.tsx +3 -1
  453. package/src/plugins/builtin/yield-curve/series.ts +86 -0
  454. package/src/plugins/catalog-browser.ts +2 -1
  455. package/src/plugins/registry/index.ts +89 -4
  456. package/src/{plugins/builtin/portfolio-list → portfolio}/position-metrics.ts +6 -6
  457. package/src/portfolio/position-value.ts +40 -0
  458. package/src/public/__snapshots__/public-api.test.ts.snap +7 -0
  459. package/src/public/time-series.ts +17 -0
  460. package/src/react/auto-refresh.ts +13 -3
  461. package/src/remote/app-host.tsx +20 -4
  462. package/src/remote/confirm-change.tsx +85 -0
  463. package/src/remote/controller-utils.ts +8 -1
  464. package/src/remote/controller.ts +123 -6
  465. package/src/remote/pane-screenshot.ts +47 -0
  466. package/src/remote/schema.ts +62 -0
  467. package/src/remote/types.ts +30 -0
  468. package/src/remote/watchlist.ts +306 -0
  469. package/src/renderers/browser/main.tsx +3 -2
  470. package/src/renderers/cloudflare/worker.ts +1 -1
  471. package/src/renderers/dom/data-table/frozen-column.ts +3 -1
  472. package/src/renderers/dom/data-table/index.tsx +33 -6
  473. package/src/renderers/dom/data-table/row.tsx +27 -1
  474. package/src/renderers/dom/desktop/control-styles.ts +63 -1
  475. package/src/renderers/dom/desktop/controls.tsx +30 -23
  476. package/src/renderers/dom/desktop/dialog-actions.tsx +170 -0
  477. package/src/renderers/dom/desktop/icons.tsx +7 -0
  478. package/src/renderers/dom/dom-ui-host.tsx +3 -0
  479. package/src/renderers/dom/host/box.tsx +3 -0
  480. package/src/renderers/dom/host/chart-surface.tsx +6 -2
  481. package/src/renderers/dom/host/live-frame.ts +6 -2
  482. package/src/renderers/dom/host/overflow-fade.ts +14 -1
  483. package/src/renderers/dom/host/size-observer.ts +26 -0
  484. package/src/renderers/dom/host/surface-3d.tsx +7 -2
  485. package/src/renderers/dom/host/text.tsx +1 -1
  486. package/src/renderers/dom/styles.css +61 -0
  487. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +17 -2
  488. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +19 -11
  489. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +10 -26
  490. package/src/renderers/electrobun/view/main.tsx +2 -0
  491. package/src/series/capability.ts +75 -0
  492. package/src/series/csv.ts +160 -0
  493. package/src/series/files.ts +107 -0
  494. package/src/series/registry.ts +198 -0
  495. package/src/series/resolve.ts +107 -0
  496. package/src/series/standalone.ts +48 -0
  497. package/src/series/types.ts +145 -0
  498. package/src/series/validate.ts +171 -0
  499. package/src/sources/gloomberb-cloud/index.ts +7 -7
  500. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  501. package/src/sources/history-result.ts +1 -0
  502. package/src/sources/listing-symbols.ts +4 -1
  503. package/src/sources/provider-router/batches.ts +4 -1
  504. package/src/sources/provider-router/cache.ts +4 -2
  505. package/src/sources/provider-router/cached-routes.ts +1 -1
  506. package/src/sources/provider-router/financial-routes.ts +1 -0
  507. package/src/sources/provider-router/financials.ts +13 -0
  508. package/src/sources/provider-router/history.ts +43 -25
  509. package/src/sources/provider-router/search.ts +1 -0
  510. package/src/sync/core-contributors.ts +1 -0
  511. package/src/tickers/company-name.ts +75 -0
  512. package/src/tickers/instrument-kind.ts +25 -0
  513. package/src/tickers/open-target.ts +3 -2
  514. package/src/tickers/search/index.ts +45 -7
  515. package/src/tickers/search/ranking.ts +33 -17
  516. package/src/tickers/search/types.ts +7 -1
  517. package/src/tickers/us-listing.ts +34 -0
  518. package/src/time-series/hooks.ts +9 -0
  519. package/src/time-series/resolve.ts +95 -16
  520. package/src/time-series/types.ts +4 -2
  521. package/src/time-series/use-chart-resolution.ts +32 -0
  522. package/src/types/config.ts +6 -0
  523. package/src/types/data-provider.ts +5 -0
  524. package/src/types/financials.ts +52 -1
  525. package/src/types/headless.ts +61 -0
  526. package/src/types/plugin.ts +19 -0
  527. package/src/types/price-history.ts +7 -0
  528. package/src/types/ticker.ts +14 -0
  529. package/src/ui/host.tsx +18 -0
  530. package/src/ui/live-resize.ts +81 -0
  531. package/src/ui/live-stretch.ts +121 -0
  532. package/src/ui/measured-cells.ts +58 -0
  533. package/src/ui/pane-fill.ts +11 -0
  534. package/src/utils/basis-points.ts +17 -0
  535. package/src/utils/cli-output.ts +7 -1
  536. package/src/utils/concurrency.ts +25 -0
  537. package/src/utils/crypto-pair.ts +20 -0
  538. package/src/utils/currency-pair.ts +19 -0
  539. package/src/utils/decimal-comma.ts +19 -0
  540. package/src/utils/dom-screenshot-css.ts +265 -0
  541. package/src/utils/dom-screenshot-rasters.ts +276 -0
  542. package/src/utils/dom-screenshot.ts +437 -52
  543. package/src/utils/exchange-rate-snapshot.ts +6 -2
  544. package/src/utils/exchanges.ts +107 -1
  545. package/src/utils/format.ts +16 -3
  546. package/src/utils/fundamentals.ts +66 -0
  547. package/src/utils/futures-generic.ts +6 -12
  548. package/src/utils/fx-market-hours.ts +15 -5
  549. package/src/utils/price-history.ts +99 -42
  550. package/src/utils/sec-eps-basis.ts +24 -11
  551. package/src/utils/sec.ts +61 -7
  552. package/src/utils/utc-time.ts +51 -4
  553. package/src/plugins/builtin/fx-matrix/freshness.ts +0 -65
  554. /package/src/{plugins/builtin/cloud → tickers}/starter-symbols.ts +0 -0
@@ -3,7 +3,7 @@ import type { CurvePalette, CurveSeries } from "../../../components/chart/curve/
3
3
  import { spanDigits } from "../../../components/chart-table";
4
4
  import { compositeAxisTicks } from "../../../components/chart/composite/format";
5
5
  import type { CompositeAxisDomain } from "../../../components/chart/composite/types";
6
- import { FUTURES_CONTRACTS, tickDecimals } from "../futures/contracts";
6
+ import { FUTURES_CONTRACTS, futuresBoardContractForTicker, futuresContractByCode, tickDecimals } from "../../../market-data/futures-board";
7
7
  import { formatPercentileRank } from "../../../utils/format";
8
8
  import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
9
9
  import { parseReportTime } from "../../../utils/utc-time";
@@ -21,7 +21,7 @@ const CRYPTO_CURVE_ROOTS: readonly { code: string; name: string; tick: number }[
21
21
  const rootLabel = (code: string, name: string) => name === code ? code : `${code} ${name}`;
22
22
 
23
23
  export const CURVE_ROOTS = [
24
- ...FUTURES_CONTRACTS.filter((row) => row.curve !== false).map((row) => ({ value: row.code, label: rootLabel(row.code, row.name) })),
24
+ ...FUTURES_CONTRACTS.map((row) => ({ value: row.code, label: rootLabel(row.code, row.name) })),
25
25
  { value: "VX", label: "VX VIX Futures" },
26
26
  ...CRYPTO_CURVE_ROOTS.map((row) => ({ value: row.code, label: rootLabel(row.code, row.name) })),
27
27
  ];
@@ -34,24 +34,40 @@ export function normalizeCurveRoot(value: unknown): string | null {
34
34
  return CURVE_ROOTS.some((row) => row.value === root) ? root : null;
35
35
  }
36
36
 
37
- /** What a root nothing lists is told, with roots that work. */
37
+ /** What a root CTM does not list is told: the roots that work, with the FUT board's, VIX and CME crypto named by group. */
38
38
  export function unsupportedCurveRootMessage(value: unknown): string {
39
+ const typed = String(value).trim().toUpperCase().replace(/=F$/, "");
39
40
  const examples = ["ES", "CL", "GC", "ZN"].filter((root) => CURVE_ROOTS.some((row) => row.value === root));
40
- return `Unsupported futures root: ${String(value)}. Try ${examples.join(", ")}.`;
41
+ return `No futures curve for ${typed || "that root"}: CTM covers the FUT board's roots, VIX and CME crypto, such as ${examples.join(", ")}.`;
41
42
  }
42
43
 
44
+ /**
45
+ * Board roots whose bare code opens their curve from the ticker under the
46
+ * cursor. Several are also listed securities (ES Eversource, NG NovaGold, PL
47
+ * Planet Labs), but they followed the cursor before the board was checked
48
+ * against listed securities and keep doing so. A root added after that (ZM is
49
+ * Zoom, KE, LE, HE and ALI are tickers too) follows the cursor only as its
50
+ * `=F` symbol or a code no security answers to (`bareCode`: ZL, OJ, LBR), the
51
+ * rule FUT and COT apply, so a new board row defaults to the security.
52
+ */
53
+ const CURSOR_BARE_ROOTS: ReadonlySet<string> = new Set(["ES", "CL", "BZ", "NG", "RB", "HO", "GC", "SI", "HG", "PL", "PA", "ZC", "ZS", "KC", "SB", "CC"]);
54
+
43
55
  /**
44
56
  * The root for the ticker under the cursor, when CTM is opened without an
45
57
  * argument. The crypto roots are also US ticker symbols (Grayscale's mini
46
58
  * trusts are BTC and ETH), so a bare one is the equity's, never the future's:
47
59
  * only a futures symbol (BTC=F) or a coin pair (BTC-USD) names the crypto curve.
60
+ * A typed argument is different: `CTM ZM` is soybean meal even though ZM is also Zoom.
48
61
  */
49
62
  export function curveRootForTicker(ticker: unknown): string | null {
50
63
  if (typeof ticker !== "string") return null;
51
64
  const coin = cryptoPairCoin(ticker);
52
65
  if (coin) return CRYPTO_CURVE_ROOTS.some((row) => row.code === coin) ? coin : null;
53
66
  const root = normalizeCurveRoot(ticker);
54
- return root && CRYPTO_CURVE_ROOTS.some((row) => row.code === root) && !isFuturesSymbol(ticker) ? null : root;
67
+ if (!root) return null;
68
+ if (CRYPTO_CURVE_ROOTS.some((row) => row.code === root)) return isFuturesSymbol(ticker) ? root : null;
69
+ const isBoardRoot = futuresContractByCode(root) != null;
70
+ return isBoardRoot && !CURSOR_BARE_ROOTS.has(root) ? futuresBoardContractForTicker(ticker)?.code ?? null : root;
55
71
  }
56
72
 
57
73
  /**
@@ -67,7 +83,7 @@ function curvePriceDecimals(root: string): number {
67
83
  const rateTick = RATE_TICKS[root];
68
84
  if (rateTick != null) return tickDecimals(rateTick);
69
85
  if (root === "VX") return 4;
70
- const tick = FUTURES_CONTRACTS.find((row) => row.code === root)?.tick ?? CRYPTO_CURVE_ROOTS.find((row) => row.code === root)?.tick;
86
+ const tick = futuresContractByCode(root)?.tick ?? CRYPTO_CURVE_ROOTS.find((row) => row.code === root)?.tick;
71
87
  return tick == null ? 5 : Math.max(2, tickDecimals(tick));
72
88
  }
73
89
 
@@ -84,6 +100,39 @@ export function curveChangeText(value: number | null, root: string): string {
84
100
  return value != null && value > 0 && /[1-9]/.test(text) ? `+${text}` : text;
85
101
  }
86
102
 
103
+ /** A price as a number at the root's display precision, for reports: 59.299805 reads 59.3, never float noise. */
104
+ export function curveRoundedPrice(value: number | null | undefined, root: string): number | null {
105
+ return value == null || !Number.isFinite(value) ? null : Number(value.toFixed(curvePriceDecimals(root)));
106
+ }
107
+
108
+ /** The unit the catalogue gives a root whose contract terms the server does not send: "EUR / MWh" for Dutch TTF gas. */
109
+ export function catalogueCurveUnit(root: string): string | null {
110
+ return futuresContractByCode(root)?.unit ?? null;
111
+ }
112
+
113
+ /**
114
+ * What a price counts: the exchange's unit ("USD / troy oz") where the root's
115
+ * terms are known, the catalogue's where they are not, else the quote unit.
116
+ */
117
+ export function curveUnitLabel(data: Pick<FuturesCurvePayload, "root" | "spec" | "quoteUnit" | "currency">): string | null {
118
+ return data.spec?.unit ?? catalogueCurveUnit(data.root) ?? data.quoteUnit ?? data.currency ?? null;
119
+ }
120
+
121
+ /** A contract as traders write it: GCZ26.CMX is GCZ26; a VX symbol stays as Cboe lists it. */
122
+ export function curveContractCode(symbol: string): string {
123
+ return symbol.replace(/\.[A-Z]+$/, "");
124
+ }
125
+
126
+ /**
127
+ * The front spread named by the contracts it subtracts, "GCF27-GCZ26": the
128
+ * next contract less the front one. The front is the first contract the
129
+ * market trades, which can be past a delivery month nobody holds, so a bare
130
+ * "M2-M1" would not say which rows it means.
131
+ */
132
+ export function curveSpreadLabel(slope: Pick<FuturesCurvePayload["slope"], "frontSymbol" | "nextSymbol">): string {
133
+ return slope.frontSymbol && slope.nextSymbol ? `${curveContractCode(slope.nextSymbol)}-${curveContractCode(slope.frontSymbol)}` : "M2-M1";
134
+ }
135
+
87
136
  /**
88
137
  * Axis gridlines sit on round values, so they need no tick precision: one
89
138
  * decimal count across the gutter, as many as the plotted range asks for and
@@ -188,13 +237,14 @@ export function curveContractChanges(data: FuturesCurvePayload): CurveContractCh
188
237
 
189
238
  const CHANGE_COLUMNS: Readonly<Record<string, CurveLookback>> = { change1w: "1W", change1m: "1M" };
190
239
 
191
- type CurveSortKey = "symbol" | "expiration" | "price" | "change" | "openInterest" | "volume" | "percentile" | "asOf";
240
+ type CurveSortKey = "symbol" | "expiration" | "price" | "settlement" | "firstNotice" | "change" | "openInterest" | "volume" | "percentile" | "asOf";
192
241
 
193
242
  const BASIS_COLUMNS: Readonly<Record<string, "vsSpotPct" | "annualisedBasisPct">> = { vsSpot: "vsSpotPct", annBasis: "annualisedBasisPct" };
194
243
 
195
244
  export function sortCurveContracts(rows: readonly FuturesContract[], id: string, direction: SortDirection,
196
245
  changes?: CurveContractChanges, basis?: CurveBasisRows): FuturesContract[] {
197
- const keys: Record<string, CurveSortKey> = { symbol: "symbol", expiry: "expiration", price: "price", change: "change", oi: "openInterest", volume: "volume", percentile: "percentile", asOf: "asOf" };
246
+ const keys: Record<string, CurveSortKey> = { symbol: "symbol", expiry: "expiration", price: "price", settle: "settlement", notice: "firstNotice",
247
+ change: "change", oi: "openInterest", volume: "volume", percentile: "percentile", asOf: "asOf" };
198
248
  const key = keys[id] ?? "expiration";
199
249
  const lookback = CHANGE_COLUMNS[id];
200
250
  const basisKey = BASIS_COLUMNS[id];
@@ -399,6 +449,9 @@ export function archivedFuturesCurve(root: string, curve: FuturesCurveAsOfPayloa
399
449
  symbol: row.symbol, label: row.label, expiration: row.expiration, price: row.price, change: null, asOf: rowDate(row, stale),
400
450
  currency: curve.currency ?? "USD", quoteUnit: curve.quoteUnit ?? curve.currency ?? "USD", volume: row.volume,
401
451
  openInterest: row.openInterest, delayMinutes: null, stale, percentile: null, samples: 0, historyStart: null, historyEnd: null,
452
+ // Only a row the archive confirms is a settlement; an unconfirmed one is that session's last trade.
453
+ settlement: !stale && row.settled === true ? row.price : null,
454
+ lastTrade: row.lastTrade ?? row.expiration, firstNotice: row.firstNotice ?? null, inDelivery: row.inDelivery ?? false,
402
455
  }];
403
456
  }).sort((a, b) => a.expiration.localeCompare(b.expiration));
404
457
  const ghosts = (["1W", "1M"] as const).map((label) => {
@@ -407,8 +460,10 @@ export function archivedFuturesCurve(root: string, curve: FuturesCurveAsOfPayloa
407
460
  points: past ? past.contracts.flatMap((row) => row.expiration
408
461
  ? [{ symbol: row.symbol, expiration: row.expiration, price: row.price, asOf: rowDate(row, archivedRowStale(row, past)) }] : []) : [] };
409
462
  });
410
- // The front pair on the curve's own session; carried prices do not make a spread.
411
- const [front, next] = contracts.filter((row) => !row.stale);
463
+ // The front pair on the curve's own session: settlements where the archive has them, else that session's
464
+ // last trades. Carried prices, and a last trade beside a settlement, do not make a spread.
465
+ const settled = contracts.filter((row) => row.settlement != null);
466
+ const [front, next] = settled.length >= 2 ? settled : settled.length ? [] : contracts.filter((row) => !row.stale);
412
467
  const value = front && next ? next.price! - front.price! : null;
413
468
  const days = front && next ? (Date.parse(next.expiration) - Date.parse(front.expiration)) / DAY_MS : 0;
414
469
  const roll = front && next && front.price! > 0 && next.price! > 0 && days > 0 ? (front.price! / next.price! - 1) * 365 / days * 100 : null;
@@ -419,6 +474,6 @@ export function archivedFuturesCurve(root: string, curve: FuturesCurveAsOfPayloa
419
474
  slope: { frontSymbol: front?.symbol ?? null, nextSymbol: next?.symbol ?? null, value, annualizedRollYield: roll, percentile: null,
420
475
  rollPercentile: null, samples: 0, historyStart: null, historyEnd: null, asOf: front && next ? curve.asOf : null,
421
476
  state: value == null ? "unavailable" : Math.abs(value) < 1e-10 ? "flat" : value > 0 ? "contango" : "backwardation" },
422
- gaps: curve.gaps,
477
+ gaps: curve.gaps, spec: curve.spec ?? null, settlementDate: settled.length ? curve.asOf : null,
423
478
  };
424
479
  }
@@ -4,6 +4,7 @@ import { fitChartTableColumns } from "../../../components/chart-table";
4
4
  import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
5
5
  import { isAccessDenied } from "../../../api-client/errors";
6
6
  import type { FuturesContract } from "../../../api-client/futures-curve";
7
+ import { futuresTickText } from "../../../market-data/futures-contract";
7
8
  import { useAsyncResource, usePaneSettingValue, usePluginPaneState, useShortcut } from "../../../public/react";
8
9
  import { usePaneInstance, usePaneTitle } from "../../../state/app/context";
9
10
  import { useThemeColors } from "../../../theme/theme-context";
@@ -16,14 +17,19 @@ import { futuresSessionRefreshInterval } from "../shared/futures-session";
16
17
  import { useResearchCloudSession } from "../shared/research-cloud-session";
17
18
  import { useQuoteBoard } from "../shared/use-quote-board";
18
19
  import { getCachedFuturesCurve, loadFuturesCurve, loadFuturesCurveAsOf } from "./client";
19
- import { basisSpotSymbol, curveAsOfDate, curveAxisPrice, curveBasisPercent, curveBasisRows, curveChangeText, curveContractChanges, curvePrice, curveRank, curveSpot, curveSpotLabel, curveTimestamp, DEFAULT_CURVE_HORIZON, futuresCurveSeries, newestQuote, normalizeCurveRoot, sortCurveContracts, thinContractCount, thinContractsNotice, unsupportedCurveRootMessage, type CurveContractChanges } from "./model";
20
+ import { basisSpotSymbol, curveAsOfDate, curveAxisPrice, curveBasisPercent, curveBasisRows, curveChangeText, curveContractChanges, curveContractCode, curvePrice, curveRank, curveSpot, curveSpotLabel, curveSpreadLabel, curveTimestamp, curveUnitLabel, DEFAULT_CURVE_HORIZON, futuresCurveSeries, newestQuote, normalizeCurveRoot, sortCurveContracts, thinContractCount, thinContractsNotice, unsupportedCurveRootMessage, type CurveContractChanges } from "./model";
20
21
 
21
22
  const TABS = [{ value: "curve", label: "Curve" }, { value: "contracts", label: "Contracts" }];
23
+ // One column order for every root. SETTLE is the exchange settlement for the
24
+ // curve's settlement session, LAST the latest trade; the header dates SETTLE.
22
25
  const COLUMNS: DataTableColumn[] = [
23
26
  // The longest symbol is a three-letter root with its month and venue: RTYH27.CME.
24
27
  { id: "symbol", label: "CONTRACT", width: 11, align: "left" },
25
28
  { id: "expiry", label: "EXPIRY", width: 10, align: "left" },
26
- { id: "price", label: "PRICE", width: 12, align: "right" },
29
+ { id: "notice", label: "1ST NOTICE", width: 10, align: "left" },
30
+ // A Treasury price keeps its 1/256 tick: 103.50000000.
31
+ { id: "settle", label: "SETTLE", width: 12, align: "right" },
32
+ { id: "price", label: "LAST", width: 12, align: "right" },
27
33
  // A Treasury change keeps its 1/256 tick: +0.11718750.
28
34
  { id: "change", label: "CHG", width: 11, align: "right" },
29
35
  { id: "percentile", label: "PCTL", width: 5, align: "right" },
@@ -31,25 +37,28 @@ const COLUMNS: DataTableColumn[] = [
31
37
  { id: "volume", label: "VOLUME", width: 10, align: "right" },
32
38
  { id: "asOf", label: "AS OF UTC", width: 16, align: "left" },
33
39
  ];
40
+ const CONTRACTS_DROP_ORDER = ["percentile", "volume", "oi", "notice"];
41
+ const column = (id: string) => COLUMNS.find((entry) => entry.id === id)!;
34
42
  // The curve's rows move with the look-back curves drawn above them; the
35
43
  // footer carries the quote time every row would otherwise repeat. The session
36
44
  // change stays on Contracts, where the full table has the width for it.
37
45
  const CURVE_COLUMNS: DataTableColumn[] = [
38
- ...COLUMNS.slice(0, 3),
46
+ column("symbol"), column("expiry"), column("settle"), column("price"),
39
47
  { id: "change1w", label: "VS 1W", width: 10, align: "right" },
40
48
  { id: "change1m", label: "VS 1M", width: 10, align: "right" },
41
- ...COLUMNS.slice(4, -1),
49
+ column("percentile"), column("oi"), column("volume"),
42
50
  ];
51
+ const CURVE_DROP_ORDER = ["percentile", "volume", "change1m", "oi"];
43
52
  // A crypto curve also reads against spot. Its widths are the tightest each column
44
53
  // holds (BTCV26.CME, 128000.00), so the two new columns fit beside the rest, and
45
54
  // a narrow pane drops the least useful ones before a number would be clipped.
46
55
  const BASIS_CURVE_COLUMNS: DataTableColumn[] = [
47
- { ...CURVE_COLUMNS[0]!, width: 10 }, CURVE_COLUMNS[1]!, { ...CURVE_COLUMNS[2]!, width: 10 },
56
+ { ...column("symbol"), width: 10 }, column("expiry"), { ...column("settle"), width: 10 }, { ...column("price"), width: 10 },
48
57
  { id: "vsSpot", label: "VS SPOT", width: 9, align: "right" },
49
58
  { id: "annBasis", label: "ANN BASIS", width: 11, align: "right" },
50
- ...CURVE_COLUMNS.slice(3),
59
+ ...CURVE_COLUMNS.slice(4),
51
60
  ];
52
- const BASIS_DROP_ORDER = ["percentile", "volume", "change1m", "oi"];
61
+ const BASIS_DROP_ORDER = ["percentile", "volume", "change1m", "oi", "settle"];
53
62
  const NO_SYMBOLS: string[] = [];
54
63
  /** The spot's age is judged against the clock, so a quote that stops moving still goes stale on screen. */
55
64
  const SPOT_CLOCK_MS = 30_000;
@@ -135,17 +144,31 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
135
144
  const statItems: StatItem[] = data ? [
136
145
  { id: "roll", label: "Ann. roll yield", value: signedPercent(data.slope.annualizedRollYield),
137
146
  detail: curveRank(data.slope.rollPercentile, data.slope.samples) },
138
- { id: "spread", label: "M2-M1", value: data.slope.value == null ? "--" : curvePrice(data.slope.value, root),
147
+ // Named by the contracts it subtracts: the front is the first month the market trades, not always the first row.
148
+ { id: "spread", label: curveSpreadLabel(data.slope), value: data.slope.value == null ? "--" : curvePrice(data.slope.value, root),
139
149
  detail: [data.slope.state, curveRank(data.slope.percentile, data.slope.samples), slopeDate].filter(Boolean).join(" · ") },
150
+ ...data.spec ? [{ id: "contract", label: "Contract", value: data.spec.size, detail: `tick ${futuresTickText(data.spec)}` }] : [],
140
151
  ] : [];
141
152
  const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused, dense: true });
142
153
  // The query bar takes one row below the tabs, and a refused date one more.
143
154
  const bodyHeight = Math.max(1, height - tabRows - 1 - (dateError ? 1 : 0));
144
- const columns = useMemo(() => !curveTab ? COLUMNS : basisActive ? fitChartTableColumns(BASIS_CURVE_COLUMNS, width, BASIS_DROP_ORDER) : CURVE_COLUMNS,
145
- [basisActive, curveTab, width]);
155
+ // A Cboe price is the settlement itself, so its curve has no separate last trade.
156
+ const settlesOnly = data?.source === "cboe";
157
+ const settleLabel = data?.settlementDate ? `SETTLE ${data.settlementDate.slice(5)}` : "SETTLE";
158
+ // Cash-settled roots (ES, CL's Brent, VX) have no first notice day to show.
159
+ const noNotice = !data?.contracts.some((row) => row.firstNotice);
160
+ const columns = useMemo(() => {
161
+ // Labelled before fitting: a dated header is wider than the column's numbers.
162
+ const shown = (list: DataTableColumn[]) => list.filter((entry) => !(settlesOnly && entry.id === "price") && !(noNotice && entry.id === "notice"))
163
+ .map((entry) => entry.id === "settle" ? { ...entry, label: settleLabel } : entry);
164
+ return !curveTab ? fitChartTableColumns(shown(COLUMNS), width, CONTRACTS_DROP_ORDER)
165
+ : basisActive ? fitChartTableColumns(shown(BASIS_CURVE_COLUMNS), width, BASIS_DROP_ORDER)
166
+ : fitChartTableColumns(shown(CURVE_COLUMNS), width, CURVE_DROP_ORDER);
167
+ }, [basisActive, curveTab, noNotice, settleLabel, settlesOnly, width]);
146
168
  const formatValue = useCallback((value: number) => curvePrice(value, root), [root]);
147
169
  const formatChange = useCallback((value: number) => curveChangeText(value, root), [root]);
148
- const caption = `${sentenceCase(data?.quoteUnit ?? data?.currency ?? "price")} by contract month`;
170
+ const unit = data ? curveUnitLabel(data) : null;
171
+ const caption = `${sentenceCase(unit ?? "price")} by contract month`;
149
172
  // The strip keeps the curve's shape and the selected contract in one row.
150
173
  const strip = curveTab ? curveStrip(curves, formatValue, { caption: "Price", selectedPointId: selectedId }) : null;
151
174
  // Delayed contract quotes move all session; the curve follows them once a
@@ -177,8 +200,11 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
177
200
  // A past date with no archived curve says why in the body, not behind the warning.
178
201
  const emptyPast = !!requestedDate && !!data && !data.contracts.length;
179
202
  // Thin contracts join the data warnings: the info row has no room for them beside the quote times.
203
+ // A long in these can be assigned delivery: the hedge a trader rolls before first notice.
204
+ const delivering = data?.contracts.filter((row) => row.inDelivery).map((row) => curveContractCode(row.symbol)) ?? [];
180
205
  usePaneNoticeFooter({ registrationId: "futures-curve:notices", focused,
181
- notices: emptyPast ? [] : [...data?.gaps ?? [], ...thinCount ? [thinContractsNotice(thinCount)] : []] });
206
+ notices: emptyPast ? [] : [...data?.gaps ?? [], ...thinCount ? [thinContractsNotice(thinCount)] : [],
207
+ ...delivering.length ? [`${delivering.join(", ")} ${delivering.length === 1 ? "is" : "are"} past first notice.`] : []] });
182
208
  const delay = Math.max(0, ...(data?.contracts.map((row) => row.delayMinutes ?? 0) ?? []));
183
209
  usePaneStatusFooter({ registrationId: "futures-curve", loading: resource.loading, error: resource.error,
184
210
  hints: [
@@ -186,7 +212,7 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
186
212
  ...requestedDate ? [{ id: "latest", key: "c", label: "urrent", title: "Current Curve", onPress: () => selectDate("") }] : [],
187
213
  ],
188
214
  info: data ? [
189
- { id: "source", parts: [{ text: `${requestedDate ? "daily archive" : data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${data.quoteUnit ?? data.currency ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] },
215
+ { id: "source", parts: [{ text: `${requestedDate ? "daily archive" : data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${unit ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] },
190
216
  ...(staleCount ? [{ id: "stale", parts: [{ text: `${staleCount} of ${data.contracts.length} stale`, tone: "warning" as const }] }] : []),
191
217
  ...(spot ? [{ id: "spot", parts: [spot.status === "ok" ? { text: curveSpotLabel(spot, root, clock), tone: "muted" as const }
192
218
  : { text: `Basis blank: ${spot.reason}`, tone: "warning" as const }] }] : []),
@@ -202,10 +228,18 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
202
228
  strip,
203
229
  } : null;
204
230
  const renderCell = useCallback((row: FuturesContract, column: DataTableColumn) => {
205
- if (column.id === "symbol") return { text: row.symbol };
206
- if (column.id === "expiry") return { text: row.expiration, color: colors.textMuted };
207
- // Without the AS OF column, a stale quote shows on its price, and so does a print too old for the spot.
208
- if (column.id === "price") return { text: curvePrice(row.price, root), color: curveTab && (row.stale || basis.get(row.symbol)?.thin) ? colors.warning : undefined };
231
+ // A contract past first notice can be assigned delivery.
232
+ if (column.id === "symbol") return { text: row.symbol, color: row.inDelivery ? colors.warning : undefined };
233
+ if (column.id === "expiry") return { text: row.lastTrade ?? row.expiration, color: colors.textMuted };
234
+ if (column.id === "notice") return { text: row.firstNotice ?? "--", color: row.inDelivery ? colors.warning : colors.textMuted };
235
+ if (column.id === "settle") return { text: curvePrice(row.settlement ?? null, root) };
236
+ // A past curve's settled row has no separate last trade: its price is the settlement.
237
+ if (column.id === "price") {
238
+ const lastTrade = requestedDate && row.settlement != null ? null : row.price;
239
+ // Beside its settlement the last trade is the secondary price; without one it is the row's price.
240
+ return { text: curvePrice(lastTrade, root), color: curveTab && (row.stale || basis.get(row.symbol)?.thin) ? colors.warning
241
+ : row.settlement != null ? colors.textMuted : undefined };
242
+ }
209
243
  if (column.id === "change") {
210
244
  const text = curveChangeText(row.change ?? null, root);
211
245
  return { text, color: text.startsWith("+") ? colors.positive : text.startsWith("-") && /[1-9]/.test(text) ? colors.negative : colors.textMuted };
@@ -222,7 +256,7 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
222
256
  if (column.id === "oi") return { text: integer(row.openInterest) };
223
257
  if (column.id === "volume") return { text: integer(row.volume) };
224
258
  return { text: curveTimestamp(row.asOf), color: row.stale ? colors.warning : colors.textMuted };
225
- }, [basis, changes, colors, curveTab, root]);
259
+ }, [basis, changes, colors, curveTab, requestedDate, root]);
226
260
  return <Box width={width} height={height} flexDirection="column">
227
261
  {tabStrip}
228
262
  <QueryBar width={width} filters={[{
@@ -20,6 +20,7 @@ import { TextAttributes, useUiCapabilities } from "../../../ui";
20
20
  import { useAssetData } from "../../runtime";
21
21
  import { summarizeFxRates, fxStatusLabel } from "../../../utils/fx-status";
22
22
  import type { PluginModule } from "../plugin-module";
23
+ import { fxRateObservations } from "../shared/fx-rate-freshness";
23
24
  import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
24
25
  import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
25
26
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
@@ -28,7 +29,6 @@ import { CURRENCY_FLAG_REGIONS, FX_CURRENCIES, formatRate, resolveCurrencies, ty
28
29
  import { crossMovePercent, directionTint, nextTintLevel } from "./direction";
29
30
  import { FX_MATRIX_REPORT_OPTIONS, fxMatrixReportNotices, fxMatrixSettings } from "./settings";
30
31
  import { createFxExportMetadata } from "./export";
31
- import { fxRateObservations } from "./freshness";
32
32
 
33
33
  const FX_MATRIX_PANE_ID = "fx-matrix";
34
34
  /** Stable identity: a fresh literal here would reload the board every render. */
@@ -12,7 +12,7 @@ import { Box, Text, TextAttributes, useUiCapabilities } from "../../../ui";
12
12
  import { loadGpuHistory } from "./client";
13
13
  import { GpuPriceLadder } from "./ladder";
14
14
  import {
15
- gpuAvailability, gpuBoardSections, gpuChange, gpuChangeColor, gpuChangeWindows, gpuHeadline, gpuPrice, gpuPriceLadder, gpuShortSource, gpuSparklineSeries, gpuTime, gpuVariant,
15
+ gpuAvailability, gpuBoardSections, gpuChange, gpuChangeColor, gpuChangesLocked, gpuChangeWindows, gpuHeadline, gpuPrice, gpuPriceLadder, gpuShortSource, gpuSparklineSeries, gpuTime, gpuVariant,
16
16
  type GpuChangeKey,
17
17
  } from "./model";
18
18
 
@@ -23,10 +23,10 @@ const CHANGE_LABELS: Record<GpuChangeKey, string> = { change1d: "1D", change7d:
23
23
  /** A line needs three observations; two are a step, not a trend. */
24
24
  const MIN_TREND_POINTS = 3;
25
25
 
26
- export function GpuModelQuery({ rows, model, setModel, width, allowAll = true }: {
27
- rows: readonly GpuBoardRow[]; model: string; setModel: (value: string) => void; width: number; allowAll?: boolean;
26
+ /** The GPU picker: the GPUs on the board, or on a free board every GPU, since each has its own free preview. */
27
+ export function GpuModelQuery({ models, model, setModel, width, allowAll = true }: {
28
+ models: readonly string[]; model: string; setModel: (value: string) => void; width: number; allowAll?: boolean;
28
29
  }) {
29
- const models = useMemo(() => [...new Set(rows.map((row) => row.gpuModel))].sort(), [rows]);
30
30
  return <QueryBar width={width} filters={[{ id: "gpu", label: "GPU", value: model, ...(allowAll ? { defaultValue: "" } : {}),
31
31
  options: [...(allowAll ? [{ value: "", label: "All GPUs" }] : []), ...models.map((value) => ({ value, label: value }))], onChange: setModel }]} />;
32
32
  }
@@ -79,9 +79,9 @@ function boardColumns(width: number, options: { windows: GpuChangeKey[]; trend:
79
79
  ];
80
80
  }
81
81
 
82
- export function GpuBoard({ rows, asOf, model, setModel, selectedId, select, width, height, focused }: {
83
- rows: GpuBoardRow[]; asOf: string | null; model: string; setModel: (value: string) => void; selectedId: string; select: (row: GpuBoardRow, open?: boolean) => void;
84
- width: number; height: number; focused: boolean;
82
+ export function GpuBoard({ rows, models, asOf, model, setModel, selectedId, select, emptyTitle, width, height, focused }: {
83
+ rows: GpuBoardRow[]; models: readonly string[]; asOf: string | null; model: string; setModel: (value: string) => void; selectedId: string; select: (row: GpuBoardRow, open?: boolean) => void;
84
+ emptyTitle: string; width: number; height: number; focused: boolean;
85
85
  }) {
86
86
  const colors = useThemeColors();
87
87
  const sections = useMemo(() => gpuBoardSections(rows, model), [rows, model]);
@@ -95,6 +95,8 @@ export function GpuBoard({ rows, asOf, model, setModel, selectedId, select, widt
95
95
  const medium = width >= 100;
96
96
  const asOfDay = asOf?.slice(0, 10) ?? null;
97
97
  const columns = boardColumns(width, { windows, trend, range: visible.some((row) => row.stats), availability: visible.some((row) => row.availability) });
98
+ // A free preview row's moves are Pro's; the cell says so, and the footer's upgrade hint unlocks them.
99
+ const lockedCell: DataTableCell = { text: "Pro", value: null, color: colors.textDim };
98
100
 
99
101
  const renderCell = (item: BoardRow, column: DataTableColumn, _index: number, state: { selected: boolean }): DataTableCell => {
100
102
  if (!isSectionedItemRow(item)) return EMPTY_TABLE_CELL;
@@ -121,8 +123,9 @@ export function GpuBoard({ rows, asOf, model, setModel, selectedId, select, widt
121
123
  if (!quartiles && stats.min === stats.max) return { text: "" };
122
124
  return { text: quartiles ? `${gpuPrice(stats.p25)}–${gpuPrice(stats.p75)}` : `${gpuPrice(stats.min)}–${gpuPrice(stats.max)}`, color: colors.textDim };
123
125
  }
124
- case "unavailableChange": return { text: "-", value: null, color: colors.textMuted };
126
+ case "unavailableChange": return gpuChangesLocked(row) ? lockedCell : { text: "-", value: null, color: colors.textMuted };
125
127
  case "change1d": case "change7d": case "change30d": {
128
+ if (gpuChangesLocked(row)) return lockedCell;
126
129
  const value = row[column.id];
127
130
  return value == null ? { text: "-", value: null, color: colors.textMuted } : { text: gpuChange(value), value, color: gpuChangeColor(value, colors) };
128
131
  }
@@ -148,7 +151,7 @@ export function GpuBoard({ rows, asOf, model, setModel, selectedId, select, widt
148
151
  }
149
152
  };
150
153
 
151
- const query = <GpuModelQuery rows={rows} model={model} setModel={setModel} width={width} />;
154
+ const query = <GpuModelQuery models={models} model={model} setModel={setModel} width={width} />;
152
155
  return <DataTableView<BoardRow> columns={columns} items={items} rootWidth={width} rootHeight={height} focused={focused}
153
156
  rootBefore={ladder.length ? <ChartTableHeader width={width} height={height} tableRows={items.length} tableColumns={columns} query={query}
154
157
  chart={{ maxRows: ladder.length + 1, minRows: Math.min(3, ladder.length + 1), strip: null,
@@ -159,5 +162,5 @@ export function GpuBoard({ rows, asOf, model, setModel, selectedId, select, widt
159
162
  onChange: (_id, row) => { if (row && isSectionedItemRow(row)) select(row.item); } }}
160
163
  isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
161
164
  getItemKey={rowKey} onActivate={(row) => { if (isSectionedItemRow(row)) select(row.item, true); }} sortColumnId={null} sortDirection="asc"
162
- selectedTextOverridesCellColor renderCell={renderCell} emptyStateTitle="No GPU rental prices match." />;
165
+ selectedTextOverridesCellColor renderCell={renderCell} emptyStateTitle={emptyTitle} />;
163
166
  }
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useMemo } from "react";
2
- import type { GpuBoardRow, GpuEvent } from "../../../api-client/gpu";
2
+ import type { GpuEvent } from "../../../api-client/gpu";
3
3
  import {
4
4
  Badge, buildSectionedRows, DataTableView, EMPTY_TABLE_CELL, isSectionedItemRow, PaneStatusBody, renderSectionedRowHeader,
5
5
  usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn, type SectionedRow,
@@ -30,8 +30,8 @@ function changeColumns(width: number, allModels: boolean, availabilityWidth: num
30
30
  }
31
31
 
32
32
  /** Dated price moves, newest day first: provider, old to new, and the move as a chip coloured by its sign. */
33
- export function GpuChanges({ board, model, setModel, reloadBoard, width, height, focused }: {
34
- board: GpuBoardRow[]; model: string; setModel: (value: string) => void; reloadBoard: () => void; width: number; height: number; focused: boolean;
33
+ export function GpuChanges({ models, model, setModel, reloadBoard, width, height, focused }: {
34
+ models: readonly string[]; model: string; setModel: (value: string) => void; reloadBoard: () => void; width: number; height: number; focused: boolean;
35
35
  }) {
36
36
  const colors = useThemeColors();
37
37
  const loader = useCallback((force: boolean) => loadGpuEvents(model || undefined, force), [model]);
@@ -84,7 +84,7 @@ export function GpuChanges({ board, model, setModel, reloadBoard, width, height,
84
84
 
85
85
  return <PaneStatusBody loading={resource.loading && !resource.data} error={!resource.data ? resource.error : null} subject="GPU price changes">
86
86
  <DataTableView<ChangeRow> columns={columns} items={items} rootWidth={width} rootHeight={height} focused={focused}
87
- rootBefore={<GpuModelQuery rows={board} model={model} setModel={setModel} width={width} />}
87
+ rootBefore={<GpuModelQuery models={models} model={model} setModel={setModel} width={width} />}
88
88
  selection={{ kind: "id", selectedId: selected && items.some((item) => item.key === selected) ? selected : firstId, getId: rowKey,
89
89
  onChange: (id) => setSelected(String(id)) }}
90
90
  isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
@@ -4,7 +4,7 @@ import type { GpuBoardPayload, GpuEventsPayload, GpuHistoryPayload, GpuHistoryQu
4
4
  import { createPluginCache } from "../../../data/plugin-cache";
5
5
  import type { DataProvider } from "../../../types/data-provider";
6
6
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
7
- import { equityFiveDayReturn, GPU_EQUITIES } from "./model";
7
+ import { equityFiveDayReturn, GPU_EQUITIES, gpuPreviewRows, type GpuPreviewRow } from "./model";
8
8
 
9
9
  export const GPU_NOT_AVAILABLE = "GPU rental prices are not available yet.";
10
10
  const cacheOptions = { source: "gloom-cloud", schemaVersion: 3, policy: { staleMs: 5 * 60_000, expireMs: 7 * 86_400_000 } };
@@ -116,6 +116,24 @@ export const loadGpuHistory = (seriesId: string, force = false) => loadCloudReso
116
116
  () => fetchGpuHistory({ seriesId, limit: 10_000 }), { force, validate: validateHistory });
117
117
  export const loadGpuReferences = (force = false) => loadCloudResource(gpuHistoryCache, scopedKey("reference-catalog"),
118
118
  () => fetchGpuHistory({ basis: "reference", limit: 10_000 }), { force, validate: validateHistory });
119
+ /**
120
+ * The free board is three rows of one GPU. The rows it leaves out for the
121
+ * GPUs a reader asks about come from each GPU's own free preview, the latest
122
+ * rows the history serves without an account. A Pro board needs none.
123
+ */
124
+ export function gpuPreviewModels(board: Pick<GpuBoardPayload, "access" | "rows">, models: readonly string[]): string[] {
125
+ if (!board.access?.locked) return [];
126
+ return [...new Set(models)].filter((model) => model && !board.rows.some((row) => row.gpuModel === model));
127
+ }
128
+ export async function fetchGpuModelPreviews(models: readonly string[], client: Pick<GpuApi, "getCloudGpuHistory"> = apiClient): Promise<GpuPreviewRow[]> {
129
+ const previews = await Promise.all(models.map((gpuModel) => fetchGpuHistory({ gpuModel, limit: 10 }, client)));
130
+ return previews.flatMap((payload) => gpuPreviewRows(payload.points));
131
+ }
132
+ export const loadGpuModelPreviews = async (models: readonly string[], force = false): Promise<GpuPreviewRow[]> => {
133
+ const previews = await Promise.all(models.map((gpuModel) => loadCloudResource(gpuHistoryCache, scopedKey(`preview:${gpuModel}`),
134
+ () => fetchGpuHistory({ gpuModel, limit: 10 }), { force, validate: validateHistory })));
135
+ return previews.flatMap((preview) => gpuPreviewRows(preview.payload.points));
136
+ };
119
137
  export const loadGpuEvents = (gpuModel?: string, force = false) => loadCloudResource(gpuEventsCache, scopedKey(gpuModel ?? "all"),
120
138
  () => fetchGpuEvents(gpuModel), { force, validate: validateEvents });
121
139
 
@@ -16,11 +16,10 @@ import { displayWidth, truncateToDisplayWidth } from "../../../utils/format";
16
16
  import { quoteBoardFooterInfo, quoteBoardStatus, renderQuoteBoardCell, useQuoteBoard } from "../shared/use-quote-board";
17
17
  import { GpuModelQuery } from "./board";
18
18
  import { loadGpuEquityHistory } from "./client";
19
- import { GPU_EQUITIES, GPU_EQUITY_GROUPS, gpuChange, gpuChangeColor, gpuEquityRows, gpuPrice, gpuShortSource, gpuVariant } from "./model";
19
+ import { GPU_EQUITIES, GPU_EQUITY_GROUPS, gpuChange, gpuChangeColor, gpuChangesLocked, gpuEquityGap, gpuEquityRows, gpuPrice, gpuShortSource, gpuVariant, type GpuEquityRow } from "./model";
20
20
 
21
21
  const SYMBOLS = GPU_EQUITIES.map(({ symbol }) => symbol);
22
- type EquityRow = ReturnType<typeof gpuEquityRows>[number];
23
- type Row = SectionedRow<EquityRow>;
22
+ type Row = SectionedRow<GpuEquityRow>;
24
23
  const rowKey = (row: Row) => row.key;
25
24
 
26
25
  function equityColumns(width: number): DataTableColumn[] {
@@ -39,8 +38,9 @@ function equityColumns(width: number): DataTableColumn[] {
39
38
  }
40
39
 
41
40
  /** Related shares by what ties them to GPU rents, each beside the list price that ties it. */
42
- export function GpuEquities({ board, model, setModel, reloadBoard, width, height, focused }: {
43
- board: GpuBoardRow[]; model: string; setModel: (value: string) => void; reloadBoard: () => void; width: number; height: number; focused: boolean;
41
+ export function GpuEquities({ board, models, locked, model, setModel, reloadBoard, width, height, focused }: {
42
+ board: GpuBoardRow[]; models: readonly string[]; locked: boolean; model: string; setModel: (value: string) => void; reloadBoard: () => void;
43
+ width: number; height: number; focused: boolean;
44
44
  }) {
45
45
  const colors = useThemeColors();
46
46
  const provider = useAssetData();
@@ -54,7 +54,7 @@ export function GpuEquities({ board, model, setModel, reloadBoard, width, height
54
54
  const history = useAsyncResource(loader);
55
55
  useAutoRefresh(history.updatedAt, history.load);
56
56
  usePaneRefreshKey(() => { reloadBoard(); refresh(); void history.reload(); }, { focused });
57
- const rows = useMemo(() => gpuEquityRows(board, model || "H100"), [board, model]);
57
+ const rows = useMemo(() => gpuEquityRows(board, model || "H100", locked), [board, model, locked]);
58
58
  const items = useMemo(() => buildSectionedRows(GPU_EQUITY_GROUPS.map((group) => ({ label: group.label,
59
59
  items: rows.filter((row) => (group.roles as readonly string[]).includes(row.role)) })), (row) => row.symbol), [rows]);
60
60
  const historyMap = useMemo(() => new Map(history.data?.map((entry) => [entry.symbol, entry]) ?? []), [history.data]);
@@ -70,10 +70,13 @@ export function GpuEquities({ board, model, setModel, reloadBoard, width, height
70
70
  if (!isSectionedItemRow(item)) return EMPTY_TABLE_CELL;
71
71
  const row = item.item;
72
72
  const reference = row.reference;
73
+ // A figure the free preview leaves out reads Pro; a share with no series names the one it lacks.
74
+ const lockedCell: DataTableCell = { text: "Pro", value: null, color: colors.textDim };
75
+ const gapCell = row.gap === "pro" ? lockedCell : { text: "" };
73
76
  switch (column.id) {
74
77
  case "symbol": return { text: row.symbol, color: colors.textBright, attributes: TextAttributes.BOLD };
75
78
  case "source": {
76
- if (!reference) return { text: "", color: colors.textDim };
79
+ if (!reference) return { text: gpuEquityGap(row) ?? "", color: colors.textDim };
77
80
  const name = gpuShortSource(reference);
78
81
  const variant = [row.gpuModel, ...gpuVariant(reference)].join(" ");
79
82
  // The name keeps its room; the variant gives way first.
@@ -83,9 +86,10 @@ export function GpuEquities({ board, model, setModel, reloadBoard, width, height
83
86
  {rest >= 4 ? <Text fg={state.selected ? colors.selectedText : colors.textDim}>{` ${truncateToDisplayWidth(variant, rest)}`}</Text> : null}
84
87
  </Box> };
85
88
  }
86
- case "gpuPrice": return reference ? { text: gpuPrice(reference.pricePerGpuHr), value: reference.pricePerGpuHr, color: colors.textBright } : { text: "" };
89
+ case "gpuPrice": return reference ? { text: gpuPrice(reference.pricePerGpuHr), value: reference.pricePerGpuHr, color: colors.textBright } : gapCell;
87
90
  case "gpuChange": {
88
- if (!reference) return { text: "" };
91
+ if (!reference) return gapCell;
92
+ if (gpuChangesLocked(reference)) return lockedCell;
89
93
  const value = reference.change7d;
90
94
  return value == null ? { text: "-", value: null, color: colors.textMuted } : { text: gpuChange(value), value, color: gpuChangeColor(value, colors) };
91
95
  }
@@ -102,7 +106,7 @@ export function GpuEquities({ board, model, setModel, reloadBoard, width, height
102
106
  }
103
107
  }, [colors]);
104
108
  return <DataTableView<Row> columns={equityColumns(width)} items={items} rootWidth={width} rootHeight={height} focused={focused}
105
- rootBefore={<GpuModelQuery rows={board} model={model || "H100"} setModel={setModel} width={width} allowAll={false} />}
109
+ rootBefore={<GpuModelQuery models={models} model={model || "H100"} setModel={setModel} width={width} allowAll={false} />}
106
110
  selection={{ kind: "id", selectedId: selected, getId: rowKey, onChange: (id) => setSelected(String(id)) }} getItemKey={rowKey}
107
111
  isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
108
112
  getRowVersion={(row) => isSectionedItemRow(row) ? [quotes.get(row.item.symbol)?.quote?.lastUpdated, quotes.get(row.item.symbol)?.loading,