gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -20,6 +20,7 @@ import {
20
20
  } from "../../../../market-data/market/status";
21
21
  import { t } from "../../../../i18n";
22
22
  import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
23
+ import { delistingLabel, delistingStateLabel } from "../../../../market-data/quotes/delisting";
23
24
  import { formatQuoteNavAsOf } from "../../../../market-data/quotes/time";
24
25
  import { useQuoteFlashDirection } from "../../../../components/quote-flash";
25
26
  import { followLiveSparklinePrice } from "../../../../components/price-sparkline/model";
@@ -89,6 +90,8 @@ interface QuoteStatus {
89
90
  failed: boolean;
90
91
  text: string;
91
92
  stale?: boolean;
93
+ /** DELISTED or INACTIVE: the listing no longer trades, and `text` says since when. */
94
+ ended?: string;
92
95
  }
93
96
 
94
97
  function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quote: Quote | null | undefined): QuoteStatus {
@@ -98,6 +101,7 @@ function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quo
98
101
  ? { failed: true, text: `${symbol} not recognized` }
99
102
  : { failed: true, text: error.message || "Quote unavailable" };
100
103
  }
104
+ if (quote?.delisting) return { failed: true, text: delistingLabel(quote.delisting), stale: true, ended: delistingStateLabel(quote.delisting) };
101
105
  if (isQuoteStaleForCurrentSession(quote)) return { failed: true, text: "Stale quote", stale: true };
102
106
  if (entry?.phase === "ready") return { failed: false, text: "No quote data" };
103
107
  return { failed: false, text: "Loading quote..." };
@@ -175,8 +179,9 @@ export function QuoteMonitorCard({
175
179
  // One decimal count per instrument, so streamed ticks never narrow or widen the price column.
176
180
  const priceOptions = liveQuoteFormatOptions(quote, currency, assetCategory, cachedFinancials?.quoteMetadata?.instrumentType);
177
181
  const priceText = display ? formatMarketPriceWithCurrency(display.price, currency, priceOptions) : "";
178
- const changePercentText = display ? formatPercentRaw(display.changePercent) : "";
179
- const changeValueText = display ? formatSignedMarketPrice(display.change, priceOptions) : "";
182
+ // A listing that no longer trades shows its last close alone: no move, not even a dash for one.
183
+ const changePercentText = display && !quote?.delisting ? formatPercentRaw(display.changePercent) : "";
184
+ const changeValueText = display && !quote?.delisting ? formatSignedMarketPrice(display.change, priceOptions) : "";
180
185
  const priceColumnWidth = Math.max(priceText.length, changePercentText.length + changeValueText.length + 1);
181
186
  const nameMaxWidth = Math.max(10, width - priceColumnWidth - (nativePaneChrome ? 5 : 3));
182
187
  // The terminal sets the line beside the change, under the symbol (or after it in a
@@ -187,6 +192,8 @@ export function QuoteMonitorCard({
187
192
  const extendedParts = display && extended
188
193
  ? extendedLineParts(extended, formatMarketPriceWithCurrency(extended.price, currency, priceOptions), (text) => displayWidth(text) <= extendedRoom)
189
194
  : null;
195
+ // A desktop card says a listing no longer trades on a row of its own.
196
+ const endedRow = nativePaneChrome && quoteFailed && !!quoteStatus.ended && !extendedParts;
190
197
  const extendedColor = quoteFailed ? colors.textDim : priceColor(extended?.change ?? 0);
191
198
  const extendedDim = !!flashDirection && headlineHolds;
192
199
  const sparklineRange = resolvePriceSparklineRange(priceHistory, chartPeriod);
@@ -254,7 +261,7 @@ export function QuoteMonitorCard({
254
261
  <Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
255
262
  {symbol}
256
263
  </Text>
257
- <Text fg={quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
264
+ <Text fg={quoteStatus.ended ? colors.warning : quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
258
265
  </Box>
259
266
  ) : nativePaneChrome ? (
260
267
  <Box
@@ -276,7 +283,7 @@ export function QuoteMonitorCard({
276
283
  minWidth={0}
277
284
  style={{
278
285
  gridColumn: "1",
279
- gridRow: extendedParts ? "1" : "1 / span 2",
286
+ gridRow: extendedParts || endedRow ? "1" : "1 / span 2",
280
287
  minWidth: 0,
281
288
  overflow: "hidden",
282
289
  }}
@@ -284,8 +291,8 @@ export function QuoteMonitorCard({
284
291
  <Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
285
292
  {symbol}
286
293
  </Text>
287
- {quoteFailed && (
288
- <Text fg={colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
294
+ {quoteFailed && !endedRow && (
295
+ <Text fg={quoteStatus.ended ? colors.warning : colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
289
296
  {quoteStatus.text}
290
297
  </Text>
291
298
  )}
@@ -332,6 +339,28 @@ export function QuoteMonitorCard({
332
339
  </Box>
333
340
  </Box>
334
341
 
342
+ {endedRow && (
343
+ // Under the price, across the card, so the date is never cut on a narrow card;
344
+ // backed like the price so the sparkline does not run through it.
345
+ <Text
346
+ fg={colors.warning}
347
+ style={{
348
+ gridColumn: "1 / -1",
349
+ gridRow: "2",
350
+ justifySelf: "start",
351
+ alignSelf: "start",
352
+ fontSize: "var(--gloom-type-ui)",
353
+ lineHeight: "14px",
354
+ whiteSpace: "nowrap",
355
+ backgroundColor: colors.bg,
356
+ borderRadius: "var(--gloom-radius-sm)",
357
+ boxShadow: `0 0 0 2px ${colors.bg}`,
358
+ }}
359
+ >
360
+ {quoteStatus.text}
361
+ </Text>
362
+ )}
363
+
335
364
  {extendedParts && (
336
365
  <ExtendedSessionLine
337
366
  parts={extendedParts}
@@ -385,7 +414,7 @@ export function QuoteMonitorCard({
385
414
  </Box>
386
415
  <Box flexDirection="column">
387
416
  <FigureText fg={changeColor} dim={flashing} style={figureHalo}>
388
- {compactQuoteFailure ? `${priceText} · ${quoteStatus.stale ? "STALE" : "ERROR"}` : priceText}
417
+ {compactQuoteFailure ? `${priceText} · ${quoteStatus.ended ?? (quoteStatus.stale ? "STALE" : "ERROR")}` : priceText}
389
418
  </FigureText>
390
419
  <Box flexDirection="row" gap={1}>
391
420
  <FigureText part="sub" fg={changeColor} dim={flashing} style={figureHalo}>{changePercentText}</FigureText>
@@ -432,7 +461,7 @@ export function QuoteMonitorCard({
432
461
  )}
433
462
 
434
463
  {quoteFailed && !compactQuoteFailure && (
435
- <Box height={1} overflow="hidden"><Text fg={colors.negative}>{quoteStatus.text}</Text></Box>
464
+ <Box height={1} overflow="hidden"><Text fg={quoteStatus.ended ? colors.warning : colors.negative}>{quoteStatus.text}</Text></Box>
436
465
  )}
437
466
  {navAsOf && !compactNavAsOf && (
438
467
  <Box height={1} overflow="hidden"><Text fg={colors.textDim}>{navAsOf}</Text></Box>
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box } from "../../../ui";
3
3
  import { usePaneSettingValue, useShortcut } from "../../../public/react";
4
- import { DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
4
+ import { splitDelayedStatus, DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
5
5
  import { useThemeColors } from "../../../theme/theme-context";
6
6
  import type { PaneProps } from "../../../types/plugin";
7
7
  import type { TapeQuote, TapeSnapshot, TapeTrade } from "../../../api-client/tape";
@@ -109,7 +109,7 @@ function TimeSalesView({ width, height, focused, symbol, exchange }: PaneProps &
109
109
  usePaneNoticeFooter({ registrationId: "time-sales:notices", focused, notices: [...(data?.gaps ?? []), ...(resource.transport ? [resource.transport] : []),
110
110
  ...(pinnedBeforeTape ? [`The pinned moment is before this tape's first ${tab === "quotes" ? "quote" : "print"}, ${tapeStampNewYork(rowTime(rows.at(-1)!))}.`] : [])] });
111
111
  usePaneStatusFooter({ registrationId: "time-sales:status", loading: resource.loading, error: resource.error,
112
- info: data ? [{ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] },
112
+ info: data ? [...splitDelayedStatus({ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] }),
113
113
  ...(frozen ? [{ id: "paused", parts: [{ text: "paused", tone: "warning" as const }] }] : []),
114
114
  ...(!data.connected || resource.snapshotOnly ? [{ id: "snapshot", parts: [{ text: "snapshot", tone: "warning" as const }] }] : [])] : [] });
115
115
  const selectTab = (value: string) => { setTab(value); setDetail(null); setSelected(null); };
@@ -4,6 +4,7 @@ import { signalWithTimeout } from "../../../utils/async-deadline";
4
4
  import { parsePublicTickerKey } from "../../../utils/exchanges";
5
5
  import { httpFetch } from "../../../utils/http-transport";
6
6
  import { isoDateOrNull, numericOrNull, recordOrNull, textOrNull } from "../../../utils/guards";
7
+ import { logFailure } from "../../../utils/log-failure";
7
8
  import { zonedDateKey } from "../../../utils/zoned-date-time";
8
9
 
9
10
  const ENDPOINT = "https://services-dynarep.ddwa.finra.org/public/reporting/v2/data/group/FixedIncomeMarket/name/corporateAndAgencySecurities";
@@ -226,6 +227,7 @@ function dataRows(data: unknown): unknown[] | null {
226
227
  const parsed = JSON.parse(data) as unknown;
227
228
  return Array.isArray(parsed) ? parsed : null;
228
229
  } catch {
230
+ // Intentional ignore: malformed nested rows make postBondPage reject the response shape.
229
231
  return null;
230
232
  }
231
233
  }
@@ -340,7 +342,9 @@ async function readPayload(response: Response): Promise<unknown> {
340
342
  async function openSession(signal: AbortSignal): Promise<string> {
341
343
  const session = await httpFetch(ENDPOINT, { headers: REQUEST_HEADERS, signal });
342
344
  const token = xsrfToken(session);
343
- await session.body?.cancel().catch(() => {});
345
+ await session.body?.cancel().catch(() => {
346
+ // Intentional ignore: only session headers are needed; discarding its unused body is best effort.
347
+ });
344
348
  if (!token) throw new Error(failureText(session.status, "session"));
345
349
  return token;
346
350
  }
@@ -477,7 +481,14 @@ export async function fetchBondTape(input: string, options: BondTapeOptions = {}
477
481
  let answer = first;
478
482
  let issuer: BondIssuer | null = null;
479
483
  if (first.bonds.length === 0 && query?.kind === "issuer" && query.ticker && options.companyName) {
480
- const company = await options.companyName(query.ticker).catch(() => null);
484
+ const company = await options.companyName(query.ticker).catch((error: unknown) => {
485
+ options.signal?.throwIfAborted();
486
+ if (error instanceof Error && error.name === "AbortError") throw error;
487
+ logFailure("trace-bonds.client", "Resolve bond issuer company failed")(error);
488
+ answer = { ...first, failures: [...first.failures, "Issuer-name lookup failed."] };
489
+ return null;
490
+ });
491
+ options.signal?.throwIfAborted();
481
492
  const name = company ? tapeIssuerName(company) : null;
482
493
  if (company && name && name !== query.name) {
483
494
  issuer = { company, name };
@@ -492,7 +503,7 @@ export async function fetchBondTape(input: string, options: BondTapeOptions = {}
492
503
  // No prints for the query: date the answer by the market's newest sale.
493
504
  const market = await postBondPage(token, [TRADED], 1, deadline)
494
505
  .then((payload) => latestTradeDate(mergeBondAnswers([{ payload, accepts: anyBond, limit: 1 }]).bonds))
495
- .catch(() => null);
506
+ .catch(logFailure("trace-bonds.client", "Load bond market date failed", null));
496
507
  return { ...result, tradeDate: market };
497
508
  }
498
509
 
@@ -89,7 +89,7 @@ export const traceBondsHeadless: HeadlessPaneDefinition<"bundle"> = {
89
89
  rows: bonds.map(reportRow),
90
90
  ...(query ? { empty: noPrintsText(query, tradeDate, issuer) } : {}),
91
91
  }],
92
- complete: true,
92
+ complete: failures.length === 0,
93
93
  ...(failures.length > 0 ? { errors: failures } : {}),
94
94
  ...(truncated ? { notes: [TRUNCATED_TEXT] } : {}),
95
95
  ...(shown ? { warnings: [issuerFallbackText(query!, issuer)] } : {}),
@@ -3,7 +3,7 @@ import { getSharedMarketDataCoordinator, MarketDataCoordinator, resolveEntryValu
3
3
  import type { InstrumentRef, OptionsRequest } from "../../../market-data/request-types";
4
4
  import type { QueryEntry } from "../../../market-data/result-types";
5
5
  import type { DataProvider } from "../../../types/data-provider";
6
- import type { OptionsChain } from "../../../types/financials";
6
+ import type { OptionsChain, Quote } from "../../../types/financials";
7
7
  import { daysToExpiryFrom } from "../shared/volatility";
8
8
  import type { YieldPoint } from "../yield-curve/treasury-data";
9
9
  import {
@@ -13,6 +13,7 @@ import {
13
13
  import { abortable, abortError } from "../../../utils/async-deadline";
14
14
  import { errorMessage } from "../../../utils/errors";
15
15
  import { findListedExpiry, unlistedExpiryText } from "../../../utils/option-expiry";
16
+ import { optionModelSpot } from "../options/model-spot";
16
17
 
17
18
  const DEFAULT_SURFACE_EXPIRY_LIMIT = 18;
18
19
  const SURFACE_LOAD_CONCURRENCY = 4;
@@ -91,6 +92,14 @@ export interface SurfaceLoadRequest {
91
92
  instrument: InstrumentRef;
92
93
  spot: number;
93
94
  spotAsOf?: string | number | null;
95
+ /**
96
+ * The underlying's quote, to read the spot from as OMON reads it
97
+ * (`optionModelSpot`): a chain from a closed session is priced at that
98
+ * session's official close, which the quote carries, so the same chain
99
+ * gives the same surface whatever trades before the next open. Without
100
+ * it, or without a chain to date, `spot` is used as given.
101
+ */
102
+ quote?: Quote | null;
94
103
  settings?: Partial<SurfaceSettings>;
95
104
  limit?: number;
96
105
  /** Listed expiries required by a selection, added to the representative sample. */
@@ -134,6 +143,9 @@ export async function loadVolatilitySurface(
134
143
  let catalogueExpiration: number | null = null;
135
144
  let treasuryError: string | null = null;
136
145
  let finished = false;
146
+ let spot = request.spot;
147
+ let spotAsOf = request.spotAsOf ?? null;
148
+ let spotBasis: SurfaceSnapshot["spotBasis"] = null;
137
149
  const checkAbort = () => { if (request.signal?.aborted) throw abortError(CANCELLED); };
138
150
  const snapshot = (): SurfaceSnapshot => {
139
151
  const failures: SurfaceFailure[] = [];
@@ -152,7 +164,7 @@ export async function loadVolatilitySurface(
152
164
  }
153
165
  const cached = models.get(expiration);
154
166
  if (cached?.entry === entry && cached.curve === curve) return cached.value;
155
- const value = buildSurfaceExpiry({ chain, expiration, spot: request.spot, curve, settings, now,
167
+ const value = buildSurfaceExpiry({ chain, expiration, spot, curve, settings, now,
156
168
  source: entry.source, stale: !!entry.error || (entry.staleAt != null && entry.staleAt <= now), error });
157
169
  models.set(expiration, { entry, curve, value });
158
170
  return value;
@@ -166,7 +178,7 @@ export async function loadVolatilitySurface(
166
178
  : failures.length > 0 ? (ready ? "partial" : "error") : "ready";
167
179
  const dates = [...new Set(expiries.flatMap((expiry) => expiry.rateAsOf))];
168
180
  return {
169
- symbol, spot: request.spot, spotAsOf: request.spotAsOf ?? null, phase, settings, catalogue: [...catalogue],
181
+ symbol, spot, spotAsOf, ...spotBasis ? { spotBasis } : {}, phase, settings, catalogue: [...catalogue],
170
182
  requested: selected.length, loaded, failed, expiries, failures,
171
183
  warnings: [...new Set([...expiries.flatMap((expiry) => expiry.warnings), ...surfaceCalendarWarnings(expiries)])],
172
184
  rateAsOf: dates.length === 1 ? dates[0]! : null, fetchedAt: now,
@@ -190,6 +202,14 @@ export async function loadVolatilitySurface(
190
202
  { forceRefresh: request.expiries ? false : request.forceRefresh }), request.signal, CANCELLED);
191
203
  checkAbort();
192
204
  const chain = resolveEntryValue(initial);
205
+ // Every expiry is valued at one spot, read against the catalogue chain's time before any expiry is fitted.
206
+ const modelSpot = request.quote ? optionModelSpot(request.quote, chain, now, request.instrument.exchange) : null;
207
+ if (modelSpot) {
208
+ spot = modelSpot.price;
209
+ spotAsOf = modelSpot.observedAt;
210
+ spotBasis = { basis: modelSpot.basis, session: modelSpot.session,
211
+ sessionDate: modelSpot.basis === "close" ? request.quote!.regularCloseSessionDate ?? null : null };
212
+ }
193
213
  const initialContracts = chain ? [...chain.calls, ...chain.puts] : [];
194
214
  const representedExpiry = initialContracts[0]?.expiration;
195
215
  if (representedExpiry != null && initialContracts.every((contract) => contract.expiration === representedExpiry)
@@ -4,14 +4,15 @@ import {
4
4
  expirationOptionSeconds, findListedExpiry, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, readOptionExpiration, unlistedExpiryText,
5
5
  } from "../../../utils/option-expiry";
6
6
  import { DATE_OPTION_EXAMPLES } from "../../../utils/calendar-date";
7
- import { skewHeader } from "../iv-history/format";
7
+ import { IMPLIED_VOL_WORDS, skewHeader } from "../iv-history/format";
8
8
  import { formatStrikeLabel } from "../options/table";
9
9
  import { createSurfaceDependencies, loadVolatilitySurface } from "./client";
10
10
  import { buildSurfaceGrid, type SurfaceGridOptions, type SurfaceSettings, type SurfaceSnapshot } from "./model";
11
- import { formatCaptureTime, loadDatedSurface, storedSurfaceDate, type DatedSurfaceSnapshot } from "./stored";
11
+ import { atCapturePrecision, formatCaptureTime, loadDatedSurface, storedSurfaceDate, type DatedSurfaceSnapshot } from "./stored";
12
+ import { listingFateEmpty, loadListingFate } from "../../../tickers/listing-fate";
12
13
  import {
13
14
  expiryFeedFields, expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT, HIDDEN_YIELD_TEXT, selectSurfaceExpiry, surfaceForwardRow, surfaceSkewRow,
14
- surfaceSmileRows, surfaceTermRows, TERM_SLOPE_MIN_DAYS, type SurfaceForwardRow, type SurfaceSkewRow,
15
+ surfaceSmileRows, surfaceSpotBasisLabel, surfaceTermRows, TERM_SLOPE_MIN_DAYS, type SurfaceForwardRow, type SurfaceSkewRow,
15
16
  } from "./tables";
16
17
 
17
18
  type SurfaceAxis = NonNullable<SurfaceGridOptions["axis"]>;
@@ -26,8 +27,8 @@ export function surfaceCoordinateLabel(axis: SurfaceAxis, coordinate: number, in
26
27
 
27
28
  /** The Skew view's sign conventions and units, one line each. */
28
29
  const SKEW_NOTES = [
29
- "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.",
30
- `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.`,
30
+ "RR is 25D call IV minus 25D put IV, BF the average 25D wing IV minus the IV at the forward, 90/110 the IV at 90% of the forward minus the IV at 110%; all in vol points.",
31
+ `Slope is the change per year in the IV at the forward 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.`,
31
32
  ];
32
33
 
33
34
  /**
@@ -132,6 +133,7 @@ function forwardsSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
132
133
  export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
133
134
  shape: "bundle", argument: { kind: "ticker", description: "Underlying ticker" },
134
135
  usListingsOnly: "Options data",
136
+ discovery: { suggestFor: IMPLIED_VOL_WORDS },
135
137
  describe: (args) => `OVDV ${args.symbols[0] ?? ""}`,
136
138
  options: [
137
139
  { key: "tab", type: "enum", values: SURFACE_TABS.map((value) => ({ value })),
@@ -154,6 +156,7 @@ export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
154
156
  const date = typeof args.options.date === "string" && args.options.date ? storedSurfaceDate(args.options.date) : null;
155
157
  let snapshot: DatedSurfaceSnapshot;
156
158
  let quote: Quote | null = null;
159
+ let instrument: { symbol: string; exchange?: string } = { symbol };
157
160
  if (date) {
158
161
  // A stored close needs no current price: it carries the spot it was fitted at.
159
162
  snapshot = await loadDatedSurface(symbol, date, { signal: ctx.signal, api: ctx.apiClient });
@@ -162,21 +165,35 @@ export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
162
165
  snapshot = { ...snapshot, failures: [...snapshot.failures, { expiration: requested, message: unlistedExpiryText(requested, snapshot.catalogue) }] };
163
166
  }
164
167
  } else {
165
- const instrument = await ctx.resolveInstrument?.(symbol) ?? { symbol };
166
- quote = await ctx.marketData.getQuote(symbol, instrument.exchange);
168
+ const resolved = await ctx.resolveInstrument?.(symbol) ?? { symbol };
169
+ instrument = resolved;
170
+ quote = await ctx.marketData.getQuote(symbol, resolved.exchange);
167
171
  if (!(quote.price > 0) || !Number.isFinite(quote.price) || quote.stale) {
168
- return { sections: [], complete: false, unavailableSymbols: [symbol], errors: ["A current underlying price is required"] };
172
+ // A listing that stopped trading has no current price: what became of it is why (TWTR).
173
+ const fate = await loadListingFate(resolved, ctx.marketData, { quote });
174
+ return { sections: [], complete: false, unavailableSymbols: [symbol],
175
+ ...listingFateEmpty(fate, ["A current underlying price is required"]) };
169
176
  }
170
- snapshot = await loadVolatilitySurface({ instrument, spot: quote.price, spotAsOf: quote.lastUpdated,
177
+ // At a stored close's precision, so the live report and a --date one export the same digits. A chain
178
+ // from a closed session is priced at that session's close, as OMON prices it.
179
+ snapshot = atCapturePrecision(await loadVolatilitySurface({ instrument, spot: quote.price, spotAsOf: quote.lastUpdated, quote,
171
180
  settings: { ivSource: args.options.ivSource as SurfaceSettings["ivSource"], priceSide: args.options.priceSide as SurfaceSettings["priceSide"] },
172
181
  limit: Number(args.options.limit), signal: ctx.signal,
173
182
  requiredExpiries: requested == null ? [] : [requested],
174
- }, createSurfaceDependencies(ctx.marketData, ctx.apiClient));
183
+ }, createSurfaceDependencies(ctx.marketData, ctx.apiClient)));
175
184
  }
185
+ const available = snapshot.expiries.some((expiry) => expiry.fit);
186
+ const loadErrors = snapshot.failures.map((failure) => failure.expiration == null ? failure.message
187
+ : `${expiryLabel(failure.expiration)}: ${failure.message}`);
188
+ // A symbol with no option chain at all says what became of it when something did (FB is a fund now,
189
+ // Meta trades as META) in place of the chain's own error; a chain without clean quotes is the symbol's own.
190
+ const fate = !snapshot.stored && !available && snapshot.catalogue.length === 0
191
+ ? listingFateEmpty(await loadListingFate(instrument, ctx.marketData, { quote }), loadErrors) : null;
192
+ // The spot a chain from a closed session was priced at, and its session.
193
+ const spotBasis = available ? surfaceSpotBasisLabel(snapshot) : null;
176
194
  const stored = snapshot.stored ?? null;
177
195
  const axis = args.options.axis as SurfaceAxis;
178
196
  const tab = (SURFACE_TABS as readonly string[]).includes(String(args.options.tab)) ? args.options.tab as SurfaceTab : "surface";
179
- const available = snapshot.expiries.some((expiry) => expiry.fit);
180
197
  const expiryWarnings = snapshot.expiries.flatMap((expiry) => expiry.warnings.map((warning) =>
181
198
  ({ expiry: expiryLabel(expiry.expiration), warning })));
182
199
  const perExpiry = new Set(expiryWarnings.map((row) => row.warning));
@@ -197,9 +214,11 @@ export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
197
214
  ],
198
215
  complete: available && snapshot.failures.length === 0 && snapshot.expiries.every((expiry) => expiry.state === "ready"),
199
216
  unavailableSymbols: available ? [] : [symbol],
200
- errors: [...snapshot.failures.map((failure) => failure.expiration == null ? failure.message
201
- : `${expiryLabel(failure.expiration)}: ${failure.message}`), ...smile?.errors ?? []],
202
- ...(stored || tab === "skew" ? { notes: [...stored ? storedNotes(snapshot, args.options) : [], ...tab === "skew" ? SKEW_NOTES : []] } : {}),
217
+ errors: [...fate?.errors ?? loadErrors, ...smile?.errors ?? []],
218
+ ...(fate?.empty ? { empty: fate.empty } : {}),
219
+ ...(stored || spotBasis || tab === "skew" ? { notes: [
220
+ ...stored ? storedNotes(snapshot, args.options) : spotBasis ? [`Spot ${formatPrice(snapshot.spot)}, ${spotBasis}.`] : [],
221
+ ...tab === "skew" ? SKEW_NOTES : []] } : {}),
203
222
  // A stored close is dated by its session, whichever view prints it.
204
223
  ...(stored ? { freshness: { status: "not-a-feed" as const, basis: "stored close capture", asOf: stored.sessionDate } }
205
224
  : smile?.freshness ? { freshness: smile.freshness } : {}),
@@ -99,10 +99,23 @@ export interface SurfaceFailure {
99
99
  reasonCode?: string;
100
100
  }
101
101
 
102
+ /**
103
+ * What a surface's spot is (`optionModelSpot`): the official close of the
104
+ * session the chain was quoted in (`close`, that session's New York date),
105
+ * or the latest price (`last`, with the extended session it printed in).
106
+ */
107
+ interface SurfaceSpotBasis {
108
+ basis: "close" | "last";
109
+ session: "pre-market" | "after hours" | "overnight" | null;
110
+ sessionDate: string | null;
111
+ }
112
+
102
113
  export interface SurfaceSnapshot {
103
114
  symbol: string;
104
115
  spot: number;
105
116
  spotAsOf?: string | number | null;
117
+ /** Absent when the caller gave a bare spot, as a stored close does. */
118
+ spotBasis?: SurfaceSpotBasis | null;
106
119
  phase: "loading" | "partial" | "ready" | "error";
107
120
  settings: SurfaceSettings;
108
121
  catalogue: number[];
@@ -401,11 +414,16 @@ export function buildSurfaceExpiry(input: BuildSurfaceExpiryInput): SurfaceExpir
401
414
  }
402
415
 
403
416
  /**
404
- * A fitted expiry's skew in decimal IV: RR is the 25-delta call minus put, BF
405
- * their average minus ATM, 90/110 the IV at 90% of spot minus 110%. The
406
- * 25-delta strikes are found on the fit inside the quoted strikes, as the
407
- * Table's delta columns find them, so a fit that cannot price a far wing it
408
- * was never quoted at does not blank a quoted expiry's skew.
417
+ * A fitted expiry's skew in decimal IV, measured from its forward: `atm` is
418
+ * the IV at the forward (ATMF), RR the 25-delta call minus put, BF their
419
+ * average minus ATMF, 90/110 the IV at 90% of the forward minus 110%. The
420
+ * spot only enters through the 25-delta strikes' spot delta, whose carry is
421
+ * the parity forward's, so a pre-market print against the close's chain does
422
+ * not move a fly, and a long-dated fly is not dragged below zero by the skew
423
+ * between spot and a forward several percent above it. The 25-delta strikes
424
+ * are found on the fit inside the quoted strikes, as the Table's delta
425
+ * columns find them, so a fit that cannot price a far wing it was never
426
+ * quoted at does not blank a quoted expiry's skew.
409
427
  */
410
428
  export function surfaceExpirySkew(expiry: SurfaceExpiry, spot: number): SkewMetrics {
411
429
  const smile = (strike: number) => evaluateSurfaceSmile(expiry, strike);
@@ -413,8 +431,9 @@ export function surfaceExpirySkew(expiry: SurfaceExpiry, spot: number): SkewMetr
413
431
  const strike = deltaStrike(expiry, spot, delta);
414
432
  return strike == null ? null : smile(strike);
415
433
  };
416
- const put25 = at(-0.25), call25 = at(0.25), atm = expiry.atmIV;
417
- const low = smile(spot * 0.9), high = smile(spot * 1.1);
434
+ const forward = expiry.forward;
435
+ const put25 = at(-0.25), call25 = at(0.25), atm = positive(forward) ? smile(forward) : null;
436
+ const low = positive(forward) ? smile(forward * 0.9) : null, high = positive(forward) ? smile(forward * 1.1) : null;
418
437
  const both = put25 != null && call25 != null;
419
438
  return { atm, put25, call25, putCallSkew: both ? put25 - call25 : null, riskReversal: both ? call25 - put25 : null,
420
439
  butterfly: both && atm != null ? (put25 + call25) / 2 - atm : null,
@@ -433,12 +452,13 @@ export function surfaceCalendarWarnings(expiries: readonly SurfaceExpiry[]): str
433
452
  return count > 0 ? [`${count} calendar arbitrage ${count === 1 ? "warning" : "warnings"}`] : [];
434
453
  }
435
454
 
455
+ /** The ATM-forward IV change per year to the next expiry that has one: spot plays no part in it. */
436
456
  export function withSurfaceTermSlopes(expiries: readonly SurfaceExpiry[]): SurfaceExpiry[] {
437
457
  const sorted = [...expiries].sort((a, b) => a.years - b.years);
438
458
  return sorted.map((expiry, index) => {
439
- const next = sorted.slice(index + 1).find((item) => item.atmIV != null);
440
- return { ...expiry, termSlope: expiry.atmIV != null && next?.atmIV != null
441
- ? volatilityTermSlope({ years: expiry.years, volatility: expiry.atmIV }, { years: next.years, volatility: next.atmIV }) : null };
459
+ const next = sorted.slice(index + 1).find((item) => item.skew.atm != null);
460
+ return { ...expiry, termSlope: expiry.skew.atm != null && next?.skew.atm != null
461
+ ? volatilityTermSlope({ years: expiry.years, volatility: expiry.skew.atm }, { years: next.years, volatility: next.skew.atm }) : null };
442
462
  });
443
463
  }
444
464
 
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { DataTableView, EmptyState, PaneStatusBody, QueryBar, StatGrid, statGridRows, usePaneFooter, usePaneMenuItems,
2
+ import { splitDelayedStatus, DataTableView, EmptyState, PaneStatusBody, QueryBar, StatGrid, statGridRows, usePaneFooter, usePaneMenuItems,
3
3
  usePaneNoticeFooter, usePaneTabs, usePaneTicker, type DataTableColumn, type DataTableKeyEvent, type StatItem } from "../../../components";
4
4
  import { useTableLoadMore } from "../../../components/table-view-shared";
5
5
  import { useStaticChartBitmapSize } from "../../../components/chart/composite/bitmap";
@@ -39,7 +39,7 @@ import { VolatilitySurface } from "./surface";
39
39
  import { SmileChart, TermChart } from "./charts";
40
40
  import { surfaceCoordinateLabel } from "./headless";
41
41
  import { expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT, HIDDEN_YIELD_TEXT, selectSurfaceExpiry, surfaceForwardRow,
42
- surfaceSkewRow } from "./tables";
42
+ surfaceSkewRow, surfaceSpotBasisLabel } from "./tables";
43
43
  import { findListedExpiry, unlistedExpiryText } from "../../../utils/option-expiry";
44
44
 
45
45
  const TABS = [
@@ -95,17 +95,19 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
95
95
  const spotAvailable = !!quote && quote.price > 0 && Number.isFinite(quote.price) && !quote.stale;
96
96
  const spotAsOfRef = useRef(quote?.lastUpdated ?? null);
97
97
  spotAsOfRef.current = quote?.lastUpdated ?? null;
98
- const spotRef = useRef(quote?.price ?? 0);
99
- spotRef.current = quote?.price ?? 0;
98
+ const quoteRef = useRef(quote ?? null);
99
+ quoteRef.current = quote ?? null;
100
100
  const requestKey = JSON.stringify([target?.cacheKey, symbol, axis, tenors, ivSource, priceSide, spread, age, limit, spotAvailable, requestedExpiration]);
101
101
  // Every load reads the spot at the moment it starts; a reload in session
102
102
  // therefore uses the current stream price, not the one from the first load.
103
+ // A chain from a closed session is priced at that session's close, which
104
+ // the quote carries, so a pre-market print does not move it.
103
105
  const surfaceRequest = useCallback((force: boolean, signal: AbortSignal): SurfaceLoadRequest => {
104
106
  const instrument = target?.instrument;
105
107
  return {
106
108
  instrument: { symbol: target?.effectiveTicker ?? symbol!, exchange: target?.effectiveExchange ?? "",
107
109
  brokerId: instrument?.brokerId, brokerInstanceId: instrument?.brokerInstanceId, instrument },
108
- spot: spotRef.current, spotAsOf: spotAsOfRef.current, limit, forceRefresh: force, signal,
110
+ spot: quoteRef.current?.price ?? 0, spotAsOf: spotAsOfRef.current, quote: quoteRef.current, limit, forceRefresh: force, signal,
109
111
  requiredExpiries: expirationRef.current == null ? [] : [expirationRef.current],
110
112
  settings: { ...DEFAULT_SURFACE_SETTINGS, ivSource, priceSide, maxRelativeSpread: Number(spread), maxStaleSessions: Number(age) },
111
113
  };
@@ -153,6 +155,14 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
153
155
  const underlying = (target?.effectiveTicker ?? symbol ?? "").toUpperCase();
154
156
  const datesLoader = useCallback(async () => (await loadSurfaceDates(underlying)).dates, [underlying]);
155
157
  const dates = useAsyncResource(underlying ? datesLoader : null);
158
+ // A symbol with no current price, or no option chain at all, says what became of it when something
159
+ // did, as `quote` says it: TWTR stopped trading, FB is a fund now and Meta trades as META.
160
+ const noChain = !historyDate && !!quote && (!spotAvailable
161
+ || (!!resource.data && !resource.loading && resource.data.catalogue.length === 0));
162
+ const fateExchange = target?.effectiveExchange ?? "";
163
+ const fateLoader = useCallback(async () => (await getSharedMarketDataCoordinator()
164
+ ?.loadListingFate({ symbol: underlying, exchange: fateExchange }))?.note ?? null, [underlying, fateExchange]);
165
+ const fateNote = useAsyncResource(underlying && noChain ? fateLoader : null).data ?? null;
156
166
  const ivRank = useIvRank(underlying || null);
157
167
  useEffect(() => () => controller.current?.abort(), [request]);
158
168
  const incremental = partial?.key === requestKey ? partial.snapshot : null;
@@ -229,6 +239,11 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
229
239
  const nearestRow = (years: number | undefined) => years == null || !denseGrid?.tenors.length ? -1
230
240
  : denseGrid.tenors.reduce((best, value, index) => Math.abs(value - years) < Math.abs(denseGrid.tenors[best]! - years) ? index : best, 0);
231
241
  const selectedExpiry = snapshot ? selectSurfaceExpiry(snapshot, expiration) : null;
242
+ // Another date loads under the query bar, which keeps the last surface's expiries until it answers.
243
+ const lastShown = useRef<{ underlying: string; snapshot: DatedSurfaceSnapshot } | null>(null);
244
+ if (snapshot) lastShown.current = { underlying, snapshot };
245
+ const barSnapshot = snapshot ?? (lastShown.current?.underlying === underlying ? lastShown.current.snapshot : null);
246
+ const barExpiry = barSnapshot ? selectSurfaceExpiry(barSnapshot, expiration) : null;
232
247
  const openChain = useCallback(() => {
233
248
  if (!snapshot) return;
234
249
  const selected = pinned ?? selectedExpiry?.expiration;
@@ -334,7 +349,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
334
349
  for (const entry of snapshot?.expiries ?? []) {
335
350
  for (const warning of entry.warnings) expiryWarnings.set(warning, [...expiryWarnings.get(warning) ?? [], entry.expiration]);
336
351
  }
337
- const notices = [...(snapshot?.warnings ?? []).filter((warning) => !expiryWarnings.has(warning)), ...failures,
352
+ const notices = fateNote ? [] : [...(snapshot?.warnings ?? []).filter((warning) => !expiryWarnings.has(warning)), ...failures,
338
353
  ...(activeTab === "surface" && sheet?.omitted.length ? [`3D sheet omits ${sheet.omitted.map((entry) => expiryLabel(entry.expiration)).join(", ")}: smile fit fell back to interpolation (shown in Table)`] : []),
339
354
  ...[...expiryWarnings].map(([warning, expirations]) => `${expirations.length > 1 && expirations.length === snapshot?.expiries.length
340
355
  ? "All expiries" : expirations.map(expiryLabel).join(", ")}: ${warning}`),
@@ -345,6 +360,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
345
360
  && !snapshot.failures.some((failure) => failure.expiration === pinned)
346
361
  ? [`${expiryLabel(pinned)}: ${unlistedExpiryText(pinned, snapshot.catalogue)}`] : []),
347
362
  ...(active.error ? [active.error] : [])];
363
+ const spotBasis = snapshot && !snapshot.stored ? surfaceSpotBasisLabel(snapshot) : null;
348
364
  // What the quotes are and when they were observed; a reload moves the time.
349
365
  const freshness = !snapshot ? null : snapshot.stored ? `stored close ${snapshot.stored.sessionDate}`
350
366
  : surfaceFreshnessLabel(snapshot, CLOUD_QUOTE_DELAY_MINUTES);
@@ -360,7 +376,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
360
376
  info: [
361
377
  ...(active.loading ? [{ id: "loading", parts: [{ text: snapshot && snapshot.loaded < snapshot.requested
362
378
  ? `loading ${snapshot.loaded}/${snapshot.requested} expiries` : "loading", tone: "muted" as const }] }] : []),
363
- ...(freshness ? [{ id: "source", parts: [{ text: freshness, tone: "muted" as const }] }] : []),
379
+ ...(freshness ? splitDelayedStatus({ id: "source", parts: [{ text: freshness, tone: "muted" }] }) : []),
364
380
  ...(snapshot?.expiries.some((entry) => entry.stale) ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
365
381
  ...(selectedExpiry?.fit && activeTab === "smile" ? [{ id: "fit", parts: [{ text: `${selectedExpiry.fit.method} · RMSE ${(selectedExpiry.fit.residual * 100).toFixed(3)} vol pts`, tone: "muted" as const }] }] : []),
366
382
  ...(selectionStatus ? [{ id: "selection", parts: [{ text: selectionStatus, tone: "muted" as const }] }] : []),
@@ -447,24 +463,28 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
447
463
  useEffect(() => setTableOnScreen(tableShown), [tableShown]);
448
464
  return <Box flexDirection="column" width={width} overflow="hidden" {...(nativePaneChrome ? FILL_REST : { height })}>
449
465
  {tabStrip}
450
- {!symbol ? <EmptyState title="Choose an underlying ticker." /> : <PaneStatusBody loading={active.loading && !snapshot}
451
- error={!snapshot ? active.error : null} empty={!snapshot && !active.loading} subject="volatility surface">
452
- <QueryBar width={width}
466
+ {!symbol ? <EmptyState title="Choose an underlying ticker." /> : <>
467
+ {barSnapshot ? <QueryBar width={width}
453
468
  filters={[
454
- { id: "expiry", label: "Expiry", value: String(pinned ?? selectedExpiry?.expiration ?? ""),
455
- options: snapshot?.catalogue.map((value) => ({ value: String(value), label: expiryLabel(value) })) ?? [],
469
+ { id: "expiry", label: "Expiry", value: String(pinned ?? barExpiry?.expiration ?? ""),
470
+ options: barSnapshot.catalogue.map((value) => ({ value: String(value), label: expiryLabel(value) })),
456
471
  onChange: (value: string) => setExpiration(Number(value)) },
457
472
  ...(storedDates.length ? [{ id: "date", label: "Date", value: shownDate ?? "", defaultValue: "",
458
473
  options: [{ value: "", label: "Live" }, ...storedDates.map((date) => ({ value: date, label: date }))],
459
474
  onChange: (value: string) => setHistoryDate(value || null) }] : []),
460
475
  ]}
461
- meta={snapshot?.stored
476
+ meta={!snapshot ? undefined : snapshot.stored
462
477
  ? `Stored close · spot ${formatPrice(snapshot.spot)} · captured ${formatCaptureTime(snapshot.stored.capturedAt)} New York`
463
- : [`Spot ${formatPrice(snapshot?.spot)}${quote?.currency ? ` ${quote.currency}` : ""}`, selectedExpiry?.asOf?.slice(0, 10)].filter(Boolean).join(" · ")} />
478
+ : [`Spot ${formatPrice(snapshot.spot)}${quote?.currency ? ` ${quote.currency}` : ""}`,
479
+ // The close a closed session's chain is priced at dates the chain too.
480
+ spotBasis, snapshot.spotBasis?.basis === "close" ? null : selectedExpiry?.asOf?.slice(0, 10)].filter(Boolean).join(" · ")} /> : null}
481
+ <PaneStatusBody loading={active.loading && !snapshot && !fateNote}
482
+ error={!snapshot && !fateNote ? active.error : null} empty={!!fateNote || !snapshot && !active.loading}
483
+ emptyTitle={fateNote ?? undefined} subject="volatility surface">
464
484
  <StatGrid items={ivStats} width={width} />
465
485
  {/* The terminal sizes the view in the rows under the bar and figures; the desktop fills what is left. */}
466
486
  {nativePaneChrome ? <Box flexDirection="column" overflow="hidden" {...FILL_REST}>{content}</Box> : content}
467
- </PaneStatusBody>}
487
+ </PaneStatusBody></>}
468
488
  </Box>;
469
489
  }
470
490