@gloom-sh/terminal 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -2,7 +2,7 @@ import type { PaneTemplateInstanceConfig } from "../../../types/plugin";
2
2
  import type { PluginModule } from "../plugin-module";
3
3
  import { commandRequestParams } from "../shared/command-tab";
4
4
  import { PERP_SERIES } from "./series";
5
- import { perpsCache, perpsHistoryCache, perpsListCache, perpsMarketCache, perpsRankingsCache } from "./client";
5
+ import { perpsCache, perpsDepthCache, perpsHistoryCache, perpsLiquidationsCache, perpsListCache, perpsMarketCache, perpsRankingsCache } from "./client";
6
6
  import { perpsHeadless } from "./headless";
7
7
  import { PerpsPane } from "./pane";
8
8
 
@@ -18,16 +18,18 @@ export function perpsInstance(arg: string | undefined): PaneTemplateInstanceConf
18
18
  settings: { market }, params: commandRequestParams({ market }) };
19
19
  }
20
20
 
21
+ const PERP_CACHES = [perpsCache, perpsHistoryCache, perpsListCache, perpsMarketCache, perpsRankingsCache, perpsLiquidationsCache, perpsDepthCache] as const;
22
+
21
23
  export const perpsModule: PluginModule = {
22
24
  series: PERP_SERIES,
23
25
  panes: [{ id: "perps", name: "Perpetuals", icon: "P", component: PerpsPane, defaultPosition: "right", defaultMode: "floating",
24
26
  defaultFloatingSize: { width: 138, height: 34 }, tableExport: true, headless: perpsHeadless }],
25
- paneTemplates: [{ id: "perps-pane", paneId: "perps", label: "Perpetuals", description: "Perpetual funding, open interest and premiums by market and venue, with rankings, a venue comparison and Pro history.",
26
- keywords: ["perp", "perpetual", "perpetuals", "funding", "funding rate", "open interest", "hip-3", "stock perps", "basis", "basis trade", "carry", "venue", "funding spread", "long short ratio", "long/short", "positioning"],
27
+ paneTemplates: [{ id: "perps-pane", paneId: "perps", label: "Perpetuals", description: "Perpetual funding, open interest and premiums by market and venue, with rankings, a venue comparison, liquidations, order book depth and Pro history.",
28
+ keywords: ["perp", "perpetual", "perpetuals", "funding", "funding rate", "open interest", "hip-3", "stock perps", "basis", "basis trade", "carry", "venue", "funding spread", "long short ratio", "long/short", "positioning", "liquidations", "liquidation", "order book depth", "market depth", "liquidity"],
27
29
  shortcut: { prefix: "PERP", argKind: "text", argOptional: true, argPlaceholder: "market" }, headless: perpsHeadless,
28
30
  createInstance: (_ctx, options) => perpsInstance(options?.arg) }],
29
31
  setup(ctx) {
30
- for (const cache of [perpsCache, perpsHistoryCache, perpsListCache, perpsMarketCache, perpsRankingsCache]) cache.attach(ctx.persistence);
32
+ for (const cache of PERP_CACHES) cache.attach(ctx.persistence);
31
33
  },
32
- dispose() { for (const cache of [perpsCache, perpsHistoryCache, perpsListCache, perpsMarketCache, perpsRankingsCache]) cache.reset(); },
34
+ dispose() { for (const cache of PERP_CACHES) cache.reset(); },
33
35
  };
@@ -6,14 +6,28 @@ import { formatCompact } from "../../../utils/format";
6
6
  import { isUsListingExchange, publicTickerKey } from "../../../utils/exchanges";
7
7
  import { isRecord } from "../../../utils/guards";
8
8
  import type { ThemeColors } from "../../../theme/colors";
9
- export const PERP_TABS = ["board", "rankings", "compare", "history", "evidence"] as const;
9
+ export const PERP_TABS = ["board", "rankings", "compare", "liquidations", "depth", "history", "evidence"] as const;
10
10
  export type PerpTab = typeof PERP_TABS[number];
11
11
  /** One market's tabs; the others list many markets. */
12
12
  export const isMarketTab = (tab: string) => tab === "history" || tab === "evidence";
13
- /** A named market opens its history; nothing named opens the board. */
14
- export const openingTab = (market: string | null | undefined): PerpTab => market?.trim() ? "history" : "board";
13
+ /** One base asset's tabs, across its contracts on every venue; they share the asset. */
14
+ export const isAssetTab = (tab: string) => tab === "compare" || tab === "liquidations" || tab === "depth";
15
+ /**
16
+ * A named market opens its history, or its latest values (Evidence) where history is Pro's, so a free account
17
+ * lands on the figures it has and not on a lock; nothing named opens the board.
18
+ */
19
+ export const openingTab = (market: string | null | undefined, pro = true): PerpTab => market?.trim() ? pro ? "history" : "evidence" : "board";
15
20
  export const ASSET_FILTERS = ["all", "stocks", "indices", "energy", "metals", "fx", "crypto"] as const;
16
- export const VENUES = ["hyperliquid", "binance", "bybit", "okx", "deribit", "coinbase", "kraken", "dydx"] as const;
21
+ /** The venues Gloom collects perpetuals from: what the venue filter and `--venue` take. */
22
+ export const VENUES = ["hyperliquid", "binance", "bybit", "okx"] as const;
23
+ /** Every venue a market identity can name (`kraken:PF_XBTUSD`), collected or not. */
24
+ export const IDENTITY_VENUES = [...VENUES, "deribit", "coinbase", "kraken", "dydx"] as const;
25
+ /** What `--venue` says of a venue Gloom does not collect yet; null for a covered venue and for any other word. */
26
+ export function uncoveredVenueNote(venue: string): string | null {
27
+ const id = venue.trim().toLowerCase();
28
+ return (IDENTITY_VENUES as readonly string[]).includes(id) && !(VENUES as readonly string[]).includes(id)
29
+ ? `Gloom doesn't cover ${venueName(id)} perps yet.` : null;
30
+ }
17
31
  export const BOARD_SORTS = ["oi", "funding", "oi-change", "premium", "long-short"] as const;
18
32
  export const sortLabel = (sort: string) => ({ oi: "OI", funding: "Funding", "oi-change": "OI change", premium: "Premium", "long-short": "Long %" })[sort] ?? sort;
19
33
  export const label = (s: string) => s === "fx" ? "FX" : s.charAt(0).toUpperCase() + s.slice(1);
@@ -5,13 +5,10 @@ import type { PerpBoardRow, PerpMarketPayload } from "../../../api-client/perps"
5
5
  import { buildSectionedRows, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, SectionHeading, PaneStatusBody, QueryBar, StatGrid, useChartTableSelection,
6
6
  usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch, type DataTableCell, type KeyValueRowProps, type PaneFooterSegment, type PaneHint, type SectionedRow,
7
7
  type StatItem } from "../../../components";
8
- import { deriveHeadlessFreshness, paneStatusLineVariants } from "../../../cli/pane-functions/freshness";
9
8
  import { openFormModal } from "../../../components/form-modal";
10
9
  import { tf } from "../../../i18n";
11
10
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
12
11
  import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePaneTitle, usePluginAppActions, usePluginPaneState } from "../../../public/react";
13
- import { useAgeClock } from "../../../react/auto-refresh";
14
- import { useRemoteUiNode } from "../../../remote/semantic-tree";
15
12
  import { useThemeColors } from "../../../theme/theme-context";
16
13
  import type { PaneProps } from "../../../types/plugin";
17
14
  import { Box, ScrollBox, useRendererHost, useUiCapabilities } from "../../../ui";
@@ -25,9 +22,11 @@ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/resea
25
22
  import { boardSearchText, cachedPerpSelection, cachedPerpsBoard, cachedPerpsCompare, cachedPerpsRankings, loadPerpSelection, loadPerpsBoard, loadPerpsCompare,
26
23
  loadPerpsHistoryForRange, loadPerpsRankings } from "./client";
27
24
  import { perpFreshness } from "./freshness";
25
+ import { Depth, Liquidations } from "./cascade-tabs";
26
+ import { useFreshnessStatusLine } from "./status-line";
28
27
  import { useLivePerpRows, useLivePerpTableRows } from "./live-rows";
29
28
  import { aprFormula, ASSET_FILTERS, BOARD_SORTS, boardColumns, collectedVenueCount, compact, compareColumns, dayChange, fundingInterval, fundingSpread, historyCaption, historyCell, historyChange, historyColumns,
30
- HISTORY_METRICS, historyRows, historySeries, historyValue, inPoints, isMarketTab, label, longShortReason, marketLabel, metricLabel, openingTab, perpCell, perpHistoryProText, PERP_TABS, percent,
29
+ HISTORY_METRICS, historyRows, historySeries, historyValue, inPoints, isAssetTab, isMarketTab, label, longShortReason, marketLabel, metricLabel, openingTab, perpCell, perpHistoryProText, PERP_TABS, percent,
31
30
  positioningFields, price, rankingColumns, rankingRows, RANKINGS, ratioText, share, sortLabel, time, underlyingTicker, venueLabel, venueName, venuesOf, type HistoryMetric,
32
31
  type PerpColumnId, type PerpTab, type RankingKey } from "./model";
33
32
 
@@ -45,17 +44,8 @@ const withRankedRow = (entry: SectionedRow<Ranked>, row: PerpBoardRow): Sectione
45
44
  * same way.
46
45
  */
47
46
  function usePerpStatusLine(rows: readonly PerpBoardRow[], funding: readonly PerpBoardRow[], since: number | null, live = false): PaneFooterSegment[] {
48
- const now = useAgeClock(since);
49
- const declared = useMemo(() => rows.length ? perpFreshness(rows, { funding, now, live }) : null, [rows, funding, now, live]);
50
- useRemoteUiNode(declared ? { role: "report-freshness", label: "Venue freshness", getMetadata: () => ({ ...declared }) } : null);
51
- return useMemo(() => {
52
- if (!declared) return [];
53
- const freshness = deriveHeadlessFreshness({ shape: "rows" }, { rows: [], freshness: declared }, now);
54
- const tone = freshness.status === "stale" ? "warning" as const : "muted" as const;
55
- // A footer too narrow beside its keys gives up the funding part before any venue's time.
56
- const [line, ...shorter] = paneStatusLineVariants(freshness).map((text) => [{ text, tone }]);
57
- return [{ id: "perp-freshness", asOf: true, parts: line!, variants: shorter }];
58
- }, [declared, rows, now]);
47
+ const declare = useCallback((now: number) => rows.length ? perpFreshness(rows, { funding, now, live }) : null, [rows, funding, live]);
48
+ return useFreshnessStatusLine(declare, since);
59
49
  }
60
50
 
61
51
  type EvidenceField = [label: string, value: string, detail?: string, link?: KeyValueRowProps["link"]];
@@ -378,11 +368,12 @@ export function PerpsPane({ width, height, focused, marketAction }: Pick<PanePro
378
368
  const openDescription = useOpenDescription();
379
369
  const upgrade = useCloudUpgradeAction("perp");
380
370
  const [openingMarket] = usePaneSettingValue("market", "");
381
- const [openingTabValue] = usePaneSettingValue("tab", openingTab(openingMarket));
371
+ const opening = openingTab(openingMarket, access.hasProAccess);
372
+ const [openingTabValue] = usePaneSettingValue("tab", opening);
382
373
  // A market asked for again (a stock's perpetual row, PERP) comes back to it on its opening tab, whatever this pane
383
374
  // shows by then.
384
375
  const [savedTab, setTab] = useCommandValue<string>("perps", "perps:tab", (params) => params.market ? openingTabValue : null, openingTabValue);
385
- const tab = PERP_TABS.includes(savedTab as PerpTab) ? savedTab as PerpTab : openingTab(openingMarket);
376
+ const tab = PERP_TABS.includes(savedTab as PerpTab) ? savedTab as PerpTab : opening;
386
377
  const marketTab = isMarketTab(tab);
387
378
  const [search, setSearch] = useCommandValue("perps", "perps:search", (params) => params.market || null, openingMarket);
388
379
  const [compareAsset, setCompareAsset] = usePluginPaneState<string>("perps:compare", compareOpening(openingMarket));
@@ -409,13 +400,17 @@ export function PerpsPane({ width, height, focused, marketAction }: Pick<PanePro
409
400
  // The market on screen goes with the user: History and Evidence open the row picked on a list, Compare its base asset.
410
401
  const current = marketTab ? active : listRow;
411
402
  const carried = marketTab ? active : listRow ?? picked;
403
+ // Liquidations and Depth list no board rows: a market tab after them opens their asset, and they keep Compare's.
404
+ const assetOnly = tab === "liquidations" || tab === "depth";
412
405
  const selectTab = useCallback((next: string) => {
413
- if (carried && isMarketTab(next) && !marketTab) setSearch(carried.marketId);
414
- if (carried && next === "compare") setCompareAsset(carried.baseAsset);
406
+ if (assetOnly && isMarketTab(next)) setSearch(compareAsset);
407
+ else if (carried && isMarketTab(next) && !marketTab) setSearch(carried.marketId);
408
+ if (carried && isAssetTab(next) && !assetOnly) setCompareAsset(carried.baseAsset);
415
409
  setListRow(null);
416
410
  setTab(next);
417
- }, [carried, marketTab, setSearch, setCompareAsset, setTab]);
418
- const openMarket = useCallback((row: PerpBoardRow, next: PerpTab = "history") => { setSearch(row.marketId); setTab(next); }, [setSearch, setTab]);
411
+ }, [assetOnly, carried, compareAsset, marketTab, setSearch, setCompareAsset, setTab]);
412
+ const openMarket = useCallback((row: PerpBoardRow, next: PerpTab = openingTab(row.marketId, access.hasProAccess)) => { setSearch(row.marketId); setTab(next); }, [setSearch, setTab, access.hasProAccess]);
413
+ const openMarketId = useCallback((marketId: string) => { setSearch(marketId); setTab(openingTab(marketId, access.hasProAccess)); }, [setSearch, setTab, access.hasProAccess]);
419
414
  const { strip, rows: tabRows } = usePaneTabs({ tabs: PERP_TABS.map((value) => ({ value, label: label(value) })), activeValue: tab, onSelect: selectTab, focused, dense: true });
420
415
  const refreshMarket = useCallback(() => { void resource.reload(); }, [resource.reload]);
421
416
  useAutoRefresh(resource.updatedAt, resource.load, { intervalMs: 60_000 });
@@ -450,7 +445,9 @@ export function PerpsPane({ width, height, focused, marketAction }: Pick<PanePro
450
445
  return <Box width={width} height={height} flexDirection="column">
451
446
  {strip}
452
447
  {tab === "board" ? <Board {...list} /> : tab === "rankings" ? <Rankings {...list} />
453
- : tab === "compare" ? <Compare {...list} asset={compareAsset} onAssetChange={setCompareAsset} /> : <>
448
+ : tab === "compare" ? <Compare {...list} asset={compareAsset} onAssetChange={setCompareAsset} />
449
+ : tab === "liquidations" ? <Liquidations accessKey={accessKey} width={width} height={listHeight} focused={focused} asset={compareAsset} onAssetChange={setCompareAsset} onOpenMarket={openMarketId} />
450
+ : tab === "depth" ? <Depth accessKey={accessKey} width={width} height={listHeight} focused={focused} asset={compareAsset} onAssetChange={setCompareAsset} onOpenMarket={openMarketId} /> : <>
454
451
  <QueryBar width={width} search={{ focused, value: search, onChange: setSearch, placeholder: "Market or canonical identity", debounceMs: 300, ...querySearch.searchProps }}
455
452
  filters={tab === "history" ? [
456
453
  { id: "metric", label: "Series", value: metric, options: HISTORY_METRICS.map((value) => ({ value, label: metricLabel(value) })), onChange: setMetric },
@@ -0,0 +1,25 @@
1
+ import { useMemo } from "react";
2
+ import { deriveHeadlessFreshness, paneStatusLineVariants } from "../../../cli/pane-functions/freshness";
3
+ import type { PaneFooterSegment } from "../../../components";
4
+ import { useAgeClock } from "../../../react/auto-refresh";
5
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
6
+ import type { HeadlessPaneFreshness } from "../../../types/headless";
7
+
8
+ /**
9
+ * The footer line a tab ends with, from the same freshness its text report declares (`declare`, null while there is
10
+ * nothing to date): the source, its time and how old it is, the line `fn PERP` and `shot PERP` end with. It keeps ageing
11
+ * while the pane is seen (`since` is when the data loaded), and is published for a capture to date itself the same way.
12
+ */
13
+ export function useFreshnessStatusLine(declare: ((now: number) => HeadlessPaneFreshness | null) | null, since: number | null): PaneFooterSegment[] {
14
+ const now = useAgeClock(since);
15
+ const declared = useMemo(() => declare?.(now) ?? null, [declare, now]);
16
+ useRemoteUiNode(declared ? { role: "report-freshness", label: "Venue freshness", getMetadata: () => ({ ...declared }) } : null);
17
+ return useMemo(() => {
18
+ if (!declared) return [];
19
+ const freshness = deriveHeadlessFreshness({ shape: "rows" }, { rows: [], freshness: declared }, now);
20
+ const tone = freshness.status === "stale" ? "warning" as const : "muted" as const;
21
+ // A footer too narrow beside its keys gives up the note before any venue's time.
22
+ const [line, ...shorter] = paneStatusLineVariants(freshness).map((text) => [{ text, tone }]);
23
+ return [{ id: "perp-freshness", asOf: true, parts: line!, variants: shorter }];
24
+ }, [declared, now]);
25
+ }
@@ -161,7 +161,11 @@ export function composeBuiltinPlugin(options: CompositePluginOptions): GloomPlug
161
161
  for (const module of modules) {
162
162
  startedModules.push(module);
163
163
  try {
164
- await module.setup?.(ctx);
164
+ // Only an async setup is waited for. Awaiting a synchronous one hands
165
+ // the tick to the panes first: Gloom Cloud restores the saved session
166
+ // in its second module, and the panes' first requests went out without it.
167
+ const pending = module.setup?.(ctx);
168
+ if (pending) await pending;
165
169
  } catch (error) {
166
170
  // Logged as the plugin, so the failure counts toward its health.
167
171
  ctx.log.error("Module setup failed", { error: error instanceof Error ? error.message : String(error) });
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { peekPersistedBrokerAccounts } from "../../../brokers/account-cache";
2
3
  import type { AppResourceStorePort } from "../../../core/app-service-ports";
3
4
  import type { AppConfig } from "../../../types/config";
@@ -20,8 +21,9 @@ export function peekCachedBrokerAccounts(
20
21
  try {
21
22
  const accounts = peekPersistedBrokerAccounts(resources, instance);
22
23
  if (accounts?.length) brokerAccounts[instance.id] = accounts;
23
- } catch {
24
+ } catch (error) {
24
25
  // An unreadable snapshot leaves the portfolio without account figures.
26
+ logFailure("portfolio-list.cached-account", "Read cached broker accounts failed")(error);
25
27
  }
26
28
  }
27
29
  return brokerAccounts;
@@ -8,6 +8,7 @@ import { splitSymbolArgs, splitSymbolWords, symbolInputKey } from "../../../../c
8
8
  import { notASymbolError } from "../../../../cli/symbol-cells";
9
9
  import { readSymbolList, SymbolListError } from "../../../../cli/symbol-list";
10
10
  import { cliStyles } from "../../../../utils/cli-output";
11
+ import { shellWord } from "../../../../utils/shell-word";
11
12
  import { dryRunNote } from "../../../../cli/helpers";
12
13
  import {
13
14
  addSymbol,
@@ -46,14 +47,7 @@ export const SYMBOL_LIST_HELP = {
46
47
  ],
47
48
  };
48
49
 
49
- /**
50
- * One word of a command line as a shell reads it back: as written when it
51
- * holds nothing a shell treats specially (`NVDA`, `SAN:EPA`, `BRK.B`), else
52
- * in single quotes (`'Altria Group'`, `'$nvda'`, `'AT&T'`).
53
- */
54
- export function shellWord(word: string): string {
55
- return /^[\p{L}\p{N}._:@%+,/][\p{L}\p{N}._:@%+,/=-]*$/u.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
56
- }
50
+ export { shellWord };
57
51
 
58
52
  /**
59
53
  * A row's line, and under it the listing a company's name was read as, the
@@ -50,7 +50,7 @@ import { importSource, notionalPositions, rankThirteenFLines, type ThirteenFImpo
50
50
  import { importedFromText } from "../imported-from";
51
51
  import { savedListingCoinNote } from "../../../../cli/commands/crypto-hints";
52
52
  import { findNamedCollection } from "../../../../portfolio/collection-name";
53
- import { wholeUnitQuantityProblem } from "../../../../portfolio/position-quantity";
53
+ import { ambiguousQuantityProblem } from "../../../../portfolio/position-quantity";
54
54
  import {
55
55
  CLI_COST_FX_WORDING, loadPurchaseDateCostFx, readCostFxRate, readPurchaseDate, resolvePositionCostFx, type CostFxHistoryClient,
56
56
  } from "../../../../portfolio/cost-fx";
@@ -363,8 +363,8 @@ async function setPositionCommand(
363
363
  const avgCost = parseAmountArg(avgCostValue, "Average cost");
364
364
  // The notes say which listing a bare symbol went to, and that a bare coin ticker is not the coin, as `portfolio add` does.
365
365
  const { ticker, notes } = await resolveCliTicker(symbol, store, dataProvider, exchange);
366
- // `1.250` on a listing held in whole lots asks which was meant, as `1,250` did above.
367
- const lots = wholeUnitQuantityProblem(sharesValue, ticker, { listing: savedListingName(ticker).key });
366
+ // `1.250` and `1.000` ask which was meant, as `1,250` did above: a dot before three digits is as often thousands.
367
+ const lots = ambiguousQuantityProblem(sharesValue, ticker, { listing: savedListingName(ticker).key });
368
368
  if (lots) throw new PortfolioCliError(lots);
369
369
  // A future is held in contracts of the size its terms give, as `quote` states it, and a cost typed as
370
370
  // corn is quoted counts cents.
@@ -942,7 +942,7 @@ const AMOUNTS_HELP = {
942
942
  lines: [
943
943
  "Numbers take either decimal mark: 1.234,5, 1234,5 and 1,234.5 are 1234.5, and 18,50 is 18.5.",
944
944
  "Cash and --notional are amounts of a currency (cash in the portfolio's unless named, --notional in USD), so 500,000 and 1.234 are thousands, a third decimal being below a cent. In a currency with three decimals (KWD, BHD, OMR) they could be either, and the command asks which.",
945
- "Shares, contracts and average cost read a lone dot as a decimal point (123.456) and ask which a lone comma before three digits meant (1,234). On a listing held in whole lots (shares listed outside the US, a future's contracts) a lone dot before three digits (1.250) asks too.",
945
+ "Average cost reads a lone dot as a decimal point (123.456). Shares and contracts ask which a lone dot or comma before three digits meant (1.000, 1,234), thousands or a decimal, and name both readings to type.",
946
946
  ],
947
947
  };
948
948
 
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { createBaseConverter } from "../../../../cli/base-converter";
2
3
  import { savedListingName } from "../../../../cli/listing-arg";
3
4
  import { currentListingName } from "../../../../tickers/listing-name";
@@ -257,8 +258,9 @@ export async function loadCollectionQuotes(
257
258
  const quote = await dataProvider.getQuote(ticker.metadata.ticker, ticker.metadata.exchange,
258
259
  datesChangeBasis ? { ...context, datesChangeBasis } : context);
259
260
  quotes.set(ticker.metadata.ticker, quote);
260
- } catch {
261
- // Ignore partial quote failures so the rest of the table still renders.
261
+ } catch (error) {
262
+ // A partial quote failure leaves the rest of the table rendering.
263
+ logFailure("portfolio-list.cli.render", "Load collection quote failed")(error);
262
264
  }
263
265
  }),
264
266
  );
@@ -1269,7 +1271,7 @@ async function loadMarketCapFinancials(
1269
1271
  const targets = needed.map((ticker) => ({ symbol: ticker.metadata.ticker, exchange: ticker.metadata.exchange }));
1270
1272
  if (dataProvider.getTickerFinancialsBatch) {
1271
1273
  await Promise.all(chunked(targets, FINANCIALS_BATCH_SIZE).map(async (batch) => {
1272
- const results = await dataProvider.getTickerFinancialsBatch!(batch).catch(() => []);
1274
+ const results = await dataProvider.getTickerFinancialsBatch!(batch).catch(logFailure("portfolio-list.cli.render", "Load market cap financials batch failed", []));
1273
1275
  batch.forEach((target, index) => {
1274
1276
  const value = (results.find((result) => result.target === target) ?? results[index])?.financials;
1275
1277
  if (value) financials.set(target.symbol, value);
@@ -1278,7 +1280,7 @@ async function loadMarketCapFinancials(
1278
1280
  return financials;
1279
1281
  }
1280
1282
  await mapWithConcurrency(targets, FINANCIALS_CONCURRENCY, async (target) => {
1281
- const value = await dataProvider.getTickerFinancials(target.symbol, target.exchange).catch(() => null);
1283
+ const value = await dataProvider.getTickerFinancials(target.symbol, target.exchange).catch(logFailure("portfolio-list.cli.render", "Load market cap financials failed", null));
1282
1284
  if (value) financials.set(target.symbol, value);
1283
1285
  });
1284
1286
  return financials;
@@ -1298,7 +1300,7 @@ async function loadPerpetualFunding(tickers: readonly TickerRecord[]): Promise<M
1298
1300
  });
1299
1301
  const funding = new Map<string, number | null>();
1300
1302
  await mapWithConcurrency(markets, PERPETUAL_CONCURRENCY, async ({ symbol, listing }) => {
1301
- funding.set(symbol, await loadPerpetualMarket(listing).then((market) => market?.row.fundingRate8h ?? null, () => null));
1303
+ funding.set(symbol, await loadPerpetualMarket(listing).then((market) => market?.row.fundingRate8h ?? null, logFailure("portfolio-list.cli.render", "Load perpetual funding failed", null)));
1302
1304
  });
1303
1305
  return funding;
1304
1306
  }
@@ -0,0 +1,40 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import { getSharedMarketDataCoordinator } from "../../../../market-data/coordinator";
3
+ import { formatMarketPrice, quoteFormatOptions, withCurrencyMinorDigits } from "../../../../market-data/market/format";
4
+ import { buildQuoteKey, instrumentFromQuoteTarget, resolveEntryData } from "../../../../market-data/selectors";
5
+ import type { PortfolioFuturesTerms } from "../../../../portfolio/futures-positions";
6
+ import { useLiveQuoteEntries } from "../../../../state/hooks/quote-streaming";
7
+ import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
8
+ import { logFailure } from "../../../../utils/log-failure";
9
+
10
+ /** Front references share one snapshot and stream per contract, independent of each row's own price. */
11
+ export function useFuturesFrontNotes(
12
+ terms: ReadonlyMap<string, PortfolioFuturesTerms>,
13
+ { enabled, liveStreaming }: { enabled: boolean; liveStreaming: boolean },
14
+ ): ReadonlyMap<string, string> {
15
+ const symbolsKey = useMemo(() => enabled
16
+ ? [...new Set([...terms.values()].flatMap((value) => value.front?.contractSymbol ?? []))].sort().join("\u001f") : "", [enabled, terms]);
17
+ const targets = useMemo<QuoteSubscriptionTarget[]>(() => symbolsKey ? symbolsKey.split("\u001f").map((symbol) => ({
18
+ symbol, exchange: "", route: "provider", surface: "watchlist", visible: true,
19
+ })) : [], [symbolsKey]);
20
+ const instruments = useMemo(() => targets.map(instrumentFromQuoteTarget), [targets]);
21
+ const coordinator = getSharedMarketDataCoordinator();
22
+ const { entries } = useLiveQuoteEntries(targets, { liveStreaming });
23
+ useEffect(() => {
24
+ // Polling primes its own batch immediately; a stream needs one snapshot while connecting.
25
+ if (!liveStreaming || !coordinator || !instruments.length) return;
26
+ void coordinator.loadQuotesBatch(instruments).catch(logFailure("portfolio-futures", "Read front contract quotes failed"));
27
+ }, [coordinator, instruments, liveStreaming]);
28
+ return useMemo(() => {
29
+ const notes = new Map<string, string>();
30
+ if (!enabled) return notes;
31
+ for (const [symbol, value] of terms) {
32
+ if (!value.front) continue;
33
+ const quote = resolveEntryData(entries.get(buildQuoteKey({ symbol: value.front.contractSymbol, exchange: "" })));
34
+ if (!quote || !Number.isFinite(quote.price)) continue;
35
+ const options = withCurrencyMinorDigits(quoteFormatOptions(quote, "FUTURE"), quote.currency);
36
+ notes.set(symbol, `front ${value.front.contract} ${formatMarketPrice(quote.price, options)}`);
37
+ }
38
+ return notes;
39
+ }, [enabled, entries, terms]);
40
+ }
@@ -104,6 +104,7 @@ import { hasUnknownOptionMultiplier } from "../../../../portfolio/position-metri
104
104
  import { withFuturesContractSizes } from "../../../../portfolio/futures-positions";
105
105
  import { withPurchaseDateCostFx } from "../../../../portfolio/cost-fx";
106
106
  import { usePortfolioFuturesTerms } from "./futures-terms";
107
+ import { useFuturesFrontNotes } from "./futures-front";
107
108
  import { usePurchaseDateCostFx } from "./purchase-date-fx";
108
109
  import {
109
110
  allocationWeightBase,
@@ -185,7 +186,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
185
186
  [config.baseCurrency, currentPortfolio],
186
187
  );
187
188
  // A future counts contracts of the size its terms give, and at the close is valued at its settlement.
188
- const futuresTerms = usePortfolioFuturesTerms(collectionTickers, { enabled: isPortfolioTab, settlement: valuationBasis === "regular-close" });
189
+ const futuresTerms = usePortfolioFuturesTerms(collectionTickers, { enabled: true, settlement: isPortfolioTab && valuationBasis === "regular-close" });
190
+ const futuresFrontNotes = useFuturesFrontNotes(futuresTerms.terms, { enabled: appActive && viewMode === "table", liveStreaming });
189
191
  // A dated lot in another currency costs what its purchase date's FX close says in that currency, as portfolio show values it.
190
192
  const purchaseDateFx = usePurchaseDateCostFx(collectionTickers, { enabled: isPortfolioTab, portfolioId: activeCollectionId, into: totalsCurrency });
191
193
  const tickers = useMemo(
@@ -796,6 +798,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
796
798
  onCursorChange={handleCursorChange}
797
799
  financialsMap={financialsMap}
798
800
  columnContext={columnContext}
801
+ futuresFrontNotes={futuresFrontNotes}
799
802
  flashSymbols={flashSymbols}
800
803
  sinceLastSeen={sinceLastSeen}
801
804
  onRootKeyDown={handleTableKeyDown}
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { useEffect, useMemo, useRef } from "react";
2
3
  import { getSharedMarketDataCoordinator } from "../../../../market-data/coordinator";
3
4
  import { instrumentFromTicker, quoteSubscriptionTargetFromTicker } from "../../../../market-data/request-types";
@@ -251,8 +252,9 @@ export function usePortfolioPaneStreaming({
251
252
  ? sharedCoordinator.loadSnapshotsBatch(normalSnapshotEntries.map((entry) => entry.instrument))
252
253
  : Promise.resolve(),
253
254
  ]);
254
- } catch {
255
+ } catch (error) {
255
256
  // Best-effort warmup for visible rows only.
257
+ logFailure("portfolio-list.pane.streaming", "Warm visible portfolio rows failed")(error);
256
258
  } finally {
257
259
  for (const entry of [...quoteEntries, ...forcedSnapshotEntries, ...normalSnapshotEntries]) {
258
260
  warmupInFlightRef.current.delete(entry.key);
@@ -304,8 +306,9 @@ export function usePortfolioPaneStreaming({
304
306
 
305
307
  try {
306
308
  await sharedCoordinator.loadQuotesBatch(quoteEntries.map((entry) => entry.instrument), { forceRefresh: true });
307
- } catch {
309
+ } catch (error) {
308
310
  // Best-effort watchdog for visible rows only.
311
+ logFailure("portfolio-list.pane.streaming", "Refresh visible portfolio quotes failed")(error);
309
312
  } finally {
310
313
  for (const entry of quoteEntries) {
311
314
  warmupInFlightRef.current.delete(entry.key);
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
2
3
  import type { ColumnConfig } from "../../../../types/config";
3
4
  import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../../types/financials";
@@ -171,7 +172,7 @@ function useAnalystSupplemental(
171
172
  }
172
173
  })).then((entries) => {
173
174
  if (mountedRef.current) setState((current) => setAnalystValues(current, entries));
174
- }).catch(() => {});
175
+ }).catch(logFailure("portfolio-list.pane.supplemental", "Apply analyst research failed"));
175
176
  }, [enabled, mountedRef, provider, setState, state.analystResearch, targetsKey]);
176
177
  }
177
178
 
@@ -211,7 +212,7 @@ function useCorporateSupplemental(
211
212
  }
212
213
  })).then((entries) => {
213
214
  if (mountedRef.current) setState((current) => setCorporateValues(current, entries));
214
- }).catch(() => {});
215
+ }).catch(logFailure("portfolio-list.pane.supplemental", "Apply corporate actions failed"));
215
216
  }, [enabled, mountedRef, provider, setState, state.corporateActions, targetsKey]);
216
217
  }
217
218
 
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { getSharedRegistry } from "../../../registry";
2
3
  import {
3
4
  AmbiguousContractError,
@@ -178,7 +179,8 @@ export async function resolveQuickAddValidation({
178
179
  exchangeFromResolved(resolved),
179
180
  quoteContextFromResolved(resolved),
180
181
  );
181
- } catch {
182
+ } catch (error) {
183
+ logFailure("portfolio-list.quick-add.resolution", "Read quick-add quote failed")(error);
182
184
  quote = null;
183
185
  }
184
186
  }
@@ -111,6 +111,7 @@ export function PortfolioTickerTable({
111
111
  onCursorChange,
112
112
  financialsMap,
113
113
  columnContext,
114
+ futuresFrontNotes,
114
115
  flashSymbols,
115
116
  sinceLastSeen,
116
117
  onRootKeyDown,
@@ -134,6 +135,7 @@ export function PortfolioTickerTable({
134
135
  onCursorChange?: (ticker: TickerRecord, index: number) => void;
135
136
  financialsMap: Map<string, TickerFinancials>;
136
137
  columnContext: ColumnContext;
138
+ futuresFrontNotes?: ReadonlyMap<string, string>;
137
139
  flashSymbols: Map<string, QuoteFlashDirection>;
138
140
  /** For a few seconds after coming back to the pane, LAST is tinted by each row's move since the user left. */
139
141
  sinceLastSeen?: SinceLastSeenRecap | null;
@@ -155,21 +157,26 @@ export function PortfolioTickerTable({
155
157
  openPerpetualRef.current = onOpenPerpetual;
156
158
  const sparklineVisible = columns.some((column) => column.id === PRICE_SPARKLINE_COLUMN_ID);
157
159
  const tableFinancialsMap = useLiveSparklineFinancials(financialsMap, sortedTickers, sparklineVisible);
160
+ const noteColumnId = columns.some((column) => column.id === "name") ? "name" : "ticker";
161
+ const hasFrontReference = useMemo(() => sortedTickers.some((ticker) => columnContext.futuresTerms?.get(ticker.metadata.ticker)?.front),
162
+ [columnContext.futuresTerms, sortedTickers]);
158
163
  const resolveCell = useCallback(
159
164
  (column: ColumnConfig, ticker: TickerRecord, financials: TickerFinancials | undefined) => {
160
165
  const key = `${ticker.metadata.ticker}:${column.id}`;
161
166
  const version = buildCellVersion(column, ticker, financials, columnContext);
162
- const cell = cellCacheRef.current.get(key, version, () => {
167
+ const cached = cellCacheRef.current.get(key, version, () => {
163
168
  const cell = getColumnValue(column, ticker, financials, columnContext);
164
169
  const marketId = PERPETUAL_COLUMN_IDS.has(column.id) ? columnContext.perpetuals?.get(ticker.metadata.ticker)?.marketId : undefined;
165
170
  return marketId ? { ...cell, onPress: () => openPerpetualRef.current?.(marketId) } : cell;
166
171
  });
172
+ const note = column.id === noteColumnId ? futuresFrontNotes?.get(ticker.metadata.ticker) : undefined;
173
+ const cell = note ? { ...cached, note } : cached;
167
174
  // LAST keeps its price: a move since the user last looked only tints the cell.
168
175
  if (column.id !== "price" || !sinceLastSeen) return cell;
169
176
  const move = sinceLastSeen.moves.get(ticker.metadata.ticker);
170
177
  return move == null ? cell : { ...cell, backgroundColor: sinceLastSeenTint(move, sinceLastSeen.fading) };
171
178
  },
172
- [columnContext, sinceLastSeen],
179
+ [columnContext, futuresFrontNotes, noteColumnId, sinceLastSeen],
173
180
  );
174
181
  const pnlColumn = columns.find((column) => column.id === "pnl" || column.id === "pnl_pct");
175
182
  // Reads the same cached P&L cells the rows show; only changed rows recompute.
@@ -202,7 +209,8 @@ export function PortfolioTickerTable({
202
209
  : pnlLabel !== "P&L" && (column.id === "pnl" || column.id === "pnl_pct") ? {
203
210
  ...column,
204
211
  label: column.id === "pnl_pct" ? pnlLabel.replace("P&L", "%") : pnlLabel,
205
- } : column), [columns, grossWeights, hasNonShareQuantity, pnlLabel, sharesWidth, tickerWidth]);
212
+ } : column).map((column) => hasFrontReference && column.id === noteColumnId ? { ...column, flexGrow: 1 } : column),
213
+ [columns, grossWeights, hasFrontReference, hasNonShareQuantity, noteColumnId, pnlLabel, sharesWidth, tickerWidth]);
206
214
  // The table offers "Sort by…" while it has a sort column. A watchlist starts
207
215
  // unsorted and a third header click turns sorting off, so the unsorted state
208
216
  // needs its own entry until TickerListTableView takes `sortable`.
@@ -1,3 +1,4 @@
1
+ import { createTtlCache } from "../../../data/ttl-cache";
1
2
  import { httpFetch } from "../../../utils/http-transport";
2
3
  import { isoDateOrNull, numericOrNull, recordOrNull, textOrNull } from "../../../utils/guards";
3
4
  import {
@@ -84,20 +85,14 @@ async function readJson(url: string): Promise<unknown> {
84
85
  }
85
86
 
86
87
  /** The series break changes about every two years; one read serves the session. */
87
- let seriesBreakRead: Promise<string> | null = null;
88
+ const seriesBreaks = createTtlCache<string>({ ttlMs: Infinity });
88
89
 
89
90
  function currentSeriesBreak(force: boolean): Promise<string> {
90
- if (!seriesBreakRead || force) {
91
- const read = readJson(`${API}/list/seriesbreaks.json`).then((payload) => {
92
- const seriesBreak = newestSeriesBreak(parseSeriesBreaks(payload));
93
- if (!seriesBreak) throw new Error("No series break was listed.");
94
- return seriesBreak;
95
- });
96
- seriesBreakRead = read;
97
- // A failed read is not kept, so the next one asks again.
98
- read.catch(() => { if (seriesBreakRead === read) seriesBreakRead = null; });
99
- }
100
- return seriesBreakRead;
91
+ return seriesBreaks.load("current", async () => {
92
+ const seriesBreak = newestSeriesBreak(parseSeriesBreaks(await readJson(`${API}/list/seriesbreaks.json`)));
93
+ if (!seriesBreak) throw new Error("No series break was listed.");
94
+ return seriesBreak;
95
+ }, { force });
101
96
  }
102
97
 
103
98
  /**
@@ -117,31 +112,11 @@ export async function fetchPrimaryDealers(): Promise<DealerBoard> {
117
112
  };
118
113
  }
119
114
 
120
- interface SeriesRead {
121
- at: number;
122
- read: Promise<SeriesPoint[]>;
123
- points: SeriesPoint[] | null;
124
- }
125
-
126
- const seriesReads = new Map<string, SeriesRead>();
115
+ const seriesReads = createTtlCache<SeriesPoint[]>({ ttlMs: HISTORY_TTL_MS });
127
116
 
128
117
  function seriesRead(seriesBreak: string, key: string, force: boolean): Promise<SeriesPoint[]> {
129
- const id = `${seriesBreak}:${key}`;
130
- const known = seriesReads.get(id);
131
- if (known && !force && Date.now() - known.at < HISTORY_TTL_MS) return known.read;
132
118
  const url = `${API}/get/${encodeURIComponent(seriesBreak)}/timeseries/${encodeURIComponent(key)}.json`;
133
- const entry: SeriesRead = {
134
- at: Date.now(),
135
- points: null,
136
- read: readJson(url).then((payload) => {
137
- entry.points = parseSeriesPoints(payload);
138
- return entry.points;
139
- }),
140
- };
141
- seriesReads.set(id, entry);
142
- // A failed read is not kept, so the next one asks again.
143
- entry.read.catch(() => { if (seriesReads.get(id) === entry) seriesReads.delete(id); });
144
- return entry.read;
119
+ return seriesReads.load(`${seriesBreak}:${key}`, async () => parseSeriesPoints(await readJson(url)), { force });
145
120
  }
146
121
 
147
122
  /** A row's history, one read per series behind it, each kept for the session. */
@@ -158,9 +133,9 @@ export async function loadDealerHistory(
158
133
  function heldSeries(seriesBreak: string, row: Pick<DealerRow, "keys">): SeriesPoint[][] | null {
159
134
  const series: SeriesPoint[][] = [];
160
135
  for (const key of row.keys) {
161
- const known = seriesReads.get(`${seriesBreak}:${key}`);
162
- if (!known?.points || Date.now() - known.at >= HISTORY_TTL_MS) return null;
163
- series.push(known.points);
136
+ const points = seriesReads.peek(`${seriesBreak}:${key}`);
137
+ if (!points) return null;
138
+ series.push(points);
164
139
  }
165
140
  return series;
166
141
  }
@@ -178,6 +153,6 @@ export function cachedDealerHistory(seriesBreak: string, row: Pick<DealerRow, "i
178
153
 
179
154
  /** Forgets what was read, for a test or a refresh that must see the release again. */
180
155
  export function resetPrimaryDealersCache(): void {
181
- seriesBreakRead = null;
156
+ seriesBreaks.clear();
182
157
  seriesReads.clear();
183
158
  }
@@ -21,13 +21,13 @@ export const realizedVolModule: PluginModule = {
21
21
  { ...createTickerSurfacePaneTemplate({
22
22
  id: "realized-vol-graph-pane", paneId: "realized-vol", label: "Realized Volatility Graph",
23
23
  description: "Rolling realized volatility, price and a dated current ATM IV reference.",
24
- keywords: ["hvg", "realized", "historical", "volatility", "gv"], shortcut: "HVG", publicShare: true,
24
+ keywords: ["hvg", "realized", "historical", "volatility", "vol", "gv"], shortcut: "HVG", publicShare: true,
25
25
  viewKey: "graph", settings: () => ({ initialView: "graph" }),
26
26
  }), headless: realizedVolHeadless("graph") },
27
27
  { ...createTickerSurfacePaneTemplate({
28
28
  id: "realized-vol-table-pane", paneId: "realized-vol", label: "Realized Volatility Cone",
29
29
  description: "Current realized volatility against its historical range and percentile.",
30
- keywords: ["hvt", "volatility", "cone", "percentile"], shortcut: "HVT", publicShare: true,
30
+ keywords: ["hvt", "volatility", "vol", "cone", "percentile"], shortcut: "HVT", publicShare: true,
31
31
  viewKey: "cone", settings: () => ({ initialView: "cone" }),
32
32
  }), headless: realizedVolHeadless("cone") },
33
33
  ],