@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -2,8 +2,8 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { resolveHeadlessInstrument } from "../shared/headless-market-data";
3
3
  import { createRealizedVolatilityDependencies, loadRealizedVolatilityHistory } from "../realized-vol/client";
4
4
  import { createHvDependencies, loadIvHistory, loadIvScreen, loadScreenVolatilities } from "./client";
5
- import { formatPoints, formatRank, formatSpot, formatStat, formatVol, IMPLIED_VOL_WORDS, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
6
- import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, screenCoverage, VCA_LIMIT, VCA_PRESETS } from "./model";
5
+ import { formatPoints, formatRank, formatSpot, formatStat, formatVol, IMPLIED_VOL_WORDS, readingLabel, sharedDates, skewHeader, statSource, vcaDefinitions, verdictLabel } from "./format";
6
+ import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, screenCoverage, screenStaleNote, VCA_LIMIT, VCA_PRESETS } from "./model";
7
7
  import { vcaUniverse } from "./universe";
8
8
  import { offsetText, releaseOffsetRows } from "./events";
9
9
  import { loadMacroReleases } from "../macro-day/client";
@@ -16,6 +16,7 @@ import type { HeadlessPaneContext } from "../../../types/headless";
16
16
  import { zonedDateKey } from "../../../utils/zoned-date-time";
17
17
  import { listingFateNotes, listingFateReport, loadListingFate, MovedSymbolError } from "../../../tickers/listing-fate";
18
18
  import { startTickerChangeLookup } from "../../../tickers/ticker-changes";
19
+ import { fxVolatilityRefusal } from "../../../market-data/options-alternatives";
19
20
 
20
21
  const METHODOLOGY = "docs/research-data.md#implied-volatility-history";
21
22
  const percent = (value: unknown) => typeof value === "number" ? formatVol(value) : "--";
@@ -24,6 +25,7 @@ const points = (value: unknown) => typeof value === "number" ? formatPoints(valu
24
25
  export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
25
26
  shape: "bundle", argument: { kind: "ticker", description: "US option underlying" },
26
27
  usListingsOnly: "Implied volatility history",
28
+ refuseSymbol: fxVolatilityRefusal("HIVG"),
27
29
  freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date", oldest: null },
28
30
  describe: (args) => `HIVG ${args.symbols[0] ?? ""}`,
29
31
  discovery: { aliases: ["HIVG"], suggestFor: IMPLIED_VOL_WORDS, screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
@@ -79,7 +81,7 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
79
81
  // With nothing served, the fate is why: no "not a ticker" or missing-history errors beside it.
80
82
  const { notes: fateNotes, errors } = listingFateReport(fate, { served },
81
83
  [prices.error, ...model.warnings].filter((value): value is string => !!value));
82
- const notes = [...fateNotes, ...[model.oneYearNote, model.skewNote].filter((note): note is string => !!note)];
84
+ const notes = [...fateNotes, ...[model.oneYearNote, model.skewNote, model.staleNote].filter((note): note is string => !!note)];
83
85
  if (event && releases) {
84
86
  const lookback = String(args.options.lookback) as IvLookback;
85
87
  const label = MACRO_EVENT_LABELS[event];
@@ -117,18 +119,27 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
117
119
  releaseSource: MACRO_RELEASE_SOURCES[event], unit: "decimal annualized volatility", methodology: METHODOLOGY },
118
120
  };
119
121
  }
122
+ // The text report dates a stale measure "stale"; exports keep the bare date and say it in `stale`.
123
+ const statColumns = [
124
+ { key: "label", header: "Measure" },
125
+ { key: "value", header: "Current", format: (value: unknown, row?: unknown) => formatStat(value as number | null, (row as IvStatRow).unit) },
126
+ { key: "date", header: "As of" }, { key: "method", header: "Method" },
127
+ { key: "low", header: "52w low", format: (value: unknown, row?: unknown) => formatStat(value as number | null, (row as IvStatRow).unit) },
128
+ { key: "high", header: "52w high", format: (value: unknown, row?: unknown) => formatStat(value as number | null, (row as IvStatRow).unit) },
129
+ { key: "rank", header: "Rank", format: (value: unknown) => formatRank(value as number | null) },
130
+ { key: "percentile", header: "Pctl", format: (value: unknown) => formatRank(value as number | null) },
131
+ { key: "samples", header: "Sessions" },
132
+ ];
133
+ const statTextColumns = statColumns.map((column) => column.key === "date"
134
+ ? { ...column, textFormat: (value: unknown, row: unknown) => typeof value !== "string" ? "-" : (row as IvStatRow).stale ? `${value} stale` : value }
135
+ : column.key === "method"
136
+ // The wording the pane's source column uses; exports keep the raw method and carry the basis.
137
+ ? { ...column, textFormat: (value: unknown, row: unknown) => typeof value !== "string" ? "-" : statSource(row as IvStatRow) }
138
+ : column);
120
139
  return {
121
140
  sections: [
122
- { title: "Statistics", columns: [
123
- { key: "label", header: "Measure" },
124
- { key: "value", header: "Current", format: (value: unknown, row?: unknown) => formatStat(value as number | null, (row as IvStatRow).unit) },
125
- { key: "date", header: "As of" }, { key: "method", header: "Method" },
126
- { key: "low", header: "52w low", format: (value: unknown, row?: unknown) => formatStat(value as number | null, (row as IvStatRow).unit) },
127
- { key: "high", header: "52w high", format: (value: unknown, row?: unknown) => formatStat(value as number | null, (row as IvStatRow).unit) },
128
- { key: "rank", header: "Rank", format: (value: unknown) => formatRank(value as number | null) },
129
- { key: "percentile", header: "Pctl", format: (value: unknown) => formatRank(value as number | null) },
130
- { key: "samples", header: "Sessions" },
131
- ], rows: model.stats.map((row) => ({ ...row })) },
141
+ { title: "Statistics", columns: [...statColumns, { key: "basis", header: "Basis" }, { key: "stale", header: "Stale" }], textColumns: statTextColumns,
142
+ rows: model.stats.map((row) => ({ ...row })) },
132
143
  // Exports always carry the skew columns, so a script's header never moves; the text table drops them when none has a value.
133
144
  { title: "Daily history", columns: [...historyColumns, ...skewColumns], rows,
134
145
  ...(rows.some((row) => row.skew25d != null) ? {} : { textColumns: historyColumns }) },
@@ -151,6 +162,7 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
151
162
  freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date" },
152
163
  argument: { kind: "symbol-list", optional: true, maximum: VCA_LIMIT, description: "US option underlyings; defaults to index and sector ETFs." },
153
164
  usListingsOnly: "Implied volatility",
165
+ refuseSymbol: fxVolatilityRefusal("VCA"),
154
166
  options: [{ key: "preset", type: "enum", values: [{ value: "etfs" }, { value: "megacaps" }], defaultValue: "etfs", description: "Preset when no symbols are given" }],
155
167
  describe: "Volatility rich/cheap",
156
168
  // The pane names its universe `scope`; symbols typed open their own (custom), which a preset does not replace.
@@ -180,14 +192,15 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
180
192
  // A listing that stopped trading has its row's note, not a line saying it has no options history.
181
193
  const coverage = screenCoverage(rows, { explained: new Set([...noted].filter(([, entry]) => entry.fate.endedOn).map(([symbol]) => symbol)) });
182
194
  const dates = richCheapDates(rows);
183
- const hasSkew = rows.some((row) => row.skew != null);
195
+ const hasSkew = rows.some((row) => row.riskReversal != null);
184
196
  const hasIv1y = rows.some((row) => row.iv1y != null), hasIv1yRank = rows.some((row) => row.iv1yPercentile != null);
185
197
  const dated = (value: unknown) => typeof value === "string" ? value : "--";
186
198
  return {
187
199
  sections: [{ title: ["Rich/cheap", universe.label, ...sharedDates(dates)].join(" · "), columns: [
188
200
  { key: "symbol", header: "Symbol" }, { key: "iv30", header: "IV30", format: percent },
189
201
  ...(dates.reading ? [] : [{ key: "date", header: "As of", format: (value: unknown, row?: unknown) =>
190
- typeof value === "string" ? readingLabel(value, (row as RichCheapRow).method) : "--" }]),
202
+ typeof value === "string" ? readingLabel(value, (row as RichCheapRow).method, false,
203
+ { basis: (row as RichCheapRow).basis, stale: (row as RichCheapRow).stale }) : "--" }]),
191
204
  // The underlying price each reading was taken at, on the date the title or the As of column gives.
192
205
  { key: "spot", header: "Spot", format: (value: unknown) => typeof value === "number" ? formatSpot(value) : "--" },
193
206
  ...(dates.rankApart ? [{ key: "rankDate", header: "Rank as of", format: (value: unknown) => typeof value === "string" ? `${value} close` : "--" }] : []),
@@ -197,16 +210,18 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
197
210
  ...(hasIv1y ? [{ key: "iv1y", header: "IV1Y", format: percent }] : []),
198
211
  ...(hasIv1yRank ? [{ key: "iv1yPercentile", header: "IVP1Y", format: (value: unknown) => formatRank(value as number | null) }] : []),
199
212
  { key: "termSlope", header: "30-90", format: (value: unknown) => formatPoints(value as number | null) },
200
- ...(hasSkew ? [{ key: "skew", header: skewHeader("25D skew", "put-call"), format: (value: unknown) => formatPoints(value as number | null) }] : []),
201
- ...(hasSkew && dates.skewApart ? [{ key: "skewDate", header: "Skew as of", format: dated }] : []),
213
+ ...(hasSkew ? [{ key: "riskReversal", header: skewHeader("25D RR", "call-put"), format: (value: unknown) => formatPoints(value as number | null) }] : []),
214
+ ...(hasSkew && dates.skewApart ? [{ key: "skewDate", header: "RR as of", format: dated }] : []),
202
215
  { key: "hv", header: "HV20", format: percent },
216
+ // Said only where an HV is not the one of its IV's date: a stale reading's, or one a few sessions off.
217
+ ...(dates.hvApart ? [{ key: "hvDate", header: "HV as of", format: dated }] : []),
203
218
  { key: "ivHv", header: "IV/HV", format: (value: unknown) => typeof value === "number" ? value.toFixed(2) : "--" },
204
219
  ...(noted.size > 0 ? [{ key: "note", header: "Note", underRow: true }] : []),
205
220
  ], rows: rows.map((row) => ({ ...row, ...noted.has(row.symbol) ? { note: noted.get(row.symbol)!.note } : {} })) }],
206
221
  // A symbol with no options history is final; only history still to come leaves the screen incomplete.
207
222
  complete: !coverage.pending.length && !universe.error, unavailableSymbols: coverage.missing,
208
223
  errors: [universe.error].filter((value): value is string => !!value),
209
- notes: [...coverage.notes, ...VCA_DEFINITIONS],
224
+ notes: [...coverage.notes, ...[screenStaleNote(rows)].filter((note): note is string => !!note), ...vcaDefinitions({ skew: hasSkew })],
210
225
  metadata: { presets: VCA_PRESETS, methodology: METHODOLOGY },
211
226
  };
212
227
  },
@@ -32,13 +32,13 @@ export const ivHistoryModule: PluginModule = {
32
32
  { ...createTickerSurfacePaneTemplate({
33
33
  id: "iv-history-pane", paneId: "iv-history", label: "Implied Volatility History",
34
34
  description: "30-day, 90-day and one-year ATM implied volatility since 2024 against realized, with IV rank and percentile.",
35
- keywords: ["hivg", "implied", "volatility", "history", "iv rank", "ivr", "percentile", "vrp", "iv1y", "leaps"], shortcut: "HIVG", publicShare: true,
35
+ keywords: ["hivg", "implied", "volatility", "vol", "history", "iv rank", "ivr", "percentile", "vrp", "iv1y", "leaps"], shortcut: "HIVG", publicShare: true,
36
36
  }), headless: ivHistoryHeadless },
37
37
  {
38
38
  id: "iv-screen-pane", paneId: "iv-screen", label: "Volatility Rich/Cheap",
39
39
  description: "IV rank, percentile, one-year IV, term slope, skew and IV/HV across a list, ranked rich to cheap.",
40
40
  // Where a put seller finds rich premium; CSP and OSA own the exact put-selling phrases and rank first on them.
41
- keywords: ["vca", "rich", "cheap", "iv rank", "implied volatility", "screen", "cash-secured puts", "selling puts", "short-put candidates",
41
+ keywords: ["vca", "vol", "rich", "cheap", "iv rank", "implied volatility", "screen", "cash-secured puts", "selling puts", "short-put candidates",
42
42
  "csp candidates", "wheel candidates"],
43
43
  shortcut: { prefix: "VCA", argKind: "ticker-list" as const, argOptional: true }, headless: ivScreenHeadless,
44
44
  createInstance: (_context, options) => ({
@@ -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 ? <>