@gloom-sh/terminal 0.19.1 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -3,7 +3,10 @@ import type { CotBoardPayload, CotClass, CotClassSummary, CotContractPayload, Co
3
3
  import { createPluginCache } from "../../../data/plugin-cache";
4
4
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
5
5
  import type { PricePoint } from "../../../types/financials";
6
- import { COT_CLASSES, cotPriceMapping } from "./model";
6
+ import { COT_CLASSES, cotFrontPriceUnit, cotPriceMapping } from "./model";
7
+ import { futuresPriceUnit, loadFuturesContractTerms, loadFuturesPriceUnit } from "../../../market-data/futures-contract";
8
+ import { futuresBoardContract } from "../../../market-data/futures-board";
9
+ import { settleWithin } from "../../../utils/async-deadline";
7
10
 
8
11
  export const cotBoardCache = createPluginCache<CotBoardPayload>({ kind: "cot-board", source: "gloom-cloud", schemaVersion: 1,
9
12
  policy: { staleMs: 15 * 60_000, expireMs: 8 * 24 * 60 * 60_000 } });
@@ -88,7 +91,12 @@ export async function fetchCotContract(contractCode: string, family: CotFamily,
88
91
  catch (error) { throw unavailable(error); }
89
92
  }
90
93
  export interface CotDetailData { payload: CotContractPayload; price: PricePoint[]; priceSymbol: string | null; priceAsOf: string | null; priceWarning: string | null }
91
- export async function loadCotDetail(contractCode: string, family: CotFamily, client: Pick<typeof apiClient, "getCloudCotContract" | "getCloudHistory"> = apiClient,
94
+ /**
95
+ * A market's report and its front price. The price's unit is not waited for
96
+ * here: it joins the legend when it is known (loadCotPriceUnit).
97
+ */
98
+ export async function loadCotDetail(contractCode: string, family: CotFamily,
99
+ client: Pick<typeof apiClient, "getCloudCotContract" | "getCloudHistory"> = apiClient,
92
100
  positions: CotPositions = "futures-only"): Promise<CotDetailData> {
93
101
  const payload = await fetchCotContract(contractCode, family, client, positions);
94
102
  const mapping = cotPriceMapping(contractCode);
@@ -109,3 +117,21 @@ export async function loadCotDetail(contractCode: string, family: CotFamily, cli
109
117
  priceWarning: result.stale ? "Front-price history is stale." : price.length ? null : "Front-price history unavailable." };
110
118
  } catch { return { payload, price: [], priceSymbol: mapping.priceSymbol, priceAsOf: null, priceWarning: "Front-price history unavailable. Positioning remains available." }; }
111
119
  }
120
+
121
+ /** How long the front price's legend waits for the contract terms before it writes the catalog's unit. */
122
+ const COT_PRICE_UNIT_WAIT_MS = 1_500;
123
+
124
+ /**
125
+ * The unit a market's front price is written in, as its legend writes it
126
+ * after the price (cotFrontPriceUnit): `USD/gal` for heating oil. It is the
127
+ * contract terms' unit, else the futures catalog's where the terms fail or
128
+ * have not answered within `waitMs`; null for a market with no front price.
129
+ * The chart draws without it, so a slow answer never holds the detail up.
130
+ */
131
+ export async function loadCotPriceUnit(contractCode: string, client: Pick<typeof apiClient, "getCloudFuturesContract"> = apiClient,
132
+ waitMs = COT_PRICE_UNIT_WAIT_MS): Promise<string | null> {
133
+ const symbol = cotPriceMapping(contractCode)?.priceSymbol;
134
+ if (!symbol) return null;
135
+ const terms = await settleWithin(loadFuturesPriceUnit(symbol, (key) => loadFuturesContractTerms(key, client)), waitMs);
136
+ return cotFrontPriceUnit(contractCode, terms ?? futuresPriceUnit(futuresBoardContract(symbol), null));
137
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CotClass, CotClassSummary, CotContractPayload, CotHistoryPoint } from "../../../api-client/cot";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchCotBoard, fetchCotContract } from "./client";
4
- import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotClass, cotContractCode, cotContractUnitText, cotFamily, cotInteger, cotMarketName, cotPositions, cotPositionsText, cotScope, cotUnknownMarketMessage } from "./model";
4
+ import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotClass, cotContractCode, cotContractUnitText, cotFamily, cotInteger, cotMarketName, cotPositions, cotPositionsText, cotProxyFor, cotProxyNote, cotScope, cotUnknownMarketMessage } from "./model";
5
5
  import { COMMODITIES_VIEW_STATE } from "../commodities-energy/view-state";
6
6
  import { REPORT_ROWS } from "../shared/headless-options";
7
7
 
@@ -67,6 +67,7 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
67
67
  if (args.argument) {
68
68
  const code = cotContractCode(args.argument);
69
69
  if (!code) throw new Error(cotUnknownMarketMessage(args.argument));
70
+ const proxy = cotProxyFor(args.argument);
70
71
  const data = await fetchCotContract(code, family, ctx.apiClient, positions);
71
72
  const classes = data.positions.map((row) => row.id);
72
73
  const limit = args.options.limit == null ? null : Number(args.options.limit);
@@ -80,6 +81,8 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
80
81
  ...classes.map((id) => ({ key: `${camelClass(id)}Net`, header: `${data.positions.find((row) => row.id === id)?.label ?? id} net`, align: "right" as const, format: signed }))],
81
82
  rows: weeks.map((row) => ({ reportDate: row.reportDate, openInterest: row.openInterest,
82
83
  ...Object.fromEntries(row.positions.map((position) => [`${camelClass(position.id)}Net`, position.net])) })) }],
84
+ // "jet fuel" reads heating oil's positions, and says so.
85
+ ...(proxy ? { notes: [cotProxyNote(proxy)] } : {}),
83
86
  errors: data.gaps, metadata: { ...data, complete: data.status === "available" } };
84
87
  }
85
88
  const traderClass = cotClass(family, args.options.traderClass);
@@ -1,13 +1,12 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { cotBoardCache } from "./client";
3
3
  import { cotHeadless } from "./headless";
4
- import { COT_ROOT_OPTIONS, cotContractCode, cotContractCodeForTicker, cotRoot, cotUnknownMarketMessage } from "./model";
5
- import { CotPane } from "./pane";
4
+ import { COT_ROOT_OPTIONS, cotContractCode, cotContractCodeForTicker, cotProxyFor, cotRoot, cotUnknownMarketMessage } from "./model";
6
5
  import { COT_SERIES } from "./series";
7
6
 
8
7
  export const cotModule: PluginModule = {
9
8
  series: COT_SERIES,
10
- panes: [{ id: "cot", name: "CFTC Positioning", icon: "P", component: CotPane,
9
+ panes: [{ id: "cot", name: "CFTC Positioning", icon: "P", get component(): typeof import("./pane").CotPane { const { CotPane } = require("./pane"); return CotPane; },
11
10
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 106, height: 30 },
12
11
  tableExport: true, headless: cotHeadless }],
13
12
  paneTemplates: [{ id: "cot-cot", paneId: "cot", label: "CFTC Positioning",
@@ -19,7 +18,9 @@ export const cotModule: PluginModule = {
19
18
  const input = options?.arg?.trim();
20
19
  const code = input ? cotContractCode(input) : cotContractCodeForTicker(context.activeTicker);
21
20
  if (input && !code) throw new Error(cotUnknownMarketMessage(input));
22
- return { title: code ? `COT ${cotRoot(code) ?? code}` : "CFTC Positioning", params: code ? { code } : undefined,
21
+ // "jet fuel" opens heating oil, and the title says which market stands in.
22
+ const proxy = input ? cotProxyFor(input) : null;
23
+ return { title: code ? `COT ${cotRoot(code) ?? code}${proxy ? ` (${proxy.subject} proxy)` : ""}` : "CFTC Positioning", params: code ? { code } : undefined,
23
24
  settings: { report: options?.values?.report ?? "legacy" }, placement: "floating" as const };
24
25
  },
25
26
  }],
@@ -2,10 +2,14 @@ import type { CotClass, CotContractPayload, CotFamily, CotHistoryPoint, CotPosit
2
2
  import { staticSeries } from "../../../components/chart/static/series";
3
3
  import type { ResolvedSeries } from "../../../time-series/types";
4
4
  import type { PricePoint } from "../../../types/financials";
5
- import { formatPriceObservation } from "../../../market-data/market/format";
5
+ import { formatMarketPriceWithCurrency, formatPriceObservation } from "../../../market-data/market/format";
6
+ import { futuresPricePer } from "../../../market-data/futures-contract";
7
+ import { unitCurrencyToken } from "../../../time-series/price-units";
6
8
  import { cleanFloat32Price } from "../../../cli/history-rows";
7
9
  import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
8
- import { bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByName, type FuturesContract } from "../../../market-data/futures-board";
10
+ import {
11
+ bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByCode, futuresContractByName, type FuturesContract,
12
+ } from "../../../market-data/futures-board";
9
13
 
10
14
  export const COT_CLASSES: Record<CotFamily, Array<{ value: CotClass; label: string }>> = {
11
15
  legacy: [{ value: "noncommercial", label: "Noncommercial" }, { value: "commercial", label: "Commercial" }, { value: "nonreportable", label: "Nonreportable" }],
@@ -162,7 +166,35 @@ export function cotContractCode(value: unknown): string | null {
162
166
  if (alias) return alias;
163
167
  const market = typedMarket(value);
164
168
  if (market && "root" in market) return ROOTS[market.root]!.code;
165
- return (market ? boardMarketCode(market.board) : null) ?? (market ? null : marketCode(input));
169
+ if (market) return boardMarketCode(market.board);
170
+ const proxy = proxyMarket(value);
171
+ const proxied = proxy ? futuresContractByCode(proxy.root) : null;
172
+ return (proxied ? boardMarketCode(proxied) : null) ?? marketCode(input);
173
+ }
174
+ /**
175
+ * Markets the CFTC does not report, opened as the one traders price and hedge
176
+ * them by: jet fuel (kerosene) as NY Harbor ULSD, heating oil (HO), which
177
+ * airlines' fuel hedges and analysts follow it with. Opened this way, the
178
+ * pane's title and the report say which market stands in.
179
+ */
180
+ const PROXY_MARKETS: ReadonlyArray<{ names: readonly string[]; root: string; subject: string }> = [
181
+ { names: ["jet fuel", "jet fuels", "kerosene", "kerosine", "jet kerosene", "aviation fuel", "avtur"], root: "HO", subject: "jet fuel" },
182
+ ];
183
+ function proxyMarket(value: string) {
184
+ const typed = value.trim().toLowerCase().replace(/[\s_-]+/g, " ");
185
+ return PROXY_MARKETS.find((proxy) => proxy.names.includes(typed)) ?? null;
186
+ }
187
+ /** What a typed name opens in place of a market the CFTC does not report ("jet fuel" opens HO), or null. */
188
+ export function cotProxyFor(value: unknown): { subject: string; root: string } | null {
189
+ if (typeof value !== "string" || typedMarket(value) || rootAlias(value.trim().toUpperCase())) return null;
190
+ const proxy = proxyMarket(value);
191
+ return proxy ? { subject: proxy.subject, root: proxy.root } : null;
192
+ }
193
+ /** The line a report opened through a proxy says: "Jet fuel has no CFTC market: heating oil (NY Harbor ULSD, HO) is the usual proxy." */
194
+ export function cotProxyNote(proxy: { subject: string; root: string }): string {
195
+ const subject = `${proxy.subject[0]!.toUpperCase()}${proxy.subject.slice(1)}`;
196
+ return `${subject} has no CFTC market: heating oil (NY Harbor ULSD, ${proxy.root}) is the usual proxy. Its price: gloom quote ${proxy.root}=F; `
197
+ + "US jet fuel stocks (EIA, weekly): gloom fn G CAP:doe:jet";
166
198
  }
167
199
  /** The markets the unknown-market hint names, by the code and the word a newcomer knows them by. */
168
200
  const SUGGESTED_MARKETS: ReadonlyArray<readonly [string, string]> = [
@@ -234,10 +266,27 @@ export function cotInteger(value: number | null, signed = false): string {
234
266
  return value == null ? "--" : `${signed && value > 0 ? "+" : ""}${value.toLocaleString("en-US", { maximumFractionDigits: 0 })}`;
235
267
  }
236
268
 
269
+ /**
270
+ * The front price's unit as a quote of the future writes it after the price:
271
+ * `USD/gal` for heating oil ($4.6218/gal), `USD` for gold, from the
272
+ * contract's unit ("USD / gallon"). Null where the unit names no currency
273
+ * (index points, Treasury points) or is unknown: the price reads bare.
274
+ */
275
+ export function cotFrontPriceUnit(code: string, contractUnit: string | null | undefined): string | null {
276
+ const currency = contractUnit ? unitCurrencyToken(contractUnit.split("/")[0] ?? "") : null;
277
+ if (!currency) return null;
278
+ const root = cotRoot(code) ?? undefined;
279
+ return `${currency}${futuresPricePer({ root, spec: { unit: contractUnit } } as Parameters<typeof futuresPricePer>[0], currency)}`;
280
+ }
281
+
237
282
  /** Legend values stay exact: a compact 7.8K hides the futures price and -100K the net position. */
238
- export function cotLegendValue(value: number, series: Pick<ResolvedSeries, "id">): string {
283
+ export function cotLegendValue(value: number, series: Pick<ResolvedSeries, "id"> & Partial<Pick<ResolvedSeries, "unit">>): string {
284
+ if (series.id !== "price") return cotInteger(value, true);
239
285
  // Drop the float32 tail of provider bars without rounding away a 1/128 Treasury tick.
240
- return series.id === "price" ? formatPriceObservation(cleanFloat32Price(value)) : cotInteger(value, true);
286
+ const price = cleanFloat32Price(value);
287
+ // In its currency and per its quantity, as a quote of the future reads: $4.6218/gal.
288
+ const [currency, per] = [series.unit?.split("/")[0]?.trim(), series.unit?.includes("/") ? `/${series.unit.split("/")[1]}` : ""];
289
+ return currency ? `${formatMarketPriceWithCurrency(price, currency, { assetCategory: "FUT" })}${per}` : formatPriceObservation(price);
241
290
  }
242
291
 
243
292
  /**
@@ -263,7 +312,9 @@ export function cotNetPoints(history: readonly CotHistoryPoint[], traderClass: C
263
312
  }
264
313
  return points;
265
314
  }
266
- export function cotChartSeries(data: CotContractPayload, traderClass: CotClass, price: readonly PricePoint[], colors: { positive: string; warning: string }): ResolvedSeries[] {
315
+ /** `priceUnit` is the front price's (cotFrontPriceUnit), which its legend value is written in. */
316
+ export function cotChartSeries(data: CotContractPayload, traderClass: CotClass, price: readonly PricePoint[], colors: { positive: string; warning: string },
317
+ priceUnit: string | null = null): ResolvedSeries[] {
267
318
  const series: ResolvedSeries[] = [{
268
319
  ...staticSeries(cotNetPoints(data.history, traderClass), { id: "net", label: `${COT_CLASSES[data.reportFamily].find((row) => row.value === traderClass)?.label ?? traderClass} net`, color: colors.warning, calendarSpaced: true }),
269
320
  panelId: "net", unit: "contracts", unitGroup: "positions",
@@ -271,7 +322,7 @@ export function cotChartSeries(data: CotContractPayload, traderClass: CotClass,
271
322
  // Net positioning is the subject, so it leads the legend and the strip; the front price follows.
272
323
  if (price.length) series.push({
273
324
  ...staticSeries(price.map((row) => ({ date: row.date, observedAt: row.date, value: row.close })), { id: "price", label: "Front price", color: colors.positive, calendarSpaced: true }),
274
- panelId: "price", unit: "", unitGroup: "price",
325
+ panelId: "price", unit: priceUnit ?? "", unitGroup: "price",
275
326
  });
276
327
  return series;
277
328
  }
@@ -12,7 +12,7 @@ import { Box } from "../../../ui";
12
12
  import { useAutoRefresh } from "../../../react/auto-refresh";
13
13
  import { compareSortValues, nextHeaderSort, type SortPreference } from "../../../utils/sort-values";
14
14
  import { useResearchCloudSession } from "../shared/research-cloud-session";
15
- import { cachedCotBoard, loadCotBoard, loadCotDetail } from "./client";
15
+ import { cachedCotBoard, loadCotBoard, loadCotDetail, loadCotPriceUnit } from "./client";
16
16
  import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotChartSeries, cotClass, cotContractUnitText, cotFamily, cotInteger, cotLegendValue, cotMarketName, cotPositions, cotPositionsText, cotScope, type CotScope } from "./model";
17
17
 
18
18
  const BOARD_COLUMNS: DataTableColumn[] = [
@@ -159,10 +159,14 @@ function CotDetail({ width, height, focused, code, family, positions, traderClas
159
159
  const session = useResearchCloudSession();
160
160
  const loader = useCallback(() => loadCotDetail(code, family, undefined, positions), [code, family, positions, session.requestKey]);
161
161
  const resource = useAsyncResource(loader, { clearOnError: isAccessDenied });
162
+ // The front price's unit joins the legend once the contract terms answer, or
163
+ // the catalog's after a short wait: the chart draws without waiting for it.
164
+ const unitLoader = useCallback(() => loadCotPriceUnit(code), [code]);
165
+ const priceUnit = useAsyncResource(unitLoader).data ?? null;
162
166
  const data = resource.data;
163
167
  const payload = data?.payload;
164
168
  const current = payload?.positions.find((row) => row.id === traderClass);
165
- const series = useMemo(() => data ? cotChartSeries(data.payload, traderClass, data.price, colors) : [], [data, traderClass, colors]);
169
+ const series = useMemo(() => data ? cotChartSeries(data.payload, traderClass, data.price, colors, priceUnit) : [], [data, traderClass, colors, priceUnit]);
166
170
  // The selected class row already carries the net and both ranks, and the
167
171
  // legend names the class it charts, so the detail opens on the chart. A rank
168
172
  // over a short history is a limitation, so it sits behind the warning.
@@ -1,8 +1,8 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { sourceDidNotAnswer } from "../../../api-client/errors";
2
3
  import type { CpiBoardPayload, CpiRow } from "../../../api-client/cpi";
3
- import { ApiRequestError } from "../../../api-client/errors";
4
4
  import { createPluginCache } from "../../../data/plugin-cache";
5
- import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
5
+ import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
6
6
 
7
7
  /** What the pane says while the server has no months to serve: before the backend ships, or before its first read. */
8
8
  export const CPI_NOT_AVAILABLE = "US consumer prices are not available yet.";
@@ -54,11 +54,12 @@ export async function fetchCpiBoard(client: Pick<typeof apiClient, "getCloudCpiB
54
54
  try {
55
55
  payload = await client.getCloudCpiBoard();
56
56
  } catch (error) {
57
- if (error instanceof ApiRequestError && (error.status === 404 || error.status === 503)) throw new Error(CPI_NOT_AVAILABLE);
58
- throw error;
57
+ throw unavailableOnServer(error, CPI_NOT_AVAILABLE, [404, 503]);
59
58
  }
60
59
  const board = validateCpiBoard(payload);
61
- if (board.status === "unavailable" || !board.release.period || !board.rows.length) throw new Error(CPI_NOT_AVAILABLE);
60
+ // The server's source did not answer this time: a saved copy answers, as it does a 5xx.
61
+ if (board.status === "unavailable") throw sourceDidNotAnswer(CPI_NOT_AVAILABLE);
62
+ if (!board.release.period || !board.rows.length) throw new Error(CPI_NOT_AVAILABLE);
62
63
  return board;
63
64
  }
64
65
 
@@ -3,7 +3,6 @@ import type { PluginModule } from "../plugin-module";
3
3
  import { cpiBoardCache } from "./client";
4
4
  import { cpiHeadless } from "./headless";
5
5
  import { CPI_ROW_OPTIONS, cpiRowOption } from "./model";
6
- import { CpiPane } from "./pane";
7
6
 
8
7
  /** Opens on the table, or on the component typed after the mnemonic. */
9
8
  function cpiInstance(options?: PaneTemplateCreateOptions): PaneTemplateInstanceConfig {
@@ -14,7 +13,7 @@ function cpiInstance(options?: PaneTemplateCreateOptions): PaneTemplateInstanceC
14
13
  }
15
14
 
16
15
  export const cpiModule: PluginModule = {
17
- panes: [{ id: "cpi", name: "US Consumer Prices", icon: "C", component: CpiPane,
16
+ panes: [{ id: "cpi", name: "US Consumer Prices", icon: "C", get component(): typeof import("./pane").CpiPane { const { CpiPane } = require("./pane"); return CpiPane; },
18
17
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 100, height: 36 },
19
18
  tableExport: true, headless: cpiHeadless }],
20
19
  paneTemplates: [{
@@ -32,6 +32,7 @@ import {
32
32
  formatLevel,
33
33
  formatMove,
34
34
  partialDayText,
35
+ SETTLEMENT_TEXT,
35
36
  type CdxRow,
36
37
  } from "./model";
37
38
 
@@ -69,7 +70,7 @@ function IndexDetail({ row, width, height, focused }: { row: CdxRow; width: numb
69
70
  const through = day.partial && day.changeDate ? `to ${day.changeDate}` : undefined;
70
71
  const items: StatItem[] = [
71
72
  { id: "level", label: index.quote === "spread" ? "5Y spread" : "5Y price", value: formatLevel(index.quote, index.level),
72
- detail: `${formatPercentileRank(year.percentile, "1Y")} · ${index.date ?? "--"}` },
73
+ detail: [formatPercentileRank(year.percentile, "1Y"), index.date ?? "--", ...(index.kind === "settlement" ? [SETTLEMENT_TEXT] : [])].join(" · ") },
73
74
  ...(index.quote === "price" && index.spreadBp !== undefined ? [{ id: "spread", label: "Spread eq", value: formatLevel("spread", index.spreadBp ?? null),
74
75
  ...(index.recovery != null ? { detail: `${Math.round(index.recovery * 100)}% recovery` } : {}) }] : []),
75
76
  { id: "1d", label: "1D", value: formatMove(index.quote, day.change1D), ...(through ? { detail: through } : {}) },
@@ -84,8 +84,10 @@ export type PriceHistoryLoader = (symbol: string) => Promise<PricePoint[]>;
84
84
 
85
85
  /** Closes a month back are yesterday's news for half an hour. */
86
86
  const FX_HISTORY_TTL_MS = 30 * 60_000;
87
- /** Twenty-odd currencies at once; a few requests at a time keep the feed from refusing them. */
87
+ /** Twenty-odd currencies, a few at a time from a source that takes them one by one. */
88
88
  const FX_HISTORY_CONCURRENCY = 4;
89
+ /** All at once from a source that batches them: the API client sends them sixteen a request. */
90
+ const FX_HISTORY_BATCHED_CONCURRENCY = 32;
89
91
  const fxMoves = createTtlCache<number | null>({ ttlMs: FX_HISTORY_TTL_MS });
90
92
 
91
93
  /** A load that fails reads as unknown for everyone waiting on it and is asked again next time, never remembered as unknown. */
@@ -97,10 +99,13 @@ function currencyMove(symbol: string, dollarsPerUnit: boolean, loadHistory: Pric
97
99
  /**
98
100
  * Each currency's 1M move against the dollar from its daily closes, shared
99
101
  * across panes. A currency with no pair or no history reads as unknown.
102
+ * `batched`: `loadHistory` asks a source that batches them
103
+ * (`DataProvider.batchesReads`) with `batch`, so they are all asked at once.
100
104
  */
101
105
  export async function loadCurrencyMoves(
102
106
  currencies: readonly string[],
103
107
  loadHistory: PriceHistoryLoader,
108
+ batched = false,
104
109
  ): Promise<Map<string, number | null>> {
105
110
  const queue = [...new Set(currencies)];
106
111
  const moves = new Map<string, number | null>();
@@ -110,6 +115,6 @@ export async function loadCurrencyMoves(
110
115
  moves.set(currency, pair ? await currencyMove(pair.symbol, pair.dollarsPerUnit, loadHistory) : null);
111
116
  }
112
117
  };
113
- await Promise.all(Array.from({ length: Math.min(FX_HISTORY_CONCURRENCY, queue.length) }, worker));
118
+ await Promise.all(Array.from({ length: Math.min(batched ? FX_HISTORY_BATCHED_CONCURRENCY : FX_HISTORY_CONCURRENCY, queue.length) }, worker));
114
119
  return moves;
115
120
  }
@@ -1,6 +1,7 @@
1
1
  import type { HeadlessPaneColumn, HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/plugin";
2
2
  import type { CdxBoardIndex, CreditIndexQuote } from "../../../api-client/credit-boards";
3
3
  import { CDX_MAX_DAYS, fetchCdxBoard, fetchSovrBoard, loadCurrencyMoves } from "./client";
4
+ import { readsInBatches } from "../../../market-data/batched-reads";
4
5
  import {
5
6
  cdxDay,
6
7
  cdxRows,
@@ -10,6 +11,7 @@ import {
10
11
  formatCurrencyMove,
11
12
  formatLevel,
12
13
  formatMove,
14
+ levelKinds,
13
15
  partialDayText,
14
16
  quoteUnits,
15
17
  sovrRows,
@@ -74,6 +76,8 @@ function historySection(index: CdxBoardIndex, days: number, now: number) {
74
76
  const priced = index.quote === "price" && index.points.some((point) => point.spreadBp !== undefined);
75
77
  const partial = today.partial && latest === index.date;
76
78
  const symbol = cdxSeries(index.id);
79
+ const shown = index.points.filter((point) => point.date > from).reverse();
80
+ const kinds = levelKinds(shown, (point) => point.kind);
77
81
  return {
78
82
  title: `${index.name} 5Y daily ${index.quote === "spread" ? "spread" : "price"}`,
79
83
  columns: [
@@ -83,11 +87,12 @@ function historySection(index: CdxBoardIndex, days: number, now: number) {
83
87
  { key: "prints", header: "Prints", align: "right" as const },
84
88
  { key: "maturity", header: "Contract", textFormat: (value: unknown) => formatContract(typeof value === "string" ? value : null) },
85
89
  ...(partial ? [dayColumn] : []),
90
+ ...kinds.columns,
86
91
  ],
87
- rows: index.points.filter((point) => point.date > from).reverse().map((point) => ({
92
+ rows: shown.map((point) => ({
88
93
  day: point.date, level: point.level, unit,
89
94
  ...(priced ? { spreadBp: point.spreadBp ?? null, spreadUnit: "bp" } : {}),
90
- prints: point.prints, maturity: point.maturity, series: symbol,
95
+ prints: point.prints, maturity: point.maturity, series: symbol, ...kinds.row(point),
91
96
  ...(partial && point.date === latest ? { session: partialDayText(point.prints, today.lastPrintAt), partial: true, lastPrintAt: today.lastPrintAt } : {}),
92
97
  })),
93
98
  rowUnitKey: "unit",
@@ -133,6 +138,7 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
133
138
  const shown = index ? [index] : payload.indexes;
134
139
  // A server that predates the spread equivalent sends none: no column of blanks.
135
140
  const spreads = shown.some((entry) => entry.spreadBp !== undefined);
141
+ const kinds = levelKinds(shown, (entry) => entry.kind);
136
142
  const rows = cdxRows(shown, payload.asOf, now).map((row) => {
137
143
  const units = quoteUnits(row.index.quote);
138
144
  const priced = spreads && row.index.quote === "price";
@@ -151,6 +157,7 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
151
157
  contract: formatContract(row.index.maturity),
152
158
  date: row.index.date,
153
159
  changeDate: row.day.changeDate,
160
+ ...kinds.row(row.index),
154
161
  ...(row.day.partial ? { session: partialDayText(row.index.prints, row.day.lastPrintAt), partial: true, lastPrintAt: row.day.lastPrintAt } : {}),
155
162
  };
156
163
  });
@@ -158,7 +165,7 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
158
165
  return {
159
166
  sections: [
160
167
  {
161
- title: "On the run, 5Y", columns: [...CDX_COLUMNS.filter((column) => spreads || column !== spreadColumn), ...(anyPartial ? [dayColumn] : [])], rows,
168
+ title: "On the run, 5Y", columns: [...CDX_COLUMNS.filter((column) => spreads || column !== spreadColumn), ...(anyPartial ? [dayColumn] : []), ...kinds.columns], rows,
162
169
  rowUnitKey: "unit", rowSymbolKey: "series", rowAsOfKey: "date", interval: "daily",
163
170
  },
164
171
  ...(index ? [historySection(index, days, now)] : []),
@@ -170,6 +177,15 @@ export const cdxHeadless: HeadlessPaneDefinition<"bundle"> = {
170
177
  },
171
178
  };
172
179
 
180
+ const SOVR_COLUMNS: HeadlessPaneColumn[] = [
181
+ { key: "name", header: "Sovereign" },
182
+ levelColumn,
183
+ moveColumn("change1M", "1M", "unit"),
184
+ { key: "currency", header: "Ccy 1M", align: "right" },
185
+ { key: "percentile", header: "Pctl 1Y", align: "right", format: percentile },
186
+ { key: "date", header: "As of" },
187
+ ];
188
+
173
189
  export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
174
190
  discovery: { aliases: ["SOVR", "WCDS"], dataRequirements: ["Gloom Cloud sovereign CDS endpoint"],
175
191
  limitations: ["Daily 5Y levels from public DTCC prints; thinly traded names can miss days."] },
@@ -178,14 +194,7 @@ export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
178
194
  argument: { kind: "free-text", optional: true, placeholder: "country",
179
195
  description: "A country (Brazil, BR) to list its row alone; omit for every sovereign." },
180
196
  options: [],
181
- columns: [
182
- { key: "name", header: "Sovereign" },
183
- levelColumn,
184
- moveColumn("change1M", "1M", "unit"),
185
- { key: "currency", header: "Ccy 1M", align: "right" },
186
- { key: "percentile", header: "Pctl 1Y", align: "right", format: percentile },
187
- { key: "date", header: "As of" },
188
- ],
197
+ columns: SOVR_COLUMNS,
189
198
  describe: "Sovereign CDS",
190
199
  async load(args, ctx) {
191
200
  const payload = await fetchSovrBoard(ctx.apiClient);
@@ -193,10 +202,13 @@ export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
193
202
  const country = query ? findSovereign(payload.sovereigns, query) : null;
194
203
  if (query && !country) throw new Error(`No sovereign ${query}. Use one of: ${payload.sovereigns.map((entry) => entry.name).join(", ")}.`);
195
204
  const sovereigns = country ? [country] : payload.sovereigns;
205
+ const batch = await readsInBatches(ctx.marketData, "getPriceHistory");
196
206
  const moves = await loadCurrencyMoves(sovereigns.map((row) => row.currency),
197
- (symbol) => ctx.marketData.getPriceHistory(symbol, "", "3M"));
207
+ (symbol) => ctx.marketData.getPriceHistory(symbol, "", "3M", batch ? { batch } : undefined), batch);
198
208
  const publisher = boardPublisher(payload.source);
209
+ const kinds = levelKinds(sovereigns, (row) => row.kind);
199
210
  return {
211
+ ...(kinds.columns.length ? { columns: [...SOVR_COLUMNS, ...kinds.columns] } : {}),
200
212
  rows: sovrRows(sovereigns, payload.asOf, moves).map((row) => ({
201
213
  name: row.label,
202
214
  series: sovereignSeries(row.sovereign.id),
@@ -206,6 +218,7 @@ export const sovrHeadless: HeadlessPaneDefinition<"rows"> = {
206
218
  currency: formatCurrencyMove(row.sovereign.currency, row.currencyMove),
207
219
  percentile: rank(row.percentile),
208
220
  date: row.sovereign.date,
221
+ ...kinds.row(row.sovereign),
209
222
  })),
210
223
  rowUnitKey: "unit",
211
224
  rowSymbolKey: "series",
@@ -2,12 +2,14 @@ import type {
2
2
  CdxBoardIndex,
3
3
  CreditBoardPoint,
4
4
  CreditIndexQuote,
5
+ CreditLevelKind,
5
6
  SovrBoardSovereign,
6
7
  } from "../../../api-client/credit-boards";
7
8
  import { spanAxisFormatter, formatBpAxis } from "../../../components/chart-table/axis";
8
9
  import { historyStatistics } from "../../../components/chart/curve/model";
9
10
  import type { MarketBoardRow } from "../../../components/market-board";
10
11
  import type { PricePoint } from "../../../types/financials";
12
+ import type { HeadlessPaneColumn } from "../../../types/plugin";
11
13
 
12
14
  const YEAR_DAYS = 365;
13
15
  /** A level older than this is not the current market. */
@@ -93,6 +95,33 @@ function utcClock(instant: string): string | null {
93
95
  return Number.isFinite(time) ? new Date(time).toISOString().slice(11, 16) : null;
94
96
  }
95
97
 
98
+ /**
99
+ * How a level that is the day's settlement price, not its trades, is marked
100
+ * beside its date.
101
+ */
102
+ export const SETTLEMENT_TEXT = "settlement";
103
+
104
+ /**
105
+ * A report's mark for the levels that are settlement prices: blank for a
106
+ * level from the day's trades. Shown only when a row is one, and each row
107
+ * then carries `kind`.
108
+ */
109
+ const levelKindColumn: HeadlessPaneColumn = {
110
+ key: "kind", header: "Level",
111
+ description: "Blank for a level from the day's trades. settlement for the day's end-of-day settlement price, which stands in "
112
+ + "on a day without enough agreeing trades; its prints are 0.",
113
+ format: (value) => value === "settlement" ? SETTLEMENT_TEXT : "",
114
+ };
115
+
116
+ /** `kind` on every row once one level is a settlement; nothing otherwise, so a report without one is unchanged. */
117
+ export function levelKinds<T>(items: readonly T[], kindOf: (item: T) => CreditLevelKind | null | undefined) {
118
+ const shown = items.some((item) => kindOf(item) === "settlement");
119
+ return {
120
+ columns: shown ? [levelKindColumn] : [],
121
+ row: (item: T) => shown ? { kind: kindOf(item) ?? "traded" } : {},
122
+ };
123
+ }
124
+
96
125
  /**
97
126
  * What a day still trading rests on: `so far, 38 prints, last 08:09 UTC`.
98
127
  * The print time is left out when the server does not say it.
@@ -1,6 +1,6 @@
1
1
  import type { CreditBoardPoint } from "../../../api-client/credit-boards";
2
2
  import type { SeriesDefinition, SeriesLoadResult } from "../../../series/types";
3
- import { cachedSeriesWarning } from "../shared/series-warning";
3
+ import { cachedSeriesNote } from "../shared/series-warning";
4
4
  import { loadCdxBoard, loadSovrBoard } from "./client";
5
5
 
6
6
  /** The five on-the-run 5Y index contracts the CDX tab lists; IG and iTraxx quote spread, HY and EM price. */
@@ -27,14 +27,13 @@ function boardPoints(
27
27
  rows: ReadonlyArray<{ id: string; points: readonly CreditBoardPoint[] }>,
28
28
  id: string,
29
29
  label: string,
30
- resource: { stale?: boolean; refreshError?: string | null },
30
+ resource: { stale?: boolean; refreshError?: string | null; savedAt?: number },
31
31
  ): SeriesLoadResult {
32
32
  const row = rows.find((entry) => entry.id === id);
33
33
  if (!row) throw new Error(`${label} is not on the board right now.`);
34
- const warning = cachedSeriesWarning(resource);
35
34
  return {
36
35
  points: row.points.map((point) => ({ date: point.date, value: point.level })),
37
- ...(warning ? { warning } : {}),
36
+ ...cachedSeriesNote(resource),
38
37
  };
39
38
  }
40
39
 
@@ -25,12 +25,14 @@ import { formatAsOf } from "../cds/model";
25
25
  import { FIXED_INCOME_PANE_ID } from "../fixed-income/model";
26
26
  import { useCommandValue } from "../shared/command-tab";
27
27
  import { getCachedSovrBoard, loadCurrencyMoves, loadSovrBoard } from "./client";
28
+ import { readsInBatches } from "../../../market-data/batched-reads";
28
29
  import {
29
30
  findSovereign,
30
31
  formatContract,
31
32
  formatCurrencyMove,
32
33
  formatLevel,
33
34
  formatMove,
35
+ SETTLEMENT_TEXT,
34
36
  sovrRows,
35
37
  type SovrRow,
36
38
  } from "./model";
@@ -65,7 +67,7 @@ function SovereignDetail({ row, width, height, focused }: { row: SovrRow; width:
65
67
  const { sovereign, year } = row;
66
68
  const items: StatItem[] = [
67
69
  { id: "level", label: "5Y spread", value: formatLevel("spread", sovereign.level),
68
- detail: `${formatPercentileRank(year.percentile, "1Y")} · ${sovereign.date}` },
70
+ detail: [formatPercentileRank(year.percentile, "1Y"), sovereign.date, ...(sovereign.kind === "settlement" ? [SETTLEMENT_TEXT] : [])].join(" · ") },
69
71
  { id: "1m", label: "1M", value: formatMove("spread", sovereign.change1M) },
70
72
  { id: "1w", label: "1W", value: formatMove("spread", sovereign.change1W) },
71
73
  { id: "fx", label: "Currency 1M", value: formatCurrencyMove(sovereign.currency, row.currencyMove) },
@@ -99,9 +101,12 @@ export function SovrPane({ focused, width, height }: PaneProps) {
99
101
  // Each currency's month against the dollar, from the same daily closes the FX panes use.
100
102
  const currencies = useMemo(() => [...new Set(payload?.sovereigns.map((row) => row.currency) ?? [])].sort().join(","),
101
103
  [payload]);
102
- const movesLoader = useCallback(() => provider && currencies
103
- ? loadCurrencyMoves(currencies.split(","), (symbol) => provider.getPriceHistory(symbol, "", "3M"))
104
- : Promise.resolve(NO_MOVES), [currencies, provider]);
104
+ const movesLoader = useCallback(async () => {
105
+ if (!provider || !currencies) return NO_MOVES;
106
+ // Every currency at once, in batch requests, from a source that batches them; a few at a time otherwise.
107
+ const batch = await readsInBatches(provider, "getPriceHistory");
108
+ return loadCurrencyMoves(currencies.split(","), (symbol) => provider.getPriceHistory(symbol, "", "3M", batch ? { batch } : undefined), batch);
109
+ }, [currencies, provider]);
105
110
  const moves = useAsyncResource(movesLoader, { keepPreviousData: true });
106
111
  const rows = useMemo(() => payload ? sovrRows(payload.sovereigns, payload.asOf, moves.data ?? NO_MOVES) : EMPTY_ROWS,
107
112
  [moves.data, payload]);
@@ -66,20 +66,28 @@ async function loadSeries(
66
66
  loader: CreditSeriesLoader,
67
67
  ) {
68
68
  const request = requestFor(definition.seriesId);
69
- const result = await loadCachedFredSeries(
70
- request,
71
- async () => {
72
- const data = trimHistory(await loader(definition.seriesId, {
73
- limit: HISTORY_LIMIT,
74
- sortOrder: "desc",
75
- }));
76
- // Reject incompatible source responses before they replace usable cache.
77
- // The shared cache can then retain the dated prior value as stale fallback.
78
- normalizeCreditSeries(definition, data);
79
- return data;
80
- },
81
- { force },
82
- );
69
+ let result: Awaited<ReturnType<typeof loadCachedFredSeries>>;
70
+ try {
71
+ result = await loadCachedFredSeries(
72
+ request,
73
+ async () => {
74
+ const data = trimHistory(await loader(definition.seriesId, {
75
+ limit: HISTORY_LIMIT,
76
+ sortOrder: "desc",
77
+ }));
78
+ // Reject incompatible source responses before they replace usable cache.
79
+ normalizeCreditSeries(definition, data);
80
+ return data;
81
+ },
82
+ { force },
83
+ );
84
+ } catch (error) {
85
+ // A refusal or a reply that fails validation is no saved copy's to answer: it is this row's
86
+ // error, beside the dated prior value already read, which stays as stale rather than vanish.
87
+ const kept = getCachedFredSeries(request, { allowExpired: true });
88
+ if (!kept) throw error;
89
+ return { row: normalizeCreditSeries(definition, kept.data, true), refreshError: error instanceof Error ? error.message : String(error) };
90
+ }
83
91
  return {
84
92
  row: normalizeCreditSeries(definition, result.data, result.stale),
85
93
  refreshError: result.refreshError,