@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
@@ -1,6 +1,7 @@
1
+ import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
1
2
  import type { PricePoint } from "../../../types/financials";
2
3
  import { rollingRealizedVolatility } from "../shared/volatility";
3
- import type { IvCoverageStatus, IvHistoryPayload, IvMethod, IvPoint, IvScreenRow, IvStats } from "./client";
4
+ import type { IvCloseBasis, IvCoverageStatus, IvHistoryPayload, IvMethod, IvPoint, IvScreenRow, IvStats, ScreenHv } from "./client";
4
5
 
5
6
  /**
6
7
  * A history's warnings in words for the reader. A symbol demand coverage could
@@ -18,6 +19,75 @@ export type IvLookback = "1Y" | "2Y" | "ALL";
18
19
  const RICH_PERCENTILE = 80;
19
20
  const CHEAP_PERCENTILE = 20;
20
21
 
22
+ /** A reading trailing the symbol's newest session by more sessions than this is stale. */
23
+ const STALE_AFTER_SESSIONS = 5;
24
+ const DAY_MS = 86_400_000;
25
+
26
+ /** Whether `date` ("2026-10-23") is a US equity session: the published NYSE calendar where it covers, else a weekday. */
27
+ function isSession(date: string): boolean {
28
+ const published = getPublishedUsEquityCalendarDay("NYSE", date);
29
+ if (published) return published === "session";
30
+ const weekday = new Date(`${date}T00:00:00Z`).getUTCDay();
31
+ return weekday !== 0 && weekday !== 6;
32
+ }
33
+
34
+ /** US sessions after `date` up to and including `through`: how far behind that session a reading is. */
35
+ export function sessionsBehind(date: string, through: string): number {
36
+ const end = Date.parse(`${through}T00:00:00Z`);
37
+ let behind = 0;
38
+ for (let time = Date.parse(`${date}T00:00:00Z`) + DAY_MS; time <= end; time += DAY_MS) {
39
+ if (isSession(new Date(time).toISOString().slice(0, 10))) behind += 1;
40
+ }
41
+ return behind;
42
+ }
43
+
44
+ /** The newest of the dates given: the newest session the symbol's stored readings, backfill and prices reach. */
45
+ function newestSession(...dates: ReadonlyArray<string | null | undefined>): string | null {
46
+ return dates.filter((date): date is string => !!date).map((date) => date.slice(0, 10)).sort().at(-1) ?? null;
47
+ }
48
+
49
+ /** How many sessions a reading is behind the newest, and whether that makes it stale. */
50
+ function staleness(date: string | null | undefined, newest: string | null): { sessionsBehind: number | null; stale: boolean } {
51
+ const behind = date && newest ? sessionsBehind(date.slice(0, 10), newest) : null;
52
+ return { sessionsBehind: behind, stale: behind != null && behind > STALE_AFTER_SESSIONS };
53
+ }
54
+
55
+ /** An HV further than this many sessions from the IV's date says nothing about it: IV/HV is left empty. */
56
+ const HV_MAX_SESSIONS_APART = 3;
57
+
58
+ /**
59
+ * HV after the close of the last session on or before `date`, when that is within
60
+ * `HV_MAX_SESSIONS_APART` sessions of it; null before the series starts or when the
61
+ * nearest HV is older than that.
62
+ */
63
+ export function hvOn(hv: ScreenHv, date: string): { date: string; value: number } | null {
64
+ for (let index = hv.series.length - 1; index >= 0; index -= 1) {
65
+ const [sessionDate, value] = hv.series[index]!;
66
+ if (sessionDate <= date) return sessionsBehind(sessionDate, date) <= HV_MAX_SESSIONS_APART ? { date: sessionDate, value } : null;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * How a reading's method is worded wherever it is dated: "quote", "close", or "close mid" for a
73
+ * close solved from quote midpoints (mid or mixed). Two readings that word alike share one label.
74
+ */
75
+ export function readingSource(method: IvMethod | null | undefined, basis?: IvCloseBasis | null): string {
76
+ return method === "quote-mid" ? "quote" : basis === "mid" || basis === "mixed" ? "close mid" : "close";
77
+ }
78
+
79
+ const STALE_NOTE_LIMIT = 5;
80
+ /**
81
+ * The stale readings, dated, and what that leaves out, in one wording for HIVG and VCA: "Stale, left
82
+ * out of rank: IV 1Y ATM 2025-01-17 (over 5 sessions behind)." The list stops at five, then "N more".
83
+ */
84
+ export function staleReadingsNote(leftOut: string, items: ReadonlyArray<{ label: string; date: string }>): string | null {
85
+ if (!items.length) return null;
86
+ const listed = items.slice(0, STALE_NOTE_LIMIT).map((item) => `${item.label} ${item.date}`);
87
+ const more = items.length - listed.length;
88
+ return `Stale, left out of ${leftOut}: ${listed.join(", ")}${more > 0 ? `, ${more} more` : ""} (over ${STALE_AFTER_SESSIONS} sessions behind).`;
89
+ }
90
+
21
91
  export interface DatedValue { date: Date; value: number | null }
22
92
  export type IvStatUnit = "vol" | "points" | "ratio";
23
93
  /** One measure's latest value ranked against its own prior 52 weeks. */
@@ -28,11 +98,21 @@ export interface IvStatRow {
28
98
  value: number | null;
29
99
  date: string | null;
30
100
  method: IvMethod | "prices" | null;
101
+ /** Prices a trade close was solved from, when the server says (see `IvCloseBasis`). */
102
+ basis: IvCloseBasis | null;
31
103
  low: number | null;
32
104
  high: number | null;
33
105
  rank: number | null;
34
106
  percentile: number | null;
35
107
  samples: number;
108
+ /**
109
+ * The reading trails the symbol's newest session by more than `STALE_AFTER_SESSIONS`
110
+ * sessions: its rank and percentile are left out, since they would rank an old
111
+ * figure as though it were current.
112
+ */
113
+ stale: boolean;
114
+ /** Sessions between the reading and the symbol's newest session. */
115
+ sessionsBehind: number | null;
36
116
  }
37
117
  /** Rank and percentile need this many prior sessions, matching the server. */
38
118
  const MIN_RANK_SAMPLES = 120;
@@ -96,6 +176,8 @@ export interface IvHistoryModel {
96
176
  /** "skew from 2026-09-23", or why there is no skew at all. */
97
177
  skewNote: string | null;
98
178
  stats: IvStatRow[];
179
+ /** Which measures are stale and what that leaves out, when any is. */
180
+ staleNote: string | null;
99
181
  since: string | null;
100
182
  /** Where the 1Y series starts and how deep it is, when that differs from the rest or leaves it unranked. */
101
183
  oneYearNote: string | null;
@@ -127,15 +209,16 @@ function lookbackStart(asOf: string, lookback: IvLookback): string | null {
127
209
  /** The latest value against the 52 weeks before it, with the same rules as the server's IV rank. */
128
210
  function seriesStat(id: string, label: string, unit: IvStatUnit, points: readonly DatedValue[], method: IvStatRow["method"]): IvStatRow {
129
211
  const latest = [...points].reverse().find((point) => point.value != null);
130
- if (!latest) return { id, label, unit, value: null, date: null, method, low: null, high: null, rank: null, percentile: null, samples: 0 };
212
+ if (!latest) return { id, label, unit, value: null, date: null, method, basis: null, low: null, high: null, rank: null, percentile: null, samples: 0,
213
+ stale: false, sessionsBehind: null };
131
214
  const start = lookbackStart(day(latest.date), "1Y")!;
132
215
  const window = points.filter((point) => point.value != null && day(point.date) >= start && point.date < latest.date).map((point) => point.value!);
133
216
  const value = latest.value!;
134
217
  const low = window.length ? Math.min(value, ...window) : null, high = window.length ? Math.max(value, ...window) : null;
135
218
  const enough = window.length >= MIN_RANK_SAMPLES;
136
- return { id, label, unit, value, date: day(latest.date), method, low, high,
219
+ return { id, label, unit, value, date: day(latest.date), method, basis: null, low, high,
137
220
  rank: enough && high! > low! ? (value - low!) / (high! - low!) * 100 : null,
138
- percentile: midrankPercentile(window, value), samples: window.length };
221
+ percentile: midrankPercentile(window, value), samples: window.length, stale: false, sessionsBehind: null };
139
222
  }
140
223
 
141
224
  function byMethod(series: readonly IvPoint[], method: IvMethod, field: "iv30" | "iv90" | "iv365") {
@@ -192,25 +275,37 @@ export function projectIvHistory(
192
275
  return { date: point.date, value: point.value == null || iv90 == null || !(iv90 > 0) ? null : point.value / iv90 };
193
276
  });
194
277
  const fromServer = (id: string, label: string, stats: IvStats | null): IvStatRow => ({ id, label, unit: "vol",
195
- value: stats?.value ?? null, date: stats?.date ?? null, method: stats?.method ?? null, low: stats?.low ?? null, high: stats?.high ?? null,
196
- rank: stats?.rank ?? null, percentile: stats?.percentile ?? null, samples: stats?.samples ?? 0 });
278
+ value: stats?.value ?? null, date: stats?.date ?? null, method: stats?.method ?? null, basis: stats?.basis ?? null,
279
+ low: stats?.low ?? null, high: stats?.high ?? null,
280
+ rank: stats?.rank ?? null, percentile: stats?.percentile ?? null, samples: stats?.samples ?? 0, stale: false, sessionsBehind: null });
197
281
  const stats: IvStatRow[] = [fromServer("iv30", "IV 30d ATM", payload.stats.iv30)];
198
282
  // A newer quote capture is shown on its own row: its rank would mix methods.
199
283
  if (latest?.method === "quote-mid" && latest.iv30 != null && (!payload.stats.iv30 || latest.date > payload.stats.iv30.date)) {
200
- stats.push({ id: "iv30-quote", label: "IV 30d quote", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid",
201
- low: null, high: null, rank: null, percentile: null, samples: 0 });
284
+ stats.push({ id: "iv30-quote", label: "IV 30d quote", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid", basis: null,
285
+ low: null, high: null, rank: null, percentile: null, samples: 0, stale: false, sessionsBehind: null });
202
286
  }
203
287
  // IV1Y ranks trade closes only; until there is one, a stored quote gives its level, unranked.
204
288
  const oneYear = payload.stats.iv365 ?? null;
205
289
  stats.push(fromServer("iv90", "IV 90d ATM", payload.stats.iv90),
206
290
  !oneYear && latest?.iv365 != null
207
- ? { id: "iv365", label: "IV 1Y ATM", unit: "vol", value: latest.iv365, date: latest.date, method: latest.method,
208
- low: null, high: null, rank: null, percentile: null, samples: 0 }
291
+ ? { id: "iv365", label: "IV 1Y ATM", unit: "vol", value: latest.iv365, date: latest.date, method: latest.method, basis: latest.basis ?? null,
292
+ low: null, high: null, rank: null, percentile: null, samples: 0, stale: false, sessionsBehind: null }
209
293
  : fromServer("iv365", "IV 1Y ATM", oneYear),
210
294
  seriesStat("hv", `HV ${window}`, "vol", rolling, "prices"),
211
295
  seriesStat("spread", `IV 30d - HV ${window}`, "points", spreadAll, "trade-close"),
212
296
  seriesStat("term", "IV 30d / 90d", "ratio", termAll, "trade-close"));
213
297
 
298
+ // The newest session the symbol reaches, by its prices, its backfill and every reading it has: a reading more
299
+ // than STALE_AFTER_SESSIONS behind it is shown as it is, marked stale, and ranks nothing.
300
+ const newest = newestSession(payload.coverage?.backfilledThrough, latest?.date, latestDate,
301
+ ...payload.series.map((point) => point.sessionDate), ...prices.map((price) => day(price.date)));
302
+ const measured = stats.map((row): IvStatRow => {
303
+ const behind = staleness(row.date, newest);
304
+ return { ...row, ...behind, ...behind.stale ? { rank: null, percentile: null } : {} };
305
+ });
306
+ const staleRows = measured.filter((row) => row.stale);
307
+ const staleText = staleReadingsNote("rank", staleRows.map((row) => ({ label: row.label, date: row.date! })));
308
+
214
309
  // Worded as VCA words each symbol (screenCoverage).
215
310
  if (payload.status === "queued") warnings.push(`${payload.symbol} ${queuedText(payload.coverage?.addedAt, options.now ?? Date.now())}.`);
216
311
  else if (payload.status === "backfilling") {
@@ -232,7 +327,7 @@ export function projectIvHistory(
232
327
  iv30: visible(tradeIv30), iv90: visible(byMethod(payload.series, "trade-close", "iv90")), iv365: visible(tradeIv365),
233
328
  quoteIv30: visible(byMethod(payload.series, "quote-mid", "iv30")),
234
329
  hv, spread: visible(spreadAll), skew: visible(skew.points),
235
- skewSince: skew.points[0] ? day(skew.points[0].date) : null, skewNote: skew.note, stats,
330
+ skewSince: skew.points[0] ? day(skew.points[0].date) : null, skewNote: skew.note, stats: measured, staleNote: staleText,
236
331
  since, oneYearNote, asOf: latestDate, warnings,
237
332
  };
238
333
  }
@@ -251,6 +346,16 @@ export interface RichCheapRow {
251
346
  iv30: number | null;
252
347
  date: string | null;
253
348
  method: IvMethod | null;
349
+ /** Prices a trade close was solved from, when the server says (see `IvCloseBasis`). */
350
+ basis: IvCloseBasis | null;
351
+ /**
352
+ * The reading trails the symbol's newest session by more than `STALE_AFTER_SESSIONS`
353
+ * sessions. It is shown, marked stale, and left out of IV/HV, rank, percentile and
354
+ * Rich/Cheap, and so are its skew and IV1Y rank when they are as old.
355
+ */
356
+ stale: boolean;
357
+ /** Sessions between the reading and the symbol's newest session. */
358
+ sessionsBehind: number | null;
254
359
  /** The underlying price the reading was taken at: the session's close, or the spot at the quote capture. */
255
360
  spot: number | null;
256
361
  rank: number | null;
@@ -265,10 +370,18 @@ export interface RichCheapRow {
265
370
  * series has no reading that session, or there is no IV1Y beside it to describe.
266
371
  */
267
372
  iv1yPercentile: number | null;
268
- skew: number | null;
269
- /** Session of the quote capture the skew comes from. */
373
+ /** The 25-delta risk reversal at 30 days: call IV minus put IV, so positive means calls are richer, as OVDV signs it. */
374
+ riskReversal: number | null;
375
+ /** Session of the quote capture the risk reversal comes from. */
270
376
  skewDate: string | null;
377
+ /**
378
+ * Realized volatility on the reading's own date, so the row reads as IV30 over HV;
379
+ * the newest HV for a stale reading or one with no HV on its date.
380
+ */
271
381
  hv: number | null;
382
+ /** Session of the HV shown. */
383
+ hvDate: string | null;
384
+ /** IV30 over the HV of its own date; empty for a stale reading. */
272
385
  ivHv: number | null;
273
386
  verdict: RichCheap;
274
387
  }
@@ -280,24 +393,29 @@ export interface RichCheapRow {
280
393
  * is measured on. A group the rows disagree on gets its own column.
281
394
  */
282
395
  export interface RichCheapDates {
283
- reading: { date: string; method: IvMethod } | null;
396
+ reading: { date: string; method: IvMethod; basis: IvCloseBasis | null } | null;
284
397
  /** The trade close every ranked row is ranked on. */
285
398
  rank: string | null;
286
399
  /** Ranked rows are ranked on different closes. */
287
400
  rankApart: boolean;
288
- /** Some skew comes from a different session than its row's reading. */
401
+ /** Some risk reversal comes from a different session than its row's reading. */
289
402
  skewApart: boolean;
403
+ /** Some HV is dated on a different session than the IV it sits beside. */
404
+ hvApart: boolean;
290
405
  }
291
406
  export function richCheapDates(rows: readonly RichCheapRow[]): RichCheapDates {
292
407
  const read = rows.filter((row) => row.date && row.method);
293
408
  const ranked = rows.filter((row) => row.rankDate);
294
409
  const first = read[0], firstRanked = ranked[0];
295
410
  return {
296
- reading: first && read.every((row) => row.date === first.date && row.method === first.method)
297
- ? { date: first.date!, method: first.method! } : null,
411
+ // A stale row's own date is said in its row, never once for all.
412
+ // The label the rows would be dated with, not the raw basis: a mid and a mixed close are both "close mid".
413
+ reading: first && read.every((row) => row.date === first.date && readingSource(row.method, row.basis) === readingSource(first.method, first.basis) && !row.stale)
414
+ ? { date: first.date!, method: first.method!, basis: first.basis } : null,
298
415
  rank: firstRanked && ranked.every((row) => row.rankDate === firstRanked.rankDate) ? firstRanked.rankDate : null,
299
416
  rankApart: !!firstRanked && ranked.some((row) => row.rankDate !== firstRanked.rankDate),
300
- skewApart: rows.some((row) => row.skew != null && row.skewDate !== row.date),
417
+ skewApart: rows.some((row) => row.riskReversal != null && row.skewDate !== row.date),
418
+ hvApart: rows.some((row) => row.hv != null && row.hvDate != null && row.hvDate !== row.date),
301
419
  };
302
420
  }
303
421
 
@@ -307,22 +425,34 @@ export function verdictFor(percentile: number | null): RichCheap {
307
425
  return percentile >= RICH_PERCENTILE ? "rich" : percentile <= CHEAP_PERCENTILE ? "cheap" : "fair";
308
426
  }
309
427
 
310
- export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<string, number | null> = new Map()): RichCheapRow[] {
428
+ export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<string, ScreenHv | null> = new Map()): RichCheapRow[] {
311
429
  return rows.map((row) => {
312
430
  const latest = row.latest;
313
431
  const iv30 = latest?.iv30 ?? null;
314
- const realized = hv.get(row.symbol) ?? null;
315
- const percentile = row.iv30?.percentile ?? null;
432
+ const series = hv.get(row.symbol) ?? null;
433
+ // The newest session the symbol reaches: its prices, its backfill and every reading it has.
434
+ const newest = newestSession(row.coverage?.backfilledThrough, latest?.date, row.iv30?.date, row.iv90?.date, row.iv365?.date,
435
+ row.skew?.date, series?.date);
436
+ const { stale, sessionsBehind: behind } = staleness(latest?.date, newest);
437
+ // A rank on a close that old ranks an old figure; so does a skew.
438
+ const rankStale = stale || staleness(row.iv30?.date, newest).stale;
439
+ const skewStale = row.skew ? staleness(row.skew.date, newest).stale : false;
440
+ const percentile = rankStale ? null : row.iv30?.percentile ?? null;
441
+ // HV on the reading's own date: today's HV beside an old IV says nothing about it.
442
+ const atReading = series && !stale && latest?.date ? hvOn(series, latest.date) : null;
443
+ const shown = atReading ?? (series?.value != null && series.date ? { date: series.date, value: series.value } : null);
316
444
  return {
317
445
  symbol: row.symbol, status: row.coverageStatus ?? row.status, queuedAt: row.coverage?.addedAt ?? null,
318
446
  backfilledThrough: row.coverage?.backfilledThrough ?? null, warning: row.warnings?.[0] ?? null,
319
- iv30, date: latest?.date ?? null, method: latest?.method ?? null, spot: latest?.spot ?? null,
320
- rank: row.iv30?.rank ?? null, percentile, rankDate: row.iv30?.date ?? null,
447
+ iv30, date: latest?.date ?? null, method: latest?.method ?? null, basis: latest?.basis ?? null,
448
+ stale, sessionsBehind: behind, spot: latest?.spot ?? null,
449
+ rank: rankStale ? null : row.iv30?.rank ?? null, percentile, rankDate: rankStale ? null : row.iv30?.date ?? null,
321
450
  termSlope: iv30 != null && latest?.iv90 != null ? iv30 - latest.iv90 : null,
322
451
  iv1y: latest?.iv365 ?? null,
323
- iv1yPercentile: latest?.iv365 != null && row.iv365 && row.iv365.date === row.iv30?.date ? row.iv365.percentile : null,
324
- skew: row.skew?.skew ?? null, skewDate: row.skew?.date ?? null, hv: realized,
325
- ivHv: iv30 != null && realized != null && realized > 0 ? iv30 / realized : null,
452
+ iv1yPercentile: !rankStale && latest?.iv365 != null && row.iv365 && row.iv365.date === row.iv30?.date ? row.iv365.percentile : null,
453
+ riskReversal: skewStale || !row.skew ? null : row.skew.call25 - row.skew.put25, skewDate: skewStale ? null : row.skew?.date ?? null,
454
+ hv: shown?.value ?? null, hvDate: shown?.date ?? null,
455
+ ivHv: iv30 != null && atReading != null && atReading.value > 0 ? iv30 / atReading.value : null,
326
456
  verdict: verdictFor(percentile),
327
457
  };
328
458
  });
@@ -375,6 +505,11 @@ export function screenCoverage(
375
505
  missing: pending.filter((row) => row.iv30 == null && row.date == null).map((row) => row.symbol) };
376
506
  }
377
507
 
508
+ /** The symbols whose reading is stale, dated, and what that leaves out of the screen. */
509
+ export function screenStaleNote(rows: readonly RichCheapRow[]): string | null {
510
+ return staleReadingsNote("IV/HV, rank and Rich/Cheap", rows.flatMap((row) => row.stale && row.date ? [{ label: row.symbol, date: row.date }] : []));
511
+ }
512
+
378
513
  /**
379
514
  * The VCA pane's notices: the report's wording without a time to come back
380
515
  * at, since the pane checks again by itself, and a count of ready symbols with
@@ -383,7 +518,8 @@ export function screenCoverage(
383
518
  export function screenPaneNotices(rows: readonly RichCheapRow[], now = Date.now()): string[] {
384
519
  const empty = rows.filter((row) => row.status === "ready" && row.iv30 == null).length;
385
520
  return [...screenCoverage(rows, { now, retry: false }).notes,
386
- ...(empty ? [`${empty} symbol${empty === 1 ? " has" : "s have"} no stored implied volatility yet.`] : [])];
521
+ ...(empty ? [`${empty} symbol${empty === 1 ? " has" : "s have"} no stored implied volatility yet.`] : []),
522
+ ...[screenStaleNote(rows)].filter((note): note is string => !!note)];
387
523
  }
388
524
 
389
525
  export const VCA_PRESETS = {
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { listingIdentity } from "../shared/ticker-request";
3
3
  import { DataTableView, EmptyState, PaneStatusBody, QueryBar, usePaneFooter, usePaneNoticeFooter, usePaneTicker, type DataTableColumn } from "../../../components";
4
4
  import { instrumentFromTicker } from "../../../market-data/request-types";
5
+ import { fxVolatilityNote } from "../../../market-data/options-alternatives";
5
6
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
6
7
  import { useAsyncResource } from "../../../react/async-resource";
7
8
  import { useShortcut } from "../../../react/input";
@@ -24,7 +25,7 @@ const HV_OPTIONS = HV_WINDOWS.map((window) => ({ value: String(window), label: `
24
25
  const STAT_COLUMNS: DataTableColumn[] = [
25
26
  { id: "label", label: "Measure", width: 16, align: "left" },
26
27
  { id: "value", label: "Current", width: 9, align: "right" },
27
- { id: "date", label: "As of", width: 17, align: "left" },
28
+ { id: "date", label: "As of", width: 20, align: "left" },
28
29
  { id: "low", label: "52w low", width: 9, align: "right" },
29
30
  { id: "high", label: "52w high", width: 9, align: "right" },
30
31
  { id: "rank", label: "Rank", width: 6, align: "right" },
@@ -54,7 +55,9 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
54
55
  ]);
55
56
  return { payload, prices };
56
57
  }, [instrumentKey]);
57
- const resource = useAsyncResource(instrument ? loader : null);
58
+ // An FX pair has no implied volatility history: the pane says what stands in, and asks for nothing.
59
+ const fxNote = symbol ? fxVolatilityNote(symbol, ticker?.metadata.exchange) : null;
60
+ const resource = useAsyncResource(instrument && !fxNote ? loader : null);
58
61
  useEffect(() => () => controller.current?.abort(), [loader]);
59
62
  useAutoRefresh(resource.updatedAt, resource.load);
60
63
  const model = useMemo(() => resource.data
@@ -66,7 +69,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
66
69
  const timer = setTimeout(() => { void resource.reload(); }, 60_000);
67
70
  return () => clearTimeout(timer);
68
71
  }, [model?.status, resource.updatedAt]);
69
- const notices = [identityError, resource.error, resource.data?.prices.error, ...(model?.warnings ?? [])]
72
+ const notices = [identityError, resource.error, resource.data?.prices.error, ...(model?.warnings ?? []), model?.staleNote]
70
73
  .filter((value): value is string => !!value);
71
74
  usePaneNoticeFooter({ registrationId: "iv-history-notices", notices: [...new Set(notices)], focused });
72
75
  const cycleLookback = () => setLookback(lookback === "1Y" ? "2Y" : lookback === "2Y" ? "ALL" : "1Y");
@@ -104,8 +107,9 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
104
107
  const tone = row.percentile != null && (row.percentile >= 80 || row.percentile <= 20) ? colors.warning : colors.text;
105
108
  switch (id) {
106
109
  case "label": return { text: row.label, color: colors.textBright };
107
- case "value": return { text: formatStat(row.value, row.unit), color: row.id.startsWith("iv30") ? colors.warning : colors.textBright };
108
- case "date": return { text: row.date ? `${row.date} ${statSource(row)}` : "--", color: colors.textDim };
110
+ case "value": return { text: formatStat(row.value, row.unit), color: row.stale ? colors.textDim : row.id.startsWith("iv30") ? colors.warning : colors.textBright };
111
+ // A stale reading says so in place of how it was taken: it is no current figure either way.
112
+ case "date": return { text: row.date ? `${row.date} ${row.stale ? "stale" : statSource(row)}` : "--", color: row.stale ? colors.warning : colors.textDim };
109
113
  case "low": return { text: formatStat(row.low, row.unit), color: colors.textDim };
110
114
  case "high": return { text: formatStat(row.high, row.unit), color: colors.textDim };
111
115
  case "rank": return { text: formatRank(row.rank), color: tone };
@@ -119,7 +123,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
119
123
  { id: "lookback", label: "Lookback", value: lookback, options: LOOKBACKS, onChange: setLookback },
120
124
  { id: "realized", label: "Realized", value: String(hvWindow), options: HV_OPTIONS, onChange: setHvWindow },
121
125
  ]} />
122
- {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
126
+ {!symbol ? <EmptyState title="Select a ticker." /> : fxNote ? <PaneStatusBody empty emptyTitle={fxNote} /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
123
127
  error={!model ? resource.error ?? identityError ?? null : null} empty={!!model && !model.iv30.length && !model.quoteIv30.length}
124
128
  emptyTitle={model?.status === "queued" || model?.status === "backfilling" ? `Backfilling ${symbol} implied volatility history...` : undefined}>
125
129
  {model ? <>
@@ -8,14 +8,14 @@ import { Box, Text, useUiCapabilities } from "../../../ui";
8
8
  import { wrapTextLines } from "../../../utils/text-wrap";
9
9
  import { useAutoRefresh } from "../../../react/auto-refresh";
10
10
  import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
11
- import { loadIvScreen, loadScreenVolatilities } from "./client";
12
- import { formatPoints, formatRank, formatSpot, formatVol, readingLabel, sharedDates, shortDate, VCA_DEFINITIONS, verdictLabel } from "./format";
13
- import { projectRichCheap, type RichCheapRow, richCheapDates, screenPaneNotices, type VcaPreset } from "./model";
11
+ import { loadIvScreen, loadScreenVolatilities, type ScreenHv } from "./client";
12
+ import { formatPoints, formatRank, formatSpot, formatVol, readingLabel, sharedDates, shortDate, vcaDefinitions, verdictLabel } from "./format";
13
+ import { projectRichCheap, readingSource, type RichCheapRow, richCheapDates, screenPaneNotices, type VcaPreset } from "./model";
14
14
  import { vcaUniverse } from "./universe";
15
15
  import { MovedTickerState } from "../../../components/layout/pane/ticker-fate-gate";
16
16
  import type { MovedTickerChange } from "../../../tickers/ticker-changes";
17
17
 
18
- type SortId = keyof RichCheapRow;
18
+ type SortId = Exclude<keyof RichCheapRow, "stale">;
19
19
  const HV_WINDOW = 20;
20
20
  /** Pane borders and the footer's own padding. */
21
21
  const FOOTER_MARGIN = 6;
@@ -31,9 +31,10 @@ const COLUMNS: DataTableColumn[] = [
31
31
  { id: "iv1y", label: "IV1Y", width: 7, align: "right" },
32
32
  { id: "iv1yPercentile", label: "IVP1Y", width: 6, align: "right" },
33
33
  { id: "termSlope", label: "30-90", width: 7, align: "right" },
34
- { id: "skew", label: "25D skew", width: 9, align: "right" },
35
- { id: "skewDate", label: "Skew as of", width: 11, align: "right" },
34
+ { id: "riskReversal", label: "25D RR", width: 9, align: "right" },
35
+ { id: "skewDate", label: "RR as of", width: 11, align: "right" },
36
36
  { id: "hv", label: `HV${HV_WINDOW}`, width: 7, align: "right" },
37
+ { id: "hvDate", label: "HV as of", width: 9, align: "right" },
37
38
  { id: "ivHv", label: "IV/HV", width: 6, align: "right" },
38
39
  ];
39
40
 
@@ -49,7 +50,7 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
49
50
  const universe = useMemo(() => vcaUniverse(scope, symbolsText, collectionId, [...tickers.values()]), [scope, symbolsText, collectionId, tickers]);
50
51
  const key = universe.instruments.map((instrument) => instrument.symbol).join(",");
51
52
  const controller = useRef<AbortController | null>(null);
52
- const [hv, setHv] = useState<Map<string, number | null>>(new Map());
53
+ const [hv, setHv] = useState<Map<string, ScreenHv | null>>(new Map());
53
54
  // Symbols whose company moved: their rows have no HV and the footer says where each company went.
54
55
  const [moved, setMoved] = useState<Array<{ symbol: string; change: MovedTickerChange }>>([]);
55
56
  const loader = useCallback(async () => {
@@ -83,17 +84,21 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
83
84
  }, [resource.data, hv, sort]);
84
85
  const dates = useMemo(() => richCheapDates(rows), [rows]);
85
86
  // A date every row shares moves to the footer; a column no row fills, or whose dates agree, is left out.
87
+ const hasSkew = useMemo(() => rows.some((row) => row.riskReversal != null), [rows]);
88
+ // A close solved from quote midpoints names itself, which a date alone has no room for.
89
+ const dateWidth = useMemo(() => rows.some((row) => !row.stale && readingSource(row.method, row.basis) === "close mid") ? 15 : 11, [rows]);
86
90
  const columns = useMemo(() => COLUMNS.filter((column) => {
87
91
  switch (column.id) {
88
92
  case "date": return !dates.reading;
89
93
  case "rankDate": return dates.rankApart;
90
94
  case "iv1y": return rows.some((row) => row.iv1y != null);
91
95
  case "iv1yPercentile": return rows.some((row) => row.iv1yPercentile != null);
92
- case "skew": return rows.some((row) => row.skew != null);
96
+ case "riskReversal": return hasSkew;
93
97
  case "skewDate": return dates.skewApart;
98
+ case "hvDate": return dates.hvApart;
94
99
  default: return true;
95
100
  }
96
- }), [rows, dates]);
101
+ }).map((column) => column.id === "date" ? { ...column, width: dateWidth } : column), [rows, dates, hasSkew, dateWidth]);
97
102
  const [selected, setSelected] = useState<string | null>(null);
98
103
  const coverageNotices = useMemo(() => screenPaneNotices(rows), [rows]);
99
104
  // One symbol to screen whose company moved has nothing to show but where it went, as a ticker's panes say.
@@ -117,10 +122,10 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
117
122
  const cell = (row: RichCheapRow, id: string): { text: string; color?: string } => {
118
123
  switch (id) {
119
124
  case "symbol": return { text: row.symbol, color: colors.textBright };
120
- case "iv30": return { text: formatVol(row.iv30), color: colors.warning };
125
+ case "iv30": return { text: formatVol(row.iv30), color: row.stale ? colors.textDim : colors.warning };
121
126
  case "spot": return { text: formatSpot(row.spot), color: colors.textDim };
122
- case "date": return { text: row.date ? readingLabel(row.date, row.method, true)
123
- : row.status === "queued" || row.status === "backfilling" ? row.status : "--", color: colors.textDim };
127
+ case "date": return { text: row.date ? readingLabel(row.date, row.method, true, { basis: row.basis, stale: row.stale })
128
+ : row.status === "queued" || row.status === "backfilling" ? row.status : "--", color: row.stale ? colors.warning : colors.textDim };
124
129
  case "rankDate": return { text: row.rankDate ? `${shortDate(row.rankDate)} close` : "--", color: colors.textDim };
125
130
  case "rank": return { text: formatRank(row.rank) };
126
131
  case "percentile": return { text: formatRank(row.percentile) };
@@ -128,16 +133,18 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
128
133
  case "iv1y": return { text: formatVol(row.iv1y) };
129
134
  case "iv1yPercentile": return { text: formatRank(row.iv1yPercentile) };
130
135
  case "termSlope": return { text: formatPoints(row.termSlope), color: row.termSlope != null && row.termSlope > 0 ? colors.negative : colors.text };
131
- case "skew": return { text: formatPoints(row.skew) };
136
+ case "riskReversal": return { text: formatPoints(row.riskReversal) };
132
137
  case "skewDate": return { text: row.skewDate ? readingLabel(row.skewDate, "quote-mid", true) : "--", color: colors.textDim };
133
138
  case "hv": return { text: hv.has(row.symbol) ? formatVol(row.hv) : "...", color: colors.positive };
139
+ case "hvDate": return { text: row.hvDate ? shortDate(row.hvDate) : "--", color: colors.textDim };
134
140
  case "ivHv": return { text: row.ivHv == null ? "--" : row.ivHv.toFixed(2) };
135
141
  default: return { text: "" };
136
142
  }
137
143
  };
138
144
  const custom = scope === "custom";
139
145
  // The skew's sign and VIX beside IV30 are units of the figures above them, so they stay under the table.
140
- const definitions = useMemo(() => VCA_DEFINITIONS.flatMap((line) => wrapTextLines(line, Math.max(1, width - 2))), [width]);
146
+ const definitionLines = useMemo(() => vcaDefinitions({ skew: hasSkew }), [hasSkew]);
147
+ const definitions = useMemo(() => definitionLines.flatMap((line) => wrapTextLines(line, Math.max(1, width - 2))), [definitionLines, width]);
141
148
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
142
149
  <QueryBar width={width} filters={[
143
150
  { id: "universe", label: "Universe", value: VCA_SCOPE_OPTIONS.some((option) => option.value === scope) ? scope : "etfs",
@@ -159,7 +166,7 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
159
166
  {nativePaneChrome ? (
160
167
  // The desktop and the web wrap each definition at the pane's width, under a table that fills the rest.
161
168
  <Box flexDirection="column" paddingX={1} flexShrink={0}>
162
- {VCA_DEFINITIONS.map((line) => <Prose key={line} text={line} width={Math.max(1, width - 2)} color={colors.textDim} figures={false} />)}
169
+ {definitionLines.map((line) => <Prose key={line} text={line} width={Math.max(1, width - 2)} color={colors.textDim} figures={false} />)}
163
170
  </Box>
164
171
  ) : (
165
172
  <Box flexDirection="column" paddingX={1} height={definitions.length} flexShrink={0}>
@@ -1,3 +1,4 @@
1
+ import { fxVolatilityNote } from "../../../market-data/options-alternatives";
1
2
  import type { InstrumentRef } from "../../../market-data/request-types";
2
3
  import type { TickerRecord } from "../../../types/ticker";
3
4
  import { canonicalExchange } from "../../../utils/exchanges";
@@ -33,7 +34,10 @@ export function vcaUniverse(scope: string, symbolsText: string, collectionId: st
33
34
  instruments = VCA_PRESETS[preset].map((symbol) => ({ symbol, exchange: "" }));
34
35
  }
35
36
  const valid = [...new Map(instruments.filter((instrument) => SYMBOL.test(instrument.symbol)).map((instrument) => [instrument.symbol, instrument])).values()];
36
- const error = !valid.length ? scope === "collection" ? "Link a watchlist or portfolio with US-listed symbols in pane settings." : "Add symbols in pane settings."
37
+ // An FX pair or currency future typed in a list is left out with what stands in for it, not as a symbol that does not parse.
38
+ const fx = scope === "custom" ? instruments.flatMap((instrument) => fxVolatilityNote(instrument.symbol, instrument.exchange) ?? []) : [];
39
+ const error = !valid.length ? fx[0] ?? (scope === "collection" ? "Link a watchlist or portfolio with US-listed symbols in pane settings." : "Add symbols in pane settings.")
40
+ : fx.length ? fx[0]!
37
41
  : valid.length > VCA_LIMIT ? `Only the first ${VCA_LIMIT} symbols are screened.`
38
42
  : skipped ? `${skipped} non-US listing${skipped === 1 ? "" : "s"} left out: stored implied volatility covers US option underlyings.` : null;
39
43
  return { label, instruments: valid.slice(0, VCA_LIMIT), error };
@@ -1,4 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { createTtlCache } from "../../../data/ttl-cache";
2
3
  import { ApiRequestError } from "../../../api-client/errors";
3
4
  import type {
4
5
  CloudJobsMoversPayload,
@@ -25,43 +26,38 @@ export type JobsCompanyState =
25
26
  | { kind: "denied"; status: 401 | 402 | 403; message: string };
26
27
 
27
28
  const SUMMARY_TTL_MS = 5 * 60_000;
28
- const summaryCache = new Map<string, { at: number; state: JobsCompanyState }>();
29
-
30
- function cachedJobs(ticker: string): JobsCompanyState | null {
31
- const entry = summaryCache.get(ticker.toUpperCase());
32
- if (!entry || Date.now() - entry.at > SUMMARY_TTL_MS) return null;
33
- return entry.state;
34
- }
29
+ /**
30
+ * `pending` is never kept, so the pane's poll reads it afresh each time, and
31
+ * `denied` is not either: the account can change under the pane.
32
+ */
33
+ const summaries = createTtlCache<JobsCompanyState>({
34
+ ttlMs: SUMMARY_TTL_MS,
35
+ keep: (state) => state.kind === "ready" || state.kind === "uncovered",
36
+ });
35
37
 
36
- export async function fetchJobs(
37
- ticker: string,
38
- options: { name?: string | null; force?: boolean; client?: JobsCloudClient } = {},
39
- ): Promise<JobsCompanyState> {
40
- const key = ticker.toUpperCase();
41
- if (!options.force) {
42
- const cached = cachedJobs(key);
43
- // A pending answer is never served from cache: the point is to poll.
44
- if (cached && cached.kind !== "pending") return cached;
45
- }
46
- const client = options.client ?? apiClient;
47
- let state: JobsCompanyState;
38
+ async function loadJobs(key: string, name: string | null | undefined, client: JobsCloudClient): Promise<JobsCompanyState> {
48
39
  try {
49
- const response: CloudJobsResponse = await client.getCloudJobs(key, options.name);
50
- state = response.status === "pending"
40
+ const response: CloudJobsResponse = await client.getCloudJobs(key, name);
41
+ return response.status === "pending"
51
42
  ? { kind: "pending", ticker: key, message: response.message }
52
43
  : { kind: "ready", summary: response };
53
44
  } catch (error) {
54
45
  if (error instanceof ApiRequestError && error.status === 404) {
55
- state = { kind: "uncovered", ticker: key, message: error.message };
56
- } else if (error instanceof ApiRequestError && (error.status === 401 || error.status === 402 || error.status === 403)) {
57
- // Not cached: the account can change under the pane.
46
+ return { kind: "uncovered", ticker: key, message: error.message };
47
+ }
48
+ if (error instanceof ApiRequestError && (error.status === 401 || error.status === 402 || error.status === 403)) {
58
49
  return { kind: "denied", status: error.status, message: error.message };
59
- } else {
60
- throw error;
61
50
  }
51
+ throw error;
62
52
  }
63
- summaryCache.set(key, { at: Date.now(), state });
64
- return state;
53
+ }
54
+
55
+ export function fetchJobs(
56
+ ticker: string,
57
+ options: { name?: string | null; force?: boolean; client?: JobsCloudClient } = {},
58
+ ): Promise<JobsCompanyState> {
59
+ const key = ticker.toUpperCase();
60
+ return summaries.load(key, () => loadJobs(key, options.name, options.client ?? apiClient), { force: options.force });
65
61
  }
66
62
 
67
63
  export function fetchJobsPostings(
@@ -81,5 +77,5 @@ export function fetchJobsMovers(
81
77
  }
82
78
 
83
79
  export function resetJobsCache(): void {
84
- summaryCache.clear();
80
+ summaries.clear();
85
81
  }