gloomberb 0.15.8 → 0.16.0

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 (473) hide show
  1. package/README.md +10 -2
  2. package/package.json +1 -1
  3. package/src/api-client/app-attention.ts +147 -0
  4. package/src/api-client/attention.ts +26 -0
  5. package/src/api-client/awards.ts +215 -0
  6. package/src/api-client/catalysts.ts +160 -0
  7. package/src/api-client/chat.ts +137 -14
  8. package/src/api-client/company-kpis.ts +153 -0
  9. package/src/api-client/cpi.ts +52 -0
  10. package/src/api-client/credit-documents.ts +107 -0
  11. package/src/api-client/data.ts +187 -3
  12. package/src/api-client/distress.ts +164 -0
  13. package/src/api-client/equity-screener.ts +11 -0
  14. package/src/api-client/exposure.ts +202 -0
  15. package/src/api-client/gpu.ts +93 -0
  16. package/src/api-client/hiring.ts +110 -0
  17. package/src/api-client/index.ts +77 -3
  18. package/src/api-client/normalizers.ts +31 -1
  19. package/src/api-client/perps.ts +127 -0
  20. package/src/api-client/power.ts +148 -0
  21. package/src/api-client/request.ts +58 -1
  22. package/src/api-client/research-activity.ts +172 -13
  23. package/src/api-client/socket.ts +75 -27
  24. package/src/api-client/supply-chain-graph.ts +40 -0
  25. package/src/api-client/supply-chain.ts +98 -0
  26. package/src/api-client/telemetry.ts +29 -0
  27. package/src/api-client/themes.ts +28 -0
  28. package/src/api-client/types.ts +31 -1
  29. package/src/api-client/wall-experiments.ts +100 -0
  30. package/src/api-client/wall-summary.ts +49 -0
  31. package/src/api-client/web-experiments.ts +136 -0
  32. package/src/app/pane-runtime/plugin-bindings.ts +5 -0
  33. package/src/app/pane-runtime/ticker-open-runtime.ts +8 -2
  34. package/src/app/runtime/update.ts +13 -1
  35. package/src/cli/commands/system.ts +2 -0
  36. package/src/cli/desktop-pane-shot.ts +3 -4
  37. package/src/cli/pane-functions/data.ts +78 -0
  38. package/src/cli/pane-functions/financial-ratio-shot.ts +160 -0
  39. package/src/cli/pane-functions/function-help.ts +205 -8
  40. package/src/cli/pane-functions/headless.ts +4 -0
  41. package/src/cli/pane-functions/screenshot-evidence.ts +21 -0
  42. package/src/cli/pane-functions/screenshot.ts +49 -5
  43. package/src/components/chart-table/index.ts +1 -0
  44. package/src/components/chart-table/layout.ts +15 -0
  45. package/src/components/command-bar/action-runtime.ts +1 -0
  46. package/src/components/command-bar/commands/direct/index.ts +32 -0
  47. package/src/components/command-bar/commands/registry.ts +7 -0
  48. package/src/components/command-bar/pane-templates/items.ts +11 -0
  49. package/src/components/command-bar/pane-templates/workflow.ts +19 -4
  50. package/src/components/command-bar/routes/root/command-items.ts +6 -0
  51. package/src/components/command-bar/routes/root/provider-search.ts +2 -0
  52. package/src/components/command-bar/routes/root/results.ts +64 -0
  53. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  54. package/src/components/command-bar/routes/ticker-search/results.ts +89 -24
  55. package/src/components/command-bar/routes/ticker-search/route.ts +2 -0
  56. package/src/components/command-bar/surface/index.tsx +15 -0
  57. package/src/components/command-bar/view-model.ts +3 -0
  58. package/src/components/command-bar/workflow/ops.ts +19 -0
  59. package/src/components/command-bar/workflow/tickers.ts +5 -0
  60. package/src/components/company-logo.tsx +15 -2
  61. package/src/components/data-table/table-pane.ts +2 -1
  62. package/src/components/index.ts +1 -0
  63. package/src/components/layout/detached-pane-shell.tsx +12 -1
  64. package/src/components/layout/missing-pane.tsx +87 -23
  65. package/src/components/layout/pane/footer/index.tsx +64 -18
  66. package/src/components/layout/pane/footer/model.ts +85 -0
  67. package/src/components/layout/pane/sidebar.tsx +19 -1
  68. package/src/components/layout/shell/pane/management-shortcuts.ts +13 -1
  69. package/src/components/layout/status-bar.tsx +93 -28
  70. package/src/components/layout/status-widget-space.ts +72 -0
  71. package/src/components/onboarding/first-run-workspace.ts +8 -2
  72. package/src/components/onboarding/onboarding-wizard.tsx +35 -16
  73. package/src/components/price-performance.tsx +31 -34
  74. package/src/components/ui/data-table/index.tsx +11 -8
  75. package/src/components/ui/display.tsx +9 -3
  76. package/src/components/ui/figure.tsx +94 -0
  77. package/src/components/ui/icon.tsx +3 -1
  78. package/src/components/ui/message-composer.tsx +8 -1
  79. package/src/components/ui/ratio-bar.tsx +84 -0
  80. package/src/components/ui/toggle.tsx +3 -1
  81. package/src/components/use-scrollbox-layout.ts +17 -0
  82. package/src/core/state/app/layout.ts +35 -1
  83. package/src/core/state/app/state.ts +15 -0
  84. package/src/core/state/app/types.ts +1 -0
  85. package/src/data/config/store/normalize.ts +24 -0
  86. package/src/i18n/es.ts +1 -0
  87. package/src/i18n/ja.ts +1 -0
  88. package/src/i18n/ko.ts +1 -0
  89. package/src/i18n/zh-cn.ts +1 -0
  90. package/src/i18n/zh-tw.ts +1 -0
  91. package/src/layout-marketplace/pane-imagery.ts +1 -0
  92. package/src/market-data/coordinator/entries.ts +13 -0
  93. package/src/market-data/coordinator/financials.ts +33 -8
  94. package/src/market-data/market/freshness.ts +16 -5
  95. package/src/market-data/published-cn-sessions.ts +25 -0
  96. package/src/market-data/published-nse-sessions.ts +20 -0
  97. package/src/news/aggregator.ts +4 -0
  98. package/src/plugins/builtin/account-management/model.ts +6 -1
  99. package/src/plugins/builtin/account-management/pane.tsx +36 -25
  100. package/src/plugins/builtin/alerts/catalysts.ts +77 -0
  101. package/src/plugins/builtin/alerts/events-pane.tsx +3 -0
  102. package/src/plugins/builtin/alerts/events.ts +10 -4
  103. package/src/plugins/builtin/alerts/history-pane.tsx +1 -1
  104. package/src/plugins/builtin/alerts/history.ts +1 -0
  105. package/src/plugins/builtin/alerts/index.tsx +6 -0
  106. package/src/plugins/builtin/alerts/research-builder.ts +10 -3
  107. package/src/plugins/builtin/alerts/research-rules.ts +15 -1
  108. package/src/plugins/builtin/analytics/risk-client.ts +15 -3
  109. package/src/plugins/builtin/analytics/risk-headless.ts +123 -18
  110. package/src/plugins/builtin/attention/client.ts +80 -0
  111. package/src/plugins/builtin/attention/evidence.ts +30 -0
  112. package/src/plugins/builtin/attention/headless.ts +33 -0
  113. package/src/plugins/builtin/attention/index.tsx +18 -0
  114. package/src/plugins/builtin/attention/model.ts +59 -0
  115. package/src/plugins/builtin/attention/pane.tsx +211 -0
  116. package/src/plugins/builtin/attention/trending.tsx +59 -0
  117. package/src/plugins/builtin/awards/alerts.ts +32 -0
  118. package/src/plugins/builtin/awards/client.ts +82 -0
  119. package/src/plugins/builtin/awards/detail.tsx +107 -0
  120. package/src/plugins/builtin/awards/evidence.ts +36 -0
  121. package/src/plugins/builtin/awards/headless.ts +55 -0
  122. package/src/plugins/builtin/awards/index.tsx +20 -0
  123. package/src/plugins/builtin/awards/model.ts +42 -0
  124. package/src/plugins/builtin/awards/pane.tsx +263 -0
  125. package/src/plugins/builtin/catalysts/client.ts +28 -0
  126. package/src/plugins/builtin/catalysts/detail.tsx +88 -0
  127. package/src/plugins/builtin/catalysts/evidence.ts +47 -0
  128. package/src/plugins/builtin/catalysts/headless.ts +42 -0
  129. package/src/plugins/builtin/catalysts/history.ts +26 -0
  130. package/src/plugins/builtin/catalysts/index.tsx +29 -0
  131. package/src/plugins/builtin/catalysts/model.ts +135 -0
  132. package/src/plugins/builtin/catalysts/pane.tsx +159 -0
  133. package/src/plugins/builtin/chart-composer/index.tsx +8 -1
  134. package/src/plugins/builtin/chat/attachments/desktop.tsx +610 -0
  135. package/src/plugins/builtin/chat/attachments/files.ts +45 -0
  136. package/src/plugins/builtin/chat/attachments/model.ts +164 -0
  137. package/src/plugins/builtin/chat/attachments/terminal.tsx +72 -0
  138. package/src/plugins/builtin/chat/attachments/transfer.ts +165 -0
  139. package/src/plugins/builtin/chat/content/channel-navigation.ts +13 -12
  140. package/src/plugins/builtin/chat/content/composer-runtime.ts +25 -4
  141. package/src/plugins/builtin/chat/content/composer.tsx +46 -4
  142. package/src/plugins/builtin/chat/content/footer.tsx +46 -8
  143. package/src/plugins/builtin/chat/content/index.tsx +263 -62
  144. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -2
  145. package/src/plugins/builtin/chat/content/open-targets.ts +17 -3
  146. package/src/plugins/builtin/chat/content/scroll.ts +7 -1
  147. package/src/plugins/builtin/chat/content/shortcuts.ts +36 -11
  148. package/src/plugins/builtin/chat/content/snapshot.ts +3 -0
  149. package/src/plugins/builtin/chat/content/transcript.tsx +17 -1
  150. package/src/plugins/builtin/chat/content/types.ts +7 -0
  151. package/src/plugins/builtin/chat/controller/attachments.ts +213 -0
  152. package/src/plugins/builtin/chat/controller/connections.ts +3 -1
  153. package/src/plugins/builtin/chat/controller/index.ts +114 -2
  154. package/src/plugins/builtin/chat/controller/lifecycle.ts +9 -0
  155. package/src/plugins/builtin/chat/controller/messages.ts +35 -2
  156. package/src/plugins/builtin/chat/controller/send.ts +128 -25
  157. package/src/plugins/builtin/chat/controller/state.ts +12 -1
  158. package/src/plugins/builtin/chat/controller/utils.ts +7 -5
  159. package/src/plugins/builtin/chat/controller/view.ts +2 -0
  160. package/src/plugins/builtin/chat/index.tsx +21 -0
  161. package/src/plugins/builtin/chat/layout.ts +9 -4
  162. package/src/plugins/builtin/chat/message/desktop.tsx +55 -20
  163. package/src/plugins/builtin/chat/message/header.tsx +30 -5
  164. package/src/plugins/builtin/chat/message/inline-tokens.tsx +9 -0
  165. package/src/plugins/builtin/chat/message/pending-send.ts +35 -0
  166. package/src/plugins/builtin/chat/message/profile-popover.tsx +5 -1
  167. package/src/plugins/builtin/chat/message/render-state.ts +12 -2
  168. package/src/plugins/builtin/chat/message/terminal.tsx +51 -4
  169. package/src/plugins/builtin/chat/message/types.ts +6 -1
  170. package/src/plugins/builtin/chat/pane-state.ts +59 -1
  171. package/src/plugins/builtin/chat/profile-popover.ts +50 -1
  172. package/src/plugins/builtin/chat/sidebar.tsx +8 -3
  173. package/src/plugins/builtin/chat/status-widget.tsx +29 -12
  174. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +143 -0
  175. package/src/plugins/builtin/chat/unread-inbox.ts +91 -0
  176. package/src/plugins/builtin/clinical-trials/client.ts +237 -0
  177. package/src/plugins/builtin/clinical-trials/index.tsx +74 -0
  178. package/src/plugins/builtin/clinical-trials/pane.tsx +225 -0
  179. package/src/plugins/builtin/clinical-trials/types.ts +2 -0
  180. package/src/plugins/builtin/cloud/askg/controller.ts +37 -6
  181. package/src/plugins/builtin/cloud/askg/executor.ts +59 -44
  182. package/src/plugins/builtin/cloud/askg/host.ts +8 -1
  183. package/src/plugins/builtin/cloud/askg/manifest.ts +29 -2
  184. package/src/plugins/builtin/cloud/askg/model.ts +47 -13
  185. package/src/plugins/builtin/cloud/askg/notes.ts +100 -0
  186. package/src/plugins/builtin/cloud/askg/pane.tsx +50 -14
  187. package/src/plugins/builtin/cloud/askg/protocol.ts +39 -1
  188. package/src/plugins/builtin/cloud/askg/result-budget.ts +156 -0
  189. package/src/plugins/builtin/cloud/askg/user-data.ts +89 -0
  190. package/src/plugins/builtin/cloud/auth-actions.tsx +55 -31
  191. package/src/plugins/builtin/cloud/plugin.tsx +4 -1
  192. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -1
  193. package/src/plugins/builtin/cloud/team/pane.tsx +3 -2
  194. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +2 -2
  195. package/src/plugins/builtin/cloud/thesis/detail.tsx +1 -1
  196. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +2 -2
  197. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +202 -0
  198. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  199. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +29 -3
  200. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +1 -1
  201. package/src/plugins/builtin/cloud-tweets/table.tsx +1 -1
  202. package/src/plugins/builtin/comment-letters/client.ts +342 -0
  203. package/src/plugins/builtin/comment-letters/index.tsx +74 -0
  204. package/src/plugins/builtin/comment-letters/pane.tsx +295 -0
  205. package/src/plugins/builtin/comment-letters/types.ts +43 -0
  206. package/src/plugins/builtin/company-attention/app-rank.tsx +79 -0
  207. package/src/plugins/builtin/company-attention/client.ts +69 -0
  208. package/src/plugins/builtin/company-attention/evidence.ts +45 -0
  209. package/src/plugins/builtin/company-attention/headless.ts +47 -0
  210. package/src/plugins/builtin/company-attention/index.tsx +30 -0
  211. package/src/plugins/builtin/company-attention/model.ts +129 -0
  212. package/src/plugins/builtin/company-attention/pane.tsx +183 -0
  213. package/src/plugins/builtin/company-kpis/client.ts +92 -0
  214. package/src/plugins/builtin/company-kpis/headless.ts +32 -0
  215. package/src/plugins/builtin/company-kpis/index.tsx +26 -0
  216. package/src/plugins/builtin/company-kpis/model.ts +124 -0
  217. package/src/plugins/builtin/company-kpis/pane.tsx +226 -0
  218. package/src/plugins/builtin/company-kpis/related.ts +11 -0
  219. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +44 -0
  220. package/src/plugins/builtin/composite-plugins.ts +32 -0
  221. package/src/plugins/builtin/correlation/history.ts +4 -1
  222. package/src/plugins/builtin/correlation/index.tsx +139 -80
  223. package/src/plugins/builtin/correlation/matrix/pair-chart.tsx +48 -0
  224. package/src/plugins/builtin/correlation/matrix/selection.ts +77 -0
  225. package/src/plugins/builtin/correlation/relationship/model.ts +2 -2
  226. package/src/plugins/builtin/cpi/client.ts +66 -0
  227. package/src/plugins/builtin/cpi/headless.ts +78 -0
  228. package/src/plugins/builtin/cpi/index.tsx +31 -0
  229. package/src/plugins/builtin/cpi/model.ts +198 -0
  230. package/src/plugins/builtin/cpi/pane.tsx +157 -0
  231. package/src/plugins/builtin/credit-documents/client.ts +99 -0
  232. package/src/plugins/builtin/credit-documents/detail.tsx +109 -0
  233. package/src/plugins/builtin/credit-documents/evidence.ts +64 -0
  234. package/src/plugins/builtin/credit-documents/gauge.tsx +55 -0
  235. package/src/plugins/builtin/credit-documents/headless.ts +40 -0
  236. package/src/plugins/builtin/credit-documents/index.tsx +19 -0
  237. package/src/plugins/builtin/credit-documents/model.ts +336 -0
  238. package/src/plugins/builtin/credit-documents/pane.tsx +185 -0
  239. package/src/plugins/builtin/custom-view/view-spec.ts +14 -2
  240. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -0
  241. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  242. package/src/plugins/builtin/earnings/board.tsx +3 -0
  243. package/src/plugins/builtin/earnings/index.tsx +2 -0
  244. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -1
  245. package/src/plugins/builtin/earnings-ripple/client.ts +60 -0
  246. package/src/plugins/builtin/earnings-ripple/headless.ts +35 -0
  247. package/src/plugins/builtin/earnings-ripple/index.tsx +35 -0
  248. package/src/plugins/builtin/earnings-ripple/model.ts +98 -0
  249. package/src/plugins/builtin/earnings-ripple/pane.tsx +103 -0
  250. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  251. package/src/plugins/builtin/equity-screener/criterion-editor.tsx +1 -1
  252. package/src/plugins/builtin/equity-screener/model.ts +74 -9
  253. package/src/plugins/builtin/equity-screener/pane.tsx +2 -0
  254. package/src/plugins/builtin/estimate-revisions/pane.tsx +3 -0
  255. package/src/plugins/builtin/executives/pane.tsx +3 -1
  256. package/src/plugins/builtin/exposure/charts.tsx +66 -0
  257. package/src/plugins/builtin/exposure/client.ts +53 -0
  258. package/src/plugins/builtin/exposure/drivers.ts +29 -0
  259. package/src/plugins/builtin/exposure/editor.tsx +61 -0
  260. package/src/plugins/builtin/exposure/evidence.ts +39 -0
  261. package/src/plugins/builtin/exposure/headless.ts +51 -0
  262. package/src/plugins/builtin/exposure/holdings.ts +56 -0
  263. package/src/plugins/builtin/exposure/index.tsx +13 -0
  264. package/src/plugins/builtin/exposure/model.ts +104 -0
  265. package/src/plugins/builtin/exposure/pane.tsx +247 -0
  266. package/src/plugins/builtin/fear-greed/pane.tsx +2 -9
  267. package/src/plugins/builtin/filing-events/pane.tsx +2 -1
  268. package/src/plugins/builtin/gpu/board.tsx +163 -0
  269. package/src/plugins/builtin/gpu/changes.tsx +94 -0
  270. package/src/plugins/builtin/gpu/client.ts +130 -0
  271. package/src/plugins/builtin/gpu/equities.tsx +112 -0
  272. package/src/plugins/builtin/gpu/headless.ts +67 -0
  273. package/src/plugins/builtin/gpu/history.tsx +140 -0
  274. package/src/plugins/builtin/gpu/index.tsx +18 -0
  275. package/src/plugins/builtin/gpu/ladder.tsx +127 -0
  276. package/src/plugins/builtin/gpu/model.ts +317 -0
  277. package/src/plugins/builtin/gpu/pane.tsx +63 -0
  278. package/src/plugins/builtin/help/index.tsx +19 -2
  279. package/src/plugins/builtin/holders/pane.tsx +4 -8
  280. package/src/plugins/builtin/insider/detail.tsx +119 -0
  281. package/src/plugins/builtin/insider/display.ts +221 -0
  282. package/src/plugins/builtin/insider/headless.ts +24 -3
  283. package/src/plugins/builtin/insider/index.tsx +206 -108
  284. package/src/plugins/builtin/insider/insider-data.ts +31 -3
  285. package/src/plugins/builtin/insider/model.ts +53 -4
  286. package/src/plugins/builtin/insider/table-model.ts +226 -0
  287. package/src/plugins/builtin/insider/table.tsx +79 -0
  288. package/src/plugins/builtin/iv-history/screen-pane.tsx +1 -1
  289. package/src/plugins/builtin/jobs/pane.tsx +14 -7
  290. package/src/plugins/builtin/macro-day/client.ts +50 -0
  291. package/src/plugins/builtin/macro-day/headless.ts +51 -0
  292. package/src/plugins/builtin/macro-day/index.tsx +21 -0
  293. package/src/plugins/builtin/macro-day/model.ts +137 -0
  294. package/src/plugins/builtin/macro-day/pane.tsx +118 -0
  295. package/src/plugins/builtin/macro-day/releases.ts +58 -0
  296. package/src/plugins/builtin/market-halts/model.ts +30 -17
  297. package/src/plugins/builtin/market-halts/pane.tsx +15 -21
  298. package/src/plugins/builtin/market-heatmap/data.ts +1 -1
  299. package/src/plugins/builtin/market-heatmap/index.tsx +198 -50
  300. package/src/plugins/builtin/market-heatmap/portfolio.ts +220 -0
  301. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  302. package/src/plugins/builtin/market-movers/session-body.tsx +5 -2
  303. package/src/plugins/builtin/mna/distress/client.ts +326 -0
  304. package/src/plugins/builtin/mna/distress/detail.tsx +247 -0
  305. package/src/plugins/builtin/mna/distress/headless.ts +257 -0
  306. package/src/plugins/builtin/mna/distress/model.ts +427 -0
  307. package/src/plugins/builtin/mna/distress/pane.tsx +606 -0
  308. package/src/plugins/builtin/mna/index.tsx +15 -0
  309. package/src/plugins/builtin/mna/pane.tsx +42 -1
  310. package/src/plugins/builtin/news/index.tsx +62 -16
  311. package/src/plugins/builtin/news/wire/filter-articles.ts +54 -0
  312. package/src/plugins/builtin/news/wire/index.tsx +37 -3
  313. package/src/plugins/builtin/news/wire/industry-pane.tsx +29 -13
  314. package/src/plugins/builtin/news/wire/mutes.ts +253 -0
  315. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +117 -0
  316. package/src/plugins/builtin/news/wire/news/footer.ts +4 -0
  317. package/src/plugins/builtin/news/wire/news/list-search.ts +34 -0
  318. package/src/plugins/builtin/news/wire/news/pop-out.tsx +158 -0
  319. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +64 -19
  320. package/src/plugins/builtin/openfda/client.ts +300 -0
  321. package/src/plugins/builtin/openfda/index.tsx +71 -0
  322. package/src/plugins/builtin/openfda/pane.tsx +382 -0
  323. package/src/plugins/builtin/openfda/types.ts +76 -0
  324. package/src/plugins/builtin/options/footer.ts +1 -0
  325. package/src/plugins/builtin/options-calculator/pane.tsx +0 -1
  326. package/src/plugins/builtin/options-positioning/index.tsx +4 -2
  327. package/src/plugins/builtin/options-positioning/pane.tsx +1 -1
  328. package/src/plugins/builtin/pe-band/client.ts +61 -0
  329. package/src/plugins/builtin/pe-band/headless.ts +51 -0
  330. package/src/plugins/builtin/pe-band/index.tsx +20 -0
  331. package/src/plugins/builtin/pe-band/model.ts +267 -0
  332. package/src/plugins/builtin/pe-band/pane.tsx +159 -0
  333. package/src/plugins/builtin/perps/client.ts +66 -0
  334. package/src/plugins/builtin/perps/equity-row.tsx +28 -0
  335. package/src/plugins/builtin/perps/headless.ts +34 -0
  336. package/src/plugins/builtin/perps/index.tsx +14 -0
  337. package/src/plugins/builtin/perps/model.ts +58 -0
  338. package/src/plugins/builtin/perps/pane.tsx +185 -0
  339. package/src/plugins/builtin/plugin-marketplace/pane.tsx +0 -1
  340. package/src/plugins/builtin/portfolio-list/cli/render.ts +177 -87
  341. package/src/plugins/builtin/portfolio-list/headless.ts +237 -0
  342. package/src/plugins/builtin/portfolio-list/index.tsx +2 -0
  343. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  344. package/src/plugins/builtin/portfolio-list/pane/data.ts +17 -0
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +88 -5
  346. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +85 -0
  347. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +18 -12
  348. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -0
  349. package/src/plugins/builtin/power/client.ts +100 -0
  350. package/src/plugins/builtin/power/detail.tsx +82 -0
  351. package/src/plugins/builtin/power/evidence.ts +56 -0
  352. package/src/plugins/builtin/power/generation.tsx +37 -0
  353. package/src/plugins/builtin/power/headless.ts +43 -0
  354. package/src/plugins/builtin/power/index.tsx +16 -0
  355. package/src/plugins/builtin/power/model.ts +89 -0
  356. package/src/plugins/builtin/power/pane.tsx +209 -0
  357. package/src/plugins/builtin/power/query.ts +25 -0
  358. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -3
  359. package/src/plugins/builtin/research/analyst-pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +13 -4
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +5 -3
  362. package/src/plugins/builtin/research-search/pane.tsx +21 -7
  363. package/src/plugins/builtin/revenue-breakdown/pane.tsx +1 -1
  364. package/src/plugins/builtin/reverse-dcf/client.ts +35 -0
  365. package/src/plugins/builtin/reverse-dcf/headless.ts +44 -0
  366. package/src/plugins/builtin/reverse-dcf/index.tsx +20 -0
  367. package/src/plugins/builtin/reverse-dcf/model.ts +87 -0
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +107 -0
  369. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  370. package/src/plugins/builtin/scanner/denied.tsx +4 -2
  371. package/src/plugins/builtin/scanner/feed.ts +1 -0
  372. package/src/plugins/builtin/scanner/flow-pane.tsx +3 -2
  373. package/src/plugins/builtin/scanner/hilo-pane.tsx +1 -1
  374. package/src/plugins/builtin/seasonality/client.ts +4 -1
  375. package/src/plugins/builtin/sec/command-bar-search.ts +217 -0
  376. package/src/plugins/builtin/sec/forms.ts +54 -0
  377. package/src/plugins/builtin/sec/headless.ts +31 -5
  378. package/src/plugins/builtin/sec/index.tsx +67 -11
  379. package/src/plugins/builtin/sec/model.ts +35 -5
  380. package/src/plugins/builtin/sectors/headless.ts +1 -0
  381. package/src/plugins/builtin/sectors/index.tsx +72 -26
  382. package/src/plugins/builtin/shared/cloud-upgrade.ts +143 -39
  383. package/src/plugins/builtin/shared/pro-feature-wall.tsx +8 -4
  384. package/src/plugins/builtin/shared/research-cells.tsx +107 -0
  385. package/src/plugins/builtin/shared/series-list-detail.tsx +5 -1
  386. package/src/plugins/builtin/shared/wall-teaser-catalog.ts +80 -0
  387. package/src/plugins/builtin/shared/wall-teaser.tsx +192 -0
  388. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +43 -0
  389. package/src/plugins/builtin/short-interest/index.tsx +31 -0
  390. package/src/plugins/builtin/short-interest/watch-client.ts +70 -0
  391. package/src/plugins/builtin/short-interest/watch-headless.ts +49 -0
  392. package/src/plugins/builtin/short-interest/watch-model.ts +185 -0
  393. package/src/plugins/builtin/short-interest/watch-pane.tsx +169 -0
  394. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  395. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  396. package/src/plugins/builtin/supply-chain/client.ts +104 -0
  397. package/src/plugins/builtin/supply-chain/disclosure-quote.tsx +17 -0
  398. package/src/plugins/builtin/supply-chain/evidence.ts +88 -0
  399. package/src/plugins/builtin/supply-chain/flow-layout.ts +133 -0
  400. package/src/plugins/builtin/supply-chain/flow.tsx +323 -0
  401. package/src/plugins/builtin/supply-chain/graph-client.ts +83 -0
  402. package/src/plugins/builtin/supply-chain/graph-evidence.ts +25 -0
  403. package/src/plugins/builtin/supply-chain/graph-model.ts +45 -0
  404. package/src/plugins/builtin/supply-chain/graph-pane.tsx +218 -0
  405. package/src/plugins/builtin/supply-chain/graph.tsx +163 -0
  406. package/src/plugins/builtin/supply-chain/headless.ts +105 -0
  407. package/src/plugins/builtin/supply-chain/index.tsx +17 -0
  408. package/src/plugins/builtin/supply-chain/model.ts +145 -0
  409. package/src/plugins/builtin/supply-chain/pane.tsx +276 -0
  410. package/src/plugins/builtin/supply-chain/row-evidence.tsx +96 -0
  411. package/src/plugins/builtin/supply-chain/trust.ts +34 -0
  412. package/src/plugins/builtin/themes/client.ts +39 -0
  413. package/src/plugins/builtin/themes/headless.ts +38 -0
  414. package/src/plugins/builtin/themes/model.ts +92 -0
  415. package/src/plugins/builtin/themes/pane.tsx +125 -0
  416. package/src/plugins/builtin/thirteenf/pane.tsx +6 -15
  417. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -6
  418. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +201 -0
  419. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +327 -0
  420. package/src/plugins/builtin/ticker-detail/financials/period-end-history.ts +5 -2
  421. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +18 -2
  422. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +2 -1
  423. package/src/plugins/builtin/ticker-detail/headless.ts +7 -2
  424. package/src/plugins/builtin/ticker-detail/index.tsx +22 -0
  425. package/src/plugins/builtin/ticker-detail/overview/components.tsx +53 -30
  426. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -0
  427. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +111 -46
  428. package/src/plugins/builtin/ticker-detail/pane.tsx +50 -6
  429. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +20 -41
  430. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +3 -0
  431. package/src/plugins/builtin/time-sales/pane.tsx +1 -1
  432. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  433. package/src/plugins/catalog-browser.ts +38 -0
  434. package/src/plugins/catalog-manifest.ts +3 -0
  435. package/src/plugins/catalog-ui.ts +6 -0
  436. package/src/plugins/current-target.ts +0 -9
  437. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  438. package/src/renderers/browser/cloud-transport.ts +66 -1
  439. package/src/renderers/browser/main.tsx +2 -0
  440. package/src/renderers/dom/data-table/row.tsx +2 -0
  441. package/src/renderers/dom/desktop/controls.tsx +38 -25
  442. package/src/renderers/dom/desktop/icons.tsx +8 -0
  443. package/src/renderers/dom/host/chart-surface.tsx +36 -2
  444. package/src/renderers/dom/host/focus-scope.ts +11 -1
  445. package/src/renderers/dom/styles.css +28 -0
  446. package/src/renderers/electrobun/view/backend-rpc.ts +3 -2
  447. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  448. package/src/renderers/electrobun/view/http-fetch.ts +2 -0
  449. package/src/renderers/electrobun/view/main.tsx +3 -1
  450. package/src/renderers/electrobun/view/rpc-timeout.ts +59 -0
  451. package/src/renderers/opentui/start.tsx +3 -0
  452. package/src/shares/api.ts +3 -1
  453. package/src/sources/sec-edgar/identity.ts +36 -0
  454. package/src/sources/sec-edgar.ts +1 -34
  455. package/src/state/app/context/index.tsx +8 -0
  456. package/src/sync/core-contributors.ts +2 -0
  457. package/src/telemetry/attention-counts.ts +164 -0
  458. package/src/telemetry/live-config.ts +7 -0
  459. package/src/test-support/rpc-loopback.ts +7 -3
  460. package/src/test-support/shell-pane-keys.tsx +34 -0
  461. package/src/tickers/instrument-kind.ts +12 -6
  462. package/src/tickers/search/asset-classes.ts +91 -0
  463. package/src/tickers/search/index.ts +18 -4
  464. package/src/tickers/search/ranking.ts +22 -3
  465. package/src/types/config.ts +23 -1
  466. package/src/types/headless.ts +17 -0
  467. package/src/ui/host.tsx +6 -0
  468. package/src/utils/double-escape-close.ts +20 -0
  469. package/src/utils/html-entities.ts +15 -0
  470. package/src/utils/http-proxy-response.ts +44 -7
  471. package/src/utils/plugin-proxy-hosts.json +3 -0
  472. package/src/utils/sec.ts +19 -1
  473. package/src/utils/text-wrap.ts +13 -8
@@ -0,0 +1,140 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import type { GpuBasis, GpuBoardRow } from "../../../api-client/gpu";
3
+ import {
4
+ buildSectionedRows, ChartTableHeader, DataTableView, EMPTY_TABLE_CELL, isSectionedItemRow, PaneStatusBody, renderSectionedRowHeader,
5
+ usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn, type SectionedRow, type StatItem,
6
+ } from "../../../components";
7
+ import { useAsyncResource, useAutoRefresh, usePluginPaneState } from "../../../public/react";
8
+ import { priceColor } from "../../../theme/colors";
9
+ import { useThemeColors } from "../../../theme/theme-context";
10
+ import { Box, Text, TextAttributes } from "../../../ui";
11
+ import { SeriesListDetail, useSeriesList } from "../shared/series-list-detail";
12
+ import { loadGpuHistory, loadGpuReferences } from "./client";
13
+ import {
14
+ GPU_MODELS, gpuBasisLabel, gpuBoardSections, gpuChange, gpuChangeColor, gpuHeadline, gpuHistoryChart, gpuHistoryViewport, gpuProvenanceLabel, gpuMatches, gpuPricePeriods, gpuPrice, gpuReferenceRows, gpuShortSource,
15
+ gpuTime, gpuVariant, type GpuPricePeriod,
16
+ } from "./model";
17
+
18
+ const BASES = [{ value: "all", label: "All bases" }, { value: "list", label: "List price" }, { value: "spot", label: "Provider-declared spot" },
19
+ { value: "ask", label: "Ask" }, { value: "reserved", label: "Reserved" }, { value: "reference", label: "Reference" }];
20
+ type ListRow = SectionedRow<GpuBoardRow>;
21
+ const listRowId = (row: ListRow) => row.key;
22
+ const listColumns = (width: number): DataTableColumn[] => [
23
+ // Gutters and the scrollbar take four cells beside the two columns.
24
+ { id: "series", label: "Series", width: Math.max(16, width - 9 - 5), flexGrow: 1, align: "left" },
25
+ { id: "price", label: "$/GPU-hr", width: 9, align: "right" },
26
+ ];
27
+ const PERIOD_COLUMNS: DataTableColumn[] = [
28
+ { id: "from", label: "From (UTC)", width: 14, align: "left" }, { id: "to", label: "To", width: 14, flexGrow: 1, align: "left" },
29
+ { id: "price", label: "$/GPU-hr", width: 10, align: "right" }, { id: "change", label: "Change", width: 9, align: "right" },
30
+ { id: "provenance", label: "Record", width: 10, align: "left" },
31
+ ];
32
+ /** The selected series and the medians it is read against; never the whole board. */
33
+ const MAX_PLOTTED = 5;
34
+
35
+ function HistoryDetail({ row, peers, width, height, refreshVersion }: {
36
+ row: GpuBoardRow; peers: readonly GpuBoardRow[]; width: number; height: number; refreshVersion: number;
37
+ }) {
38
+ const colors = useThemeColors();
39
+ const plotted = useMemo(() => [row,
40
+ ...peers.filter((peer) => peer.id !== row.id && peer.basis === "list" && gpuHeadline(peer)),
41
+ ...peers.filter((peer) => peer.id !== row.id && peer.basis === "reference"),
42
+ ...peers.filter((peer) => peer.id !== row.id && peer.basis === row.basis && gpuHeadline(peer) && peer.basis !== "list"),
43
+ ].slice(0, MAX_PLOTTED), [row, peers]);
44
+ const ids = plotted.map((entry) => entry.id).join("\n");
45
+ const loader = useCallback(async (force: boolean) => {
46
+ const refresh = force || refreshVersion > 0;
47
+ const histories = await Promise.all(ids.split("\n").map((id) => loadGpuHistory(id, refresh)));
48
+ return { histories };
49
+ }, [ids, row.gpuModel, refreshVersion]);
50
+ const resource = useAsyncResource(loader, { keepPreviousData: true });
51
+ useAutoRefresh(resource.updatedAt, resource.load);
52
+ const points = useMemo(() => resource.data?.histories.flatMap((history) => history.payload.points) ?? [], [resource.data]);
53
+ const chart = useMemo(() => gpuHistoryChart(plotted, points, { selected: colors.textBright, marker: colors.warning }), [plotted, points, colors]);
54
+ const viewport = useMemo(() => gpuHistoryViewport(chart), [chart]);
55
+ const own = useMemo(() => points.filter((point) => `${point.source}:${point.skuKey}` === row.id).sort((a, b) => a.observedAt.localeCompare(b.observedAt)), [points, row.id]);
56
+ const periods = useMemo(() => gpuPricePeriods(own), [own]);
57
+ const refreshErrors = resource.data?.histories.map((history) => history.refreshError) ?? [];
58
+ usePaneStatusFooter({ registrationId: "gpu:history", loading: resource.loading, stale: resource.data?.histories.some((history) => history.stale),
59
+ error: resource.error, info: own.length ? [{ id: "start", parts: [{ text: `history since ${gpuTime(own[0]!.observedAt, true)} UTC`, tone: "muted" }] }] : [] });
60
+ usePaneNoticeFooter({ registrationId: "gpu:history-notice", focused: true, notices: refreshErrors.filter((value): value is string => !!value) });
61
+ const first = own[0], last = own.at(-1);
62
+ const longHistory = first && last && Date.parse(last.observedAt) - Date.parse(first.observedAt) >= 365 * 86_400_000;
63
+ const historyDate = (value: string) => longHistory ? value.slice(0, 10) : gpuTime(value, true);
64
+ const stats = row.stats;
65
+ const figures: StatItem[] = [
66
+ { id: "latest", label: "$/GPU-hr", value: gpuPrice(row.pricePerGpuHr), detail: gpuTime(row.observedAt, true) },
67
+ // A change needs the same three observations a line does; before that the history is still being collected.
68
+ ...(first && last && own.length >= 3 ? [{ id: "change", label: "Change", value: gpuChange((last.pricePerGpuHr / first.pricePerGpuHr - 1) * 100),
69
+ color: priceColor(last.pricePerGpuHr - first.pricePerGpuHr), detail: `since ${historyDate(first.observedAt)}` }] : []),
70
+ ...(stats && stats.p25 !== undefined && stats.p75 !== undefined ? [{ id: "iqr", label: "Middle half", value: `${gpuPrice(stats.p25)}–${gpuPrice(stats.p75)}`, detail: `${stats.n} offers` }]
71
+ : stats && stats.min !== stats.max ? [{ id: "range", label: "Range", value: `${gpuPrice(stats.min)}–${gpuPrice(stats.max)}`,
72
+ detail: `${stats.n} ${row.providerClass === "aggregate" ? "providers" : "samples"}` }] : []),
73
+ ];
74
+ const renderCell = (period: GpuPricePeriod, column: DataTableColumn): DataTableCell => {
75
+ if (column.id === "from") return { text: historyDate(period.from), value: period.from };
76
+ if (column.id === "to") return { text: historyDate(period.to), value: period.to, color: colors.textDim };
77
+ if (column.id === "provenance") return { text: gpuProvenanceLabel(period), color: period.provenance === "archive" ? colors.warning : colors.textDim };
78
+ if (column.id === "price") return { text: gpuPrice(period.price), value: period.price, color: colors.textBright };
79
+ return period.change == null ? { text: "", value: null } : { text: gpuChange(period.change), value: period.change, color: gpuChangeColor(period.change, colors) };
80
+ };
81
+ return <PaneStatusBody loading={resource.loading && !resource.data} error={!resource.data ? resource.error : null} subject="GPU price history">
82
+ <DataTableView columns={PERIOD_COLUMNS} items={periods} rootWidth={width} rootHeight={height} focused={false}
83
+ rootBefore={<ChartTableHeader width={width} height={height} tableRows={periods.length} tableColumns={PERIOD_COLUMNS} figures={figures}
84
+ chart={resource.error && !resource.data ? null : { series: chart, viewport,
85
+ ...(width >= 80 && plotted.some(entry => entry.basis === "reference") ? {
86
+ legendAccessory: <Text fg={colors.textDim}>Reference index (third party), anonymised</Text>, legendAccessoryWidth: 41,
87
+ } : {}), formatValue: (value) => `$${gpuPrice(value)}`, remoteKind: "gpu-rental-history",
88
+ loading: resource.loading && !resource.data,
89
+ empty: resource.data?.histories.some((history) => history.payload.access?.locked) ? "Full history is available with Pro." : chart.length ? undefined : first ? `Collecting history since ${gpuTime(first.observedAt, true)} UTC` : "Collecting history" }} />}
90
+ selection={{ kind: "none" }} getItemKey={(period) => period.from} sortColumnId={null} sortDirection="desc"
91
+ renderCell={renderCell} emptyStateTitle="No observations for this series yet." />
92
+ </PaneStatusBody>;
93
+ }
94
+
95
+ export function GpuHistory({ rows, model, setModel, selectedId, onSelect, reloadBoard, width, height, focused }: {
96
+ rows: GpuBoardRow[]; model: string; setModel: (value: string) => void; selectedId: string; onSelect: (id: string) => void;
97
+ reloadBoard: () => void; width: number; height: number; focused: boolean;
98
+ }) {
99
+ const colors = useThemeColors();
100
+ const [basis, setBasis] = usePluginPaneState<string>("historyBasis", "all");
101
+ const [refreshVersion, setRefreshVersion] = usePluginPaneState<number>("historyRefresh", 0);
102
+ const referenceLoader = useCallback((force: boolean) => loadGpuReferences(force || refreshVersion > 0), [refreshVersion]);
103
+ const references = useAsyncResource(referenceLoader, { keepPreviousData: true });
104
+ useAutoRefresh(references.updatedAt, references.load);
105
+ usePaneNoticeFooter({ registrationId: "gpu:reference-notice", focused,
106
+ notices: [references.error, references.data?.refreshError].filter((value): value is string => !!value) });
107
+ const allRows = useMemo(() => [...rows, ...gpuReferenceRows(references.data?.payload.points ?? [])], [rows, references.data]);
108
+ // History always reads one model: the one picked on the board, else the selected series\' model.
109
+ const models = useMemo(() => [...new Set(allRows.map((row) => row.gpuModel))].sort(), [allRows]);
110
+ const activeModel = model || allRows.find((row) => row.id === selectedId)?.gpuModel || (models.includes("H100") ? "H100" : models[0] ?? GPU_MODELS[0]!);
111
+ const sections = useMemo(() => gpuBoardSections(allRows, activeModel).filter((section) => basis === "all" || section.basis === basis), [allRows, activeModel, basis]);
112
+ const ordered = useMemo(() => sections.flatMap((section) => section.rows), [sections]);
113
+ const modelRows = useMemo(() => allRows.filter((row) => row.gpuModel === activeModel), [allRows, activeModel]);
114
+ const refresh = useCallback(() => { reloadBoard(); setRefreshVersion(refreshVersion + 1); }, [reloadBoard, refreshVersion, setRefreshVersion]);
115
+ const list = useSeriesList({ focused, items: ordered, getId: (row) => row.id, matchesQuery: gpuMatches,
116
+ selectedId, onSelect, reload: refresh, range: { value: basis, options: BASES, onChange: setBasis } });
117
+ const visibleIds = useMemo(() => new Set(list.visible.map((row) => row.id)), [list.visible]);
118
+ const listRows = useMemo(() => buildSectionedRows(sections.map((section) => ({ label: section.label, items: section.rows.filter((row) => visibleIds.has(row.id)) })),
119
+ (row) => row.id), [sections, visibleIds]);
120
+ const renderCell = (item: ListRow, column: DataTableColumn, _index: number, state: { selected: boolean }): DataTableCell => {
121
+ if (!isSectionedItemRow(item)) return EMPTY_TABLE_CELL;
122
+ const row = item.item;
123
+ const headline = gpuHeadline(row);
124
+ if (column.id === "price") return { text: gpuPrice(row.pricePerGpuHr), value: row.pricePerGpuHr, color: headline ? colors.textBright : colors.text };
125
+ const name = gpuShortSource(row);
126
+ const variant = row.basis === "reference" ? "" : gpuVariant(row).join(" ");
127
+ return { text: `${name} ${variant}`, content: <Box flexDirection="row" height={1} overflow="hidden">
128
+ <Text fg={state.selected ? colors.selectedText : headline ? colors.textBright : colors.text} attributes={headline ? TextAttributes.BOLD : 0}>{name}</Text>
129
+ <Text fg={state.selected ? colors.selectedText : colors.textDim}>{variant ? ` ${variant}` : ""}</Text>
130
+ </Box> };
131
+ };
132
+ return <SeriesListDetail list={list} width={width} height={height} focused={focused} listWidth={52}
133
+ searchPlaceholder="Provider or variant" columns={listColumns} rows={listRows} getRowId={listRowId}
134
+ filters={[{ id: "gpu", label: "GPU", value: activeModel, options: models.map((value) => ({ value, label: value })), onChange: setModel }]}
135
+ isNavigable={isSectionedItemRow} renderSectionHeader={renderSectionedRowHeader}
136
+ renderCell={renderCell} emptyStateTitle="No series match."
137
+ renderDetail={(size) => list.selected ? <HistoryDetail key={list.selected.id} row={list.selected} peers={modelRows} refreshVersion={refreshVersion} {...size} />
138
+ : <PaneStatusBody empty emptyTitle={`No ${gpuBasisLabel(basis === "all" ? "list" : basis as GpuBasis)} series yet.`} />} />;
139
+ }
140
+
@@ -0,0 +1,18 @@
1
+ import type { PluginModule } from "../plugin-module";
2
+ import { gpuBoardCache, gpuEventsCache, gpuHistoryCache } from "./client";
3
+ import { gpuHeadless } from "./headless";
4
+ import { GPU_MODELS, gpuArgument } from "./model";
5
+ import { GpuPane } from "./pane";
6
+
7
+ export const gpuModule: PluginModule = {
8
+ panes: [{ id: "gpu", name: "GPU Rental Prices", icon: "G", component: GpuPane, defaultPosition: "right", defaultMode: "floating",
9
+ defaultFloatingSize: { width: 120, height: 34 }, tableExport: true, headless: gpuHeadless }],
10
+ paneTemplates: [{ id: "gpu-pane", paneId: "gpu", label: "GPU Rental Prices",
11
+ description: "Pro GPU rental list prices, provider-declared spot and marketplace asks, with sourced price history and related equities. Free preview available.",
12
+ keywords: ["gpu", "h100", "h200", "b200", "gb200", "compute", "rental", "neocloud", "hourly"],
13
+ shortcut: { prefix: "GPU", argKind: "text", argPlaceholder: "GPU", argOptional: true,
14
+ argOptions: () => GPU_MODELS.map((value) => ({ value, label: value })) }, headless: gpuHeadless,
15
+ createInstance: (_context, options) => ({ placement: "floating", params: { gpuModel: gpuArgument(options?.arg) } }) }],
16
+ setup(ctx) { for (const cache of [gpuBoardCache, gpuHistoryCache, gpuEventsCache]) cache.attach(ctx.persistence); },
17
+ dispose() { for (const cache of [gpuBoardCache, gpuHistoryCache, gpuEventsCache]) cache.reset(); },
18
+ };
@@ -0,0 +1,127 @@
1
+ import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../ui";
2
+ import { useThemeColors } from "../../../theme/theme-context";
3
+ import { blendHex } from "../../../theme/colors";
4
+ import { displayWidth, truncateToDisplayWidth } from "../../../utils/format";
5
+ import { gpuAxisTicks, gpuBasisColor, gpuBasisLabel, gpuPrice, type GpuLadderRow } from "./model";
6
+
7
+ const LABEL_CELLS = 28;
8
+ const VALUE_CELLS = 17;
9
+
10
+ const position = (value: number, low: number, high: number) => high > low ? (value - low) / (high - low) : 0.5;
11
+ const percent = (ratio: number) => `${(Math.max(0, Math.min(1, ratio)) * 100).toFixed(2)}%`;
12
+
13
+ /** The domain every basis shares, padded to whole axis steps so the ends never touch a band. */
14
+ function gpuLadderDomain(rows: readonly GpuLadderRow[]): { low: number; high: number; ticks: number[] } | null {
15
+ if (!rows.length) return null;
16
+ const min = Math.min(...rows.map((row) => row.min));
17
+ const max = Math.max(...rows.map((row) => row.max));
18
+ const pad = Math.max((max - min) * 0.04, max * 0.02, 0.05);
19
+ const low = Math.max(0, min - pad), high = max + pad;
20
+ return { low, high, ticks: gpuAxisTicks(low, high).filter((tick) => tick >= low && tick <= high) };
21
+ }
22
+
23
+ /** Terminal cells of one track: thin rule for the range, heavy for the quartiles, a bar at the median. */
24
+ function trackCells(row: GpuLadderRow, low: number, high: number, cells: number, marker: number | null) {
25
+ const at = (value: number) => Math.max(0, Math.min(cells - 1, Math.round(position(value, low, high) * (cells - 1))));
26
+ const glyphs = Array.from({ length: cells }, () => ({ char: " ", kind: "none" as "none" | "range" | "iqr" | "median" | "marker" }));
27
+ for (let i = at(row.min); i <= at(row.max); i++) glyphs[i] = { char: "─", kind: "range" };
28
+ for (let i = at(row.p25); i <= at(row.p75); i++) glyphs[i] = { char: "━", kind: "iqr" };
29
+ glyphs[at(row.median)] = { char: "┃", kind: "median" };
30
+ if (marker !== null) glyphs[at(marker)] = { char: "◆", kind: "marker" };
31
+ return glyphs;
32
+ }
33
+
34
+ /**
35
+ * Where each basis prices one GPU model on a shared dollar axis: the full
36
+ * range as a thin rule, the middle half as a band, the median as a tick, and
37
+ * the selected row as a marker on its own basis. The desktop draws real
38
+ * elements; the terminal draws box-drawing runs.
39
+ */
40
+ export function GpuPriceLadder({ rows, width, height, selected }: {
41
+ rows: readonly GpuLadderRow[]; width: number; height: number;
42
+ selected?: { basis: string; price: number } | null;
43
+ }) {
44
+ const colors = useThemeColors();
45
+ const desktop = useUiCapabilities().nativePaneChrome === true;
46
+ const domain = gpuLadderDomain(rows);
47
+ if (!domain) return null;
48
+ const shown = rows.slice(0, Math.max(1, height - 1));
49
+ const wide = width >= 120;
50
+ const labelCells = wide ? LABEL_CELLS : 14;
51
+ const valueCells = wide ? VALUE_CELLS : 6;
52
+ // Two cells of padding, two gaps, and one spare so the value never clips.
53
+ const trackCellsWidth = Math.max(8, width - labelCells - valueCells - 5);
54
+ const { low, high, ticks } = domain;
55
+ return (
56
+ <Box flexDirection="column" width={width} height={shown.length + 1} paddingX={1} data-gloom-role="gpu-price-ladder">
57
+ {shown.map((row) => {
58
+ const color = gpuBasisColor(row.basis);
59
+ const marker = selected && selected.basis === row.basis ? selected.price : null;
60
+ const summary = `${gpuPrice(row.median)}`;
61
+ const range = wide && row.n > 1 ? `${gpuPrice(row.min)}–${gpuPrice(row.max)}` : "";
62
+ return (
63
+ <Box key={row.basis} flexDirection="row" height={1} gap={1}>
64
+ <Box width={labelCells} flexShrink={0} flexDirection="row" overflow="hidden">
65
+ <Text fg={color}>●</Text>
66
+ <Text fg={colors.text}>{` ${truncateToDisplayWidth(gpuBasisLabel(row.basis, !wide), labelCells - 6)}`}</Text>
67
+ <Text fg={colors.textDim}>{` ${row.n}`}</Text>
68
+ </Box>
69
+ <Box width={trackCellsWidth} height={1} flexShrink={0} overflow="hidden">
70
+ {desktop ? (
71
+ <Box width={trackCellsWidth} height={1} style={{ position: "relative" }}>
72
+ <Box style={{ position: "absolute", left: percent(position(row.min, low, high)), width: percent(position(row.max, low, high) - position(row.min, low, high)),
73
+ top: "50%", height: "2px", marginTop: "-1px", borderRadius: "1px", backgroundColor: blendHex(colors.bg, color, 0.55) }} />
74
+ <Box style={{ position: "absolute", left: percent(position(row.p25, low, high)), width: percent(position(row.p75, low, high) - position(row.p25, low, high)),
75
+ top: "50%", height: "8px", marginTop: "-4px", minWidth: "3px", borderRadius: "2px", backgroundColor: blendHex(colors.bg, color, 0.85) }} />
76
+ <Box style={{ position: "absolute", left: percent(position(row.median, low, high)), top: "50%", width: "2px", height: "12px", marginTop: "-6px",
77
+ marginLeft: "-1px", borderRadius: "1px", backgroundColor: colors.textBright }} />
78
+ {marker !== null ? <Box style={{ position: "absolute", left: percent(position(marker, low, high)), top: "50%", width: "8px", height: "8px", marginTop: "-4px",
79
+ marginLeft: "-4px", borderRadius: "50%", backgroundColor: colors.bg, border: `2px solid ${colors.textBright}`, boxSizing: "border-box" }} /> : null}
80
+ </Box>
81
+ ) : (
82
+ <Text>{trackCells(row, low, high, trackCellsWidth, marker).map((cell, index) => (
83
+ <Span key={index} fg={cell.kind === "median" || cell.kind === "marker" ? colors.textBright : cell.kind === "iqr" ? color : blendHex(colors.bg, color, 0.6)}>{cell.char}</Span>
84
+ ))}</Text>
85
+ )}
86
+ </Box>
87
+ <Box width={valueCells} flexShrink={0} flexDirection="row" justifyContent="flex-end" overflow="hidden">
88
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{summary}</Text>
89
+ {range ? <Text fg={colors.textDim}>{` ${range}`}</Text> : null}
90
+ </Box>
91
+ </Box>
92
+ );
93
+ })}
94
+ <Box flexDirection="row" height={1} gap={1}>
95
+ <Box width={labelCells} flexShrink={0} />
96
+ <GpuLadderAxis ticks={ticks} low={low} high={high} width={trackCellsWidth} desktop={desktop} />
97
+ </Box>
98
+ </Box>
99
+ );
100
+ }
101
+
102
+ function GpuLadderAxis({ ticks, low, high, width, desktop }: { ticks: number[]; low: number; high: number; width: number; desktop: boolean }) {
103
+ const colors = useThemeColors();
104
+ const label = (tick: number) => `$${Number.isInteger(tick) ? tick : tick.toFixed(1)}`;
105
+ if (desktop) {
106
+ return (
107
+ <Box width={width} height={1} flexShrink={0} style={{ position: "relative" }}>
108
+ {ticks.map((tick, index) => {
109
+ const ratio = position(tick, low, high);
110
+ const align = index === 0 ? "0%" : index === ticks.length - 1 ? "-100%" : "-50%";
111
+ return <Box key={tick} style={{ position: "absolute", left: percent(ratio), transform: `translateX(${align})` }}>
112
+ <Text fg={colors.textDim}>{label(tick)}</Text>
113
+ </Box>;
114
+ })}
115
+ </Box>
116
+ );
117
+ }
118
+ const line = Array.from({ length: width }, () => " ");
119
+ for (const [index, tick] of ticks.entries()) {
120
+ const text = label(tick);
121
+ const center = Math.round(position(tick, low, high) * (width - 1));
122
+ const start = index === 0 ? 0 : index === ticks.length - 1 ? width - displayWidth(text) : center - Math.floor(text.length / 2);
123
+ if (start < 0 || start + text.length > width || line.slice(Math.max(0, start - 1), start + text.length + 1).some((char) => char !== " ")) continue;
124
+ for (let i = 0; i < text.length; i++) line[start + i] = text[i]!;
125
+ }
126
+ return <Box width={width} height={1} flexShrink={0}><Text fg={colors.textDim}>{line.join("")}</Text></Box>;
127
+ }
@@ -0,0 +1,317 @@
1
+ import type { GpuBasis, GpuBoardRow, GpuEvent, GpuObservation } from "../../../api-client/gpu";
2
+ import { staticSeries } from "../../../components/chart/static/series";
3
+ import { SERIES_COLORS } from "../../../time-series/resolve";
4
+ import type { ResolvedSeries } from "../../../time-series/types";
5
+ import type { PricePoint } from "../../../types/financials";
6
+
7
+ export const GPU_TABS = [{ value: "board", label: "Board" }, { value: "history", label: "History" },
8
+ { value: "changes", label: "Changes" }, { value: "equities", label: "Equities" }] as const;
9
+ export type GpuTab = typeof GPU_TABS[number]["value"];
10
+ export const gpuTab = (value: unknown): GpuTab => GPU_TABS.find((tab) => tab.value === value)?.value ?? "board";
11
+ export const GPU_MODELS = ["H100", "H200", "B200", "B300", "GB200", "GB300", "A100", "MI300X", "MI355X", "L40S", "RTX PRO 6000"];
12
+ export const gpuArgument = (value: string | null | undefined) => (value ?? "").trim().toUpperCase().replace(/\s+/g, " ");
13
+ export const gpuBasisLabel = (basis: GpuBasis, compact = false): string => compact
14
+ ? ({ list: "List", spot: "Spot", ask: "Ask", reserved: "Rsvd", index: "Locked", reference: "Reference" })[basis]
15
+ : ({ list: "List price", spot: "Provider-declared spot", ask: "Ask", reserved: "Reserved", index: "Index (licensed)", reference: "Reference" })[basis];
16
+ export const gpuPrice = (value: number | null | undefined) => value == null ? "-" : value.toFixed(2);
17
+ export function gpuChange(value: number | null | undefined): string {
18
+ if (value == null) return "-";
19
+ const rounded = Math.round(Math.abs(value) * 10) / 10;
20
+ return `${rounded === 0 ? "" : value > 0 ? "+" : "-"}${rounded.toFixed(1)}%`;
21
+ }
22
+ /** A move's colour; an unchanged rate reads quiet rather than as a figure. */
23
+ export const gpuChangeColor = (value: number, colors: { positive: string; negative: string; textMuted: string }) =>
24
+ Math.round(Math.abs(value) * 10) === 0 ? colors.textMuted : value > 0 ? colors.positive : colors.negative;
25
+ export const gpuLabel = (row: Pick<GpuObservation, "gpuModel" | "memoryGb" | "formFactor">) =>
26
+ [row.gpuModel, row.memoryGb ? `${row.memoryGb}GB` : null, row.formFactor].filter(Boolean).join(" ");
27
+
28
+ const PROVIDER_NAMES: Record<string, string> = {
29
+ aws: "AWS", azure: "Azure", gcp: "GCP", oci: "OCI", lambdalabs: "Lambda", lambda: "Lambda",
30
+ coreweave: "CoreWeave", digitalocean: "DigitalOcean", nebius: "Nebius", voltagepark: "Voltage Park",
31
+ massedcompute: "Massed Compute", paperspace: "Paperspace", hyperstack: "Hyperstack", denvr: "Denvr",
32
+ scaleway: "Scaleway", horizon: "Horizon", verda: "Verda", latitude: "Latitude", imwt: "IMWT",
33
+ };
34
+
35
+ /** Cloud operators are the product being compared. The data collection intermediary stays invisible. */
36
+ export function gpuSource(row: Pick<GpuObservation, "source" | "provider" | "skuKey">): string {
37
+ if (row.source === "vast" || row.source === "vast-ai") return "Marketplace ask median";
38
+ if (row.source === "akash") return "Decentralized ask median";
39
+ if (row.source === "runpod") return /community/i.test(`${row.provider} ${row.skuKey}`) ? "Community cloud asks" : "Secure cloud asks";
40
+ return PROVIDER_NAMES[row.provider.toLowerCase()] ?? row.provider;
41
+ }
42
+
43
+ /** A source inside its basis section: the section already says list or ask, so a median drops the word. */
44
+ export const gpuShortSource = (row: Pick<GpuObservation, "source" | "provider" | "skuKey">) => gpuSource(row).replace(/ (?:list|ask) median$/, " median");
45
+
46
+ export function gpuTime(value: string | null | undefined, compact = false): string {
47
+ if (!value) return "-";
48
+ const date = new Date(value);
49
+ const day = date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
50
+ const time = date.toISOString().slice(11, 16);
51
+ return compact ? `${day} ${time}` : `${day}, ${date.getUTCFullYear()} ${time} UTC`;
52
+ }
53
+
54
+ export function gpuRows(rows: readonly GpuBoardRow[], model = "", basis = "all"): GpuBoardRow[] {
55
+ const rank = (row: GpuBoardRow) => row.providerClass === "aggregate" ? 0 : row.basis === "list" ? 1 : row.basis === "spot" ? 2 : row.basis === "reserved" ? 3 : 4;
56
+ return rows.filter((row) => (!model || row.gpuModel === model) && (basis === "all" || row.basis === basis)).sort((a, b) => {
57
+ const modelRank = (name: string) => GPU_MODELS.indexOf(name) < 0 ? 99 : GPU_MODELS.indexOf(name);
58
+ return modelRank(a.gpuModel) - modelRank(b.gpuModel) || a.gpuModel.localeCompare(b.gpuModel) || rank(a) - rank(b)
59
+ || gpuSource(a).localeCompare(gpuSource(b)) || (b.memoryGb ?? 0) - (a.memoryGb ?? 0) || (a.formFactor ?? "").localeCompare(b.formFactor ?? "");
60
+ });
61
+ }
62
+
63
+ /** The bases in board order. The licensed index never reaches the board. */
64
+ const GPU_BASES = ["list", "spot", "reserved", "ask"] as const satisfies readonly GpuBasis[];
65
+ const basisRank = (basis: GpuBasis) => { const index = (GPU_BASES as readonly GpuBasis[]).indexOf(basis); return index < 0 ? GPU_BASES.length : index; };
66
+
67
+ /** One colour per basis from the shared series palette, the same in the ladder, the chart and the table. */
68
+ export function gpuBasisColor(basis: GpuBasis): string {
69
+ return SERIES_COLORS[basis === "list" ? 0 : basis === "spot" ? 1 : basis === "ask" ? 2 : 3];
70
+ }
71
+
72
+ /** Hardware variant chips: form factor, then memory. */
73
+ export const gpuVariant = (row: Pick<GpuObservation, "formFactor" | "memoryGb">): string[] =>
74
+ [row.formFactor, row.memoryGb ? `${row.memoryGb}GB` : null].filter((part): part is string => !!part);
75
+
76
+ /** Medians lead their section: provider-class medians and the marketplace offer medians. */
77
+ export const gpuHeadline = (row: Pick<GpuObservation, "providerClass" | "source">) =>
78
+ !row.source.startsWith("ref-") && (row.providerClass === "aggregate" || row.source === "vast" || row.source === "vast-ai" || row.source === "akash");
79
+
80
+ /** Reference list entries come only from published history, never from our price basket. */
81
+ export function gpuReferenceRows(points: readonly GpuObservation[]): GpuBoardRow[] {
82
+ const latest = new Map<string, GpuObservation>();
83
+ for (const point of points) {
84
+ if (point.provenance !== "reference" || point.basis !== "reference") continue;
85
+ const id = `${point.source}:${point.skuKey}`;
86
+ if (!latest.has(id) || point.observedAt > latest.get(id)!.observedAt) latest.set(id, point);
87
+ }
88
+ return [...latest].map(([id, point]) => ({ ...point, id, label: point.provider, sourceLabel: "Reference index (third party), anonymised",
89
+ change1d: null, change7d: null, change30d: null, stale: false, lastError: null }));
90
+ }
91
+
92
+ export interface GpuBoardSection { id: string; label: string; basis: GpuBasis; gpuModel: string; rows: GpuBoardRow[] }
93
+
94
+ /**
95
+ * The board grouped by model, then basis, medians first. One model reads its
96
+ * sections by basis alone; every model names the model in each heading.
97
+ */
98
+ export function gpuBoardSections(rows: readonly GpuBoardRow[], model = ""): GpuBoardSection[] {
99
+ const modelRank = (name: string) => GPU_MODELS.indexOf(name) < 0 ? 99 : GPU_MODELS.indexOf(name);
100
+ const ordered = rows.filter((row) => (!model || row.gpuModel === model) && row.basis !== "index").sort((a, b) =>
101
+ modelRank(a.gpuModel) - modelRank(b.gpuModel) || a.gpuModel.localeCompare(b.gpuModel) || basisRank(a.basis) - basisRank(b.basis)
102
+ || Number(gpuHeadline(b)) - Number(gpuHeadline(a)) || gpuSource(a).localeCompare(gpuSource(b))
103
+ || (b.memoryGb ?? 0) - (a.memoryGb ?? 0) || (a.formFactor ?? "").localeCompare(b.formFactor ?? ""));
104
+ const sections: GpuBoardSection[] = [];
105
+ for (const row of ordered) {
106
+ const id = `${row.gpuModel}:${row.basis}`;
107
+ const last = sections.at(-1);
108
+ if (last?.id === id) last.rows.push(row);
109
+ else sections.push({ id, basis: row.basis, gpuModel: row.gpuModel, rows: [row],
110
+ label: model ? gpuBasisLabel(row.basis) : `${row.gpuModel} ${gpuBasisLabel(row.basis)}` });
111
+ }
112
+ return sections;
113
+ }
114
+
115
+ export type GpuChangeKey = "change1d" | "change7d" | "change30d";
116
+ /** Change windows with at least one figure; missing windows remain unavailable. */
117
+ export const gpuChangeWindows = (rows: readonly GpuBoardRow[]): GpuChangeKey[] =>
118
+ (["change1d", "change7d", "change30d"] as const).filter((key) => rows.some((row) => row[key] != null));
119
+
120
+ /**
121
+ * The series a board fetches history for: only the medians of the one model
122
+ * shown, and only once some change figure exists. All GPUs, or no comparable
123
+ * observation, fetches nothing; no board row ever fetches its own.
124
+ */
125
+ export function gpuSparklineSeries(rows: readonly GpuBoardRow[], model: string): string[] {
126
+ if (!model) return [];
127
+ const modelRows = rows.filter((row) => row.gpuModel === model);
128
+ return gpuChangeWindows(modelRows).length ? modelRows.filter(gpuHeadline).map((row) => row.id) : [];
129
+ }
130
+
131
+ export interface GpuLadderRow { basis: GpuBasis; n: number; min: number; p25: number; median: number; p75: number; max: number }
132
+
133
+ function quantile(sorted: readonly number[], q: number): number {
134
+ const at = (sorted.length - 1) * q;
135
+ const low = Math.floor(at), high = Math.ceil(at);
136
+ return sorted[low]! + (sorted[high]! - sorted[low]!) * (at - low);
137
+ }
138
+
139
+ /**
140
+ * One model's prices per basis: range, quartiles and median over the quotes
141
+ * the board lists. Provider-class medians are left out: their constituents
142
+ * are already rows, and counting both would weigh those providers twice.
143
+ */
144
+ export function gpuPriceLadder(rows: readonly GpuBoardRow[], model: string): GpuLadderRow[] {
145
+ return GPU_BASES.flatMap((basis) => {
146
+ const prices = rows.filter((row) => row.gpuModel === model && row.basis === basis && row.providerClass !== "aggregate")
147
+ .map((row) => row.pricePerGpuHr).sort((a, b) => a - b);
148
+ return prices.length ? [{ basis, n: prices.length, min: prices[0]!, max: prices.at(-1)!,
149
+ p25: quantile(prices, 0.25), median: quantile(prices, 0.5), p75: quantile(prices, 0.75) }] : [];
150
+ });
151
+ }
152
+
153
+ /** Round dollar ticks for a price axis: one, two or five times a power of ten, four to six of them. */
154
+ export function gpuAxisTicks(low: number, high: number): number[] {
155
+ if (!(high > low)) return [low];
156
+ const raw = (high - low) / 5;
157
+ const power = 10 ** Math.floor(Math.log10(raw));
158
+ const step = [1, 2, 2.5, 5, 10].map((factor) => factor * power).find((candidate) => candidate >= raw) ?? raw;
159
+ const ticks: number[] = [];
160
+ for (let tick = Math.floor(low / step) * step; tick <= high + step * 0.999; tick += step) ticks.push(Math.round(tick * 1000) / 1000);
161
+ return ticks;
162
+ }
163
+
164
+ /** Published notices use their effective date; observed changes use the dated evidence. */
165
+ export const gpuEventDate = (event: Pick<GpuEvent, "origin" | "effectiveAt" | "observedAt">) =>
166
+ event.origin === "published" ? event.effectiveAt ?? event.observedAt : event.observedAt;
167
+
168
+ /** Price changes as a timeline: one section per UTC day, newest first, keeping the server's order within a day. */
169
+ export function gpuEventSections(events: readonly GpuEvent[]): { label: string; items: GpuEvent[] }[] {
170
+ const days = new Map<string, GpuEvent[]>();
171
+ for (const event of [...events].sort((a, b) => gpuEventDate(b).localeCompare(gpuEventDate(a)))) {
172
+ const day = gpuEventDate(event).slice(0, 10);
173
+ days.set(day, [...(days.get(day) ?? []), event]);
174
+ }
175
+ return [...days].map(([day, items]) => ({ label: new Date(`${day}T00:00:00Z`).toLocaleDateString("en-US",
176
+ { weekday: "short", month: "short", day: "numeric", year: "numeric", timeZone: "UTC" }), items }));
177
+ }
178
+
179
+ export interface GpuAvailability { label: string; tone: "positive" | "warning" | "muted" }
180
+ /** A provider's capacity note as a short word and a tone, for a status dot. */
181
+ export function gpuAvailability(text: string | null | undefined): GpuAvailability | null {
182
+ const value = text?.trim();
183
+ if (!value) return null;
184
+ const pooled = value.match(/^pooled:\s*(\w+)/i)?.[1]?.toLowerCase();
185
+ if (pooled) return { label: pooled, tone: pooled === "low" ? "warning" : pooled === "none" ? "muted" : "positive" };
186
+ if (/^unavailable$/i.test(value) || /^0\s/.test(value)) return { label: value.toLowerCase(), tone: "muted" };
187
+ const offers = value.match(/^(\d+)\s+rentable offers?$/i);
188
+ if (offers) return { label: `${offers[1]} offers`, tone: "positive" };
189
+ return { label: value.toLowerCase().startsWith("available") ? "available" : value, tone: "positive" };
190
+ }
191
+
192
+ const gpuSeriesLabel = (row: GpuObservation) => `${gpuLabel(row)} / ${gpuSource(row)} / ${gpuBasisLabel(row.basis)}`;
193
+ export const gpuMatches = (row: GpuBoardRow, query: string) => query.split(/\s+/).every((part) => gpuSeriesLabel(row).toLowerCase().includes(part));
194
+
195
+ /** Observations are dated by their evidence, never by a backdated effective-date label. */
196
+ export function gpuHistorySeries(row: GpuBoardRow, points: readonly GpuObservation[], color: string, archiveColor: string): ResolvedSeries[] {
197
+ const own = points.filter((point) => `${point.source}:${point.skuKey}` === row.id).sort((a, b) => a.observedAt.localeCompare(b.observedAt));
198
+ if (own.length < 3) return [];
199
+ const plotted = (observations: readonly GpuObservation[]) => observations.map((point) => ({
200
+ date: new Date(point.observedAt), observedAt: new Date(point.observedAt), value: point.pricePerGpuHr,
201
+ }));
202
+ const unit = { unit: "$/GPU-hr", unitGroup: "gpu-rental-usd" };
203
+ return [{ ...staticSeries(plotted(own), { id: "rental-price", label: `${gpuSource(row)} ${gpuBasisLabel(row.basis).toLowerCase()}`,
204
+ color, style: "step", calendarSpaced: true }), ...unit, interpolation: "step-after" },
205
+ ...([false, true] as const).flatMap((archived) => {
206
+ const observations = own.filter((point) => (point.provenance === "archive") === archived);
207
+ return observations.length ? [{ ...staticSeries(plotted(observations), { id: archived ? "archived-observations" : "observations",
208
+ label: archived ? "Archived" : "Observations", color: archived ? archiveColor : color, style: "points", calendarSpaced: true }), ...unit }] : [];
209
+ })];
210
+ }
211
+
212
+ /** The selected series has real-observation markers; peer medians retain their own step lines. */
213
+ export function gpuHistoryChart(rows: readonly GpuBoardRow[], points: readonly GpuObservation[],
214
+ colors: { selected: string; marker: string }): ResolvedSeries[] {
215
+ const lines: ResolvedSeries[] = [];
216
+ const markers: ResolvedSeries[] = [];
217
+ const others = [0, 1, 3, 4, 5] as const;
218
+ rows.forEach((row, index) => {
219
+ const reference = row.provenance === "reference";
220
+ const color = reference ? row.source === "ref-a" ? "#879da5" : row.skuKey.endsWith("-hs") ? "#9daa8d" : "#a39ab0" : index === 0 ? colors.selected : SERIES_COLORS[others[(index - 1) % others.length]!];
221
+ const [line, ...observations] = gpuHistorySeries(row, points, color, colors.marker);
222
+ if (!line) return;
223
+ const ambiguous = rows.some((other) => other.id !== row.id && gpuShortSource(other) === gpuShortSource(row));
224
+ const name = ambiguous ? `${gpuShortSource(row)} ${gpuVariant(row).join(" ")}` : gpuShortSource(row);
225
+ lines.push({ ...line, id: `rental-price:${row.id}`, label: reference ? `${name.replace("Reference index ", "")} (reference index, third party, anonymised)` : name });
226
+ if (index === 0) markers.push(...observations.map((entry) => reference
227
+ ? { ...entry, label: "Published readings", color } : entry));
228
+ });
229
+ return [...lines, ...markers];
230
+ }
231
+
232
+ /** Override the daily-chart minimum: no blank fortnight before the first evidence point. */
233
+ export function gpuHistoryViewport(series: readonly ResolvedSeries[]): { start: Date; end: Date } | undefined {
234
+ let first = Infinity, last = -Infinity;
235
+ for (const entry of series) for (const point of entry.points) {
236
+ const time = point.date.getTime();
237
+ if (point.value == null || !Number.isFinite(time)) continue;
238
+ first = Math.min(first, time);
239
+ last = Math.max(last, time);
240
+ }
241
+ if (!Number.isFinite(first)) return undefined;
242
+ // Five percent on the right lets the latest marker breathe; never more than a day.
243
+ const padding = Math.min(86_400_000, Math.max(60_000, (last - first) * 0.05));
244
+ return { start: new Date(first), end: new Date(last + padding) };
245
+ }
246
+
247
+ export const gpuProvenanceLabel = (point: Pick<GpuObservation, "provenance">, full = false) =>
248
+ point.provenance === "reference" ? full ? "Reference index (third party), anonymised" : "Reference"
249
+ : point.provenance === "archive" ? full ? "archived page, reconstructed" : "Archived"
250
+ : point.provenance === "official-history" ? full ? "official published history" : "Published" : full ? "live observation" : "Observed";
251
+
252
+ export interface GpuPricePeriod { from: string; to: string; price: number; change: number | null; provenance: GpuObservation["provenance"] }
253
+
254
+ /** Fold equal-price observations only within the same provenance. End at the last real observation. */
255
+ export function gpuPricePeriods(points: readonly Pick<GpuObservation, "observedAt" | "pricePerGpuHr" | "provenance">[]): GpuPricePeriod[] {
256
+ const periods: GpuPricePeriod[] = [];
257
+ for (const point of [...points].sort((a, b) => a.observedAt.localeCompare(b.observedAt))) {
258
+ const last = periods.at(-1);
259
+ const provenance = point.provenance ?? "live";
260
+ if (last && Math.abs(last.price - point.pricePerGpuHr) < 1e-9 && last.provenance === provenance) {
261
+ last.to = point.observedAt;
262
+ continue;
263
+ }
264
+ if (last) last.to = point.observedAt;
265
+ periods.push({ from: point.observedAt, to: point.observedAt, price: point.pricePerGpuHr, provenance,
266
+ change: last ? (point.pricePerGpuHr / last.price - 1) * 100 : null });
267
+ }
268
+ return periods.reverse();
269
+ }
270
+
271
+ export const GPU_EQUITIES = [
272
+ { symbol: "NVDA", exchange: "NASDAQ", role: "Chips" }, { symbol: "AMD", exchange: "NASDAQ", role: "Chips" },
273
+ { symbol: "AVGO", exchange: "NASDAQ", role: "Chips" },
274
+ { symbol: "AMZN", exchange: "NASDAQ", role: "Hyperscaler", provider: "AWS" }, { symbol: "MSFT", exchange: "NASDAQ", role: "Hyperscaler", provider: "Azure" },
275
+ { symbol: "GOOGL", exchange: "NASDAQ", role: "Hyperscaler", provider: "GCP" }, { symbol: "ORCL", exchange: "NYSE", role: "Hyperscaler", provider: "OCI" },
276
+ { symbol: "CRWV", exchange: "NASDAQ", role: "Neocloud", provider: "CoreWeave" }, { symbol: "NBIS", exchange: "NASDAQ", role: "Neocloud", provider: "Nebius" },
277
+ { symbol: "DOCN", exchange: "NYSE", role: "Neocloud", provider: "DigitalOcean" }, { symbol: "IREN", exchange: "NASDAQ", role: "Host" },
278
+ { symbol: "APLD", exchange: "NASDAQ", role: "Host" }, { symbol: "CIFR", exchange: "NASDAQ", role: "Host" },
279
+ { symbol: "WULF", exchange: "NASDAQ", role: "Host" }, { symbol: "SMCI", exchange: "NASDAQ", role: "Server builder" },
280
+ { symbol: "DELL", exchange: "NYSE", role: "Server builder" },
281
+ ] as const;
282
+
283
+ /** How the Equities tab groups the shares: who makes the chips, who rents them out, who hosts and builds the machines. */
284
+ export const GPU_EQUITY_GROUPS = [
285
+ { label: "Chip makers", roles: ["Chips"] }, { label: "Hyperscalers", roles: ["Hyperscaler"] },
286
+ { label: "Neoclouds", roles: ["Neocloud"] }, { label: "Hosts and builders", roles: ["Host", "Server builder"] },
287
+ ] as const;
288
+
289
+ export function gpuEquityRows(rows: readonly GpuBoardRow[], selectedModel: string) {
290
+ return GPU_EQUITIES.map((equity) => {
291
+ const model = equity.symbol === "AMD" && !selectedModel.startsWith("MI") ? "MI300X"
292
+ : equity.symbol === "NVDA" && selectedModel.startsWith("MI") ? "H100" : selectedModel;
293
+ const candidates = gpuRows(rows, model, "list");
294
+ const aggregates = candidates.filter((row) => row.providerClass === "aggregate").sort((a, b) =>
295
+ Number(b.formFactor === "SXM") - Number(a.formFactor === "SXM") || (b.stats?.n ?? 0) - (a.stats?.n ?? 0) || (b.memoryGb ?? 0) - (a.memoryGb ?? 0));
296
+ const reference = "provider" in equity ? candidates.find((row) => gpuSource(row) === equity.provider)
297
+ : aggregates.find((row) => row.provider === "Neocloud list median") ?? aggregates[0];
298
+ return { ...equity, gpuModel: model, reference: reference ?? null };
299
+ });
300
+ }
301
+
302
+ export function equityFiveDayReturn(history: readonly PricePoint[]): { value: number | null; asOf: string | null } {
303
+ const dates = new Map<string, { time: number; close: number }>();
304
+ for (const point of history) {
305
+ const time = new Date(point.date).getTime();
306
+ if (Number.isFinite(time) && Number.isFinite(point.close) && point.close > 0) {
307
+ const day = new Date(time).toISOString().slice(0, 10);
308
+ if (time >= (dates.get(day)?.time ?? -Infinity)) dates.set(day, { time, close: point.close });
309
+ }
310
+ }
311
+ const ordered = [...dates].sort(([a], [b]) => a.localeCompare(b));
312
+ const latest = ordered.at(-1);
313
+ const previous = ordered.at(-6);
314
+ // Six sessions spanning more than two weeks indicate a gap, not a five-day return.
315
+ return { value: latest && previous && Date.parse(latest[0]) - Date.parse(previous[0]) <= 14 * 86_400_000
316
+ ? (latest[1].close / previous[1].close - 1) * 100 : null, asOf: latest?.[0] ?? null };
317
+ }