gloomberb 0.16.2 → 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 (527) 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/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -12,6 +12,7 @@ import {
12
12
  } from "./model";
13
13
  import { abortable, abortError } from "../../../utils/async-deadline";
14
14
  import { errorMessage } from "../../../utils/errors";
15
+ import { findListedExpiry, unlistedExpiryText } from "../../../utils/option-expiry";
15
16
 
16
17
  const DEFAULT_SURFACE_EXPIRY_LIMIT = 18;
17
18
  const SURFACE_LOAD_CONCURRENCY = 4;
@@ -198,13 +199,16 @@ export async function loadVolatilitySurface(
198
199
  if (!chain) catalogueError ??= "Options expiry catalogue unavailable";
199
200
  catalogue = [...new Set((chain?.expirationDates ?? []).filter((expiration) =>
200
201
  Number.isFinite(expiration) && expiration > 0 && daysToExpiryFrom(expiration, now) > 0))].sort((a, b) => a - b);
202
+ // A typed date is that day's UTC midnight; the catalogue's stamp for the
203
+ // same expiry is matched by calendar date, as OMON matches it.
201
204
  const required = [...new Set([...(request.requiredExpiries ?? []), ...(request.expiries ?? [])]
202
- .filter((expiration) => Number.isFinite(expiration) && expiration > 0))];
205
+ .filter((expiration) => Number.isFinite(expiration) && expiration > 0)
206
+ .map((expiration) => findListedExpiry(expiration, catalogue) ?? expiration))];
203
207
  const listed = new Set(catalogue);
204
208
  selected = [...new Set([...request.expiries ? [] : selectSurfaceExpiries(catalogue, limit, now),
205
209
  ...required.filter((expiration) => listed.has(expiration))])].sort((a, b) => a - b);
206
210
  requiredFailures = required.filter((expiration) => !listed.has(expiration)).map((expiration) => ({
207
- expiration, message: "Selected expiration unavailable in the current option catalogue",
211
+ expiration, message: unlistedExpiryText(expiration, catalogue),
208
212
  }));
209
213
  if (chain && catalogue.length === 0) catalogueError ??= "No unexpired option expiries available";
210
214
  } catch (error) {
@@ -3,6 +3,8 @@ import { useRemoteUiNode } from "../../../remote/semantic-tree";
3
3
  import { parsePublicTickerKey } from "../../../utils/exchanges";
4
4
  import type { SurfaceExpiry, SurfaceGrid, SurfaceSnapshot } from "./model";
5
5
  import { isFiniteNumber, isRecord } from "../../../utils/guards";
6
+ import { expiryIsoDate } from "../../../utils/option-expiry";
7
+ import { surfaceSkewRow } from "./tables";
6
8
 
7
9
  export interface VolSurfaceEvidenceInput {
8
10
  snapshot: SurfaceSnapshot | null | undefined;
@@ -88,11 +90,12 @@ export function volSurfaceSemanticEvidence(input: VolSurfaceEvidenceInput): VolS
88
90
  const term = view === "term" ? (snapshot?.expiries ?? []).filter((expiry) => iv(expiry.atmIV))
89
91
  .map((expiry) => ({ expiration: expiry.expiration, years: expiry.years, atm: expiry.atmIV!,
90
92
  put25: expiry.skew.put25, call25: expiry.skew.call25 })) : [];
91
- const table = view === "skew" || view === "forwards" ? (snapshot?.expiries ?? []).map((expiry) => ({
92
- expiration: expiry.expiration,
93
- values: view === "forwards" ? [expiry.forward, expiry.dividendYield, expiry.rate]
94
- : [expiry.skew.put25, expiry.skew.call25, expiry.skew.riskReversal, expiry.skew.butterfly, expiry.skew.moneynessSkew, expiry.termSlope],
95
- })) : [];
93
+ const table = view === "skew" || view === "forwards" ? (snapshot?.expiries ?? []).map((expiry) => {
94
+ // The slope the table leaves out under a week to expiry is not a rendered observation.
95
+ const skew = surfaceSkewRow(expiry);
96
+ return { expiration: expiry.expiration, values: view === "forwards" ? [expiry.forward, expiry.dividendYield, expiry.rate]
97
+ : [skew.put25, skew.call25, skew.riskReversal, skew.butterfly, skew.moneynessSkew, skew.termSlope] };
98
+ }) : [];
96
99
  const counts = grid ? gridCounts(grid.values) : { values: 0, quads: 0 };
97
100
  const plottedValueCount = grid ? counts.values : smile ? smile.points.length : term.length
98
101
  || table.reduce((total, row) => total + row.values.filter(isFiniteNumber).length, 0);
@@ -186,7 +189,9 @@ export const volSurfaceScreenshotEvidence: PaneScreenshotEvidenceHook<VolSurface
186
189
  if (evidence.ivSource !== (resolved.options.ivSource ?? "recomputed")) mismatches.push("rendered volatility source does not match");
187
190
  if (evidence.priceSide !== (resolved.options.priceSide ?? "mid")) mismatches.push("rendered option quote side does not match");
188
191
  const expiration = resolved.instance?.settings?.expiration;
189
- if (expiration != null && Number(expiration) !== evidence.selectedExpiration) mismatches.push("rendered volatility expiry does not match");
192
+ // A typed date names the listed expiry of that calendar day, whatever its stamp's time.
193
+ if (expiration != null && (evidence.selectedExpiration == null
194
+ || expiryIsoDate(Number(expiration)) !== expiryIsoDate(evidence.selectedExpiration))) mismatches.push("rendered volatility expiry does not match");
190
195
  return mismatches;
191
196
  },
192
197
  unavailable(evidence, { resolved, payload }) {
@@ -1,9 +1,18 @@
1
- import type { HeadlessPaneDefinition } from "../../../types/plugin";
1
+ import type { HeadlessBundleSection, HeadlessPaneDefinition, HeadlessPaneFreshness, HeadlessPaneRow } from "../../../types/plugin";
2
+ import {
3
+ expirationOptionSeconds, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, readOptionExpiration,
4
+ } from "../../../utils/option-expiry";
2
5
  import { formatStrikeLabel } from "../options/table";
3
6
  import { createSurfaceDependencies, loadVolatilitySurface } from "./client";
4
- import { buildSurfaceGrid, type SurfaceGridOptions, type SurfaceSettings } from "./model";
7
+ import { buildSurfaceGrid, type SurfaceGridOptions, type SurfaceSettings, type SurfaceSnapshot } from "./model";
8
+ import {
9
+ expiryFeedFields, expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT, HIDDEN_YIELD_TEXT, selectSurfaceExpiry, surfaceForwardRow, surfaceSkewRow,
10
+ surfaceSmileRows, surfaceTermRows, TERM_SLOPE_MIN_DAYS, type SurfaceForwardRow, type SurfaceSkewRow,
11
+ } from "./tables";
5
12
 
6
13
  type SurfaceAxis = NonNullable<SurfaceGridOptions["axis"]>;
14
+ const SURFACE_TABS = ["surface", "table", "smile", "term", "skew", "forwards"] as const;
15
+ type SurfaceTab = typeof SURFACE_TABS[number];
7
16
 
8
17
  /** Column label for a surface coordinate: strike/spot or strike/forward ratio, delta bucket, or listed strike. */
9
18
  export function surfaceCoordinateLabel(axis: SurfaceAxis, coordinate: number, index: number): string {
@@ -11,18 +20,113 @@ export function surfaceCoordinateLabel(axis: SurfaceAxis, coordinate: number, in
11
20
  : axis === "strike" ? formatStrikeLabel(coordinate) : `${Math.round(coordinate * 100)}%`;
12
21
  }
13
22
 
23
+ /** The Skew view's sign conventions and units, one line each. */
24
+ const SKEW_NOTES = [
25
+ "RR is 25D call IV minus 25D put IV, BF the average 25D wing IV minus ATM IV, 90/110 the IV at 90% of spot minus the IV at 110%; all in vol points.",
26
+ `Slope is the ATM IV change per year to the next listed expiry, in vol points; left out under ${TERM_SLOPE_MIN_DAYS} days to expiry, where it annualises a few hours of noise.`,
27
+ ];
28
+
29
+ const number = (value: unknown) => typeof value === "number" && Number.isFinite(value) ? value : null;
30
+ const iv = (value: unknown) => formatIv(number(value));
31
+ const price = (value: unknown) => formatPrice(number(value));
32
+ const points = (value: unknown) => formatVolPoints(number(value));
33
+ const days = (value: unknown) => typeof value === "number" ? value.toFixed(value < 10 ? 1 : 0) : "--";
34
+
35
+ function surfaceSections(snapshot: SurfaceSnapshot, axis: SurfaceAxis, tenors: "listed" | "fixed"): HeadlessBundleSection[] {
36
+ const grid = buildSurfaceGrid(snapshot, { axis, tenors });
37
+ return [
38
+ // One row per tenor, one IV column per coordinate, as the pane's Table tab lays it out.
39
+ { title: "Surface", columns: [
40
+ { key: "tenor", header: "Expiry / tenor" },
41
+ ...(grid.rows[0]?.cells.map((cell, index) => ({ key: String(index), header: surfaceCoordinateLabel(axis, cell.coordinate, index),
42
+ format: (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--" })) ?? []),
43
+ ], rows: grid.rows.map((row) => ({ tenor: `${row.label}${row.extrapolated ? " E" : row.interpolated ? " I" : ""}`,
44
+ ...Object.fromEntries(row.cells.map((cell, index) => [String(index), cell.volatility])) })) },
45
+ { title: "Expiries", columns: [
46
+ { key: "expiry", header: "Expiry" }, { key: "state", header: "State" }, { key: "forward", header: "Forward", format: price },
47
+ { key: "rate", header: "Rate", format: (value) => typeof value === "number" ? `${(value * 100).toFixed(2)}%` : "--" }, { key: "fit", header: "Fit" }, { key: "asOf", header: "As of" },
48
+ ], rows: snapshot.expiries.map((expiry) => ({ ...expiry, expiry: expiryLabel(expiry.expiration), fit: expiry.fit?.method ?? null })) },
49
+ ];
50
+ }
51
+
52
+ /** The selected expiry's clean quotes against its fitted smile. */
53
+ function smileSections(snapshot: SurfaceSnapshot, requested: number | null): {
54
+ sections: HeadlessBundleSection[]; errors: string[]; freshness?: HeadlessPaneFreshness;
55
+ } {
56
+ const expiry = selectSurfaceExpiry(snapshot, requested);
57
+ // An unlisted request is already an error naming the listed dates.
58
+ if (!expiry) return { sections: [], errors: requested == null ? ["No expiry loaded"] : [] };
59
+ // The quotes are one chain's, so its observation dates the report.
60
+ const freshness: HeadlessPaneFreshness = { asOf: expiry.asOf, ...expiry.stale ? { status: "stale" as const }
61
+ : expiry.dataSource ? { status: expiry.dataSource } : {}, ...expiry.delayMinutes ? { delayMinutes: expiry.delayMinutes } : {} };
62
+ if (!expiry.fit || expiry.points.length < 2) {
63
+ return { sections: [], freshness, errors: [`${expiryLabel(expiry.expiration)}: no clean quoted smile`] };
64
+ }
65
+ return { errors: [], freshness, sections: [
66
+ { title: `Smile ${expiryLabel(expiry.expiration)}`, entries: [
67
+ { key: "days", label: "Days to expiry", value: expiry.years * 365, formatted: days(expiry.years * 365) },
68
+ { key: "forward", label: "Forward", value: expiry.forward, formatted: price(expiry.forward) },
69
+ { key: "atm", label: "ATM IV (at spot)", value: expiry.atmIV, formatted: iv(expiry.atmIV) },
70
+ { key: "fit", label: "Fit", value: expiry.fit.method, formatted: `${expiry.fit.method}, RMSE ${points(expiry.fit.residual)} vol pts` },
71
+ ] },
72
+ { title: "Clean quotes", columns: [
73
+ { key: "strike", header: "Strike", format: (value) => typeof value === "number" ? formatStrikeLabel(value) : "--" },
74
+ { key: "side", header: "Side" },
75
+ { key: "moneyness", header: "Spot %", format: (value) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--" },
76
+ { key: "volatility", header: "Quote IV", format: iv }, { key: "fitted", header: "Fitted IV", format: iv },
77
+ { key: "residual", header: "Residual pts", format: points }, { key: "mid", header: "Mid", format: price },
78
+ { key: "openInterest", header: "OI" },
79
+ ], rows: surfaceSmileRows(expiry) },
80
+ ] };
81
+ }
82
+
83
+ function termSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
84
+ return { title: "Term structure", columns: [
85
+ { key: "expiry", header: "Expiry" }, { key: "days", header: "Days", format: days },
86
+ { key: "atm", header: "ATM IV", format: iv }, { key: "put25", header: "25D put IV", format: iv }, { key: "call25", header: "25D call IV", format: iv },
87
+ { key: "straddle", header: "Straddle", format: price }, { key: "sigma", header: "1-sigma", format: price },
88
+ { key: "sigmaPercent", header: "1-sigma %", format: (value) => typeof value === "number" ? `${value.toFixed(2)}%` : "--" },
89
+ ], rows: surfaceTermRows(snapshot).map((row) => ({ ...row, expiry: expiryLabel(row.expiration) })) };
90
+ }
91
+
92
+ function skewSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
93
+ const gap = (value: unknown, row: HeadlessPaneRow) => typeof value === "number" ? points(value) : (row as unknown as SurfaceSkewRow).gap ?? "--";
94
+ return { title: "Skew", columns: [
95
+ { key: "expiry", header: "Expiry" }, { key: "days", header: "Days", format: days },
96
+ { key: "put25", header: "25D put IV", format: iv }, { key: "call25", header: "25D call IV", format: iv },
97
+ { key: "riskReversal", header: "RR pts", format: gap }, { key: "butterfly", header: "BF pts", format: gap },
98
+ { key: "moneynessSkew", header: "90/110 pts", format: (value, row) => typeof value === "number" ? points(value)
99
+ : (row as unknown as SurfaceSkewRow).moneynessGap ?? "--" },
100
+ { key: "termSlope", header: "Slope pts/yr", format: (value, row) => typeof value === "number" ? points(value)
101
+ : (row as unknown as SurfaceSkewRow).termSlopeHidden ? HIDDEN_SLOPE_TEXT : "--" },
102
+ ], rows: snapshot.expiries.map((expiry) => ({ ...surfaceSkewRow(expiry), ...expiryFeedFields(expiry), expiry: expiryLabel(expiry.expiration) })) };
103
+ }
104
+
105
+ function forwardsSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
106
+ const percent = (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(2)}%` : "--";
107
+ return { title: "Forwards", columns: [
108
+ { key: "expiry", header: "Expiry" }, { key: "forward", header: "Forward", format: price },
109
+ { key: "basis", header: "Basis", format: (value) => typeof value === "number" ? `${value > 0 ? "+" : ""}${formatPrice(value)}` : "--" },
110
+ { key: "dividendYield", header: "Div yield", format: (value, row) => typeof value === "number" ? percent(value)
111
+ : (row as unknown as SurfaceForwardRow).dividendYieldHidden ? HIDDEN_YIELD_TEXT : "--" }, { key: "rate", header: "Rate", format: percent },
112
+ { key: "pairs", header: "Pairs" }, { key: "asOf", header: "As of", format: (value) => typeof value === "string" ? value.slice(0, 10) : "--" },
113
+ ], rows: snapshot.expiries.map((expiry) => ({ ...surfaceForwardRow(expiry, snapshot), ...expiryFeedFields(expiry), expiry: expiryLabel(expiry.expiration) })) };
114
+ }
115
+
14
116
  export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
15
117
  shape: "bundle", argument: { kind: "ticker", description: "Underlying ticker" },
16
118
  describe: (args) => `OVDV ${args.symbols[0] ?? ""}`,
17
119
  options: [
18
- { key: "tab", type: "enum", values: ["surface", "table", "smile", "term", "skew", "forwards"].map((value) => ({ value })),
19
- defaultValue: "surface", description: "Initial view", pluginState: { pluginId: "ticker-research", key: "activeTabId" } },
120
+ { key: "tab", type: "enum", values: SURFACE_TABS.map((value) => ({ value })),
121
+ defaultValue: "surface", description: "Initial view; the report prints that view's figures", pluginState: { pluginId: "ticker-research", key: "activeTabId" } },
20
122
  { key: "ivSource", type: "enum", values: [{ value: "recomputed" }, { value: "provider" }], defaultValue: "recomputed", description: "Quote-derived IV or provider comparison" },
21
123
  { key: "priceSide", type: "enum", values: [{ value: "mid" }, { value: "bid" }, { value: "ask" }], defaultValue: "mid", description: "Quote side used by the IV solver" },
22
124
  { key: "axis", type: "enum", values: ["spot", "forward", "delta", "strike"].map((value) => ({ value })), defaultValue: "spot", description: "Surface coordinates" },
23
125
  { key: "tenors", type: "enum", values: [{ value: "listed" }, { value: "fixed" }], defaultValue: "listed", description: "Listed or interpolated tenors" },
24
126
  { key: "limit", type: "integer", minimum: 1, maximum: 100, defaultValue: 18, description: "Representative expiry request limit; selected expiry may add one", pluginState: { pluginId: "ticker-research", key: "expiryLimit" } },
25
- { key: "expiration", type: "integer", minimum: 1, description: "Selected expiry in Unix seconds", settingKey: "expiration" },
127
+ { key: "expiration", type: "string", placeholder: OPTION_EXPIRATION_PLACEHOLDER, example: "--expiration 2027-01-15 --tab smile",
128
+ description: `Selected expiry, as ${OPTION_EXPIRATION_FORMAT}; the first at least four weeks out when omitted`,
129
+ settingKey: "expiration", normalize: (value) => expirationOptionSeconds(value) },
26
130
  ],
27
131
  async load(args, ctx) {
28
132
  const symbol = args.symbols[0]!;
@@ -31,39 +135,39 @@ export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
31
135
  if (!(quote.price > 0) || !Number.isFinite(quote.price) || quote.stale) {
32
136
  return { sections: [], complete: false, unavailableSymbols: [symbol], errors: ["A current underlying price is required"] };
33
137
  }
138
+ const requested = readOptionExpiration(args.options.expiration);
34
139
  const snapshot = await loadVolatilitySurface({ instrument, spot: quote.price, spotAsOf: quote.lastUpdated,
35
140
  settings: { ivSource: args.options.ivSource as SurfaceSettings["ivSource"], priceSide: args.options.priceSide as SurfaceSettings["priceSide"] },
36
141
  limit: Number(args.options.limit), signal: ctx.signal,
37
- requiredExpiries: typeof args.options.expiration === "number" ? [args.options.expiration] : [],
142
+ requiredExpiries: requested == null ? [] : [requested],
38
143
  }, createSurfaceDependencies(ctx.marketData, ctx.apiClient));
39
144
  const axis = args.options.axis as SurfaceAxis;
40
- const grid = buildSurfaceGrid(snapshot, { axis, tenors: args.options.tenors as "listed" | "fixed" });
145
+ const tab = (SURFACE_TABS as readonly string[]).includes(String(args.options.tab)) ? args.options.tab as SurfaceTab : "surface";
41
146
  const available = snapshot.expiries.some((expiry) => expiry.fit);
42
147
  const expiryWarnings = snapshot.expiries.flatMap((expiry) => expiry.warnings.map((warning) =>
43
- ({ expiry: new Date(expiry.expiration * 1000).toISOString().slice(0, 10), warning })));
148
+ ({ expiry: expiryLabel(expiry.expiration), warning })));
44
149
  const perExpiry = new Set(expiryWarnings.map((row) => row.warning));
45
150
  // Calendar arbitrage compares expiries, so it belongs to the whole surface.
46
151
  const warnings = [...expiryWarnings, ...snapshot.warnings.filter((warning) => !perExpiry.has(warning))
47
152
  .map((warning) => ({ expiry: "All", warning }))];
153
+ const smile = tab === "smile" ? smileSections(snapshot, requested) : null;
154
+ // The report prints the view it was asked for, as the pane opens on it.
155
+ const view: HeadlessBundleSection[] = tab === "smile" ? smile!.sections : tab === "term" ? [termSection(snapshot)]
156
+ : tab === "skew" ? [skewSection(snapshot)] : tab === "forwards" ? [forwardsSection(snapshot)]
157
+ : surfaceSections(snapshot, axis, args.options.tenors as "listed" | "fixed");
48
158
  return {
49
159
  sections: [
50
- // One row per tenor, one IV column per coordinate, as the pane's Table tab lays it out.
51
- { title: "Surface", columns: [
52
- { key: "tenor", header: "Expiry / tenor" },
53
- ...(grid.rows[0]?.cells.map((cell, index) => ({ key: String(index), header: surfaceCoordinateLabel(axis, cell.coordinate, index),
54
- format: (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--" })) ?? []),
55
- ], rows: grid.rows.map((row) => ({ tenor: `${row.label}${row.extrapolated ? " E" : row.interpolated ? " I" : ""}`,
56
- ...Object.fromEntries(row.cells.map((cell, index) => [String(index), cell.volatility])) })) },
57
- { title: "Expiries", columns: [
58
- { key: "expiry", header: "Expiry" }, { key: "state", header: "State" }, { key: "forward", header: "Forward", format: (value) => typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "--" },
59
- { key: "rate", header: "Rate", format: (value) => typeof value === "number" ? `${(value * 100).toFixed(2)}%` : "--" }, { key: "fit", header: "Fit" }, { key: "asOf", header: "As of" },
60
- ], rows: snapshot.expiries.map((expiry) => ({ ...expiry, expiry: new Date(expiry.expiration * 1000).toISOString().slice(0, 10), fit: expiry.fit?.method ?? null })) },
160
+ ...view,
61
161
  // The pane lists these (arbitrage, SVI fallback) beside the surface; here each names its expiry.
62
162
  ...(warnings.length ? [{ title: "Warnings", columns: [{ key: "expiry", header: "Expiry" }, { key: "warning", header: "Warning" }],
63
163
  rows: warnings }] : []),
64
164
  ],
65
165
  complete: available && snapshot.failures.length === 0 && snapshot.expiries.every((expiry) => expiry.state === "ready"),
66
- unavailableSymbols: available ? [] : [symbol], errors: snapshot.failures.map((failure) => failure.message),
166
+ unavailableSymbols: available ? [] : [symbol],
167
+ errors: [...snapshot.failures.map((failure) => failure.expiration == null ? failure.message
168
+ : `${expiryLabel(failure.expiration)}: ${failure.message}`), ...smile?.errors ?? []],
169
+ ...(tab === "skew" ? { notes: SKEW_NOTES } : {}),
170
+ ...(smile?.freshness ? { freshness: smile.freshness } : {}),
67
171
  metadata: { ...snapshot, underlyingQuote: quote, unit: "decimal annualized IV", methodology: "docs/research-data.md#shared-volatility-calculations" },
68
172
  };
69
173
  },
@@ -3,7 +3,7 @@ import {
3
3
  daysToExpiryFrom, detectButterflyArbitrage, detectCalendarArbitrage, evaluateSmile, expectedMove,
4
4
  extractImpliedForward, fitVolatilitySmile, FIXED_VOLATILITY_TENORS,
5
5
  interpolateTotalVariance, logForwardMoneyness, optionDelta, optionMid,
6
- smileSkew, solveImpliedVolatility, valueOption, volatilityTermSlope,
6
+ solveImpliedVolatility, valueOption, volatilityTermSlope,
7
7
  type ExpectedMove, type ImpliedForward, type OptionSide, type SkewMetrics, type SmileFit,
8
8
  } from "../shared/volatility";
9
9
  import type { YieldPoint } from "../yield-curve/treasury-data";
@@ -354,16 +354,7 @@ export function buildSurfaceExpiry(input: BuildSurfaceExpiryInput): SurfaceExpir
354
354
  const smile = (strike: number) => evaluateSurfaceSmile(result, strike);
355
355
  for (const point of result.points) point.fitResidual = point.volatility - (smile(point.strike) ?? point.volatility);
356
356
  result.atmIV = smile(spot);
357
- const skew = smileSkew({ spot, years: result.years, rate: rate.rate, dividendYield: result.dividendYield },
358
- (strike) => evaluateSmile(fit, logForwardMoneyness(strike, forward)!));
359
- const putCovered = deltaStrike(result, spot, -0.25) != null;
360
- const callCovered = deltaStrike(result, spot, 0.25) != null;
361
- const moneynessCovered = smile(spot * 0.9) != null && smile(spot * 1.1) != null;
362
- result.skew = { ...skew, atm: result.atmIV, put25: putCovered ? skew.put25 : null, call25: callCovered ? skew.call25 : null,
363
- putCallSkew: putCovered && callCovered ? skew.putCallSkew : null,
364
- riskReversal: putCovered && callCovered ? skew.riskReversal : null,
365
- butterfly: putCovered && callCovered && result.atmIV != null ? skew.butterfly : null,
366
- moneynessSkew: moneynessCovered ? skew.moneynessSkew : null };
357
+ result.skew = surfaceExpirySkew(result, spot);
367
358
  result.expectedMove = expectedMove(cleaned.calls, cleaned.puts, spot, result.years, result.atmIV);
368
359
  const calls = result.points.map((point) => ({ strike: point.strike, callPrice: valueOption({
369
360
  side: "call", spot: forward, strike: point.strike,
@@ -376,6 +367,27 @@ export function buildSurfaceExpiry(input: BuildSurfaceExpiryInput): SurfaceExpir
376
367
  return result;
377
368
  }
378
369
 
370
+ /**
371
+ * A fitted expiry's skew in decimal IV: RR is the 25-delta call minus put, BF
372
+ * their average minus ATM, 90/110 the IV at 90% of spot minus 110%. The
373
+ * 25-delta strikes are found on the fit inside the quoted strikes, as the
374
+ * Table's delta columns find them, so a fit that cannot price a far wing it
375
+ * was never quoted at does not blank a quoted expiry's skew.
376
+ */
377
+ export function surfaceExpirySkew(expiry: SurfaceExpiry, spot: number): SkewMetrics {
378
+ const smile = (strike: number) => evaluateSurfaceSmile(expiry, strike);
379
+ const at = (delta: number) => {
380
+ const strike = deltaStrike(expiry, spot, delta);
381
+ return strike == null ? null : smile(strike);
382
+ };
383
+ const put25 = at(-0.25), call25 = at(0.25), atm = expiry.atmIV;
384
+ const low = smile(spot * 0.9), high = smile(spot * 1.1);
385
+ const both = put25 != null && call25 != null;
386
+ return { atm, put25, call25, putCallSkew: both ? put25 - call25 : null, riskReversal: both ? call25 - put25 : null,
387
+ butterfly: both && atm != null ? (put25 + call25) / 2 - atm : null,
388
+ moneynessSkew: low != null && high != null ? low - high : null };
389
+ }
390
+
379
391
  export function surfaceCalendarWarnings(expiries: readonly SurfaceExpiry[]): string[] {
380
392
  let count = 0;
381
393
  for (const ratio of SURFACE_MONEYNESS) {
@@ -32,8 +32,11 @@ import {
32
32
  DEFAULT_SURFACE3D_CAMERA, rotateSurface3DCamera, zoomSurface3DCamera, type Surface3DCamera,
33
33
  } from "../../../components/chart/surface3d/model";
34
34
  import { VolatilitySurface } from "./surface";
35
- import { expiryLabel, formatIv, formatPrice, SmileChart, TermChart } from "./charts";
35
+ import { SmileChart, TermChart } from "./charts";
36
36
  import { surfaceCoordinateLabel } from "./headless";
37
+ import { expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT, HIDDEN_YIELD_TEXT, selectSurfaceExpiry, surfaceForwardRow,
38
+ surfaceSkewRow } from "./tables";
39
+ import { findListedExpiry, unlistedExpiryText } from "../../../utils/option-expiry";
37
40
 
38
41
  const TABS = [
39
42
  { value: "surface", label: "3D surface" }, { value: "table", label: "Table" },
@@ -41,9 +44,6 @@ const TABS = [
41
44
  { value: "skew", label: "Skew" }, { value: "forwards", label: "Forwards" },
42
45
  ];
43
46
  type Axis = "spot" | "forward" | "delta" | "strike";
44
- const DEFAULT_EXPIRY_MIN_DAYS = 28;
45
- /** Annualising a few days of parity carry produces meaningless yields. */
46
- const DIVIDEND_YIELD_MIN_DAYS = 30;
47
47
 
48
48
  export function VolSurfacePane({ focused, width, height }: PaneProps) {
49
49
  const colors = useThemeColors();
@@ -176,14 +176,17 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
176
176
  ] : [];
177
177
  const tableHeight = Math.max(3, height - 1 - tabRows - statGridRows(ivStats, width));
178
178
  const bitmapAvailable = !!useStaticChartBitmapSize(width, tableHeight);
179
+ // A typed date (`--expiration 2027-01-15`) is that day's UTC midnight; the
180
+ // catalogue may stamp the same expiry later in the day.
181
+ const pinned = expiration == null || !snapshot ? expiration : findListedExpiry(expiration, snapshot.catalogue) ?? expiration;
179
182
  useEffect(() => {
180
183
  // Existing slices only change selection. A new pin causes one load and then
181
184
  // remains in the request identity after its partial snapshots arrive.
182
- if (expiration == null || expiration === requestedExpiration || resource.loading || !snapshot
183
- || snapshot.expiries.some((entry) => entry.expiration === expiration)
184
- || !snapshot.catalogue.includes(expiration)) return;
185
- setRequestedExpiration(expiration);
186
- }, [expiration, requestedExpiration, resource.loading, snapshot]);
185
+ if (pinned == null || pinned === requestedExpiration || resource.loading || !snapshot
186
+ || snapshot.expiries.some((entry) => entry.expiration === pinned)
187
+ || !snapshot.catalogue.includes(pinned)) return;
188
+ setRequestedExpiration(pinned);
189
+ }, [pinned, requestedExpiration, resource.loading, snapshot]);
187
190
  const grid = useMemo(() => snapshot ? buildSurfaceGrid(snapshot, { axis, tenors }) : null, [axis, snapshot, tenors]);
188
191
  // Delta columns give every expiry the same quoted wing span, so the 3D
189
192
  // surface is a full sheet; moneyness keeps each row within 2.5 ATM sigmas.
@@ -207,19 +210,16 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
207
210
  // Sheet rows may be constant maturities: pair them with the nearest listed expiry.
208
211
  const nearestRow = (years: number | undefined) => years == null || !denseGrid?.tenors.length ? -1
209
212
  : denseGrid.tenors.reduce((best, value, index) => Math.abs(value - years) < Math.abs(denseGrid.tenors[best]! - years) ? index : best, 0);
210
- // A one-day front expiry is the noisiest smile on the board; the default
211
- // selection is the first expiry at least four weeks out.
212
- const defaultExpiry = snapshot?.expiries.find((entry) => entry.years * 365 >= DEFAULT_EXPIRY_MIN_DAYS) ?? snapshot?.expiries[0];
213
- const selectedExpiry = snapshot?.expiries.find((entry) => entry.expiration === (expiration ?? defaultExpiry?.expiration)) ?? null;
213
+ const selectedExpiry = snapshot ? selectSurfaceExpiry(snapshot, expiration) : null;
214
214
  const openChain = useCallback(() => {
215
215
  if (!snapshot) return;
216
- const selected = expiration ?? selectedExpiry?.expiration;
216
+ const selected = pinned ?? selectedExpiry?.expiration;
217
217
  createPaneFromTemplate("options-pane", { symbol: symbol ?? snapshot.symbol, ticker, instrument: target?.instrument,
218
218
  ...(ticker ? { listing: { name: ticker.metadata.name, exchange: ticker.metadata.exchange,
219
219
  currency: ticker.metadata.currency, type: ticker.metadata.assetCategory ?? "STK" } } : {}),
220
220
  values: selected == null ? {} : { expiration: String(selected) },
221
221
  });
222
- }, [createPaneFromTemplate, expiration, selectedExpiry?.expiration, snapshot?.symbol, symbol, ticker, target?.instrument]);
222
+ }, [createPaneFromTemplate, pinned, selectedExpiry?.expiration, snapshot?.symbol, symbol, ticker, target?.instrument]);
223
223
  useVolSurfaceEvidence({ snapshot, view: activeTab, grid: activeTab === "surface" && bitmapAvailable ? denseGrid : grid,
224
224
  selectedExpiry, loading: active.loading, bitmapAvailable, axis: activeTab === "surface" && bitmapAvailable ? (deltaSurface ? "delta" : "forward") : axis,
225
225
  tenors: activeTab === "surface" && bitmapAvailable ? "listed" : tenors, overlaySmiles });
@@ -323,8 +323,9 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
323
323
  ...(!spotAvailable && symbol && !historyDate ? ["Underlying price unavailable or stale"] : []),
324
324
  ...(historyDate && !stored.loading && !stored.data && !stored.error ? [`No stored ${underlying} surface for ${historyDate}`] : []),
325
325
  ...(fallbackDate ? [`Live chain has no two-sided quotes; showing the ${fallbackDate} close`] : []),
326
- ...(expiration != null && snapshot && !resource.loading && !snapshot.catalogue.includes(expiration)
327
- ? [`${expiryLabel(expiration)}: selected expiration unavailable`] : []),
326
+ ...(pinned != null && snapshot && !resource.loading && !snapshot.catalogue.includes(pinned)
327
+ && !snapshot.failures.some((failure) => failure.expiration === pinned)
328
+ ? [`${expiryLabel(pinned)}: ${unlistedExpiryText(pinned, snapshot.catalogue)}`] : []),
328
329
  ...(active.error ? [active.error] : [])];
329
330
  // What the quotes are and when they were observed; a reload moves the time.
330
331
  const freshness = !snapshot ? null : snapshot.stored ? `stored close ${snapshot.stored.sessionDate}`
@@ -429,7 +430,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
429
430
  error={!snapshot ? active.error : null} empty={!snapshot && !active.loading} subject="volatility surface">
430
431
  <QueryBar width={width}
431
432
  filters={[
432
- { id: "expiry", label: "Expiry", value: String(expiration ?? selectedExpiry?.expiration ?? ""),
433
+ { id: "expiry", label: "Expiry", value: String(pinned ?? selectedExpiry?.expiration ?? ""),
433
434
  options: snapshot?.catalogue.map((value) => ({ value: String(value), label: expiryLabel(value) })) ?? [],
434
435
  onChange: (value: string) => setExpiration(Number(value)) },
435
436
  ...(storedDates.length ? [{ id: "date", label: "Date", value: shownDate ?? "", defaultValue: "",
@@ -468,25 +469,35 @@ function ExpiryTable({ snapshot, selected, onSelect, forwards, width, height, fo
468
469
  forwards: boolean; width: number; height: number; focused: boolean;
469
470
  onKey: (event: DataTableKeyEvent) => boolean; metadata: () => unknown[][];
470
471
  }) {
471
- const fields = forwards ? ["Forward", "Basis", "Div %", "Rate %", "Pairs", "As of"] : ["25d put", "25d call", "RR pts", "BF pts", "90/110 pts", "Slope/yr"];
472
+ // IVs in percent, IV differences in vol points; a missing 25-delta figure says why.
473
+ const fields = forwards ? ["Forward", "Basis", "Div %", "Rate %", "Pairs", "As of"] : ["25d put %", "25d call %", "RR pts", "BF pts", "90/110 pts", "Slope pts/yr"];
472
474
  const integerColumn = forwards ? "4" : null;
473
475
  const [sort, setSort] = useState<{ columnId: string; direction: SortDirection }>({ columnId: "expiry", direction: "asc" });
474
- const value = (entry: SurfaceExpiry, id: string): number | string | null => id === "expiry" ? entry.expiration
475
- : forwards ? [entry.forward, entry.forward == null ? null : entry.forward - snapshot.spot,
476
- entry.dividendYield == null || entry.years * 365 < DIVIDEND_YIELD_MIN_DAYS ? null : entry.dividendYield * 100,
477
- entry.rate == null ? null : entry.rate * 100, snapshot.stored ? null : entry.parity.pairs.length, entry.asOf?.slice(0, 10) ?? null][Number(id)] ?? null
478
- : [entry.skew.put25, entry.skew.call25, entry.skew.riskReversal, entry.skew.butterfly, entry.skew.moneynessSkew, entry.termSlope][Number(id)] ?? null;
476
+ const rowsByExpiry = useMemo(() => new Map(snapshot.expiries.map((entry) => [entry.expiration,
477
+ { skew: surfaceSkewRow(entry), forward: surfaceForwardRow(entry, snapshot) }])), [snapshot]);
478
+ const value = (entry: SurfaceExpiry, id: string): number | string | null => {
479
+ if (id === "expiry") return entry.expiration;
480
+ const { skew, forward } = rowsByExpiry.get(entry.expiration)!;
481
+ return (forwards ? [forward.forward, forward.basis, forward.dividendYield == null ? null : forward.dividendYield * 100,
482
+ forward.rate == null ? null : forward.rate * 100, forward.pairs, entry.asOf?.slice(0, 10) ?? null]
483
+ : [skew.put25, skew.call25, skew.riskReversal, skew.butterfly, skew.moneynessSkew, skew.termSlope])[Number(id)] ?? null;
484
+ };
485
+ const missing = (entry: SurfaceExpiry, id: string) => {
486
+ const { skew, forward } = rowsByExpiry.get(entry.expiration)!;
487
+ if (forwards) return id === "2" && forward.dividendYieldHidden ? HIDDEN_YIELD_TEXT : "--";
488
+ return (id === "2" || id === "3" ? skew.gap : id === "4" ? skew.moneynessGap : id === "5" && skew.termSlopeHidden ? HIDDEN_SLOPE_TEXT : null) ?? "--";
489
+ };
479
490
  const rows = [...snapshot.expiries].sort((a, b) => {
480
491
  const x = value(a, sort.columnId), y = value(b, sort.columnId);
481
492
  const difference = typeof x === "number" && typeof y === "number" ? x - y : String(x ?? "").localeCompare(String(y ?? ""));
482
493
  return sort.direction === "asc" ? difference : -difference;
483
494
  });
484
495
  return <DataTableView<SurfaceExpiry> focused={focused} rootWidth={width} rootHeight={height} items={rows}
485
- columns={[{ id: "expiry", label: "Expiry", width: 13, align: "left" }, ...fields.map((label, i) => ({ id: String(i), label, width: 13, align: "right" as const }))]}
496
+ columns={[{ id: "expiry", label: "Expiry", width: 13, align: "left" }, ...fields.map((label, i) => ({ id: String(i), label, width: 14, align: "right" as const }))]}
486
497
  getItemKey={(entry) => String(entry.expiration)} selection={{ kind: "id", selectedId: String(selected?.expiration), getId: (entry) => String(entry.expiration), onChange: (_id, entry) => onSelect(entry) }}
487
498
  onActivate={onSelect} onRootKeyDown={onKey} getExportMetadata={metadata} freezeFirstColumn
488
499
  sortColumnId={sort.columnId} sortDirection={sort.direction} onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id))}
489
500
  renderCell={(entry, column) => { const raw = value(entry, column.id); return { text: column.id === "expiry" ? expiryLabel(entry.expiration)
490
- : raw == null ? "--" : typeof raw === "string" ? raw : column.id === integerColumn ? String(raw)
491
- : forwards ? (column.id === "1" && raw > 0 ? `+${formatPrice(raw)}` : formatPrice(raw)) : `${(raw * 100).toFixed(2)}` }; }} emptyStateTitle="No expiry observations." />;
501
+ : raw == null ? missing(entry, column.id) : typeof raw === "string" ? raw : column.id === integerColumn ? String(raw)
502
+ : forwards ? (column.id === "1" && raw > 0 ? `+${formatPrice(raw)}` : formatPrice(raw)) : formatVolPoints(raw) }; }} emptyStateTitle="No expiry observations." />;
492
503
  }
@@ -0,0 +1,124 @@
1
+ import { findListedExpiry } from "../../../utils/option-expiry";
2
+ import { evaluateSurfaceSmile, type SurfaceExpiry, type SurfaceSnapshot } from "./model";
3
+
4
+ export const formatIv = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : `${(value * 100).toFixed(2)}%`;
5
+ export const formatPrice = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : value.toFixed(2);
6
+ export const expiryLabel = (expiration: number) => new Date(expiration * 1000).toISOString().slice(0, 10);
7
+
8
+ /** A one-day front expiry is the noisiest smile on the board; the default selection is the first at least four weeks out. */
9
+ const DEFAULT_EXPIRY_MIN_DAYS = 28;
10
+ /** Annualising a few days of parity carry produces meaningless yields. */
11
+ const DIVIDEND_YIELD_MIN_DAYS = 30;
12
+ /**
13
+ * Below this many days to expiry, the ATM term slope to the next expiry
14
+ * annualises a few hours of IV change into hundreds of points a year.
15
+ */
16
+ export const TERM_SLOPE_MIN_DAYS = 7;
17
+ /** What the Skew view prints in place of a slope it leaves out. */
18
+ export const HIDDEN_SLOPE_TEXT = `under ${TERM_SLOPE_MIN_DAYS}d`;
19
+ /** What the Forwards view prints in place of a dividend yield it leaves out. */
20
+ export const HIDDEN_YIELD_TEXT = `under ${DIVIDEND_YIELD_MIN_DAYS}d`;
21
+ /** A decimal IV difference in vol points, as the Skew view prints it. */
22
+ export const formatVolPoints = (value: number | null | undefined) =>
23
+ value == null || !Number.isFinite(value) ? "--" : (value * 100).toFixed(2);
24
+
25
+ /** Calendar days to the expiry's close, as the model prices it. */
26
+ const expiryDays = (expiry: Pick<SurfaceExpiry, "years">) => expiry.years * 365;
27
+
28
+ /**
29
+ * The expiry the smile, chain and pricer actions use: the requested one,
30
+ * matched by calendar date as OMON matches it, else the first at least four
31
+ * weeks out. A requested expiry that is not loaded selects nothing.
32
+ */
33
+ export function selectSurfaceExpiry(snapshot: Pick<SurfaceSnapshot, "expiries">, requested: number | null | undefined): SurfaceExpiry | null {
34
+ if (requested != null) {
35
+ const listed = findListedExpiry(requested, snapshot.expiries.map((entry) => entry.expiration));
36
+ return snapshot.expiries.find((entry) => entry.expiration === listed) ?? null;
37
+ }
38
+ return snapshot.expiries.find((entry) => expiryDays(entry) >= DEFAULT_EXPIRY_MIN_DAYS) ?? snapshot.expiries[0] ?? null;
39
+ }
40
+
41
+ /**
42
+ * Why an expiry has no 25-delta risk reversal or butterfly: no fitted smile,
43
+ * or clean quotes that stop short of a 25-delta strike (the fit is never
44
+ * extrapolated past them). Null when both are there, or while it loads.
45
+ */
46
+ export function skewGap(expiry: SurfaceExpiry): string | null {
47
+ const { put25, call25, riskReversal, butterfly } = expiry.skew;
48
+ if (riskReversal != null && butterfly != null) return null;
49
+ if (!expiry.fit) return expiry.state === "loading" ? null : "no smile fit";
50
+ if (put25 == null && call25 == null) return "no 25D quotes";
51
+ if (put25 == null) return "no 25D put";
52
+ if (call25 == null) return "no 25D call";
53
+ return "no ATM IV";
54
+ }
55
+
56
+ export interface SurfaceSkewRow {
57
+ expiration: number;
58
+ days: number;
59
+ /** Decimal IVs at the 25-delta put and call. */
60
+ put25: number | null;
61
+ call25: number | null;
62
+ /** Decimal IV differences: call minus put, wing average minus ATM, 90% of spot minus 110%. */
63
+ riskReversal: number | null;
64
+ butterfly: number | null;
65
+ moneynessSkew: number | null;
66
+ /** Decimal ATM IV change per year to the next expiry; null under TERM_SLOPE_MIN_DAYS, which `termSlopeHidden` marks. */
67
+ termSlope: number | null;
68
+ termSlopeHidden: boolean;
69
+ /** Why the risk reversal and butterfly are missing. */
70
+ gap: string | null;
71
+ /** Why the 90/110 figure is missing. */
72
+ moneynessGap: string | null;
73
+ }
74
+
75
+ export function surfaceSkewRow(expiry: SurfaceExpiry): SurfaceSkewRow {
76
+ const days = expiryDays(expiry);
77
+ const termSlopeHidden = expiry.termSlope != null && days < TERM_SLOPE_MIN_DAYS;
78
+ return { expiration: expiry.expiration, days, put25: expiry.skew.put25, call25: expiry.skew.call25,
79
+ riskReversal: expiry.skew.riskReversal, butterfly: expiry.skew.butterfly, moneynessSkew: expiry.skew.moneynessSkew,
80
+ termSlope: termSlopeHidden ? null : expiry.termSlope, termSlopeHidden, gap: skewGap(expiry),
81
+ // 90% and 110% of spot lie past the clean quotes of a near expiry.
82
+ moneynessGap: expiry.skew.moneynessSkew != null ? null : !expiry.fit ? skewGap(expiry) : "not quoted" };
83
+ }
84
+
85
+ export interface SurfaceForwardRow {
86
+ expiration: number;
87
+ forward: number | null;
88
+ /** Forward minus spot, in price. */
89
+ basis: number | null;
90
+ /** Decimal implied dividend yield; null under 30 days out, which `dividendYieldHidden` marks. */
91
+ dividendYield: number | null;
92
+ dividendYieldHidden: boolean;
93
+ rate: number | null;
94
+ /** Put-call pairs behind the forward; a stored close keeps the forward, not its pairs. */
95
+ pairs: number | null;
96
+ }
97
+
98
+ export function surfaceForwardRow(expiry: SurfaceExpiry, snapshot: SurfaceSnapshot & { stored?: unknown }): SurfaceForwardRow {
99
+ const dividendYieldHidden = expiry.dividendYield != null && expiryDays(expiry) < DIVIDEND_YIELD_MIN_DAYS;
100
+ return { expiration: expiry.expiration, forward: expiry.forward, basis: expiry.forward == null ? null : expiry.forward - snapshot.spot,
101
+ dividendYield: dividendYieldHidden ? null : expiry.dividendYield, dividendYieldHidden,
102
+ rate: expiry.rate, pairs: snapshot.stored ? null : expiry.parity.pairs.length };
103
+ }
104
+
105
+ /** When and how an expiry's quotes were observed, as a report reads its freshness from each row. */
106
+ export const expiryFeedFields = (expiry: SurfaceExpiry) => ({
107
+ asOf: expiry.asOf, dataSource: expiry.dataSource, delayMinutes: expiry.delayMinutes, stale: expiry.stale,
108
+ });
109
+
110
+ /** The Term view's points: ATM at spot and the 25-delta wings, with the expiry's straddle and one-sigma move. */
111
+ export function surfaceTermRows(snapshot: SurfaceSnapshot) {
112
+ return snapshot.expiries.filter((expiry) => expiry.atmIV != null).sort((a, b) => a.years - b.years).map((expiry) => ({
113
+ expiration: expiry.expiration, days: expiryDays(expiry), atm: expiry.atmIV, put25: expiry.skew.put25, call25: expiry.skew.call25,
114
+ straddle: expiry.expectedMove.straddle, sigma: expiry.expectedMove.sigma, sigmaPercent: expiry.expectedMove.sigmaPercent,
115
+ ...expiryFeedFields(expiry),
116
+ }));
117
+ }
118
+
119
+ /** The Smile view's clean quotes against the fitted smile, in strike order. */
120
+ export function surfaceSmileRows(expiry: SurfaceExpiry) {
121
+ return expiry.points.map((point) => ({ strike: point.strike, side: point.side, moneyness: point.moneyness,
122
+ volatility: point.volatility, fitted: evaluateSurfaceSmile(expiry, point.strike), residual: point.fitResidual,
123
+ mid: point.mid, openInterest: point.openInterest }));
124
+ }