gloomberb 0.16.1 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (574) hide show
  1. package/README.md +2 -1
  2. package/package.json +2 -1
  3. package/src/api-client/beneficial-owners.ts +92 -0
  4. package/src/api-client/data.ts +26 -8
  5. package/src/api-client/errors.ts +12 -0
  6. package/src/api-client/geo.ts +363 -0
  7. package/src/api-client/index.ts +2 -0
  8. package/src/api-client/market-discovery.ts +10 -0
  9. package/src/api-client/paths.ts +30 -1
  10. package/src/api-client/perps.ts +37 -0
  11. package/src/api-client/request.ts +5 -2
  12. package/src/api-client/types.ts +2 -0
  13. package/src/brokers/account-cache.ts +24 -0
  14. package/src/cli/commands/command-utils.ts +8 -0
  15. package/src/cli/commands/crypto-hints.ts +75 -0
  16. package/src/cli/commands/market.ts +414 -106
  17. package/src/cli/commands/overview.ts +77 -32
  18. package/src/cli/commands/ticker.ts +101 -43
  19. package/src/cli/desktop-pane-shot.ts +34 -4
  20. package/src/cli/freshness.ts +61 -0
  21. package/src/cli/help.ts +1 -0
  22. package/src/cli/helpers.ts +4 -8
  23. package/src/cli/index.ts +12 -2
  24. package/src/cli/listing-arg.ts +321 -0
  25. package/src/cli/pane-functions/access-gate.ts +42 -0
  26. package/src/cli/pane-functions/catalog.ts +28 -1
  27. package/src/cli/pane-functions/data.ts +3 -1
  28. package/src/cli/pane-functions/dom.ts +20 -2
  29. package/src/cli/pane-functions/freshness.ts +400 -0
  30. package/src/cli/pane-functions/function-help.ts +27 -20
  31. package/src/cli/pane-functions/headless.ts +39 -11
  32. package/src/cli/pane-functions/index.ts +15 -4
  33. package/src/cli/pane-functions/options.ts +3 -0
  34. package/src/cli/pane-functions/report.ts +4 -0
  35. package/src/cli/pane-functions/resolver.ts +35 -7
  36. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  37. package/src/cli/pane-functions/screenshot.ts +7 -3
  38. package/src/cli/result.ts +41 -27
  39. package/src/cli/ticker-resolution.ts +7 -5
  40. package/src/components/chart/composite/composite-chart.tsx +88 -2536
  41. package/src/components/chart/composite/drawing-store.ts +59 -0
  42. package/src/components/chart/composite/legend.tsx +319 -0
  43. package/src/components/chart/composite/panel-bitmap.ts +139 -0
  44. package/src/components/chart/composite/panel-gestures.ts +59 -0
  45. package/src/components/chart/composite/panel-labels.ts +107 -0
  46. package/src/components/chart/composite/panel-surface.tsx +876 -0
  47. package/src/components/chart/composite/tool-catalog.ts +98 -0
  48. package/src/components/chart/composite/toolbar.tsx +186 -0
  49. package/src/components/chart/composite/tools.ts +8 -0
  50. package/src/components/chart/composite/use-composite-chart-keyboard.ts +703 -0
  51. package/src/components/chart/composite/use-composite-viewport.ts +143 -0
  52. package/src/components/command-bar/list/model.ts +5 -0
  53. package/src/components/command-bar/list/view.tsx +16 -3
  54. package/src/components/command-bar/pane-templates/workflow.ts +1 -0
  55. package/src/components/command-bar/routes/ticker-search/actions.ts +5 -0
  56. package/src/components/command-bar/workflow/ops.ts +1 -1
  57. package/src/components/command-bar/workflow/tickers.ts +6 -0
  58. package/src/components/data-table/view.tsx +29 -10
  59. package/src/components/feedback-dialog.tsx +2 -1
  60. package/src/components/form-modal/field-row.tsx +2 -2
  61. package/src/components/layout/detached-pane-shell.tsx +6 -3
  62. package/src/components/layout/pane/content.tsx +23 -10
  63. package/src/components/layout/pane/error-boundary.tsx +103 -0
  64. package/src/components/layout/pane/footer/registration.tsx +68 -23
  65. package/src/components/layout/pane/header.tsx +11 -11
  66. package/src/components/layout/shell/menu.ts +2 -1
  67. package/src/components/layout/shell/pane/actions.ts +2 -1
  68. package/src/components/layout/shell/pane/layers.tsx +14 -3
  69. package/src/components/metric-treemap/heat-surface.tsx +43 -16
  70. package/src/components/metric-treemap/heat-tiles.ts +18 -0
  71. package/src/components/metric-treemap/layout.ts +2 -0
  72. package/src/components/onboarding/onboarding-keyboard.ts +311 -0
  73. package/src/components/onboarding/onboarding-wizard.tsx +101 -788
  74. package/src/components/onboarding/use-onboarding-broker-form.ts +263 -0
  75. package/src/components/onboarding/use-onboarding-progress.ts +298 -0
  76. package/src/components/onboarding/use-onboarding-upgrade.ts +83 -0
  77. package/src/components/ticker/list-table-view.tsx +13 -1
  78. package/src/components/ui/fields.tsx +8 -0
  79. package/src/components/ui/range-track.tsx +10 -3
  80. package/src/components/ui/status.tsx +8 -2
  81. package/src/components/ui/tabs.tsx +7 -4
  82. package/src/components/ui/text-prompt-dialog.tsx +4 -2
  83. package/src/data/config/store/normalize.ts +17 -1
  84. package/src/i18n/es.ts +19 -1
  85. package/src/i18n/ja.ts +19 -1
  86. package/src/i18n/ko.ts +19 -1
  87. package/src/i18n/zh-cn.ts +19 -1
  88. package/src/i18n/zh-tw.ts +19 -1
  89. package/src/market-data/coordinator/chart.ts +1 -1
  90. package/src/market-data/coordinator/quotes.ts +23 -4
  91. package/src/market-data/fx-closes.ts +72 -0
  92. package/src/market-data/market/freshness.ts +78 -6
  93. package/src/market-data/market/session-close-note.ts +57 -0
  94. package/src/market-data/market/status.ts +151 -0
  95. package/src/market-data/published-apac-sessions.ts +66 -0
  96. package/src/market-data/quotes/contributions.ts +2 -0
  97. package/src/market-data/quotes/resolution.ts +2 -0
  98. package/src/market-data/quotes/screener-live-quotes.ts +49 -4
  99. package/src/plugins/builtin/account-management/pane.tsx +4 -0
  100. package/src/plugins/builtin/alerts/cli.ts +10 -5
  101. package/src/plugins/builtin/alerts/index.tsx +1 -0
  102. package/src/plugins/builtin/analytics/metrics.ts +8 -3
  103. package/src/plugins/builtin/analytics/risk-client.ts +191 -66
  104. package/src/plugins/builtin/analytics/risk-fx.ts +102 -0
  105. package/src/plugins/builtin/analytics/risk-headless.ts +19 -13
  106. package/src/plugins/builtin/analytics/risk-model.ts +45 -17
  107. package/src/plugins/builtin/analytics/risk-pane.tsx +1 -0
  108. package/src/plugins/builtin/analytics/risk-test-data.ts +25 -1
  109. package/src/plugins/builtin/attention/headless.ts +6 -4
  110. package/src/plugins/builtin/attention/index.tsx +1 -1
  111. package/src/plugins/builtin/attention/pane.tsx +1 -1
  112. package/src/plugins/builtin/awards/headless.ts +1 -0
  113. package/src/plugins/builtin/awards/pane.tsx +1 -1
  114. package/src/plugins/builtin/backtest/headless.ts +1 -0
  115. package/src/plugins/builtin/bond-calculator/headless.ts +1 -0
  116. package/src/plugins/builtin/buildout/index.tsx +1 -0
  117. package/src/plugins/builtin/catalysts/headless.ts +3 -1
  118. package/src/plugins/builtin/cds/headless.ts +2 -0
  119. package/src/plugins/builtin/central-bank-rates/headless.ts +1 -0
  120. package/src/plugins/builtin/chart-composer/headless.ts +38 -3
  121. package/src/plugins/builtin/chart-composer/index.tsx +3 -1
  122. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -2
  123. package/src/plugins/builtin/chat/channels.ts +19 -3
  124. package/src/plugins/builtin/chat/content/composer-runtime.ts +28 -13
  125. package/src/plugins/builtin/chat/content/footer.tsx +15 -6
  126. package/src/plugins/builtin/chat/content/index.tsx +120 -21
  127. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +261 -113
  128. package/src/plugins/builtin/chat/content/transcript.tsx +20 -2
  129. package/src/plugins/builtin/chat/direct-messages.ts +200 -0
  130. package/src/plugins/builtin/chat/index.tsx +1 -0
  131. package/src/plugins/builtin/chat/message/desktop.tsx +3 -0
  132. package/src/plugins/builtin/chat/message/header.tsx +16 -6
  133. package/src/plugins/builtin/chat/message/inline-tokens.tsx +72 -32
  134. package/src/plugins/builtin/chat/message/profile-popover.tsx +96 -12
  135. package/src/plugins/builtin/chat/message/terminal.tsx +3 -0
  136. package/src/plugins/builtin/chat/message/types.ts +14 -2
  137. package/src/plugins/builtin/chat/profile-popover.ts +18 -13
  138. package/src/plugins/builtin/clinical-trials/index.tsx +1 -0
  139. package/src/plugins/builtin/cloud/askg/executor.ts +2 -1
  140. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  141. package/src/plugins/builtin/cloud/team/module.tsx +1 -0
  142. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -0
  143. package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
  144. package/src/plugins/builtin/comment-letters/index.tsx +1 -0
  145. package/src/plugins/builtin/company-attention/headless.ts +5 -1
  146. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  147. package/src/plugins/builtin/company-kpis/headless.ts +1 -0
  148. package/src/plugins/builtin/congress-trades/headless.ts +1 -0
  149. package/src/plugins/builtin/correlation/compute.ts +59 -16
  150. package/src/plugins/builtin/correlation/geo.ts +34 -0
  151. package/src/plugins/builtin/correlation/headless-schema.ts +4 -1
  152. package/src/plugins/builtin/correlation/headless.ts +42 -8
  153. package/src/plugins/builtin/correlation/history.ts +2 -0
  154. package/src/plugins/builtin/correlation/index.tsx +68 -14
  155. package/src/plugins/builtin/correlation/matrix/model.ts +17 -2
  156. package/src/plugins/builtin/correlation/matrix/selection.ts +5 -3
  157. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  158. package/src/plugins/builtin/correlation/relationship/pane.tsx +4 -2
  159. package/src/plugins/builtin/correlation/relationship/view-model.ts +16 -1
  160. package/src/plugins/builtin/correlation/settings.ts +1 -1
  161. package/src/plugins/builtin/cot/headless.ts +6 -3
  162. package/src/plugins/builtin/cot/index.tsx +3 -3
  163. package/src/plugins/builtin/cot/model.ts +36 -3
  164. package/src/plugins/builtin/cpi/headless.ts +4 -0
  165. package/src/plugins/builtin/credit-boards/headless.ts +2 -0
  166. package/src/plugins/builtin/credit-conditions/headless.ts +1 -0
  167. package/src/plugins/builtin/credit-documents/gauge.tsx +5 -8
  168. package/src/plugins/builtin/credit-documents/headless.ts +2 -0
  169. package/src/plugins/builtin/crypto-board/headless.ts +3 -0
  170. package/src/plugins/builtin/custom-view/loader.ts +1 -1
  171. package/src/plugins/builtin/debt-maturities/headless.ts +4 -0
  172. package/src/plugins/builtin/dividend-yield/headless.ts +1 -0
  173. package/src/plugins/builtin/doe/headless.ts +7 -0
  174. package/src/plugins/builtin/doe/range-bar.tsx +7 -37
  175. package/src/plugins/builtin/earnings/headless.ts +5 -0
  176. package/src/plugins/builtin/earnings-calls/headless.ts +1 -0
  177. package/src/plugins/builtin/earnings-ripple/client.ts +91 -18
  178. package/src/plugins/builtin/earnings-ripple/headless.ts +28 -8
  179. package/src/plugins/builtin/earnings-ripple/index.tsx +2 -2
  180. package/src/plugins/builtin/earnings-ripple/model.ts +97 -7
  181. package/src/plugins/builtin/earnings-ripple/pane.tsx +148 -23
  182. package/src/plugins/builtin/econ/index.tsx +1 -0
  183. package/src/plugins/builtin/econ-statistics/headless.ts +2 -0
  184. package/src/plugins/builtin/equity-screener/headless.ts +2 -0
  185. package/src/plugins/builtin/estimate-revisions/headless.ts +1 -0
  186. package/src/plugins/builtin/executives/index.tsx +1 -0
  187. package/src/plugins/builtin/exposure/headless.ts +3 -1
  188. package/src/plugins/builtin/fear-greed/headless.ts +2 -0
  189. package/src/plugins/builtin/filing-events/index.tsx +1 -0
  190. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  191. package/src/plugins/builtin/futures-curve/headless.ts +39 -10
  192. package/src/plugins/builtin/futures-curve/index.tsx +3 -3
  193. package/src/plugins/builtin/futures-curve/model.ts +188 -4
  194. package/src/plugins/builtin/futures-curve/pane.tsx +51 -9
  195. package/src/plugins/builtin/gpu/headless.ts +7 -3
  196. package/src/plugins/builtin/gpu/ladder.tsx +26 -30
  197. package/src/plugins/builtin/gpu/model.ts +27 -1
  198. package/src/plugins/builtin/holders/beneficial-client.ts +209 -0
  199. package/src/plugins/builtin/holders/beneficial-model.ts +279 -0
  200. package/src/plugins/builtin/holders/beneficial-report.ts +126 -0
  201. package/src/plugins/builtin/holders/beneficial-view.tsx +241 -0
  202. package/src/plugins/builtin/holders/headless.ts +139 -5
  203. package/src/plugins/builtin/holders/index.tsx +10 -3
  204. package/src/plugins/builtin/holders/pane.tsx +20 -3
  205. package/src/plugins/builtin/holders/report-header.ts +95 -0
  206. package/src/plugins/builtin/holders/table-model.ts +20 -5
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +29 -1
  208. package/src/plugins/builtin/holders/types.ts +2 -1
  209. package/src/plugins/builtin/insider/headless.ts +2 -0
  210. package/src/plugins/builtin/ipo-calendar/headless.ts +1 -0
  211. package/src/plugins/builtin/iv-history/headless.ts +4 -1
  212. package/src/plugins/builtin/jobs/headless.ts +1 -0
  213. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -0
  214. package/src/plugins/builtin/macro-day/headless.ts +2 -0
  215. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  216. package/src/plugins/builtin/market-heatmap/data.ts +2 -0
  217. package/src/plugins/builtin/market-heatmap/headless.ts +135 -0
  218. package/src/plugins/builtin/market-heatmap/index.tsx +39 -46
  219. package/src/plugins/builtin/market-heatmap/model.ts +146 -0
  220. package/src/plugins/builtin/market-heatmap/portfolio.ts +23 -4
  221. package/src/plugins/builtin/market-movers/headless.ts +2 -0
  222. package/src/plugins/builtin/market-valuation/headless.ts +2 -0
  223. package/src/plugins/builtin/members/client.ts +20 -3
  224. package/src/plugins/builtin/members/headless.ts +5 -3
  225. package/src/plugins/builtin/members/model.ts +10 -2
  226. package/src/plugins/builtin/members/pane.tsx +13 -6
  227. package/src/plugins/builtin/mna/distress/headless.ts +1 -0
  228. package/src/plugins/builtin/mna/headless.ts +1 -0
  229. package/src/plugins/builtin/money-markets/headless.ts +1 -0
  230. package/src/plugins/builtin/news/headless.ts +2 -0
  231. package/src/plugins/builtin/news/wire/index.tsx +8 -1
  232. package/src/plugins/builtin/notes/cli.ts +14 -6
  233. package/src/plugins/builtin/notes/index.tsx +1 -0
  234. package/src/plugins/builtin/openfda/index.tsx +1 -0
  235. package/src/plugins/builtin/options-calculator/headless.ts +2 -0
  236. package/src/plugins/builtin/options-positioning/headless.ts +3 -0
  237. package/src/plugins/builtin/options-scenario/headless.ts +5 -0
  238. package/src/plugins/builtin/pe-band/client.ts +48 -3
  239. package/src/plugins/builtin/pe-band/headless.ts +25 -7
  240. package/src/plugins/builtin/pe-band/model.ts +92 -16
  241. package/src/plugins/builtin/pe-band/pane.tsx +33 -16
  242. package/src/plugins/builtin/perps/client.ts +50 -3
  243. package/src/plugins/builtin/perps/headless.ts +144 -16
  244. package/src/plugins/builtin/perps/index.tsx +6 -6
  245. package/src/plugins/builtin/perps/model.ts +189 -12
  246. package/src/plugins/builtin/perps/pane.tsx +248 -47
  247. package/src/plugins/builtin/portfolio-list/allocation.ts +245 -0
  248. package/src/plugins/builtin/portfolio-list/cached-account.ts +33 -0
  249. package/src/plugins/builtin/portfolio-list/cell-version.ts +19 -1
  250. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +328 -29
  251. package/src/plugins/builtin/portfolio-list/cli/render.ts +230 -39
  252. package/src/plugins/builtin/portfolio-list/cli/shared.ts +67 -2
  253. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +35 -31
  254. package/src/plugins/builtin/portfolio-list/column-values.ts +124 -47
  255. package/src/plugins/builtin/portfolio-list/headless.ts +98 -16
  256. package/src/plugins/builtin/portfolio-list/index.tsx +14 -3
  257. package/src/plugins/builtin/portfolio-list/live-valuation.ts +7 -5
  258. package/src/plugins/builtin/portfolio-list/mutations.ts +55 -1
  259. package/src/plugins/builtin/portfolio-list/pane/data.ts +6 -0
  260. package/src/plugins/builtin/portfolio-list/pane/index.tsx +47 -13
  261. package/src/plugins/builtin/portfolio-list/settings.ts +33 -2
  262. package/src/plugins/builtin/portfolio-list/summary/index.tsx +24 -1
  263. package/src/plugins/builtin/portfolio-list/summary/totals.ts +26 -7
  264. package/src/plugins/builtin/power/headless.ts +3 -1
  265. package/src/plugins/builtin/rate-path/headless.ts +2 -0
  266. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  267. package/src/plugins/builtin/relative-rotation/headless.ts +2 -0
  268. package/src/plugins/builtin/research/analyst-headless.ts +3 -0
  269. package/src/plugins/builtin/research/events-headless.ts +2 -0
  270. package/src/plugins/builtin/research/headless.ts +2 -0
  271. package/src/plugins/builtin/research/index.tsx +1 -0
  272. package/src/plugins/builtin/research/relative-valuation-headless.ts +10 -5
  273. package/src/plugins/builtin/research/relative-valuation-model.ts +11 -3
  274. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
  275. package/src/plugins/builtin/research-search/index.tsx +1 -0
  276. package/src/plugins/builtin/revenue-breakdown/headless.ts +2 -0
  277. package/src/plugins/builtin/reverse-dcf/headless.ts +2 -0
  278. package/src/plugins/builtin/risk-factors/headless.ts +2 -0
  279. package/src/plugins/builtin/scanner/flow-history.ts +2 -13
  280. package/src/plugins/builtin/scanner/headless.ts +14 -0
  281. package/src/plugins/builtin/seasonality/headless.ts +2 -0
  282. package/src/plugins/builtin/sec/command-bar-search.ts +14 -4
  283. package/src/plugins/builtin/sec/filing-content.ts +42 -17
  284. package/src/plugins/builtin/sec/forms.ts +12 -0
  285. package/src/plugins/builtin/sec/headless.ts +2 -0
  286. package/src/plugins/builtin/sectors/client.ts +1 -0
  287. package/src/plugins/builtin/sectors/headless.ts +9 -1
  288. package/src/plugins/builtin/sectors/sector-model.ts +2 -0
  289. package/src/plugins/builtin/shared/crypto-pair.ts +14 -0
  290. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  291. package/src/plugins/builtin/shared/report-freshness.ts +57 -0
  292. package/src/plugins/builtin/shared/research-cells.tsx +4 -9
  293. package/src/plugins/builtin/short-interest/headless.ts +2 -0
  294. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  295. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  296. package/src/plugins/builtin/social-mentions/headless.ts +1 -0
  297. package/src/plugins/builtin/supply-chain/headless.ts +7 -1
  298. package/src/plugins/builtin/supply-chain/index.tsx +10 -1
  299. package/src/plugins/builtin/supply-chain/model.ts +27 -2
  300. package/src/plugins/builtin/supply-chain/pane.tsx +71 -32
  301. package/src/plugins/builtin/themes/headless.ts +3 -0
  302. package/src/plugins/builtin/thirteenf/headless.ts +110 -25
  303. package/src/plugins/builtin/thirteenf/report-notes.ts +149 -0
  304. package/src/plugins/builtin/ticker-detail/headless.ts +7 -1
  305. package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
  306. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +26 -26
  308. package/src/plugins/builtin/time-sales/headless.ts +5 -1
  309. package/src/plugins/builtin/treasury-auctions/headless.ts +1 -0
  310. package/src/plugins/builtin/volatility/headless.ts +6 -1
  311. package/src/plugins/builtin/world-indices/headless.ts +2 -0
  312. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +157 -0
  313. package/src/plugins/builtin/world-venue-map/basemap.ts +214 -0
  314. package/src/plugins/builtin/world-venue-map/client.ts +45 -0
  315. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +147 -0
  316. package/src/plugins/builtin/world-venue-map/entity-table.ts +59 -0
  317. package/src/plugins/builtin/world-venue-map/evidence.ts +77 -0
  318. package/src/plugins/builtin/world-venue-map/feed.ts +293 -0
  319. package/src/plugins/builtin/world-venue-map/geo-draw.tsx +248 -0
  320. package/src/plugins/builtin/world-venue-map/geo-series.ts +142 -0
  321. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +447 -0
  322. package/src/plugins/builtin/world-venue-map/headless.ts +146 -0
  323. package/src/plugins/builtin/world-venue-map/ids.ts +1 -0
  324. package/src/plugins/builtin/world-venue-map/index.ts +67 -4
  325. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +552 -0
  326. package/src/plugins/builtin/world-venue-map/layers.ts +348 -0
  327. package/src/plugins/builtin/world-venue-map/map-symbols.ts +220 -0
  328. package/src/plugins/builtin/world-venue-map/map.tsx +381 -45
  329. package/src/plugins/builtin/world-venue-map/model.ts +7 -4
  330. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +199 -0
  331. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n00.chunk.ts +4 -0
  332. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n15.chunk.ts +4 -0
  333. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n30.chunk.ts +4 -0
  334. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n45.chunk.ts +4 -0
  335. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n60.chunk.ts +4 -0
  336. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000n75.chunk.ts +4 -0
  337. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000s15.chunk.ts +4 -0
  338. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000s30.chunk.ts +4 -0
  339. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e000s60.chunk.ts +4 -0
  340. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n00.chunk.ts +4 -0
  341. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n15.chunk.ts +4 -0
  342. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n30.chunk.ts +4 -0
  343. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n45.chunk.ts +4 -0
  344. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n60.chunk.ts +4 -0
  345. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015n75.chunk.ts +4 -0
  346. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015s15.chunk.ts +4 -0
  347. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015s30.chunk.ts +4 -0
  348. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e015s45.chunk.ts +4 -0
  349. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n00.chunk.ts +4 -0
  350. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n15.chunk.ts +4 -0
  351. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n30.chunk.ts +4 -0
  352. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n45.chunk.ts +4 -0
  353. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n60.chunk.ts +4 -0
  354. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030n75.chunk.ts +4 -0
  355. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s15.chunk.ts +4 -0
  356. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s30.chunk.ts +4 -0
  357. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s45.chunk.ts +4 -0
  358. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e030s60.chunk.ts +4 -0
  359. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n00.chunk.ts +4 -0
  360. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n15.chunk.ts +4 -0
  361. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n30.chunk.ts +4 -0
  362. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n45.chunk.ts +4 -0
  363. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n60.chunk.ts +4 -0
  364. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045n75.chunk.ts +4 -0
  365. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045s15.chunk.ts +4 -0
  366. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045s30.chunk.ts +4 -0
  367. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e045s60.chunk.ts +4 -0
  368. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n00.chunk.ts +4 -0
  369. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n15.chunk.ts +4 -0
  370. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n30.chunk.ts +4 -0
  371. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n45.chunk.ts +4 -0
  372. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n60.chunk.ts +4 -0
  373. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060n75.chunk.ts +4 -0
  374. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060s15.chunk.ts +4 -0
  375. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060s30.chunk.ts +4 -0
  376. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e060s60.chunk.ts +4 -0
  377. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n00.chunk.ts +4 -0
  378. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n15.chunk.ts +4 -0
  379. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n30.chunk.ts +4 -0
  380. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n45.chunk.ts +4 -0
  381. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n60.chunk.ts +4 -0
  382. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075n75.chunk.ts +4 -0
  383. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e075s45.chunk.ts +4 -0
  384. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n00.chunk.ts +4 -0
  385. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n15.chunk.ts +4 -0
  386. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n30.chunk.ts +4 -0
  387. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n45.chunk.ts +4 -0
  388. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n60.chunk.ts +4 -0
  389. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090n75.chunk.ts +4 -0
  390. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e090s15.chunk.ts +4 -0
  391. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n00.chunk.ts +4 -0
  392. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n15.chunk.ts +4 -0
  393. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n30.chunk.ts +4 -0
  394. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n45.chunk.ts +4 -0
  395. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n60.chunk.ts +4 -0
  396. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105n75.chunk.ts +4 -0
  397. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105s15.chunk.ts +4 -0
  398. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105s30.chunk.ts +4 -0
  399. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e105s45.chunk.ts +4 -0
  400. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n00.chunk.ts +4 -0
  401. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n15.chunk.ts +4 -0
  402. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n30.chunk.ts +4 -0
  403. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n45.chunk.ts +4 -0
  404. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120n60.chunk.ts +4 -0
  405. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120s15.chunk.ts +4 -0
  406. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120s30.chunk.ts +4 -0
  407. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e120s45.chunk.ts +4 -0
  408. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n00.chunk.ts +4 -0
  409. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n15.chunk.ts +4 -0
  410. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n30.chunk.ts +4 -0
  411. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n45.chunk.ts +4 -0
  412. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n60.chunk.ts +4 -0
  413. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135n75.chunk.ts +4 -0
  414. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135s15.chunk.ts +4 -0
  415. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135s30.chunk.ts +4 -0
  416. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e135s45.chunk.ts +4 -0
  417. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n00.chunk.ts +4 -0
  418. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n15.chunk.ts +4 -0
  419. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n45.chunk.ts +4 -0
  420. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n60.chunk.ts +4 -0
  421. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150n75.chunk.ts +4 -0
  422. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s15.chunk.ts +4 -0
  423. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s30.chunk.ts +4 -0
  424. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s45.chunk.ts +4 -0
  425. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e150s60.chunk.ts +4 -0
  426. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n00.chunk.ts +4 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n15.chunk.ts +4 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n45.chunk.ts +4 -0
  429. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165n60.chunk.ts +4 -0
  430. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s15.chunk.ts +4 -0
  431. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s30.chunk.ts +4 -0
  432. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s45.chunk.ts +4 -0
  433. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/e165s60.chunk.ts +4 -0
  434. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n00.chunk.ts +4 -0
  435. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n15.chunk.ts +4 -0
  436. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n30.chunk.ts +4 -0
  437. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n45.chunk.ts +4 -0
  438. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n60.chunk.ts +4 -0
  439. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015n75.chunk.ts +4 -0
  440. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015s15.chunk.ts +4 -0
  441. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015s30.chunk.ts +4 -0
  442. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w015s45.chunk.ts +4 -0
  443. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n00.chunk.ts +4 -0
  444. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n15.chunk.ts +4 -0
  445. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n30.chunk.ts +4 -0
  446. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n60.chunk.ts +4 -0
  447. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030n75.chunk.ts +4 -0
  448. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030s30.chunk.ts +4 -0
  449. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w030s60.chunk.ts +4 -0
  450. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n30.chunk.ts +4 -0
  451. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n45.chunk.ts +4 -0
  452. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n60.chunk.ts +4 -0
  453. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045n75.chunk.ts +4 -0
  454. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045s15.chunk.ts +4 -0
  455. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045s30.chunk.ts +4 -0
  456. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w045s60.chunk.ts +4 -0
  457. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n00.chunk.ts +4 -0
  458. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n30.chunk.ts +4 -0
  459. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n45.chunk.ts +4 -0
  460. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n60.chunk.ts +4 -0
  461. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060n75.chunk.ts +4 -0
  462. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s15.chunk.ts +4 -0
  463. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s30.chunk.ts +4 -0
  464. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s45.chunk.ts +4 -0
  465. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w060s60.chunk.ts +4 -0
  466. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n00.chunk.ts +4 -0
  467. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n15.chunk.ts +4 -0
  468. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n30.chunk.ts +4 -0
  469. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n45.chunk.ts +4 -0
  470. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n60.chunk.ts +4 -0
  471. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075n75.chunk.ts +4 -0
  472. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s15.chunk.ts +4 -0
  473. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s30.chunk.ts +4 -0
  474. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s45.chunk.ts +4 -0
  475. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w075s60.chunk.ts +4 -0
  476. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n00.chunk.ts +4 -0
  477. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n15.chunk.ts +4 -0
  478. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n30.chunk.ts +4 -0
  479. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n45.chunk.ts +4 -0
  480. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n60.chunk.ts +4 -0
  481. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090n75.chunk.ts +4 -0
  482. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s15.chunk.ts +4 -0
  483. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s30.chunk.ts +4 -0
  484. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s45.chunk.ts +4 -0
  485. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w090s60.chunk.ts +4 -0
  486. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n00.chunk.ts +4 -0
  487. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n15.chunk.ts +4 -0
  488. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n30.chunk.ts +4 -0
  489. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n45.chunk.ts +4 -0
  490. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n60.chunk.ts +4 -0
  491. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105n75.chunk.ts +4 -0
  492. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w105s15.chunk.ts +4 -0
  493. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n00.chunk.ts +4 -0
  494. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n15.chunk.ts +4 -0
  495. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n30.chunk.ts +4 -0
  496. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n45.chunk.ts +4 -0
  497. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n60.chunk.ts +4 -0
  498. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120n75.chunk.ts +4 -0
  499. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w120s30.chunk.ts +4 -0
  500. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n30.chunk.ts +4 -0
  501. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n45.chunk.ts +4 -0
  502. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n60.chunk.ts +4 -0
  503. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135n75.chunk.ts +4 -0
  504. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w135s30.chunk.ts +4 -0
  505. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150n45.chunk.ts +4 -0
  506. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150n60.chunk.ts +4 -0
  507. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150s15.chunk.ts +4 -0
  508. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w150s30.chunk.ts +4 -0
  509. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n00.chunk.ts +4 -0
  510. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n15.chunk.ts +4 -0
  511. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n45.chunk.ts +4 -0
  512. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165n60.chunk.ts +4 -0
  513. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165s15.chunk.ts +4 -0
  514. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w165s30.chunk.ts +4 -0
  515. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n00.chunk.ts +4 -0
  516. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n15.chunk.ts +4 -0
  517. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n45.chunk.ts +4 -0
  518. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180n60.chunk.ts +4 -0
  519. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180s15.chunk.ts +4 -0
  520. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180s30.chunk.ts +4 -0
  521. package/src/plugins/builtin/world-venue-map/natural-earth/tiles/w180s45.chunk.ts +4 -0
  522. package/src/plugins/builtin/world-venue-map/natural-earth/world-50m.chunk.ts +4 -0
  523. package/src/plugins/builtin/world-venue-map/pane.tsx +28 -43
  524. package/src/plugins/builtin/world-venue-map/use-geo.ts +55 -0
  525. package/src/plugins/builtin/world-venue-map/venue-header.tsx +32 -0
  526. package/src/plugins/builtin/yield-curve/headless.ts +16 -0
  527. package/src/plugins/loader.ts +13 -1
  528. package/src/plugins/registry/pane-settings.ts +1 -0
  529. package/src/renderers/dom/build-assets.ts +81 -3
  530. package/src/renderers/dom/data-table/row.tsx +2 -0
  531. package/src/renderers/dom/desktop/controls.tsx +7 -3
  532. package/src/renderers/dom/desktop/list-view.tsx +35 -10
  533. package/src/renderers/dom/desktop/popover.tsx +73 -14
  534. package/src/renderers/dom/host/input.tsx +14 -3
  535. package/src/renderers/dom/styles.css +6 -7
  536. package/src/renderers/electrobun/view/backend-rpc.ts +10 -4
  537. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  538. package/src/renderers/electrobun/view/rpc-timeout.ts +14 -3
  539. package/src/renderers/opentui/host.tsx +5 -0
  540. package/src/sources/gloomberb-cloud/index.ts +25 -4
  541. package/src/sources/gloomberb-cloud/normalizers.ts +27 -3
  542. package/src/sources/provider-router/cached-routes.ts +4 -1
  543. package/src/sources/provider-router/financials.ts +8 -7
  544. package/src/sources/provider-router/history.ts +15 -10
  545. package/src/sources/sec-registrant.ts +75 -0
  546. package/src/state/app/context/index.tsx +24 -8
  547. package/src/state/hooks/quote-streaming.ts +41 -26
  548. package/src/sync/core-contributors.ts +4 -0
  549. package/src/telemetry/crash-reports.ts +4 -1
  550. package/src/test-support/cli-context.ts +10 -2
  551. package/src/theme/heat-colors.ts +266 -84
  552. package/src/time-series/resolve.ts +1 -1
  553. package/src/time-series/use-chart-resolution.ts +1 -1
  554. package/src/types/context-menu.ts +6 -0
  555. package/src/types/data-provider.ts +2 -0
  556. package/src/types/financials.ts +3 -0
  557. package/src/types/headless.ts +79 -2
  558. package/src/types/plugin.ts +20 -1
  559. package/src/types/ticker.ts +15 -0
  560. package/src/ui/host.tsx +14 -0
  561. package/src/updater/index.ts +9 -11
  562. package/src/utils/async-deadline.ts +21 -0
  563. package/src/utils/cli-output.ts +5 -1
  564. package/src/utils/crypto-pair.ts +41 -0
  565. package/src/utils/currency-units.ts +14 -0
  566. package/src/utils/depositary-receipt.ts +26 -0
  567. package/src/utils/exchanges.ts +72 -0
  568. package/src/utils/fire-and-forget.ts +24 -0
  569. package/src/utils/format.ts +13 -0
  570. package/src/utils/price-earnings.ts +70 -3
  571. package/src/utils/price-history.ts +58 -13
  572. package/src/utils/rpc-timeout-error.ts +27 -0
  573. package/src/utils/sec.ts +13 -1
  574. package/src/utils/utc-time.ts +79 -0
@@ -14,6 +14,7 @@ import {
14
14
 
15
15
  export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
16
16
  shape: "bundle",
17
+ freshness: { status: "not-a-feed", basis: "screener snapshot" },
17
18
  argument: { kind: "none" },
18
19
  options: [
19
20
  {
@@ -49,6 +50,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
49
50
  // Payload status also counts ordinary per-field sparsity; only generation
50
51
  // warnings are failures, and more pages are not an error.
51
52
  return {
53
+ freshness: { asOf: data.snapshot?.sourceNewestAt ?? null },
52
54
  complete: !data.nextCursor && data.warnings.length === 0,
53
55
  errors: data.warnings,
54
56
  sections: [
@@ -3,6 +3,7 @@ import { fetchEstimates } from "./client";
3
3
  import { estimateCurrent, periodLabel, pinnedEstimatePeriods } from "./model";
4
4
  export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
5
5
  shape: "bundle",
6
+ freshness: { status: "not-a-feed", basis: "analyst estimates" },
6
7
  argument: {
7
8
  kind: "ticker",
8
9
  placeholder: "ticker",
@@ -30,6 +30,7 @@ export const executivesModule: PluginModule = {
30
30
  panes: [
31
31
  {
32
32
  id: EXECUTIVES_PANE_ID,
33
+ reportFreshness: { source: "SEC EDGAR", status: "not-a-feed", basis: "proxy statements" },
33
34
  name: "Executives",
34
35
  icon: "X",
35
36
  component: ExecutivesPane,
@@ -5,7 +5,9 @@ import { DEFAULT_SCENARIO, parseCustomScenario, parseHoldings, TABS } from "./mo
5
5
  import { portfolioHoldings, watchlistHoldings } from "./holdings";
6
6
 
7
7
  export const exposureHeadless: HeadlessPaneDefinition<"bundle"> = {
8
- shape: "bundle", argument: { kind: "free-text", optional: true, placeholder: "AAPL=60% NVDA=40%", description: "Tickers, signed NAV weights, PORT:<id>, or WATCH:<id>. Unweighted lists and watchlists use equal weights." },
8
+ shape: "bundle",
9
+ freshness: { status: "not-a-feed", basis: "company disclosures" },
10
+ argument: { kind: "free-text", optional: true, placeholder: "AAPL=60% NVDA=40%", description: "Tickers, signed NAV weights, PORT:<id>, or WATCH:<id>. Unweighted lists and watchlists use equal weights." },
9
11
  discovery: { aliases: ["EXPO"], dataRequirements: ["Gloom Cloud geographic revenue, disclosed supply chains, KPIs, guidance and credit disclosures", "Portfolio NAV for local PORT weights"],
10
12
  limitations: ["Pro: full holdings and up to four-hop paths; free: one holding, one hop", "Operating exposure estimates, never predicted stock returns", "Missing relationships and sensitivities remain unknown", "Basis and reporting period are kept separate"] },
11
13
  options: [
@@ -76,6 +76,8 @@ export function createFearGreedHeadless(
76
76
  return {
77
77
  ...projected,
78
78
  ...(result.refreshError ? { errors: [result.refreshError] } : {}),
79
+ // A cached index the refresh could not replace is stale; otherwise CNN's published reading.
80
+ freshness: { source: "CNN", status: result.stale ? "stale" : "not-a-feed", basis: "published index" },
79
81
  metadata: { fetchedAt: result.fetchedAt, stale: result.stale },
80
82
  };
81
83
  },
@@ -21,6 +21,7 @@ export const filingEventsModule: PluginModule = {
21
21
  panes: [
22
22
  {
23
23
  id: FILING_EVENTS_PANE_ID,
24
+ reportFreshness: { source: "SEC EDGAR", status: "not-a-feed", basis: "filed data" },
24
25
  name: "8-K Filings",
25
26
  icon: "K",
26
27
  component: FilingEventsPane,
@@ -2,7 +2,8 @@ import { apiClient } from "../../../api-client";
2
2
  import type { FuturesCurvePayload } from "../../../api-client/futures-curve";
3
3
  import { createPluginCache } from "../../../data/plugin-cache";
4
4
  import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
5
- import { archivedFuturesCurve, curveLookbackDate, normalizeCurveRoot } from "./model";
5
+ import type { DataProvider } from "../../../types/data-provider";
6
+ import { archivedFuturesCurve, curveLookbackDate, curveSpot, normalizeCurveRoot, unsupportedCurveRootMessage, type CurveSpot } from "./model";
6
7
 
7
8
  export const futuresCurveCache = createPluginCache<FuturesCurvePayload>({
8
9
  kind: "futures-curve", source: "gloom-cloud", schemaVersion: 1,
@@ -51,7 +52,7 @@ export function validateFuturesCurve(data: FuturesCurvePayload, root: string): F
51
52
 
52
53
  export async function fetchFuturesCurve(root: string, client: Pick<typeof apiClient, "getCloudFuturesCurve"> = apiClient): Promise<FuturesCurvePayload> {
53
54
  const normalized = normalizeCurveRoot(root);
54
- if (!normalized) throw new Error(`Unsupported futures root: ${root}`);
55
+ if (!normalized) throw new Error(unsupportedCurveRootMessage(root));
55
56
  try { return validateFuturesCurve(await client.getCloudFuturesCurve(normalized), normalized); }
56
57
  catch (error) { throw unavailableOnServer(error, "Futures curves are not available yet."); }
57
58
  }
@@ -71,7 +72,7 @@ export async function loadFuturesCurve(root: string, force = false): Promise<Fut
71
72
  export async function loadFuturesCurveAsOf(root: string, date: string,
72
73
  client: Pick<typeof apiClient, "getCloudFuturesCurveAsOf"> = apiClient): Promise<FuturesCurvePayload> {
73
74
  const normalized = normalizeCurveRoot(root);
74
- if (!normalized) throw new Error(`Unsupported futures root: ${root}`);
75
+ if (!normalized) throw new Error(unsupportedCurveRootMessage(root));
75
76
  try {
76
77
  const [curve, week, month] = await Promise.all([
77
78
  client.getCloudFuturesCurveAsOf(normalized, date),
@@ -85,3 +86,11 @@ export async function loadFuturesCurveAsOf(root: string, date: string,
85
86
  return archivedFuturesCurve(normalized, curve, { "1W": week, "1M": month }, new Date().toISOString());
86
87
  } catch (error) { throw unavailableOnServer(error, "Past futures curves are not available yet."); }
87
88
  }
89
+
90
+ /**
91
+ * The spot behind a crypto curve's basis, read through the same quote path every pane uses.
92
+ * A failed read is a missing spot, never a failed report: the curve stands without its basis.
93
+ */
94
+ export async function loadCurveSpot(symbol: string, provider: Pick<DataProvider, "getQuote">, now = Date.now()): Promise<CurveSpot> {
95
+ return curveSpot(symbol, await provider.getQuote(symbol, "").catch(() => null), now);
96
+ }
@@ -1,35 +1,64 @@
1
- import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
- import { fetchFuturesCurve, loadFuturesCurveAsOf } from "./client";
3
- import { curveAsOfDate, normalizeCurveRoot } from "./model";
1
+ import type { HeadlessPaneDefinition, HeadlessPaneFreshness } from "../../../types/plugin";
2
+ import { newestReportTime, oldestReportTime } from "../../../utils/utc-time";
3
+ import { fetchFuturesCurve, loadCurveSpot, loadFuturesCurveAsOf } from "./client";
4
+ import { basisSpotSymbol, contractBasis, curveAsOfDate, curveSpotLabel, normalizeCurveRoot, thinContractCount, unsupportedCurveRootMessage } from "./model";
5
+
6
+ /** The root a report shows when none is given. */
7
+ const DEFAULT_ROOT = "ES";
8
+ const EXAMPLE_ROOT = "CL";
4
9
 
5
10
  /** Percentiles read as the pane shows them, a whole rank. */
6
11
  const rank = (value: number | null) => value == null ? null : Math.round(value);
12
+ /** The basis fields are read to four decimals, finer than any cell shows. */
13
+ const basisValue = (value: number | null) => value == null ? null : Number(value.toFixed(4));
7
14
 
8
15
  export const futuresCurveHeadless: HeadlessPaneDefinition<"bundle"> = {
9
16
  discovery: { aliases: ["CTM"], dataRequirements: ["Gloom Cloud futures curve endpoint"],
10
17
  limitations: ["Listed-contract catalogues can be incomplete", "Ghosts and percentiles use the same listed contracts", "Cboe VIX is daily settlement"] },
11
- shape: "bundle", argument: { kind: "free-text", optional: true, placeholder: "root", description: "FUT root such as CL, ES, ZN or VX. Defaults to ES." },
18
+ shape: "bundle", argument: { kind: "free-text", optional: true, placeholder: "root", description: "FUT root such as CL, ES, ZN or VX, or a CME crypto root (BTC, ETH, SOL, XRP). Defaults to ES." },
12
19
  // Only the pane reads the tab; the report always carries both.
13
20
  options: [{ key: "tab", description: "Curve or the full contract table.", type: "enum",
14
21
  values: [{ value: "curve" }, { value: "contracts", aliases: ["contract", "table"] }], defaultValue: "curve",
15
22
  pluginState: { pluginId: "market-overview", key: "tab" } },
16
23
  { key: "date", type: "string", settingKey: "asOfDate", description: "Past date (YYYY-MM-DD): the curve as the daily settlement archive held it, with the curves a week and a month before." }],
17
- describe: (args) => `Futures curve ${args.argument || "ES"}`,
24
+ describe: (args) => `Futures curve ${args.argument || DEFAULT_ROOT}`,
18
25
  async load(args, ctx) {
19
- const input = args.argument || "ES";
26
+ const input = args.argument || DEFAULT_ROOT;
20
27
  const root = normalizeCurveRoot(input);
21
- if (!root) throw new Error(`Unsupported futures root: ${input}`);
28
+ if (!root) throw new Error(unsupportedCurveRootMessage(input));
22
29
  const date = curveAsOfDate(args.options.date);
23
- const data = date ? await loadFuturesCurveAsOf(root, date, ctx.apiClient) : await fetchFuturesCurve(root, ctx.apiClient);
30
+ // The archive keeps no spot history, so a past curve has no basis.
31
+ const spotSymbol = date ? null : basisSpotSymbol(root);
32
+ const now = Date.now();
33
+ const [data, spot] = await Promise.all([
34
+ date ? loadFuturesCurveAsOf(root, date, ctx.apiClient) : fetchFuturesCurve(root, ctx.apiClient),
35
+ spotSymbol ? loadCurveSpot(spotSymbol, ctx.marketData, now) : null,
36
+ ]);
37
+ const bases = spot ? data.contracts.map((row) => contractBasis(row, spot)) : [];
38
+ const notices = [...args.argument ? [] : [`Showing ${DEFAULT_ROOT}. Try fn CTM ${EXAMPLE_ROOT}.`],
39
+ ...spot ? [spot.status === "ok" ? `Basis against ${curveSpotLabel(spot, root, now)}.` : `Basis blank: ${spot.reason}.`] : []];
40
+ // Dated by the contracts' own quotes; the same-contract history rows are a year of context, not observations.
41
+ const quoted = data.contracts.map((row) => row.asOf);
42
+ const freshness: HeadlessPaneFreshness = date
43
+ ? { status: "not-a-feed", basis: "settlement archive", asOf: date }
44
+ : { ...(data.source === "cboe" ? { source: "Cboe", status: "not-a-feed", basis: "daily settlement", cadence: "daily" } as const : {}),
45
+ asOf: newestReportTime(quoted), oldest: oldestReportTime(quoted) };
24
46
  return {
47
+ freshness,
25
48
  sections: [
26
- { title: "Contracts", rows: data.contracts.map((row) => ({ ...row, percentile: rank(row.percentile) })) },
49
+ { title: "Contracts", rows: data.contracts.map((row, index) => {
50
+ const basis = bases[index];
51
+ return { ...row, percentile: rank(row.percentile),
52
+ ...(basis ? { vsSpotPct: basisValue(basis.vsSpotPct), annualisedBasisPct: basisValue(basis.annualisedBasisPct) } : {}) };
53
+ }) },
27
54
  { title: "Front spread", rows: [{ ...data.slope, annualizedRollYield: data.slope.annualizedRollYield == null ? null : Number(data.slope.annualizedRollYield.toFixed(2)),
28
55
  percentile: rank(data.slope.percentile), rollPercentile: rank(data.slope.rollPercentile) }] },
29
56
  ...data.ghosts.map((ghost) => ({ title: `${ghost.label} same-contract history`, rows: ghost.points.map((point) => ({ ...point })) })),
30
57
  ],
31
58
  errors: data.gaps,
32
- metadata: { ...data, complete: data.status === "available", percentileBasis: "Same-contract observations within one year; actual sample start/end retained" },
59
+ metadata: { ...data, ...(spot ? { spot, basisBlankedThin: thinContractCount(bases) } : {}), ...(args.argument ? {} : { defaultArgument: DEFAULT_ROOT }),
60
+ ...(notices.length ? { notices } : {}),
61
+ complete: data.status === "available", percentileBasis: "Same-contract observations within one year; actual sample start/end retained" },
33
62
  };
34
63
  },
35
64
  };
@@ -1,7 +1,7 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { futuresCurveCache } from "./client";
3
3
  import { futuresCurveHeadless } from "./headless";
4
- import { CURVE_HORIZONS, CURVE_ROOTS, DEFAULT_CURVE_HORIZON, normalizeCurveRoot } from "./model";
4
+ import { CURVE_HORIZONS, CURVE_ROOTS, curveRootForTicker, DEFAULT_CURVE_HORIZON, normalizeCurveRoot } from "./model";
5
5
  import { FuturesCurvePane } from "./pane";
6
6
 
7
7
  export const futuresCurveModule: PluginModule = {
@@ -17,14 +17,14 @@ export const futuresCurveModule: PluginModule = {
17
17
  }),
18
18
  }],
19
19
  paneTemplates: [{ id: "futures-curve-pane", paneId: "futures-curve", label: "Futures Curve",
20
- description: "Listed futures contracts, historical curves, roll yield and open interest including VIX futures.",
20
+ description: "Listed futures contracts, historical curves, roll yield and open interest including VIX and CME crypto futures.",
21
21
  keywords: ["ctm", "futures", "curve", "contango", "backwardation", "roll", "vix"],
22
22
  shortcut: { prefix: "CTM", aliases: ["CT"], argKind: "text", argPlaceholder: "root", argOptional: true,
23
23
  argOptions: () => CURVE_ROOTS },
24
24
  headless: futuresCurveHeadless,
25
25
  createInstance: (context, options) => {
26
26
  const input = options?.arg?.trim();
27
- const root = input ? normalizeCurveRoot(input) : normalizeCurveRoot(context.activeTicker) ?? "ES";
27
+ const root = input ? normalizeCurveRoot(input) : curveRootForTicker(context.activeTicker) ?? "ES";
28
28
  return { title: `CTM ${root ?? input}`, params: { root: root ?? input ?? "ES" }, placement: "floating" };
29
29
  },
30
30
  }],
@@ -5,13 +5,28 @@ import { compositeAxisTicks } from "../../../components/chart/composite/format";
5
5
  import type { CompositeAxisDomain } from "../../../components/chart/composite/types";
6
6
  import { FUTURES_CONTRACTS, tickDecimals } from "../futures/contracts";
7
7
  import { formatPercentileRank } from "../../../utils/format";
8
+ import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
9
+ import { parseReportTime } from "../../../utils/utc-time";
8
10
  import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
9
11
 
12
+ /**
13
+ * CME crypto futures have a curve but no FUT board row, so their name and tick
14
+ * live here rather than in the catalogue the board lists. Outright ticks:
15
+ * BTC $5, ETH $0.50, SOL $0.05, XRP $0.0005 (each $25 a contract).
16
+ */
17
+ const CRYPTO_CURVE_ROOTS: readonly { code: string; name: string; tick: number }[] = [
18
+ { code: "BTC", name: "Bitcoin", tick: 5 }, { code: "ETH", name: "Ether", tick: 0.5 },
19
+ { code: "SOL", name: "Solana", tick: 0.05 }, { code: "XRP", name: "XRP", tick: 0.0005 },
20
+ ];
21
+ const rootLabel = (code: string, name: string) => name === code ? code : `${code} ${name}`;
22
+
10
23
  export const CURVE_ROOTS = [
11
- ...FUTURES_CONTRACTS.filter((row) => row.curve !== false).map((row) => ({ value: row.code, label: `${row.code} ${row.name}` })),
24
+ ...FUTURES_CONTRACTS.filter((row) => row.curve !== false).map((row) => ({ value: row.code, label: rootLabel(row.code, row.name) })),
12
25
  { value: "VX", label: "VX VIX Futures" },
26
+ ...CRYPTO_CURVE_ROOTS.map((row) => ({ value: row.code, label: rootLabel(row.code, row.name) })),
13
27
  ];
14
28
 
29
+ /** A root typed as an argument, a stored pane setting or a request: every listed root, crypto included. */
15
30
  export function normalizeCurveRoot(value: unknown): string | null {
16
31
  if (typeof value !== "string") return null;
17
32
  const raw = value.trim().toUpperCase().replace(/=F$/, "");
@@ -19,6 +34,26 @@ export function normalizeCurveRoot(value: unknown): string | null {
19
34
  return CURVE_ROOTS.some((row) => row.value === root) ? root : null;
20
35
  }
21
36
 
37
+ /** What a root nothing lists is told, with roots that work. */
38
+ export function unsupportedCurveRootMessage(value: unknown): string {
39
+ 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
+ }
42
+
43
+ /**
44
+ * The root for the ticker under the cursor, when CTM is opened without an
45
+ * argument. The crypto roots are also US ticker symbols (Grayscale's mini
46
+ * trusts are BTC and ETH), so a bare one is the equity's, never the future's:
47
+ * only a futures symbol (BTC=F) or a coin pair (BTC-USD) names the crypto curve.
48
+ */
49
+ export function curveRootForTicker(ticker: unknown): string | null {
50
+ if (typeof ticker !== "string") return null;
51
+ const coin = cryptoPairCoin(ticker);
52
+ if (coin) return CRYPTO_CURVE_ROOTS.some((row) => row.code === coin) ? coin : null;
53
+ const root = normalizeCurveRoot(ticker);
54
+ return root && CRYPTO_CURVE_ROOTS.some((row) => row.code === root) && !isFuturesSymbol(ticker) ? null : root;
55
+ }
56
+
22
57
  /**
23
58
  * Treasury futures trade in fractions of a 32nd (ZT to 1/256, ZF 1/128, ZN
24
59
  * 1/64, ZB and UB 1/32), so the catalogue leaves their tick unset. A root
@@ -32,7 +67,7 @@ function curvePriceDecimals(root: string): number {
32
67
  const rateTick = RATE_TICKS[root];
33
68
  if (rateTick != null) return tickDecimals(rateTick);
34
69
  if (root === "VX") return 4;
35
- const tick = FUTURES_CONTRACTS.find((row) => row.code === root)?.tick;
70
+ const tick = FUTURES_CONTRACTS.find((row) => row.code === root)?.tick ?? CRYPTO_CURVE_ROOTS.find((row) => row.code === root)?.tick;
36
71
  return tick == null ? 5 : Math.max(2, tickDecimals(tick));
37
72
  }
38
73
 
@@ -155,17 +190,166 @@ const CHANGE_COLUMNS: Readonly<Record<string, CurveLookback>> = { change1w: "1W"
155
190
 
156
191
  type CurveSortKey = "symbol" | "expiration" | "price" | "change" | "openInterest" | "volume" | "percentile" | "asOf";
157
192
 
193
+ const BASIS_COLUMNS: Readonly<Record<string, "vsSpotPct" | "annualisedBasisPct">> = { vsSpot: "vsSpotPct", annBasis: "annualisedBasisPct" };
194
+
158
195
  export function sortCurveContracts(rows: readonly FuturesContract[], id: string, direction: SortDirection,
159
- changes?: CurveContractChanges): FuturesContract[] {
196
+ changes?: CurveContractChanges, basis?: CurveBasisRows): FuturesContract[] {
160
197
  const keys: Record<string, CurveSortKey> = { symbol: "symbol", expiry: "expiration", price: "price", change: "change", oi: "openInterest", volume: "volume", percentile: "percentile", asOf: "asOf" };
161
198
  const key = keys[id] ?? "expiration";
162
199
  const lookback = CHANGE_COLUMNS[id];
163
- const value = (row: FuturesContract) => lookback ? changes?.get(row.symbol)?.[lookback] ?? null : row[key] ?? null;
200
+ const basisKey = BASIS_COLUMNS[id];
201
+ const value = (row: FuturesContract) => lookback ? changes?.get(row.symbol)?.[lookback] ?? null
202
+ : basisKey ? basis?.get(row.symbol)?.[basisKey] ?? null : row[key] ?? null;
164
203
  return [...rows].sort((a, b) => compareSortValues(value(a), value(b), direction));
165
204
  }
166
205
 
167
206
  const DAY_MS = 86_400_000;
168
207
 
208
+ /**
209
+ * The quote each crypto root's basis is measured against: the terminal's USD
210
+ * pair, the same quote every other pane shows, not the CME reference rate, so
211
+ * a small gap to settlement is expected. A root missing here has no basis
212
+ * columns; add its spot symbol to give it some.
213
+ */
214
+ const BASIS_SPOT_SYMBOLS: ReadonlyMap<string, string> = new Map([
215
+ ["BTC", "BTC-USD"], ["ETH", "ETH-USD"], ["SOL", "SOL-USD"], ["XRP", "XRP-USD"],
216
+ ]);
217
+
218
+ export function basisSpotSymbol(root: string): string | null {
219
+ return BASIS_SPOT_SYMBOLS.get(root) ?? null;
220
+ }
221
+
222
+ /** Crypto trades around the clock, so a spot quote older than this no longer prices a live future. */
223
+ const SPOT_STALE_MS = 10 * 60_000;
224
+ /** A free account's quote is held back on purpose; that delay is not staleness, so it is allowed on top. */
225
+ const DELAYED_SPOT_MS = 15 * 60_000;
226
+
227
+ /** The spot a curve's basis uses, or why there is none. `asOf` is the quote's own time, never the fetch's. */
228
+ export interface CurveSpot {
229
+ symbol: string;
230
+ price: number | null;
231
+ /** UTC ISO instant of the quote. */
232
+ asOf: string | null;
233
+ status: "ok" | "stale" | "missing";
234
+ reason: string | null;
235
+ }
236
+
237
+ export interface SpotQuote {
238
+ price: number;
239
+ lastUpdated: number;
240
+ stale?: boolean;
241
+ dataSource?: string;
242
+ }
243
+
244
+ const positive = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value > 0;
245
+
246
+ function quoteAge(ms: number): string {
247
+ const minutes = Math.floor(ms / 60_000);
248
+ if (minutes < 60) return `${minutes}m`;
249
+ const hours = Math.floor(minutes / 60);
250
+ return hours < 48 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
251
+ }
252
+
253
+ /** Judges one spot quote at `now`: usable, older than the rule allows, or absent. */
254
+ export function curveSpot(symbol: string, quote: SpotQuote | null | undefined, now: number): CurveSpot {
255
+ if (!quote || !positive(quote.price)) return { symbol, price: null, asOf: null, status: "missing", reason: `no ${symbol} quote` };
256
+ const time = quote.lastUpdated;
257
+ if (!Number.isFinite(time) || time <= 0) {
258
+ return { symbol, price: quote.price, asOf: null, status: "stale", reason: `${symbol} quote has no time` };
259
+ }
260
+ const asOf = new Date(time).toISOString().replace(".000Z", "Z");
261
+ const age = now - time;
262
+ const limit = SPOT_STALE_MS + (quote.dataSource === "delayed" ? DELAYED_SPOT_MS : 0);
263
+ if (age > limit) return { symbol, price: quote.price, asOf, status: "stale", reason: `${symbol} quote is ${quoteAge(age)} old` };
264
+ if (quote.stale === true) return { symbol, price: quote.price, asOf, status: "stale", reason: `${symbol} quote is stale` };
265
+ return { symbol, price: quote.price, asOf, status: "ok", reason: null };
266
+ }
267
+
268
+ /** How far the contract trades over (positive) or under spot, in percent. */
269
+ export function vsSpotPct(futures: number | null | undefined, spot: number | null | undefined): number | null {
270
+ return positive(futures) && positive(spot) ? (futures / spot - 1) * 100 : null;
271
+ }
272
+
273
+ /** Calendar days from the UTC date of the spot's timestamp to the contract's expiration date. */
274
+ export function daysToExpiry(spotTime: number, expiration: string): number | null {
275
+ const expiry = Date.parse(`${expiration}T00:00:00Z`);
276
+ if (!Number.isFinite(spotTime) || !Number.isFinite(expiry)) return null;
277
+ const spotDay = Date.parse(`${new Date(spotTime).toISOString().slice(0, 10)}T00:00:00Z`);
278
+ return Math.round((expiry - spotDay) / DAY_MS);
279
+ }
280
+
281
+ /** The premium over spot as a yearly rate, on a 365-day year. Blank on or past expiry, where it has no meaning. */
282
+ export function annualisedBasisPct(futures: number | null | undefined, spot: number | null | undefined, days: number | null): number | null {
283
+ if (!positive(futures) || !positive(spot) || days == null || !Number.isFinite(days) || days <= 0) return null;
284
+ return (futures / spot - 1) * 365 / days * 100;
285
+ }
286
+
287
+ /** A contract that printed longer than this before the spot quote is too old to read against it. */
288
+ const CONTRACT_PRINT_MAX_AGE_MS = 60 * 60_000;
289
+
290
+ export interface ContractBasis {
291
+ vsSpotPct: number | null;
292
+ annualisedBasisPct: number | null;
293
+ /** Priced and not flagged stale, but its last print is over an hour before spot: a thin month. */
294
+ thin: boolean;
295
+ }
296
+
297
+ const NO_BASIS: ContractBasis = { vsSpotPct: null, annualisedBasisPct: null, thin: false };
298
+
299
+ /**
300
+ * One contract's basis. Blank without a usable spot, and for a contract whose
301
+ * own price is missing or stale: an old future against a live spot misleads.
302
+ * So does a print more than an hour older than the spot (`thin`): the server's
303
+ * stale flag allows two sessions, which a quote that moves all day outruns. A
304
+ * date-only `asOf` is UTC midnight of that date, one that cannot be read
305
+ * counts as old, and a print newer than the spot is never old (the spot of a
306
+ * delayed feed trails the futures).
307
+ */
308
+ export function contractBasis(row: Pick<FuturesContract, "price" | "stale" | "expiration" | "asOf">, spot: CurveSpot): ContractBasis {
309
+ if (spot.status !== "ok" || spot.asOf == null || row.stale) return NO_BASIS;
310
+ const vs = vsSpotPct(row.price, spot.price);
311
+ if (vs == null) return NO_BASIS;
312
+ const spotTime = Date.parse(spot.asOf);
313
+ const printed = parseReportTime(row.asOf)?.time ?? null;
314
+ if (printed == null || spotTime - printed > CONTRACT_PRINT_MAX_AGE_MS) return { ...NO_BASIS, thin: true };
315
+ return { vsSpotPct: vs, annualisedBasisPct: annualisedBasisPct(row.price, spot.price, daysToExpiry(spotTime, row.expiration)), thin: false };
316
+ }
317
+
318
+ /** A basis in percent, signed; one that rounds to zero stays unsigned. */
319
+ export function curveBasisPercent(value: number | null, decimals: number): string {
320
+ if (value == null) return "--";
321
+ const text = Math.abs(value).toFixed(decimals);
322
+ return /[1-9]/.test(text) ? `${value > 0 ? "+" : "-"}${text}%` : `${text}%`;
323
+ }
324
+
325
+ /** The footer's spot: `spot BTC-USD 67,250.10 · 07:25 UTC`, with the date when the quote is not from today. */
326
+ export function curveSpotLabel(spot: CurveSpot, root: string, now: number): string {
327
+ const decimals = curvePriceDecimals(root);
328
+ const price = spot.price == null ? "--" : spot.price.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
329
+ const stamp = curveTimestamp(spot.asOf);
330
+ const today = new Date(now).toISOString().slice(0, 10);
331
+ return `spot ${spot.symbol} ${price} · ${spot.asOf?.startsWith(today) ? stamp.slice(11) : stamp} UTC`;
332
+ }
333
+
334
+ /** The basis columns' sort values, which live beside the contract rather than on it. */
335
+ export type CurveBasisRows = ReadonlyMap<string, ContractBasis>;
336
+
337
+ export function curveBasisRows(rows: readonly FuturesContract[], spot: CurveSpot | null): CurveBasisRows {
338
+ return new Map(rows.map((row) => [row.symbol, spot ? contractBasis(row, spot) : NO_BASIS]));
339
+ }
340
+
341
+ /** The contracts a good spot left blank for their old print. */
342
+ export function thinContractCount(basis: Iterable<ContractBasis>): number {
343
+ let count = 0;
344
+ for (const row of basis) if (row.thin) count += 1;
345
+ return count;
346
+ }
347
+
348
+ /** The footer warning that thin contracts have no basis: the pane's `!` notice, which keeps the footer row for the quote times. */
349
+ export function thinContractsNotice(count: number): string {
350
+ return `Basis blank on ${count} thin contract${count === 1 ? "" : "s"} (last print over 1h before spot)`;
351
+ }
352
+
169
353
  /** A past date for the as-of view: empty (or "latest") for the live curve, else YYYY-MM-DD no later than today. */
170
354
  export function curveAsOfDate(value: unknown, now = new Date()): string {
171
355
  const date = typeof value === "string" ? value.trim() : "";
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { chartTableChromeRows, ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, Notice, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableColumn, type StatItem } from "../../../components";
3
+ import { fitChartTableColumns } from "../../../components/chart-table";
3
4
  import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
4
5
  import { isAccessDenied } from "../../../api-client/errors";
5
6
  import type { FuturesContract } from "../../../api-client/futures-curve";
@@ -13,8 +14,9 @@ import { nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
13
14
  import { useAutoRefresh } from "../../../react/auto-refresh";
14
15
  import { futuresSessionRefreshInterval } from "../shared/futures-session";
15
16
  import { useResearchCloudSession } from "../shared/research-cloud-session";
17
+ import { useQuoteBoard } from "../shared/use-quote-board";
16
18
  import { getCachedFuturesCurve, loadFuturesCurve, loadFuturesCurveAsOf } from "./client";
17
- import { curveAsOfDate, curveAxisPrice, curveChangeText, curveContractChanges, curvePrice, curveRank, curveTimestamp, DEFAULT_CURVE_HORIZON, futuresCurveSeries, newestQuote, normalizeCurveRoot, sortCurveContracts, type CurveContractChanges } from "./model";
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";
18
20
 
19
21
  const TABS = [{ value: "curve", label: "Curve" }, { value: "contracts", label: "Contracts" }];
20
22
  const COLUMNS: DataTableColumn[] = [
@@ -38,6 +40,19 @@ const CURVE_COLUMNS: DataTableColumn[] = [
38
40
  { id: "change1m", label: "VS 1M", width: 10, align: "right" },
39
41
  ...COLUMNS.slice(4, -1),
40
42
  ];
43
+ // A crypto curve also reads against spot. Its widths are the tightest each column
44
+ // holds (BTCV26.CME, 128000.00), so the two new columns fit beside the rest, and
45
+ // a narrow pane drops the least useful ones before a number would be clipped.
46
+ const BASIS_CURVE_COLUMNS: DataTableColumn[] = [
47
+ { ...CURVE_COLUMNS[0]!, width: 10 }, CURVE_COLUMNS[1]!, { ...CURVE_COLUMNS[2]!, width: 10 },
48
+ { id: "vsSpot", label: "VS SPOT", width: 9, align: "right" },
49
+ { id: "annBasis", label: "ANN BASIS", width: 11, align: "right" },
50
+ ...CURVE_COLUMNS.slice(3),
51
+ ];
52
+ const BASIS_DROP_ORDER = ["percentile", "volume", "change1m", "oi"];
53
+ const NO_SYMBOLS: string[] = [];
54
+ /** The spot's age is judged against the clock, so a quote that stops moving still goes stale on screen. */
55
+ const SPOT_CLOCK_MS = 30_000;
41
56
  const signedPercent = (value: number | null) => value == null ? "--" : formatPercentRaw(value);
42
57
  const integer = (value: number | null) => value == null ? "--" : value.toLocaleString("en-US");
43
58
  const contractKey = (row: FuturesContract) => row.symbol;
@@ -50,7 +65,7 @@ export function FuturesCurvePane(props: PaneProps) {
50
65
  const requested = pane?.settings?.root ?? pane?.params?.root ?? "ES";
51
66
  const root = normalizeCurveRoot(requested);
52
67
  return root ? <FuturesCurveView key={root} {...props} root={root} />
53
- : <PaneStatusBody error={`Unsupported futures root: ${String(requested)}`} subject="futures curve" />;
68
+ : <PaneStatusBody error={unsupportedCurveRootMessage(requested)} subject="futures curve" />;
54
69
  }
55
70
 
56
71
  function FuturesCurveView({ width, height, focused, root }: PaneProps & { root: string }) {
@@ -79,6 +94,23 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
79
94
  // A pending date change must never relabel the previous curve as the new one.
80
95
  const data = resource.data?.date === requestedDate ? resource.data.payload : null;
81
96
  usePaneTitle(`CTM ${root}`);
97
+ const curveTab = tab === "curve";
98
+ // The Curve tab of a crypto root reads against spot; a past date has no spot history.
99
+ const spotSymbol = basisSpotSymbol(root);
100
+ const basisActive = curveTab && !requestedDate && spotSymbol != null;
101
+ const spotSymbols = useMemo(() => basisActive ? [spotSymbol] : NO_SYMBOLS, [basisActive, spotSymbol]);
102
+ const { quotes: spotQuotes } = useQuoteBoard(spotSymbols);
103
+ const [clock, setClock] = useState(() => Date.now());
104
+ useEffect(() => {
105
+ if (!basisActive) return;
106
+ setClock(Date.now());
107
+ const timer = setInterval(() => setClock(Date.now()), SPOT_CLOCK_MS);
108
+ return () => clearInterval(timer);
109
+ }, [basisActive]);
110
+ const spotState = spotSymbol ? spotQuotes.get(spotSymbol) : undefined;
111
+ // Until the first answer there is nothing to say: no reason, only blank cells.
112
+ const spot = useMemo(() => !basisActive || !spotSymbol || !spotState || spotState.loading && !spotState.quote ? null
113
+ : curveSpot(spotSymbol, spotState.quote, clock), [basisActive, clock, spotState, spotSymbol]);
82
114
  const staleCount = data?.contracts.filter((row) => row.stale).length ?? 0;
83
115
  const newest = data ? newestQuote(data.contracts) : null;
84
116
  // A past curve is named by the session it holds: a weekend or holiday date shows the session before it.
@@ -86,8 +118,9 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
86
118
  requestedDate ? Date.parse(`${requestedDate}T00:00:00Z`) : Date.now(), requestedDate ? data.asOf ?? requestedDate : undefined) : [],
87
119
  [data, colors, horizon, requestedDate]);
88
120
  const changes = useMemo<CurveContractChanges>(() => data ? curveContractChanges(data) : new Map(), [data]);
89
- const rows = useMemo(() => sortCurveContracts(data?.contracts ?? [], sort.columnId, sort.direction, changes), [data, sort, changes]);
90
- const curveTab = tab === "curve";
121
+ const basis = useMemo(() => curveBasisRows(basisActive ? data?.contracts ?? [] : [], spot), [basisActive, data, spot]);
122
+ const thinCount = useMemo(() => thinContractCount(basis.values()), [basis]);
123
+ const rows = useMemo(() => sortCurveContracts(data?.contracts ?? [], sort.columnId, sort.direction, changes, basis), [data, sort, changes, basis]);
91
124
  // The curve tab lists the contracts the chart plots; Contracts keeps every one.
92
125
  const curveRows = useMemo(() => {
93
126
  const charted = new Set(curves[0]?.points.map((point) => point.id));
@@ -108,7 +141,8 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
108
141
  const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused, dense: true });
109
142
  // The query bar takes one row below the tabs, and a refused date one more.
110
143
  const bodyHeight = Math.max(1, height - tabRows - 1 - (dateError ? 1 : 0));
111
- const columns = curveTab ? CURVE_COLUMNS : COLUMNS;
144
+ const columns = useMemo(() => !curveTab ? COLUMNS : basisActive ? fitChartTableColumns(BASIS_CURVE_COLUMNS, width, BASIS_DROP_ORDER) : CURVE_COLUMNS,
145
+ [basisActive, curveTab, width]);
112
146
  const formatValue = useCallback((value: number) => curvePrice(value, root), [root]);
113
147
  const formatChange = useCallback((value: number) => curveChangeText(value, root), [root]);
114
148
  const caption = `${sentenceCase(data?.quoteUnit ?? data?.currency ?? "price")} by contract month`;
@@ -142,7 +176,9 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
142
176
  };
143
177
  // A past date with no archived curve says why in the body, not behind the warning.
144
178
  const emptyPast = !!requestedDate && !!data && !data.contracts.length;
145
- usePaneNoticeFooter({ registrationId: "futures-curve:notices", focused, notices: emptyPast ? [] : data?.gaps ?? [] });
179
+ // Thin contracts join the data warnings: the info row has no room for them beside the quote times.
180
+ usePaneNoticeFooter({ registrationId: "futures-curve:notices", focused,
181
+ notices: emptyPast ? [] : [...data?.gaps ?? [], ...thinCount ? [thinContractsNotice(thinCount)] : []] });
146
182
  const delay = Math.max(0, ...(data?.contracts.map((row) => row.delayMinutes ?? 0) ?? []));
147
183
  usePaneStatusFooter({ registrationId: "futures-curve", loading: resource.loading, error: resource.error,
148
184
  hints: [
@@ -152,6 +188,8 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
152
188
  info: data ? [
153
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" }] },
154
190
  ...(staleCount ? [{ id: "stale", parts: [{ text: `${staleCount} of ${data.contracts.length} stale`, tone: "warning" as const }] }] : []),
191
+ ...(spot ? [{ id: "spot", parts: [spot.status === "ok" ? { text: curveSpotLabel(spot, root, clock), tone: "muted" as const }
192
+ : { text: `Basis blank: ${spot.reason}`, tone: "warning" as const }] }] : []),
155
193
  ] : [],
156
194
  });
157
195
  const tableChromeRows = chartTableChromeRows(columns, width);
@@ -166,8 +204,8 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
166
204
  const renderCell = useCallback((row: FuturesContract, column: DataTableColumn) => {
167
205
  if (column.id === "symbol") return { text: row.symbol };
168
206
  if (column.id === "expiry") return { text: row.expiration, color: colors.textMuted };
169
- // Without the AS OF column, a stale quote shows on its price.
170
- if (column.id === "price") return { text: curvePrice(row.price, root), color: curveTab && row.stale ? colors.warning : undefined };
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 };
171
209
  if (column.id === "change") {
172
210
  const text = curveChangeText(row.change ?? null, root);
173
211
  return { text, color: text.startsWith("+") ? colors.positive : text.startsWith("-") && /[1-9]/.test(text) ? colors.negative : colors.textMuted };
@@ -176,11 +214,15 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
176
214
  const change = changes.get(row.symbol)?.[column.id === "change1w" ? "1W" : "1M"] ?? null;
177
215
  return { text: curveChangeText(change, root), color: colors.textMuted };
178
216
  }
217
+ if (column.id === "vsSpot" || column.id === "annBasis") {
218
+ const cell = basis.get(row.symbol);
219
+ return { text: column.id === "vsSpot" ? curveBasisPercent(cell?.vsSpotPct ?? null, 2) : curveBasisPercent(cell?.annualisedBasisPct ?? null, 1), color: colors.textMuted };
220
+ }
179
221
  if (column.id === "percentile") return { text: row.samples < 2 || row.percentile == null ? "--" : row.percentile.toFixed(0) };
180
222
  if (column.id === "oi") return { text: integer(row.openInterest) };
181
223
  if (column.id === "volume") return { text: integer(row.volume) };
182
224
  return { text: curveTimestamp(row.asOf), color: row.stale ? colors.warning : colors.textMuted };
183
- }, [changes, colors, curveTab, root]);
225
+ }, [basis, changes, colors, curveTab, root]);
184
226
  return <Box width={width} height={height} flexDirection="column">
185
227
  {tabStrip}
186
228
  <QueryBar width={width} filters={[{
@@ -2,6 +2,7 @@ import type { GpuObservation } from "../../../api-client/gpu";
2
2
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchGpuBoard, fetchGpuEvents, fetchGpuHistory, loadGpuEquityHistory } from "./client";
4
4
  import { GPU_TABS, gpuArgument, gpuBasisLabel, gpuChange, gpuEquityRows, gpuEventDate, gpuProvenanceLabel, gpuLabel, gpuRows, gpuSource, gpuTab } from "./model";
5
+ import { quoteFreshnessFields } from "../shared/report-freshness";
5
6
 
6
7
  const observationRow = (row: GpuObservation) => ({ gpu: gpuLabel(row), source: gpuSource(row), basis: gpuBasisLabel(row.basis),
7
8
  price: row.pricePerGpuHr, availability: row.availability, observedAt: row.observedAt, effectiveAt: row.effectiveAt, provenance: row.provenance ?? "live", provenanceLabel: gpuProvenanceLabel(row, true),
@@ -26,9 +27,11 @@ export const gpuHeadless: HeadlessPaneDefinition<"bundle"> = {
26
27
  const tab = gpuTab(args.options.tab);
27
28
  const board = await fetchGpuBoard(ctx.apiClient);
28
29
  const metadata = { asOf: board.asOf, stale: board.stale, complete: board.status === "available" && !board.access?.locked, unit: "USD/GPU-hour", access: board.access };
30
+ // Prices observed on providers' pages, not a feed; each board row says whether its source has gone quiet.
31
+ const freshness = { source: "GPU cloud providers", status: "not-a-feed" as const, basis: "observed prices" };
29
32
  if (tab === "history") {
30
33
  const history = await fetchGpuHistory({ ...(model ? { gpuModel: model } : {}), limit: 10_000 }, ctx.apiClient);
31
- return { sections: [{ title: "Dated observations", columns: [...columns.filter((column) => !column.key.startsWith("change")), { key: "provenanceLabel", header: "Record" }], rows: history.points.map(observationRow) }], complete: !history.access?.locked, metadata: { ...metadata, access: history.access ?? board.access }, errors: board.gaps };
34
+ return { sections: [{ title: "Dated observations", columns: [...columns.filter((column) => !column.key.startsWith("change")), { key: "provenanceLabel", header: "Record" }], rows: history.points.map(observationRow) }], freshness: { ...freshness, oldest: null }, complete: !history.access?.locked, metadata: { ...metadata, access: history.access ?? board.access }, errors: board.gaps };
32
35
  }
33
36
  if (tab === "changes") {
34
37
  const payload = await fetchGpuEvents(model || undefined, ctx.apiClient);
@@ -41,7 +44,7 @@ export const gpuHeadless: HeadlessPaneDefinition<"bundle"> = {
41
44
  oldPrice: event.oldPrice, newPrice: event.newPrice, changePct: event.changePct, kind: event.kind,
42
45
  oldMembers: event.oldMembers, newMembers: event.newMembers, provenance: event.provenance ?? "live",
43
46
  provenanceLabel: gpuProvenanceLabel(event, true), evidenceUrl: event.evidenceUrl ?? null, sourceUrl: event.sourceUrl ?? null,
44
- oldAvailability: event.oldAvailability ?? null, newAvailability: event.newAvailability ?? null })) }], complete: !payload.access?.locked, metadata: { ...metadata, access: payload.access ?? board.access }, errors: board.gaps };
47
+ oldAvailability: event.oldAvailability ?? null, newAvailability: event.newAvailability ?? null })) }], freshness: { ...freshness, oldest: null }, complete: !payload.access?.locked, metadata: { ...metadata, access: payload.access ?? board.access }, errors: board.gaps };
45
48
  }
46
49
  if (tab === "equities") {
47
50
  const related = gpuEquityRows(board.rows, model || "H100");
@@ -51,6 +54,7 @@ export const gpuHeadless: HeadlessPaneDefinition<"bundle"> = {
51
54
  const history = histories.find((entry) => entry.symbol === row.symbol);
52
55
  return { symbol: row.symbol, role: row.role, price: quote?.price ?? null, change1d: quote?.changePercent ?? null,
53
56
  change5d: history?.value ?? null, fiveDayAsOf: history?.asOf ?? null, quoteAsOf: quote?.lastUpdated ? new Date(quote.lastUpdated).toISOString() : null,
57
+ ...quoteFreshnessFields(quote),
54
58
  gpu: row.reference ? gpuLabel(row.reference) : row.gpuModel, gpuSource: row.reference ? gpuSource(row.reference) : null, gpuChange7d: row.reference?.change7d ?? null };
55
59
  }));
56
60
  return { sections: [{ title: "Related equities", columns: [
@@ -62,6 +66,6 @@ export const gpuHeadless: HeadlessPaneDefinition<"bundle"> = {
62
66
  }
63
67
  const rows = gpuRows(board.rows, model).map((row) => ({ ...observationRow(row), change1d: row.change1d, change7d: row.change7d,
64
68
  change30d: row.change30d, stale: row.stale, sample: row.stats ?? null }));
65
- return { sections: [{ title: "GPU rental prices", columns, rows }], complete: !board.access?.locked, metadata, errors: board.gaps };
69
+ return { sections: [{ title: "GPU rental prices", columns, rows }], freshness, complete: !board.access?.locked, metadata, errors: board.gaps };
66
70
  },
67
71
  };