gloomberb 0.16.3 → 0.16.4

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 (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -3,7 +3,8 @@ import { CompositeChart } from "../../../components";
3
3
  import { resolveChartPalette } from "../../../components/chart/core/palette";
4
4
  import { formatCompositeSeriesValue } from "../../../components/chart/composite/format";
5
5
  import { staticSeries } from "../../../components/chart/static/series";
6
- import { useThemeColors } from "../../../theme/theme-context";
6
+ import { useThemeColors, useThemeId } from "../../../theme/theme-context";
7
+ import { themeSeriesColors } from "../../../theme/series-colors";
7
8
  import type { ResolvedSeries } from "../../../time-series/types";
8
9
  import { formatPoints } from "./format";
9
10
  import type { DatedValue, IvHistoryModel } from "./model";
@@ -11,8 +12,15 @@ import type { DatedValue, IvHistoryModel } from "./model";
11
12
  const PANELS = [{ id: "vol", height: 3 }, { id: "spread", height: 1 }];
12
13
  /** The chart drawing blue: IV90 must read apart from amber IV30 in every theme. */
13
14
  const IV90_COLOR = "#4c9aff";
14
- /** The live capture point must stand apart from the amber IV30 line it continues. */
15
- const LIVE_COLOR = "#c084fc";
15
+ /** The quote capture point must stand apart from the amber IV30 line it continues. */
16
+ const QUOTE_COLOR = "#c084fc";
17
+ /** IV1Y: pink, apart from amber IV30, blue IV90, green HV and the violet quote points. */
18
+ const IV365_COLOR = "#f783ac";
19
+ /**
20
+ * Palette slots for a theme with its own series colours (the colour-blind
21
+ * ones), where the up colour HV takes elsewhere is the same blue as IV90.
22
+ */
23
+ const THEMED_SLOTS = { iv30: 1, iv90: 0, hv: 2, quote: 5, iv365: 4 } as const;
16
24
 
17
25
  function volSeries(id: string, label: string, color: string, points: readonly DatedValue[], panelId: string,
18
26
  style: ResolvedSeries["style"] = "line"): ResolvedSeries {
@@ -27,22 +35,28 @@ function formatLegendValue(value: number, series: ResolvedSeries): string {
27
35
  return series.id === "spread" ? `${formatPoints(value / 100)} pts` : formatCompositeSeriesValue(value, series);
28
36
  }
29
37
 
30
- function ivHistorySeries(model: IvHistoryModel, colors: { iv30: string; iv90: string; hv: string; quote: string; spread: string },
38
+ function ivHistorySeries(model: IvHistoryModel, colors: { iv30: string; iv90: string; iv365: string; hv: string; quote: string; spread: string },
31
39
  hvLabel: string): ResolvedSeries[] {
32
40
  return [
33
41
  volSeries("iv30", "IV 30d", colors.iv30, model.iv30, "vol"),
34
42
  volSeries("iv90", "IV 90d", colors.iv90, model.iv90, "vol"),
43
+ // Drawn only once there is a 1Y close, so an empty legend entry never suggests one.
44
+ ...(model.iv365.length ? [volSeries("iv365", "IV 1Y", colors.iv365, model.iv365, "vol")] : []),
35
45
  volSeries("hv", hvLabel, colors.hv, model.hv, "vol"),
36
- ...(model.quoteIv30.length ? [volSeries("iv30-quote", "IV 30d live", colors.quote, model.quoteIv30, "vol", "points")] : []),
46
+ ...(model.quoteIv30.length ? [volSeries("iv30-quote", "IV 30d quote", colors.quote, model.quoteIv30, "vol", "points")] : []),
37
47
  { ...volSeries("spread", `IV 30d - ${hvLabel}`, colors.spread, model.spread, "spread", "columns"), ...SPREAD_UNIT },
38
48
  ];
39
49
  }
40
50
 
41
51
  export function IvHistoryChart({ model, width, height, hvLabel, focused = false }: { model: IvHistoryModel; width: number; height: number; hvLabel: string; focused?: boolean }) {
42
52
  const colors = useThemeColors();
53
+ const themed = themeSeriesColors(useThemeId());
43
54
  const palette = resolveChartPalette(colors);
44
- const series = useMemo(() => ivHistorySeries(model, { iv30: colors.warning, iv90: IV90_COLOR, hv: colors.positive,
45
- quote: LIVE_COLOR, spread: colors.textDim }, hvLabel), [model, colors, hvLabel]);
55
+ const series = useMemo(() => ivHistorySeries(model, themed
56
+ ? { iv30: themed[THEMED_SLOTS.iv30]!, iv90: themed[THEMED_SLOTS.iv90]!, iv365: themed[THEMED_SLOTS.iv365]!, hv: themed[THEMED_SLOTS.hv]!,
57
+ quote: themed[THEMED_SLOTS.quote]!, spread: colors.textDim }
58
+ : { iv30: colors.warning, iv90: IV90_COLOR, iv365: IV365_COLOR, hv: colors.positive, quote: QUOTE_COLOR, spread: colors.textDim },
59
+ hvLabel), [model, colors, themed, hvLabel]);
46
60
  return <CompositeChart series={series} panels={PANELS} width={width} height={height} focused={focused} navigable={false} showLegend showTimeAxis formatValue={formatLegendValue}
47
61
  remoteKind="implied-volatility-history"
48
62
  colors={{ background: palette.bgColor, grid: palette.gridColor, crosshair: palette.crosshairColor, text: colors.text,
@@ -33,6 +33,10 @@ export interface IvStats {
33
33
  high: number | null;
34
34
  samples: number;
35
35
  windowStart: string | null;
36
+ /** Prior sessions the rank needs: 120, or 60 for IV1Y. Absent from older servers. */
37
+ minSamples?: number;
38
+ /** Why rank or percentile is empty, when one is. */
39
+ rankNote?: string | null;
36
40
  }
37
41
  interface IvReading {
38
42
  date: string;
@@ -51,8 +55,12 @@ export interface IvHistoryPayload {
51
55
  symbol: string;
52
56
  asOf: string;
53
57
  status: IvCoverageStatus;
54
- coverage: { source: "seed" | "demand"; addedAt: string; backfilledThrough: string | null; since: string | null } | null;
55
- stats: { iv30: IvStats | null; iv90: IvStats | null };
58
+ coverage: {
59
+ source: "seed" | "demand"; addedAt: string; backfilledThrough: string | null; since: string | null;
60
+ /** First trade close with a one-year reading; the 1Y series starts later and is sparser. */
61
+ iv365Since?: string | null;
62
+ } | null;
63
+ stats: { iv30: IvStats | null; iv90: IvStats | null; iv365?: IvStats | null };
56
64
  latest: IvReading | null;
57
65
  series: IvPoint[];
58
66
  warnings: string[];
@@ -62,6 +70,8 @@ export interface IvScreenRow {
62
70
  status: "ready" | "queued";
63
71
  iv30: IvStats | null;
64
72
  iv90: IvStats | null;
73
+ iv365?: IvStats | null;
74
+ iv365Since?: string | null;
65
75
  latest: IvReading | null;
66
76
  skew: { date: string; put25: number; call25: number; skew: number } | null;
67
77
  }
@@ -1,4 +1,16 @@
1
- import type { IvStatRow, IvStatUnit, RichCheap } from "./model";
1
+ import type { IvMethod } from "./client";
2
+ import type { IvStatRow, IvStatUnit, RichCheap, RichCheapDates } from "./model";
3
+
4
+ /**
5
+ * VCA's sign conventions, and what VIX is beside an at-the-money IV30, one
6
+ * line each: under the table, in the shot and in the report's notes. The skew
7
+ * is the stored 25-delta put and call IVs at a 30-day constant maturity, from
8
+ * the latest quote capture; both differences are decimal IV shown in points.
9
+ */
10
+ export const VCA_DEFINITIONS = [
11
+ "25D skew: 30-day 25-delta put IV minus 25-delta call IV, in vol points; positive means puts are richer. 30-90: IV30 minus IV90.",
12
+ "VIX is Cboe's 30-day S&P 500 implied vol from the whole SPX strike strip, skew included; IV30 here is at the money, so VIX usually reads higher.",
13
+ ] as const;
2
14
 
3
15
  export const formatVol = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : `${(value * 100).toFixed(1)}%`;
4
16
  export const formatPoints = (value: number | null | undefined) =>
@@ -13,6 +25,25 @@ export function formatStat(value: number | null | undefined, unit: IvStatUnit):
13
25
  if (value == null || !Number.isFinite(value)) return "--";
14
26
  return unit === "vol" ? formatVol(value) : unit === "points" ? formatPoints(value) : value.toFixed(2);
15
27
  }
28
+ /**
29
+ * How a stored reading was taken. A quote capture is the option quote mids
30
+ * from the minutes before the close, taken once a session and never updated:
31
+ * it is not a live feed, so it is not called live.
32
+ */
33
+ const readingSource = (method: IvMethod | null | undefined) => method === "quote-mid" ? "quote" : "close";
16
34
  export function statSource(row: Pick<IvStatRow, "method">): string {
17
- return row.method === "quote-mid" ? "live" : row.method === "prices" ? "closes" : row.method === "trade-close" ? "close" : "";
35
+ return row.method === "prices" ? "closes" : row.method ? readingSource(row.method) : "";
36
+ }
37
+ /** "2026-10-09 quote": the session a reading belongs to and how it was taken. */
38
+ export const readingLabel = (date: string, method: IvMethod | null | undefined, short = false) =>
39
+ `${short ? shortDate(date) : date} ${readingSource(method)}`;
40
+ /**
41
+ * The dates every row agrees on, said once: the reading's, and the trade close
42
+ * its rank is measured on. `short` is for a footer with little room: month and
43
+ * day, and no window.
44
+ */
45
+ export function sharedDates({ reading, rank }: Pick<RichCheapDates, "reading" | "rank">, short = false): string[] {
46
+ if (reading?.method === "trade-close" && reading.date === rank) return [readingLabel(reading.date, reading.method, short)];
47
+ return [reading && `IV30 ${readingLabel(reading.date, reading.method, short)}`,
48
+ rank && (short ? `IVR/IVP ${shortDate(rank)} close` : `IVR/IVP vs 52 weeks to ${rank} close`)].filter((part): part is string => !!part);
18
49
  }
@@ -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, loadRealizedVolatilities } from "./client";
5
- import { formatPoints, formatRank, formatStat, formatVol, verdictLabel } from "./format";
6
- import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, sharedReading, VCA_LIMIT, VCA_PRESETS } from "./model";
5
+ import { formatPoints, formatRank, formatStat, formatVol, readingLabel, sharedDates, VCA_DEFINITIONS, verdictLabel } from "./format";
6
+ import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, VCA_LIMIT, VCA_PRESETS } from "./model";
7
7
  import { vcaUniverse } from "./universe";
8
8
 
9
9
  const METHODOLOGY = "docs/research-data.md#implied-volatility-history";
@@ -14,7 +14,8 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
14
14
  freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date", oldest: null },
15
15
  describe: (args) => `HIVG ${args.symbols[0] ?? ""}`,
16
16
  discovery: { aliases: ["HIVG"], screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
17
- limitations: ["History starts February 2024 (OPRA daily trade closes)", "Rank and percentile use trade-close readings only"] },
17
+ limitations: ["History starts February 2024 (daily option trade closes)", "Rank and percentile use trade-close readings only",
18
+ "IV1Y needs long-dated trades: it can start later, has gaps, and ranks from 60 prior sessions"] },
18
19
  options: [
19
20
  { key: "lookback", type: "enum", values: [{ value: "1Y" }, { value: "2Y" }, { value: "ALL" }], defaultValue: "1Y", description: "Visible history" },
20
21
  { key: "hvWindow", type: "enum", values: HV_WINDOWS.map((window) => ({ value: String(window) })), defaultValue: "20", description: "Realized volatility window in sessions" },
@@ -28,8 +29,10 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
28
29
  const model = projectIvHistory(payload, prices.history, { lookback: String(args.options.lookback) as IvLookback,
29
30
  hvWindow: (Number(args.options.hvWindow) === 30 ? 30 : 20) as HvWindow });
30
31
  const hvByDay = new Map(model.hv.map((point) => [point.date.getTime(), point.value]));
32
+ const iv365ByDay = new Map(model.iv365.map((point) => [point.date.getTime(), point.value]));
31
33
  const rows = model.iv30.map((point, index) => ({ date: point.date.toISOString().slice(0, 10), iv30: point.value,
32
34
  iv90: model.iv90.find((entry) => entry.date.getTime() === point.date.getTime())?.value ?? null,
35
+ iv365: iv365ByDay.get(point.date.getTime()) ?? null,
33
36
  hv: hvByDay.get(point.date.getTime()) ?? null, spread: model.spread[index]?.value ?? null })).reverse();
34
37
  const errors = [prices.error, ...model.warnings].filter((value): value is string => !!value);
35
38
  return {
@@ -45,11 +48,12 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
45
48
  { key: "samples", header: "Sessions" },
46
49
  ], rows: model.stats.map((row) => ({ ...row })) },
47
50
  { title: "Daily history", columns: [{ key: "date", header: "Session" },
48
- ...["iv30", "iv90", "hv"].map((key) => ({ key, header: key.toUpperCase(), format: percent })),
51
+ ...[["iv30", "IV30"], ["iv90", "IV90"], ["iv365", "IV1Y"], ["hv", "HV"]].map(([key, header]) => ({ key: key!, header: header!, format: percent })),
49
52
  { key: "spread", header: "IV-HV", format: (value: unknown) => typeof value === "number" ? formatPoints(value) : "--" }], rows },
50
53
  ],
51
54
  complete: payload.status === "ready" && errors.length === 0,
52
55
  unavailableSymbols: model.iv30.length ? [] : [instrument.symbol], errors,
56
+ ...(model.oneYearNote ? { notes: [model.oneYearNote] } : {}),
53
57
  metadata: { status: payload.status, coverage: payload.coverage, latest: payload.latest, stats: payload.stats,
54
58
  unit: "decimal annualized volatility", methodology: METHODOLOGY },
55
59
  };
@@ -58,7 +62,8 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
58
62
 
59
63
  export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
60
64
  shape: "bundle",
61
- freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily" },
65
+ // Dated by the readings themselves; the response's own date is the day it was fetched.
66
+ freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date" },
62
67
  argument: { kind: "symbol-list", optional: true, maximum: VCA_LIMIT, description: "US option underlyings; defaults to index and sector ETFs." },
63
68
  options: [{ key: "preset", type: "enum", values: [{ value: "etfs" }, { value: "megacaps" }], defaultValue: "etfs", description: "Preset when no symbols are given" }],
64
69
  describe: "Volatility rich/cheap",
@@ -73,24 +78,31 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
73
78
  ]);
74
79
  const rows = projectRichCheap(payload.rows, hv).sort((a, b) => (b.percentile ?? -1) - (a.percentile ?? -1));
75
80
  const queued = rows.filter((row) => row.status === "queued").map((row) => row.symbol);
76
- const shared = sharedReading(rows);
81
+ const dates = richCheapDates(rows);
77
82
  const hasSkew = rows.some((row) => row.skew != null);
83
+ const hasIv1y = rows.some((row) => row.iv1y != null), hasIv1yRank = rows.some((row) => row.iv1yPercentile != null);
84
+ const dated = (value: unknown) => typeof value === "string" ? value : "--";
78
85
  return {
79
- sections: [{ title: `Rich/cheap · ${universe.label}${shared ? ` · ${shared.date} ${shared.method === "quote-mid" ? "live" : "close"}` : ""}`, columns: [
86
+ sections: [{ title: ["Rich/cheap", universe.label, ...sharedDates(dates)].join(" · "), columns: [
80
87
  { key: "symbol", header: "Symbol" }, { key: "iv30", header: "IV30", format: percent },
81
- ...(shared ? [] : [{ key: "date", header: "As of" }]),
88
+ ...(dates.reading ? [] : [{ key: "date", header: "As of", format: (value: unknown, row?: unknown) =>
89
+ typeof value === "string" ? readingLabel(value, (row as RichCheapRow).method) : "--" }]),
90
+ ...(dates.rankApart ? [{ key: "rankDate", header: "Rank as of", format: (value: unknown) => typeof value === "string" ? `${value} close` : "--" }] : []),
82
91
  { key: "rank", header: "IVR", format: (value: unknown) => formatRank(value as number | null) },
83
92
  { key: "percentile", header: "IVP", format: (value: unknown) => formatRank(value as number | null) },
84
93
  { key: "verdict", header: "Rich/Cheap", format: (value: unknown) => verdictLabel(value as never) },
94
+ ...(hasIv1y ? [{ key: "iv1y", header: "IV1Y", format: percent }] : []),
95
+ ...(hasIv1yRank ? [{ key: "iv1yPercentile", header: "IVP1Y", format: (value: unknown) => formatRank(value as number | null) }] : []),
85
96
  { key: "termSlope", header: "30-90", format: (value: unknown) => formatPoints(value as number | null) },
86
97
  ...(hasSkew ? [{ key: "skew", header: "25D skew", format: (value: unknown) => formatPoints(value as number | null) }] : []),
98
+ ...(hasSkew && dates.skewApart ? [{ key: "skewDate", header: "Skew as of", format: dated }] : []),
87
99
  { key: "hv", header: "HV20", format: percent },
88
100
  { key: "ivHv", header: "IV/HV", format: (value: unknown) => typeof value === "number" ? value.toFixed(2) : "--" },
89
101
  ], rows: rows.map((row) => ({ ...row })) }],
90
102
  complete: !queued.length && !universe.error, unavailableSymbols: queued,
91
103
  errors: [universe.error].filter((value): value is string => !!value),
92
- ...(queued.length ? { notes: [`Queued for backfill: ${queued.join(", ")}.`] } : {}),
93
- metadata: { asOf: payload.asOf, presets: VCA_PRESETS, methodology: METHODOLOGY },
104
+ notes: [...queued.length ? [`Queued for backfill: ${queued.join(", ")}.`] : [], ...VCA_DEFINITIONS],
105
+ metadata: { presets: VCA_PRESETS, methodology: METHODOLOGY },
94
106
  };
95
107
  },
96
108
  };
@@ -26,12 +26,12 @@ export const ivHistoryModule: PluginModule = {
26
26
  paneTemplates: [
27
27
  { ...createTickerSurfacePaneTemplate({
28
28
  id: "iv-history-pane", paneId: "iv-history", label: "Implied Volatility History",
29
- description: "30 and 90-day ATM implied volatility since 2024 against realized, with IV rank and percentile.",
30
- keywords: ["hivg", "implied", "volatility", "history", "iv rank", "ivr", "percentile", "vrp"], shortcut: "HIVG", publicShare: true,
29
+ description: "30-day, 90-day and one-year ATM implied volatility since 2024 against realized, with IV rank and percentile.",
30
+ keywords: ["hivg", "implied", "volatility", "history", "iv rank", "ivr", "percentile", "vrp", "iv1y", "leaps"], shortcut: "HIVG", publicShare: true,
31
31
  }), headless: ivHistoryHeadless },
32
32
  {
33
33
  id: "iv-screen-pane", paneId: "iv-screen", label: "Volatility Rich/Cheap",
34
- description: "IV rank, percentile, term slope, skew and IV/HV across a list, ranked rich to cheap.",
34
+ description: "IV rank, percentile, one-year IV, term slope, skew and IV/HV across a list, ranked rich to cheap.",
35
35
  keywords: ["vca", "rich", "cheap", "iv rank", "implied volatility", "screen"],
36
36
  shortcut: { prefix: "VCA", argKind: "ticker-list" as const, argOptional: true }, headless: ivScreenHeadless,
37
37
  createInstance: (_context, options) => ({
@@ -31,12 +31,16 @@ export interface IvHistoryModel {
31
31
  status: IvHistoryPayload["status"];
32
32
  iv30: DatedValue[];
33
33
  iv90: DatedValue[];
34
+ /** Trade-close one-year IV; it starts later than IV30 and has gaps where long-dated series did not trade. */
35
+ iv365: DatedValue[];
34
36
  /** Same-session quote captures, drawn as points over the trade-close line. */
35
37
  quoteIv30: DatedValue[];
36
38
  hv: DatedValue[];
37
39
  spread: DatedValue[];
38
40
  stats: IvStatRow[];
39
41
  since: string | null;
42
+ /** Where the 1Y series starts and how deep it is, when that differs from the rest or leaves it unranked. */
43
+ oneYearNote: string | null;
40
44
  asOf: string | null;
41
45
  warnings: string[];
42
46
  }
@@ -76,7 +80,7 @@ function seriesStat(id: string, label: string, unit: IvStatUnit, points: readonl
76
80
  percentile: midrankPercentile(window, value), samples: window.length };
77
81
  }
78
82
 
79
- function byMethod(series: readonly IvPoint[], method: IvMethod, field: "iv30" | "iv90") {
83
+ function byMethod(series: readonly IvPoint[], method: IvMethod, field: "iv30" | "iv90" | "iv365") {
80
84
  return series.filter((point) => point.method === method && point[field] != null)
81
85
  .map((point) => ({ date: utc(point.sessionDate), value: point[field] }))
82
86
  .sort((a, b) => a.date.getTime() - b.date.getTime());
@@ -119,12 +123,18 @@ export function projectIvHistory(
119
123
  value: stats?.value ?? null, date: stats?.date ?? null, method: stats?.method ?? null, low: stats?.low ?? null, high: stats?.high ?? null,
120
124
  rank: stats?.rank ?? null, percentile: stats?.percentile ?? null, samples: stats?.samples ?? 0 });
121
125
  const stats: IvStatRow[] = [fromServer("iv30", "IV 30d ATM", payload.stats.iv30)];
122
- // A newer same-day quote capture is shown on its own row: its rank would mix methods.
126
+ // A newer quote capture is shown on its own row: its rank would mix methods.
123
127
  if (latest?.method === "quote-mid" && latest.iv30 != null && (!payload.stats.iv30 || latest.date > payload.stats.iv30.date)) {
124
- stats.push({ id: "iv30-live", label: "IV 30d live", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid",
128
+ stats.push({ id: "iv30-quote", label: "IV 30d quote", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid",
125
129
  low: null, high: null, rank: null, percentile: null, samples: 0 });
126
130
  }
131
+ // IV1Y ranks trade closes only; until there is one, a stored quote gives its level, unranked.
132
+ const oneYear = payload.stats.iv365 ?? null;
127
133
  stats.push(fromServer("iv90", "IV 90d ATM", payload.stats.iv90),
134
+ !oneYear && latest?.iv365 != null
135
+ ? { id: "iv365", label: "IV 1Y ATM", unit: "vol", value: latest.iv365, date: latest.date, method: latest.method,
136
+ low: null, high: null, rank: null, percentile: null, samples: 0 }
137
+ : fromServer("iv365", "IV 1Y ATM", oneYear),
128
138
  seriesStat("hv", `HV ${window}`, "vol", rolling, "prices"),
129
139
  seriesStat("spread", `IV 30d - HV ${window}`, "points", spreadAll, "trade-close"),
130
140
  seriesStat("term", "IV 30d / 90d", "ratio", termAll, "trade-close"));
@@ -134,12 +144,19 @@ export function projectIvHistory(
134
144
  else if (payload.status === "unavailable" && !payload.warnings.length) warnings.push(`No listed options history found for ${payload.symbol}.`);
135
145
  if (!prices.length) warnings.push("Realized volatility unavailable: daily price history is missing.");
136
146
 
147
+ const tradeIv365 = byMethod(payload.series, "trade-close", "iv365");
148
+ const since = payload.coverage?.since ?? earliest;
149
+ const oneYearSince = payload.coverage?.iv365Since ?? (tradeIv365[0] ? day(tradeIv365[0].date) : null);
150
+ const oneYearNote = !oneYearSince ? (since ? "no IV1Y history yet" : null)
151
+ : oneYear?.percentile == null ? `IV1Y from ${oneYearSince}, ${tradeIv365.length} session${tradeIv365.length === 1 ? "" : "s"}`
152
+ : since && oneYearSince > since ? `IV1Y from ${oneYearSince}` : null;
153
+
137
154
  return {
138
155
  symbol: payload.symbol, status: payload.status,
139
- iv30: visible(tradeIv30), iv90: visible(byMethod(payload.series, "trade-close", "iv90")),
156
+ iv30: visible(tradeIv30), iv90: visible(byMethod(payload.series, "trade-close", "iv90")), iv365: visible(tradeIv365),
140
157
  quoteIv30: visible(byMethod(payload.series, "quote-mid", "iv30")),
141
158
  hv, spread: visible(spreadAll), stats,
142
- since: payload.coverage?.since ?? earliest, asOf: latestDate, warnings,
159
+ since, oneYearNote, asOf: latestDate, warnings,
143
160
  };
144
161
  }
145
162
 
@@ -152,21 +169,50 @@ export interface RichCheapRow {
152
169
  method: IvMethod | null;
153
170
  rank: number | null;
154
171
  percentile: number | null;
155
- /** Session the rank belongs to (latest trade close). */
172
+ /** Trade close the rank is measured on: the newest trade-close session, which a newer quote reading can be ahead of. */
156
173
  rankDate: string | null;
157
174
  termSlope: number | null;
175
+ /** The latest reading's one-year IV, dated with IV30. */
176
+ iv1y: number | null;
177
+ /**
178
+ * IV1Y percentile on the close IVR and IVP are ranked on. Empty when the 1Y
179
+ * series has no reading that session, or there is no IV1Y beside it to describe.
180
+ */
181
+ iv1yPercentile: number | null;
158
182
  skew: number | null;
183
+ /** Session of the quote capture the skew comes from. */
184
+ skewDate: string | null;
159
185
  hv: number | null;
160
186
  ivHv: number | null;
161
187
  verdict: RichCheap;
162
188
  }
163
189
 
164
- /** The one reading date every screened row shares, shown once instead of per row. */
165
- export function sharedReading(rows: readonly RichCheapRow[]): { date: string; method: IvMethod | null } | null {
166
- const dated = rows.filter((row) => row.date);
167
- const first = dated[0];
168
- return first && dated.every((row) => row.date === first.date && row.method === first.method)
169
- ? { date: first.date!, method: first.method } : null;
190
+ /**
191
+ * The dates the screened rows agree on, so each is said once instead of per
192
+ * row. The reading (IV30, term slope, IV/HV) and the rank (IVR, IVP) are dated
193
+ * apart: a quote reading can be a session ahead of the trade close its rank
194
+ * is measured on. A group the rows disagree on gets its own column.
195
+ */
196
+ export interface RichCheapDates {
197
+ reading: { date: string; method: IvMethod } | null;
198
+ /** The trade close every ranked row is ranked on. */
199
+ rank: string | null;
200
+ /** Ranked rows are ranked on different closes. */
201
+ rankApart: boolean;
202
+ /** Some skew comes from a different session than its row's reading. */
203
+ skewApart: boolean;
204
+ }
205
+ export function richCheapDates(rows: readonly RichCheapRow[]): RichCheapDates {
206
+ const read = rows.filter((row) => row.date && row.method);
207
+ const ranked = rows.filter((row) => row.rankDate);
208
+ const first = read[0], firstRanked = ranked[0];
209
+ return {
210
+ reading: first && read.every((row) => row.date === first.date && row.method === first.method)
211
+ ? { date: first.date!, method: first.method! } : null,
212
+ rank: firstRanked && ranked.every((row) => row.rankDate === firstRanked.rankDate) ? firstRanked.rankDate : null,
213
+ rankApart: !!firstRanked && ranked.some((row) => row.rankDate !== firstRanked.rankDate),
214
+ skewApart: rows.some((row) => row.skew != null && row.skewDate !== row.date),
215
+ };
170
216
  }
171
217
 
172
218
  /** Rich or cheap against the symbol's own year: IV30 percentile at or above 80, or at or below 20. */
@@ -185,7 +231,9 @@ export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<s
185
231
  symbol: row.symbol, status: row.status, iv30, date: latest?.date ?? null, method: latest?.method ?? null,
186
232
  rank: row.iv30?.rank ?? null, percentile, rankDate: row.iv30?.date ?? null,
187
233
  termSlope: iv30 != null && latest?.iv90 != null ? iv30 - latest.iv90 : null,
188
- skew: row.skew?.skew ?? null, hv: realized,
234
+ iv1y: latest?.iv365 ?? null,
235
+ iv1yPercentile: latest?.iv365 != null && row.iv365 && row.iv365.date === row.iv30?.date ? row.iv365.percentile : null,
236
+ skew: row.skew?.skew ?? null, skewDate: row.skew?.date ?? null, hv: realized,
189
237
  ivHv: iv30 != null && realized != null && realized > 0 ? iv30 / realized : null,
190
238
  verdict: verdictFor(percentile),
191
239
  };
@@ -17,6 +17,8 @@ import { loadIvHistory } from "./client";
17
17
  import { formatRank, formatStat, statSource } from "./format";
18
18
  import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory } from "./model";
19
19
 
20
+ /** Pane borders, the footer's padding and its [s]urface hint. */
21
+ const FOOTER_RESERVE = 16;
20
22
  const LOOKBACKS = [{ value: "1Y", label: "1Y" }, { value: "2Y", label: "2Y" }, { value: "ALL", label: "All" }];
21
23
  const HV_OPTIONS = HV_WINDOWS.map((window) => ({ value: String(window), label: `HV ${window}` }));
22
24
  const STAT_COLUMNS: DataTableColumn[] = [
@@ -80,14 +82,19 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
80
82
  else return;
81
83
  event.preventDefault(); event.stopPropagation();
82
84
  }, { enabled: focused });
85
+ // In a pane too narrow for the whole footer, the history start drops its prefix.
86
+ const sinceText = model?.since ? `trade closes since ${model.since}` : null;
87
+ const footerText = [sinceText, model?.oneYearNote, model?.asOf].filter(Boolean).join(" ");
88
+ const since = sinceText && footerText.length + FOOTER_RESERVE > width ? `since ${model!.since}` : sinceText;
83
89
  usePaneFooter("iv-history", () => ({ info: [
84
90
  ...(resource.loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
85
- ...(model?.since ? [{ id: "since", parts: [{ text: `trade closes since ${model.since}`, tone: "muted" as const }] }] : []),
91
+ ...(since ? [{ id: "since", parts: [{ text: since, tone: "muted" as const }] }] : []),
92
+ ...(model?.oneYearNote ? [{ id: "one-year", parts: [{ text: model.oneYearNote, tone: "muted" as const }] }] : []),
86
93
  ...(model?.asOf ? [{ id: "date", parts: [{ text: model.asOf, tone: "muted" as const }] }] : []),
87
94
  ], hints: [
88
95
  // Lookback (y) and realized window (w) keep their keys; the query bar shows them.
89
96
  ...(symbol ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
90
- ] }), [resource.loading, model?.since, model?.asOf, lookback, hvWindow, symbol]);
97
+ ] }), [resource.loading, since, model?.oneYearNote, model?.asOf, lookback, hvWindow, symbol]);
91
98
 
92
99
  const statsHeight = model ? model.stats.length + 1 : 0;
93
100
  const statSelection = statSelectionValue ?? model?.stats[0]?.id ?? "";
@@ -112,7 +119,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
112
119
  { id: "lookback", label: "Lookback", value: lookback, options: LOOKBACKS, onChange: setLookback },
113
120
  { id: "realized", label: "Realized", value: String(hvWindow), options: HV_OPTIONS, onChange: setHvWindow },
114
121
  ]} />
115
- {!symbol ? <EmptyState title="Choose a ticker." /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
122
+ {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
116
123
  error={!model ? resource.error ?? identityError ?? null : null} empty={!!model && !model.iv30.length && !model.quoteIv30.length}
117
124
  emptyTitle={model?.status === "queued" || model?.status === "backfilling" ? `Backfilling ${symbol} implied volatility history...` : undefined}>
118
125
  {model ? <>
@@ -120,6 +127,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
120
127
  getItemKey={(row) => row.id} emptyStateTitle="No statistics." sortColumnId={null} sortDirection="asc"
121
128
  selection={{ kind: "id", selectedId: statSelection, getId: (row) => row.id, onChange: (id) => setStatSelection(id) }}
122
129
  getExportMetadata={() => [["symbol", model.symbol], ["as of", model.asOf], ["history since", model.since],
130
+ ...(model.oneYearNote ? [["IV1Y", model.oneYearNote]] : []),
123
131
  ["IV", "ATM, constant maturity, annualized"], ["HV", `${hvWindow}-session close-to-close`], ["warnings", ...notices]]}
124
132
  renderCell={(row, column) => statCell(row, column.id)} />
125
133
  <IvHistoryChart model={model} width={width} height={Math.max(6, height - 1 - statsHeight)} hvLabel={`HV ${hvWindow}`} focused={focused} />
@@ -4,25 +4,32 @@ import { useAsyncResource } from "../../../react/async-resource";
4
4
  import { usePaneCollection, usePaneSettingValue, usePluginAppActions, useTickers } from "../../../public/react";
5
5
  import { useThemeColors } from "../../../theme/theme-context";
6
6
  import type { PaneProps } from "../../../types/plugin";
7
- import { Box } from "../../../ui";
7
+ import { Box, Text } from "../../../ui";
8
+ import { wrapTextLines } from "../../../utils/text-wrap";
8
9
  import { useAutoRefresh } from "../../../react/auto-refresh";
9
10
  import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
10
11
  import { loadIvScreen, loadRealizedVolatilities } from "./client";
11
- import { formatPoints, formatRank, formatVol, shortDate, verdictLabel } from "./format";
12
- import { projectRichCheap, type RichCheapRow, sharedReading, type VcaPreset } from "./model";
12
+ import { formatPoints, formatRank, formatVol, readingLabel, sharedDates, shortDate, VCA_DEFINITIONS, verdictLabel } from "./format";
13
+ import { projectRichCheap, type RichCheapRow, richCheapDates, type VcaPreset } from "./model";
13
14
  import { vcaUniverse } from "./universe";
14
15
 
15
16
  type SortId = keyof RichCheapRow;
16
17
  const HV_WINDOW = 20;
18
+ /** Pane borders and the footer's own padding. */
19
+ const FOOTER_MARGIN = 6;
17
20
  const COLUMNS: DataTableColumn[] = [
18
21
  { id: "symbol", label: "Symbol", width: 8, align: "left" },
19
22
  { id: "iv30", label: "IV30", width: 7, align: "right" },
20
23
  { id: "date", label: "As of", width: 11, align: "right" },
24
+ { id: "rankDate", label: "Rank as of", width: 11, align: "right" },
21
25
  { id: "rank", label: "IVR", width: 5, align: "right" },
22
26
  { id: "percentile", label: "IVP", width: 5, align: "right" },
23
27
  { id: "verdict", label: "Rich/Cheap", width: 11, align: "left" },
28
+ { id: "iv1y", label: "IV1Y", width: 7, align: "right" },
29
+ { id: "iv1yPercentile", label: "IVP1Y", width: 6, align: "right" },
24
30
  { id: "termSlope", label: "30-90", width: 7, align: "right" },
25
31
  { id: "skew", label: "25D skew", width: 9, align: "right" },
32
+ { id: "skewDate", label: "Skew as of", width: 11, align: "right" },
26
33
  { id: "hv", label: `HV${HV_WINDOW}`, width: 7, align: "right" },
27
34
  { id: "ivHv", label: "IV/HV", width: 6, align: "right" },
28
35
  ];
@@ -57,10 +64,19 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
57
64
  const projected = projectRichCheap(resource.data?.rows ?? [], hv);
58
65
  return projected.sort((left, right) => compareSortValues(left[sort.columnId], right[sort.columnId], sort.direction));
59
66
  }, [resource.data, hv, sort]);
60
- const shared = useMemo(() => sharedReading(rows), [rows]);
61
- // A shared reading date moves to the footer, and a column no row fills is left out.
62
- const columns = useMemo(() => COLUMNS.filter((column) => !(column.id === "date" && shared)
63
- && !(column.id === "skew" && rows.every((row) => row.skew == null))), [rows, shared]);
67
+ const dates = useMemo(() => richCheapDates(rows), [rows]);
68
+ // A date every row shares moves to the footer; a column no row fills, or whose dates agree, is left out.
69
+ const columns = useMemo(() => COLUMNS.filter((column) => {
70
+ switch (column.id) {
71
+ case "date": return !dates.reading;
72
+ case "rankDate": return dates.rankApart;
73
+ case "iv1y": return rows.some((row) => row.iv1y != null);
74
+ case "iv1yPercentile": return rows.some((row) => row.iv1yPercentile != null);
75
+ case "skew": return rows.some((row) => row.skew != null);
76
+ case "skewDate": return dates.skewApart;
77
+ default: return true;
78
+ }
79
+ }), [rows, dates]);
64
80
  const [selected, setSelected] = useState<string | null>(null);
65
81
  const queued = rows.filter((row) => row.status === "queued").length;
66
82
  const uncovered = rows.filter((row) => row.status !== "queued" && row.iv30 == null).length;
@@ -70,11 +86,13 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
70
86
  .filter((value): value is string => !!value);
71
87
  usePaneNoticeFooter({ registrationId: "iv-screen-notices", notices, focused });
72
88
  const openHistory = (row: RichCheapRow) => createPaneFromTemplate("iv-history-pane", { symbol: row.symbol });
89
+ // The full wording gives way to the short form in a pane too narrow to show the rank date.
90
+ const fullDatesText = useMemo(() => sharedDates(dates).join(" · "), [dates]);
91
+ const datesText = fullDatesText.length + FOOTER_MARGIN > width ? sharedDates(dates, true).join(" · ") : fullDatesText;
73
92
  usePaneFooter("iv-screen", () => ({ info: [
74
93
  ...(resource.loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
75
- ...(shared ? [{ id: "date", parts: [{ text: readingLabel(shared.date, shared.method), tone: "muted" as const }] }]
76
- : resource.data?.asOf ? [{ id: "date", parts: [{ text: resource.data.asOf, tone: "muted" as const }] }] : []),
77
- ] }), [resource.loading, shared, resource.data?.asOf]);
94
+ ...(datesText ? [{ id: "dates", parts: [{ text: datesText, tone: "muted" as const }] }] : []),
95
+ ] }), [resource.loading, datesText]);
78
96
  const handleKey = (event: DataTableKeyEvent): boolean => {
79
97
  if (event.ctrl || event.alt || event.meta || event.name !== "r") return false;
80
98
  void resource.reload();
@@ -84,19 +102,25 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
84
102
  switch (id) {
85
103
  case "symbol": return { text: row.symbol, color: colors.textBright };
86
104
  case "iv30": return { text: formatVol(row.iv30), color: colors.warning };
87
- case "date": return { text: row.date ? readingLabel(row.date, row.method) : row.status === "queued" ? "queued" : "--",
105
+ case "date": return { text: row.date ? readingLabel(row.date, row.method, true) : row.status === "queued" ? "queued" : "--",
88
106
  color: colors.textDim };
107
+ case "rankDate": return { text: row.rankDate ? `${shortDate(row.rankDate)} close` : "--", color: colors.textDim };
89
108
  case "rank": return { text: formatRank(row.rank) };
90
109
  case "percentile": return { text: formatRank(row.percentile) };
91
110
  case "verdict": return { text: verdictLabel(row.verdict), color: row.verdict === "rich" ? colors.negative : row.verdict === "cheap" ? colors.positive : colors.textDim };
111
+ case "iv1y": return { text: formatVol(row.iv1y) };
112
+ case "iv1yPercentile": return { text: formatRank(row.iv1yPercentile) };
92
113
  case "termSlope": return { text: formatPoints(row.termSlope), color: row.termSlope != null && row.termSlope > 0 ? colors.negative : colors.text };
93
114
  case "skew": return { text: formatPoints(row.skew) };
115
+ case "skewDate": return { text: row.skewDate ? readingLabel(row.skewDate, "quote-mid", true) : "--", color: colors.textDim };
94
116
  case "hv": return { text: hv.has(row.symbol) ? formatVol(row.hv) : "...", color: colors.positive };
95
117
  case "ivHv": return { text: row.ivHv == null ? "--" : row.ivHv.toFixed(2) };
96
118
  default: return { text: "" };
97
119
  }
98
120
  };
99
121
  const custom = scope === "custom";
122
+ // The skew's sign and VIX beside IV30 are units of the figures above them, so they stay under the table.
123
+ const definitions = useMemo(() => VCA_DEFINITIONS.flatMap((line) => wrapTextLines(line, Math.max(1, width - 2))), [width]);
100
124
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
101
125
  <QueryBar width={width} filters={[
102
126
  { id: "universe", label: "Universe", value: VCA_SCOPE_OPTIONS.some((option) => option.value === scope) ? scope : "etfs",
@@ -106,22 +130,22 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
106
130
  onChange: setSymbolsText }] : []),
107
131
  ]} />
108
132
  <PaneStatusBody subject="volatility rich/cheap" loading={resource.loading && !resource.data} error={universe.error && !universe.instruments.length ? universe.error : !resource.data ? resource.error : null}>
109
- <DataTableView<RichCheapRow> focused={focused && !symbolsActive} columns={columns} items={rows} rootWidth={width} rootHeight={Math.max(2, height - 1)}
133
+ <DataTableView<RichCheapRow> focused={focused && !symbolsActive} columns={columns} items={rows} rootWidth={width}
134
+ rootHeight={Math.max(2, height - 1 - definitions.length)}
110
135
  getItemKey={(row) => row.symbol} sortColumnId={sort.columnId} sortDirection={sort.direction} emptyStateTitle="No symbols to screen."
111
136
  onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id as SortId, { firstDirection: "desc" }))}
112
137
  selection={{ kind: "id", selectedId: selected ?? rows[0]?.symbol ?? "", getId: (row) => row.symbol, onChange: (id) => setSelected(id) }}
113
138
  onActivate={openHistory} onRootKeyDown={handleKey}
114
- getExportMetadata={() => [["universe", universe.label], ["as of", resource.data?.asOf], ["IV", "30-day ATM, annualized"],
115
- ["rank window", "prior 52 weeks of trade-close readings"], ["HV", `${HV_WINDOW}-session close-to-close`]]}
139
+ getExportMetadata={() => [["universe", universe.label], ["as of", fullDatesText || undefined],
140
+ ["IV", "30-day ATM, annualized"], ["IV1Y", "one-year ATM, annualized"], ["rank window", "prior 52 weeks of trade-close readings"], ["HV", `${HV_WINDOW}-session close-to-close`]]}
116
141
  renderCell={(row, column) => cell(row, column.id)} />
142
+ <Box flexDirection="column" paddingX={1} height={definitions.length} flexShrink={0}>
143
+ {definitions.map((line, index) => <Text key={index} fg={colors.textDim}>{line}</Text>)}
144
+ </Box>
117
145
  </PaneStatusBody>
118
146
  </Box>;
119
147
  }
120
148
 
121
- function readingLabel(date: string, method: RichCheapRow["method"]): string {
122
- return `${shortDate(date)} ${method === "quote-mid" ? "live" : "close"}`;
123
- }
124
-
125
149
  export const VCA_SCOPE_OPTIONS = [
126
150
  { value: "etfs", label: "Index and sector ETFs" },
127
151
  { value: "megacaps", label: "US mega caps" },
@@ -750,7 +750,7 @@ function JobsView({ width, height, focused, companyOnly = false }: JobsViewProps
750
750
  if (!access.hasProAccess) return <HiringProWall symbol={symbol} exchange={exchange} width={width} height={height} />;
751
751
 
752
752
  if (!symbol) {
753
- if (companyOnly) return <EmptyState title="No ticker selected." message="Select a ticker to see its hiring." />;
753
+ if (companyOnly) return <EmptyState title="Select a ticker." />;
754
754
  return <HomeView width={width} height={height} focused={focused} registrationId={registrationId} />;
755
755
  }
756
756
  return (