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
@@ -1,11 +1,12 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useUiCapabilities } from "../../../ui";
3
- import { usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
3
+ import { usePaneAppConfig, usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
4
4
  import { colors, priceColor } from "../../../theme/colors";
5
5
  import { isPlainKey } from "../../../utils/keyboard";
6
6
  import { formatCompact, formatPercentRaw } from "../../../utils/format";
7
7
  import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
8
- import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
8
+ import { getRegularSessionDisplay } from "../../../market-data/market/status";
9
+ import { chainOptionContract, formatExpDate, optionContractName, resolveOptionsTarget } from "../../../utils/options";
9
10
  import {
10
11
  expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
11
12
  } from "../../../utils/option-expiry";
@@ -34,7 +35,9 @@ import { useLiveQuoteEntries, useQuoteUpdates } from "../../../state/hooks/quote
34
35
  import { buildChartKey } from "../../../market-data/selectors";
35
36
  import type { ChartRequest } from "../../../market-data/request-types";
36
37
  import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
37
- import { usePluginAppActions } from "../../runtime";
38
+ import { usePluginAppActions, usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
39
+ import { brokerCanTrade, openTradeTicket } from "../broker-trading/command";
40
+ import type { OptionContract } from "../../../types/financials";
38
41
  import { OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
39
42
  import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
40
43
  import { useOptionsCatalogue } from "./expiry-catalogue";
@@ -66,7 +69,7 @@ import { signedPositionDirection } from "../../../portfolio/position-metrics";
66
69
  import { optionMarketReference } from "./market-reference";
67
70
  import { useOptionsEnrichment } from "./enrichment";
68
71
  import type { OptionsEnrichmentSnapshot } from "./enrichment-model";
69
- import { optionMid, type OptionSide } from "../shared/volatility";
72
+ import { daysToExpiryFrom, optionMid, type OptionSide } from "../shared/volatility";
70
73
  import type { TickerRecord } from "../../../types/ticker";
71
74
  import type { TickerFinancials } from "../../../types/financials";
72
75
  import type { IvStats } from "../iv-history/client";
@@ -74,7 +77,10 @@ import { useIvRank } from "../iv-history/rank";
74
77
  import { useOptionsSessionOpen, useThrottledValue } from "../shared/volatility/live-session";
75
78
  import { useRemoteUiNode } from "../../../remote/semantic-tree";
76
79
  import { optionCarry, optionCostOfSpot } from "./carry";
77
- import { formatModelSpotPrice, modelSpotLabel, optionModelSpot } from "./model-spot";
80
+ import {
81
+ formatModelSpotPrice, modelSpotExtendedPrint, modelSpotLabel, modelSpotStamp, modelSpotStampVariants, optionModelSpot,
82
+ type OptionModelSpot,
83
+ } from "./model-spot";
78
84
  import { chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, statedOpenInterestAsOf } from "./open-interest-as-of";
79
85
  import { useAsyncResource } from "../../../react/async-resource";
80
86
  import {
@@ -111,13 +117,35 @@ function transientTicker(symbol: string, exchange: string, currency: string): Ti
111
117
  }
112
118
 
113
119
  /**
114
- * The underlying's price and day change: the level every strike is read
115
- * against. A stale quote stays on screen, muted, while the footer says why.
120
+ * The underlying's price: the level every strike is read against. A chain
121
+ * priced at a session's close or an extended-session print shows that spot
122
+ * with its stamp (`modelSpotStamp`), and any extended print after it named by
123
+ * its session; the regular session's live price shows its day change. A stale
124
+ * quote stays on screen, muted, while the footer says why.
116
125
  */
117
- function spotItem(underlying: TickerFinancials | null | undefined, assetCategory: string | undefined): StatItem {
126
+ function spotItem(
127
+ underlying: TickerFinancials | null | undefined,
128
+ assetCategory: string | undefined,
129
+ modelSpot: OptionModelSpot | null,
130
+ ): StatItem {
118
131
  const quote = underlying?.quote;
119
132
  if (!quote || !Number.isFinite(quote.price)) return { id: "spot", label: "Spot", value: "--" };
120
133
  const options = liveQuoteFormatOptions(quote, quote.currency, assetCategory, underlying?.quoteMetadata?.instrumentType);
134
+ const stamp = modelSpot ? modelSpotStamp(modelSpot) : null;
135
+ if (modelSpot && stamp) {
136
+ const move = modelSpot.basis === "close" ? getRegularSessionDisplay(quote)?.change : quote.change;
137
+ const extended = modelSpotExtendedPrint(quote, modelSpot, (price) => formatMarketPrice(price, options));
138
+ // The band keeps its columns: the extended print, then the stamp's day, give way first.
139
+ const shorter = modelSpotStampVariants(modelSpot);
140
+ return {
141
+ id: "spot",
142
+ label: "Spot",
143
+ value: formatMarketPrice(modelSpot.price, options),
144
+ detail: extended ? `${stamp} · ${extended}` : stamp,
145
+ detailVariants: extended ? [stamp, ...shorter] : shorter,
146
+ color: quote.stale ? colors.textDim : priceColor(Number.isFinite(move) ? move! : 0),
147
+ };
148
+ }
121
149
  const change = Number.isFinite(quote.change) ? quote.change : 0;
122
150
  return {
123
151
  id: "spot",
@@ -176,6 +204,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
176
204
  const ticker = useMemo(() => savedTicker ?? (boundSymbol ? transientTicker(boundSymbol, fallbackExchange, fallbackCurrency) : null),
177
205
  [savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
178
206
  const { createPaneFromTemplate } = usePluginAppActions();
207
+ const { pinTicker } = usePluginTickerActions();
208
+ const { getBrokerAdapter } = usePluginBrokerActions();
209
+ const appConfig = usePaneAppConfig();
179
210
  const liveStreaming = useLiveStreamingSetting();
180
211
  // A number from the app, or the date or seconds `fn OMON --expiration` took as text.
181
212
  const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
@@ -238,13 +269,20 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
238
269
  const strikeWindowText = strikeWindowValue(strikeWindow);
239
270
  const initialChainEntry = useOptionsQuery(baseRequest);
240
271
  const initialChain = useResolvedEntryValue(initialChainEntry);
241
- const initialExpiration = initialChain?.expirationDates.reduce((best, expiration) => (
272
+ // A listing can still carry a date whose 16:00 ET close has passed. The chain
273
+ // never opens there, nor on a saved date that has since passed: it takes the nearest live one.
274
+ const now = Date.now();
275
+ const liveExpiration = (expiration: number) => daysToExpiryFrom(expiration, now) > 0;
276
+ const listedExpirations = initialChain?.expirationDates ?? [];
277
+ const openingExpirations = listedExpirations.some(liveExpiration) ? listedExpirations.filter(liveExpiration) : listedExpirations;
278
+ const initialExpiration = openingExpirations.length === 0 ? undefined : openingExpirations.reduce((best, expiration) => (
242
279
  parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
243
- ), initialChain.expirationDates[0]!);
280
+ ), openingExpirations[0]!);
244
281
  // A typed date names a day; the catalogue's stamp for that day is the expiry.
245
282
  const seededDate = readOptionExpiration(seededSetting);
246
- const seededExpiration = seededDate == null ? undefined
247
- : findListedExpiry(seededDate, initialChain?.expirationDates ?? []) ?? seededDate;
283
+ const seededListed = seededDate == null ? undefined : findListedExpiry(seededDate, listedExpirations) ?? seededDate;
284
+ const seededExpiration = seededListed != null && (liveExpiration(seededListed) || !openingExpirations.some(liveExpiration))
285
+ ? seededListed : undefined;
248
286
  const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
249
287
  ? seededExpiration ?? initialExpiration : initialExpiration;
250
288
  const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
@@ -281,8 +319,11 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
281
319
  ?? (matchesSelectedExpiration(initialChain) ? initialChain : null);
282
320
  const strikesLoading = strikeChain === null && !expirationUnavailable;
283
321
  const expirationDates = useMemo(() => {
284
- return selectedExpirationMissing
322
+ const listed = selectedExpirationMissing
285
323
  ? [...availableExpirations, selectedExpiration!].sort((left, right) => left - right) : availableExpirations;
324
+ // A date that has ended has no contracts left to show; the strip leaves it out, unless every date has.
325
+ const current = (expiration: number) => daysToExpiry(expiration, Date.now()) >= 0;
326
+ return listed.some(current) ? listed.filter((expiration) => expiration === selectedExpiration || current(expiration)) : listed;
286
327
  }, [availableExpirations, selectedExpiration, selectedExpirationMissing]);
287
328
  // Keep the date strip stable while a mouse press focuses this pane. A new
288
329
  // tab list asks the web host to reveal the active tab and can move the date
@@ -468,13 +509,17 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
468
509
  : { key: viewportKey, range }
469
510
  ));
470
511
  }, [viewportKey]);
512
+ // A chain read from a broker profile has its contracts quoted by that profile too.
513
+ const quoteBrokerId = instrument?.brokerId, quoteBrokerInstanceId = instrument?.brokerInstanceId;
514
+ const optionQuoteBroker = useMemo(() => quoteBrokerId || quoteBrokerInstanceId
515
+ ? { brokerId: quoteBrokerId, brokerInstanceId: quoteBrokerInstanceId } : undefined, [quoteBrokerId, quoteBrokerInstanceId]);
471
516
  const optionQuoteTargets = useMemo(
472
517
  () => buildOptionQuoteTargets(quoteRows, {
473
518
  fallbackHeight: height,
474
519
  selectedIndex: selectedStrikeIdx,
475
520
  visibleRange: visibleStrikeRange,
476
- }),
477
- [height, quoteRows, selectedStrikeIdx, visibleStrikeRange],
521
+ }, optionQuoteBroker),
522
+ [height, quoteRows, selectedStrikeIdx, visibleStrikeRange, optionQuoteBroker],
478
523
  );
479
524
  const {
480
525
  entries: optionQuoteEntries,
@@ -497,8 +542,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
497
542
  // The snapshot object itself comes back while nothing streamed, so the
498
543
  // memos below only recompute when a quote actually landed.
499
544
  const liveChain = useMemo(
500
- () => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness).chain : null,
501
- [optionQuoteEntries, optionQuoteFreshness, strikeChain],
545
+ () => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness, optionQuoteBroker).chain : null,
546
+ [optionQuoteEntries, optionQuoteFreshness, strikeChain, optionQuoteBroker],
502
547
  );
503
548
  const callsByStrike = useMemo(
504
549
  () => new Map(liveChain?.calls.map((c) => [c.strike, c]) ?? []),
@@ -533,10 +578,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
533
578
  putGreeks: volatilities ? calculateOptionGreeks(put, "put", greeksSpot, dividendYield, volatilities) : undefined,
534
579
  callExtrinsicPerYear: call && volatilities ? optionCarry(call, "call", greeksSpot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
535
580
  putExtrinsicPerYear: put && volatilities ? optionCarry(put, "put", greeksSpot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
536
- putCostOfSpot: put ? optionCostOfSpot(put, spot) : null,
581
+ putCostOfSpot: put ? optionCostOfSpot(put, greeksSpot) : null,
537
582
  isPositionStrike: !!parsed && strike === parsed.strike,
538
583
  };
539
- }), [callsByStrike, dividendYield, greeksSpot, parsed, putsByStrike, spot, strikes, volatilities]);
584
+ }), [callsByStrike, dividendYield, greeksSpot, parsed, putsByStrike, strikes, volatilities]);
540
585
  // The open interest's session: the chain's own, or for a chain that does not say, OPX's listing of the
541
586
  // same expiry, stated only when the counts are OPX's.
542
587
  const statedOpenInterest = useMemo(() => strikeChain ? statedOpenInterestAsOf(strikeChain) : undefined, [strikeChain]);
@@ -565,10 +610,12 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
565
610
  } : null);
566
611
  // What IV and the Greeks are valued at, and the open interest's date. In a
567
612
  // crowded footer it shortens, then gives way to the contract under the cursor.
568
- const basisText = [modelSpot ? `spot ${formatModelSpotPrice(modelSpot.price)} ${modelSpotLabel(modelSpot)}` : null,
613
+ // A stamped spot (a close, an extended print) is already the Spot figure's.
614
+ const footerSpot = modelSpot && !modelSpotStamp(modelSpot) ? modelSpot : null;
615
+ const basisText = [footerSpot ? `spot ${formatModelSpotPrice(footerSpot.price)} ${modelSpotLabel(footerSpot)}` : null,
569
616
  strikeChain && !listedOpenInterest.loading ? openInterestAsOfLabel(openInterestAsOf) : null].filter(Boolean).join(" · ");
570
- const shortBasisText = [modelSpot ? `${formatModelSpotPrice(modelSpot.price)} ${modelSpot.basis === "close" ? "close" : modelSpot.session ?? "last"} `
571
- + `${new Date(modelSpot.observedAt).toISOString().slice(11, 16)} UTC` : null,
617
+ const shortBasisText = [footerSpot ? `${formatModelSpotPrice(footerSpot.price)} last `
618
+ + `${new Date(footerSpot.observedAt).toISOString().slice(11, 16)} UTC` : null,
572
619
  strikeChain && !listedOpenInterest.loading ? openInterestAsOf?.date ? `OI ${openInterestAsOf.date.slice(5)}` : "OI undated" : null]
573
620
  .filter(Boolean).join(" · ");
574
621
  usePaneFooter("options-basis", () => ({ info: basisText ? [{ id: "greeks-basis",
@@ -578,9 +625,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
578
625
  // busy stream reads as a steady strip rather than flicker.
579
626
  // A new expiry, a refreshed snapshot or the underlying price arriving or
580
627
  // going stale shows at once; only the stream itself is paced.
581
- const summaryResetKey = useMemo(() => ({}), [strikeSelectionKey, strikeChain, spot == null]);
628
+ const summaryResetKey = useMemo(() => ({}), [strikeSelectionKey, strikeChain, greeksSpot == null]);
582
629
  const summaryInput = useThrottledValue(
583
- useMemo(() => ({ chain: liveChain, spot, volatilities }), [liveChain, spot, volatilities]),
630
+ useMemo(() => ({ chain: liveChain, spot: greeksSpot, volatilities }), [liveChain, greeksSpot, volatilities]),
584
631
  OPTIONS_SUMMARY_THROTTLE_MS,
585
632
  summaryResetKey,
586
633
  );
@@ -594,7 +641,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
594
641
  instrument: baseRequest?.instrument ?? null, expiration: selectedExpiration,
595
642
  selectedEntry: strikeChain === expirationChain ? expirationChainEntry
596
643
  : strikeChain === initialChain ? initialChainEntry : null,
597
- catalogue: availableExpirations, spot, spotAsOf: underlying?.quote?.lastUpdated,
644
+ catalogue: availableExpirations, spot: greeksSpot, spotAsOf: modelSpot?.observedAt,
598
645
  liveChain: liveChain !== strikeChain ? liveChain : null,
599
646
  });
600
647
  const enrichment = expirationUnavailable ? null : enrichmentState.snapshot;
@@ -672,6 +719,28 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
672
719
  }, [scenarioContract, scenarioAvailable, scenarioMid, scenarioVolatility, createPaneFromTemplate, dividendYield, effectiveTicker,
673
720
  effectiveExchange, selectedSide, spot, underlying?.quote?.lastUpdated]);
674
721
 
722
+ // A contract opens as its own listing: quote, chart and Trade. It stays on
723
+ // the chain's broker while that profile is on, else it opens by its symbol.
724
+ const contractListing = useCallback((contract: OptionContract) => {
725
+ const symbol = contract.contractSymbol.trim().toUpperCase();
726
+ const currency = contract.currency || underlying?.quote?.currency || "USD";
727
+ const brokerOn = !!instrument && appConfig.brokerInstances.some((profile) => profile.id === instrument.brokerInstanceId && profile.enabled !== false);
728
+ return { symbol, selection: { instrument: brokerOn ? chainOptionContract(symbol, instrument, currency) : null,
729
+ listing: { name: optionContractName(symbol) ?? symbol, exchange: "", currency, type: "OPT" } } };
730
+ }, [appConfig.brokerInstances, instrument, underlying?.quote?.currency]);
731
+ const openContract = useCallback((contract: OptionContract | null | undefined) => {
732
+ if (!contract) return;
733
+ const { symbol, selection } = contractListing(contract);
734
+ pinTicker(symbol, selection);
735
+ }, [contractListing, pinTicker]);
736
+ const tradable = !!scenarioContract && brokerCanTrade(appConfig.brokerInstances, getBrokerAdapter, scenarioContract.contractSymbol.trim().toUpperCase(), "OPT");
737
+ const tradeContract = useCallback(() => {
738
+ if (!scenarioContract || !tradable) return;
739
+ const { symbol, selection } = contractListing(scenarioContract);
740
+ // A new contract's ticket starts on Buy; b and s choose the side in it.
741
+ openTradeTicket({ pinTicker }, symbol, { action: "BUY" }, selection);
742
+ }, [contractListing, pinTicker, scenarioContract, tradable]);
743
+
675
744
  const openSurface = useCallback(() => {
676
745
  if (!ticker || selectedExpiration == null) return;
677
746
  createPaneFromTemplate("vol-surface-pane", { symbol: ticker.metadata.ticker, ticker, instrument,
@@ -703,12 +772,14 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
703
772
  setCalcSide(sideTarget);
704
773
  }, [selectContract, selectedRow, selectedStrikeIdx, sideTarget]);
705
774
 
775
+ // With a broker that trades the contract, [t]rade opens its ticket on Buy, and Sell is chosen there.
706
776
  const footerHints = useMemo(() => [
707
777
  ...(sideTarget ? [{ id: "side", key: SIDE_KEY, label: ` ${sideTarget}`, title: sideTarget === "put" ? "Select Put" : "Select Call", onPress: switchSide }] : []),
778
+ ...(tradable ? [{ id: "trade-contract", key: "t", label: "rade", title: "Trade Contract", onPress: tradeContract }] : []),
708
779
  ...(calcParams ? [{ id: "calc", key: "c", label: "alc", onPress: openCalculator }] : []),
709
780
  ...(scenarioAvailable ? [{ id: "scenario", key: "a", label: "dd to OSA", onPress: openScenario }] : []),
710
781
  ...(ticker && selectedExpiration != null ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
711
- ], [sideTarget, switchSide, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
782
+ ], [sideTarget, switchSide, tradable, tradeContract, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
712
783
 
713
784
  // Back to where the chain opens: the held contract's strike, else the money.
714
785
  const defaultStrikeIndex = strikes.length === 0 ? -1 : (() => {
@@ -732,8 +803,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
732
803
  { id: "default-strike", label: parsed ? "Go to Held Strike" : "Go to ATM Strike",
733
804
  enabled: defaultStrikeIndex >= 0, onSelect: goToDefaultStrike },
734
805
  ...(enrichment?.asOf && dialog ? [{ id: "analytics-as-of", label: "Analytics As Of…", onSelect: showAnalyticsAsOf }] : []),
806
+ { id: "open-contract", label: "Open Contract", accelerator: "Enter", enabled: !!scenarioContract, onSelect: () => openContract(scenarioContract) },
807
+ ...(tradable ? [{ id: "trade-contract", label: "Trade Contract", accelerator: "t", onSelect: tradeContract }] : []),
735
808
  ], [defaultStrikeIndex, dialog, enrichment?.asOf, expirationDates.length, goToDefaultStrike, parsed,
736
- selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf]);
809
+ selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf, scenarioContract, openContract, tradable, tradeContract]);
737
810
 
738
811
  const renderCell = useCallback((
739
812
  row: OptionTableRow,
@@ -756,6 +829,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
756
829
  }, [selectContract, selectedSide]);
757
830
 
758
831
  useOptionsAccessFooter({
832
+ broker: !!optionQuoteBroker,
759
833
  chain,
760
834
  error: [error, underlyingStale ? "Underlying quote stale: Greeks and calculator unavailable" : null,
761
835
  selectedContract && strikeChain && !selectedContractAvailable
@@ -789,7 +863,21 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
789
863
  // the arrows for it; in a research tab those stay with the tab strip.
790
864
  useShortcut((event) => {
791
865
  if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
792
- if (event.ctrl || event.meta || event.alt || event.shift) return;
866
+ if (event.ctrl || event.meta || event.alt) return;
867
+ const surface = isPlainKey(event, "s");
868
+ if (surface && ticker && selectedExpiration != null) {
869
+ event.preventDefault();
870
+ event.stopPropagation();
871
+ openSurface();
872
+ return;
873
+ }
874
+ if (event.shift) return;
875
+ if (tradable && isPlainKey(event, "t") && !event.repeat) {
876
+ event.preventDefault();
877
+ event.stopPropagation();
878
+ tradeContract();
879
+ return;
880
+ }
793
881
 
794
882
  const expiryStep = isPlainKey(event, EXPIRY_PREVIOUS_KEY) || (!nestedInTabs && isPlainKey(event, "h", "left")) ? -1
795
883
  : isPlainKey(event, EXPIRY_NEXT_KEY) || (!nestedInTabs && isPlainKey(event, "l", "right")) ? 1 : 0;
@@ -799,12 +887,6 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
799
887
  selectAdjacentExpiration(expiryStep);
800
888
  return;
801
889
  }
802
- if (isPlainKey(event, "s") && ticker && selectedExpiration != null) {
803
- event.preventDefault();
804
- event.stopPropagation();
805
- openSurface();
806
- return;
807
- }
808
890
  if (isPlainKey(event, "c") && calcParams) {
809
891
  event.preventDefault();
810
892
  event.stopPropagation();
@@ -847,7 +929,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
847
929
  // The root insets the terminal body, so the band gets the width inside it.
848
930
  const statWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
849
931
  const figures = optionsSummaryItems({
850
- spot: spotItem(underlying, isOpt ? undefined : ticker.metadata.assetCategory),
932
+ spot: spotItem(underlying, isOpt ? undefined : ticker.metadata.assetCategory, modelSpot),
851
933
  summary, enrichment, currency: underlying?.quote?.currency || ticker.metadata.currency || "",
852
934
  ivRank: showIvRank ? { stats: ivRank } : null,
853
935
  });
@@ -925,6 +1007,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
925
1007
  onVisibleRangeChange={handleVisibleStrikeRangeChange}
926
1008
  getItemKey={(row) => String(row.strike)}
927
1009
  renderCell={renderCell}
1010
+ onActivate={(row) => openContract(selectedSide === "put" ? row.put : selectedSide === "call" ? row.call : null)}
928
1011
  selectedTextOverridesCellColor
929
1012
  emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
930
1013
  : strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
@@ -1,5 +1,6 @@
1
+ import { unavailableText } from "../../../components/ui/status";
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
3
+ import { delayedFooterSegment, FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
3
4
  import { CompositeChart } from "../../../components/chart/composite/composite-chart";
4
5
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
5
6
  import { useAsyncResource, useInputCapture } from "../../../public/react";
@@ -194,7 +195,7 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
194
195
  const calculation = useMemo(() => {
195
196
  const unavailable = seedError ?? (american ? dividendInput.error : null)
196
197
  ?? (surfaceSource && !draft.symbol ? "Choose an underlying ticker for surface IV." : null)
197
- ?? (surfaceSource && !surfaceResource.loading && surface?.volatility == null ? surface?.error ?? surfaceResource.error ?? "Surface IV unavailable." : null);
198
+ ?? (surfaceSource && !surfaceResource.loading && surface?.volatility == null ? surface?.error ?? surfaceResource.error ?? unavailableText("Surface IV") : null);
198
199
  if (unavailable || surfaceSource && surface?.volatility == null) {
199
200
  return { valuation: null, implied: { volatility: null, note: null }, problem: unavailable };
200
201
  }
@@ -279,7 +280,9 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
279
280
  ...(surfaceSource && surfaceResource.loading ? [{ id: "loading", parts: [{ text: "loading surface", tone: "muted" as const }] }] : []),
280
281
  ...(surfaceSource && surface?.asOf ? [{ id: "surface-asof", parts: [{ text: `surface · ${surface.asOf}`, tone: "muted" as const }] }] : []),
281
282
  ...(effectiveSteps && effectiveSteps !== (draft.steps ?? 400) ? [{ id: "refined", parts: [{ text: `tree refined to ${effectiveSteps} steps`, tone: "muted" as const }] }] : []),
282
- ...(liveInputs ? [{ id: "market", parts: [{ text: liveInputs.delayed ? "delayed market" : "real-time market", tone: "muted" as const }] }] : []),
283
+ ...(liveInputs ? [liveInputs.delayed
284
+ ? delayedFooterSegment({ id: "market", detail: "Delayed market inputs" })
285
+ : { id: "market", parts: [{ text: "real-time market", tone: "muted" as const }] }] : []),
283
286
  ],
284
287
  hints: activeFieldId ? [] : [
285
288
  // Names the cell Enter or `e` edits, which the grid does not mark.
@@ -398,7 +401,7 @@ function ContractPremiumChart({
398
401
  width: number;
399
402
  focused: boolean;
400
403
  }) {
401
- const empty = unavailable ? "Premium history unavailable." : series.length < 1 ? "No premium history." : undefined;
404
+ const empty = unavailable ? unavailableText("Premium history") : series.length < 1 ? "No premium history." : undefined;
402
405
  return (
403
406
  <Box flexDirection="column" paddingX={1} height={PREMIUM_CHART_ROWS} flexShrink={0}>
404
407
  {empty && !loading ? (
@@ -2,6 +2,8 @@ import type { HeadlessPaneDefinition, HeadlessPaneEntry } from "../../../types/p
2
2
  import { expirationOptionSeconds, expiryIsoDate, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../../utils/option-expiry";
3
3
  import { formatUtcTime } from "../../../utils/utc-time";
4
4
  import { loadGamma, loadOpenInterest, positioningSymbol, type GammaPayload, type OpenInterestPayload } from "./client";
5
+ import { listingFateReport, loadListingFate } from "../../../tickers/listing-fate";
6
+ import { resolveHeadlessInstrument } from "../shared/headless-market-data";
5
7
  import {
6
8
  ALL_EXPIRIES,
7
9
  expiryLabel,
@@ -10,6 +12,7 @@ import {
10
12
  formatDistance,
11
13
  formatGamma,
12
14
  formatLevel,
15
+ formatPositioningSpot,
13
16
  formatPayout,
14
17
  formatRatio,
15
18
  formatStrike,
@@ -49,7 +52,7 @@ function summary(openInterest: OpenInterestPayload, dealer: GammaPayload | null)
49
52
  { key: "oiDate", label: "Open interest as of", value: openInterest.oiDate },
50
53
  ];
51
54
  const textEntries: HeadlessPaneEntry[] = [
52
- { label: "Spot", value: spot, formatted: spot == null ? "--" : spotAsOf ? `${formatLevel(spot)} at ${time(spotAsOf)}` : formatLevel(spot) },
55
+ { label: "Spot", value: spot, formatted: formatPositioningSpot(spot, spotAsOf) },
53
56
  ...(dealer ? [{ label: "Quotes as of", value: dealer.quotesAsOf, formatted: time(dealer.quotesAsOf) }] : []),
54
57
  { label: "Open interest as of", value: openInterest.oiDate, formatted: openInterest.oiDate ?? "--" },
55
58
  ];
@@ -118,7 +121,7 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
118
121
  // VIX has no dealer gamma, so its expiries have no column for it.
119
122
  const expiryGamma = !(allExpiries instanceof Error) && allExpiries.total ? netGammaByExpiry(allExpiries) : null;
120
123
  const gammaColumn = allExpiries instanceof Error || !!allExpiries.total;
121
- const errors = [
124
+ const loadErrors = [
122
125
  ...openInterest.warnings,
123
126
  ...(dealer instanceof Error ? [`Dealer gamma: ${dealer.message}`] : [
124
127
  ...dealer.warnings,
@@ -126,6 +129,11 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
126
129
  ]),
127
130
  ...(everyExpiry instanceof Error ? [`Net GEX by expiry: ${everyExpiry.message}`] : []),
128
131
  ];
132
+ // A symbol with no listed open interest says what became of it when something did (TWTR stopped
133
+ // trading, FB is a fund now) in place of the load's errors.
134
+ const fate = openInterest.expiries.length || expiry ? null
135
+ : listingFateReport(await loadListingFate(await resolveHeadlessInstrument(ctx, symbol), ctx.marketData), { served: false }, loadErrors);
136
+ const errors = fate?.errors ?? loadErrors;
129
137
  const changes = openInterest.previousOiDate != null;
130
138
  const gammaShown = dealer instanceof Error || !dealer.total ? null : dealer;
131
139
  const scope = dealer instanceof Error ? null : dealer.expiry ? expiryLabel(dealer.expiry) : "all expiries";
@@ -173,7 +181,7 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
173
181
  ],
174
182
  complete: !!openInterest.expiries.length && !(dealer instanceof Error) && errors.length === 0,
175
183
  unavailableSymbols: openInterest.expiries.length ? [] : [symbol],
176
- errors,
184
+ errors, ...(fate?.notes.length ? { notes: fate.notes } : {}),
177
185
  metadata: {
178
186
  underlying: openInterest.underlying,
179
187
  oiDate: openInterest.oiDate,
@@ -1,5 +1,6 @@
1
1
  import { formatCompact, formatCompactAmount } from "../../../utils/format";
2
2
  import { formatExpDate } from "../../../utils/options";
3
+ import { modelSpotLabel, modelSpotStamp, modelSpotStampVariants, observedModelSpot } from "../options/model-spot";
3
4
 
4
5
  export type PositioningTab = "strikes" | "expiries" | "gex";
5
6
  export const POSITIONING_TABS: { value: PositioningTab; label: string }[] = [
@@ -66,6 +67,29 @@ export function formatRatio(value: number | null | undefined): string {
66
67
  export function formatLevel(value: number | null | undefined): string {
67
68
  return value == null || !Number.isFinite(value) ? "--" : value.toFixed(2);
68
69
  }
70
+ /**
71
+ * The spot Gloom Cloud read the distances and dealer gamma at, stamped as
72
+ * every options pane stamps its spot (`modelSpotStamp`), as a stat cell
73
+ * carries it: `pre-market Wed 21 Oct 09:35 UTC`, its shorter forms, then
74
+ * none, so the band keeps the columns it has in regular hours. Nothing for
75
+ * the regular session's price.
76
+ */
77
+ export function positioningSpotDetail(spot: number | null | undefined, spotAsOf: string | null | undefined):
78
+ { detail?: string; detailVariants?: string[] } {
79
+ const at = spotAsOf ? Date.parse(spotAsOf) : Number.NaN;
80
+ if (spot == null || !Number.isFinite(at)) return {};
81
+ const observed = observedModelSpot(spot, at);
82
+ const stamp = modelSpotStamp(observed);
83
+ return stamp ? { detail: stamp, detailVariants: [...modelSpotStampVariants(observed), ""] } : {};
84
+ }
85
+
86
+ /** The spot as a report says it: `770.88 pre-market Wed 21 Oct 09:35 UTC`, `770.10 last Wed 21 Oct 14:05 UTC`. */
87
+ export function formatPositioningSpot(spot: number | null | undefined, spotAsOf: string | null | undefined): string {
88
+ const at = spotAsOf ? Date.parse(spotAsOf) : Number.NaN;
89
+ if (spot == null || !Number.isFinite(spot)) return "--";
90
+ return Number.isFinite(at) ? `${formatLevel(spot)} ${modelSpotLabel(observedModelSpot(spot, at))}` : formatLevel(spot);
91
+ }
92
+
69
93
  /** Where a level sits against spot: +0.6%. */
70
94
  export function formatDistance(level: number | null | undefined, spot: number | null | undefined): string {
71
95
  if (level == null || spot == null || !(spot > 0)) return "--";
@@ -57,6 +57,7 @@ import {
57
57
  formatDistance,
58
58
  formatGamma,
59
59
  formatLevel,
60
+ positioningSpotDetail,
60
61
  formatPayout,
61
62
  formatRatio,
62
63
  formatStrike,
@@ -204,6 +205,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
204
205
  const openInterest = useAsyncResource<OpenInterestPayload>(openInterestLoader, { keepPreviousData: true });
205
206
  const data = openInterest.data;
206
207
  const spot = data?.spot ?? null;
208
+ const spotStamp = useMemo(() => positioningSpotDetail(spot, data?.spotAsOf), [spot, data?.spotAsOf]);
207
209
  const expiries = data?.expiries ?? [];
208
210
  const shownExpiry = data?.expiry ?? requestedExpiry;
209
211
  const changes = data?.previousOiDate != null;
@@ -264,9 +266,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
264
266
  enabled: expiryIndex < expiryDates.length - 1, onSelect: () => stepExpiry(1) },
265
267
  ], [expiryDates.length, expiryIndex, stepExpiry, tab]);
266
268
 
267
- const { strip: tabStrip, rows: tabRows } = usePaneTabs(data?.expiries.length
268
- ? { tabs, activeValue: tab, onSelect: (value: string) => setTab(value), focused, dense: true }
269
- : null);
269
+ const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs, activeValue: tab, onSelect: (value: string) => setTab(value), focused, dense: true });
270
270
  const bodyHeight = Math.max(3, height - tabRows);
271
271
 
272
272
  // Footer: what the data is (delayed or real time, the OI session), and its gaps.
@@ -335,13 +335,13 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
335
335
  const strikeFigures = useMemo<StatItem[]>(() => !callTotal ? [] : [
336
336
  { id: "max-pain", label: "Max pain", value: maxPain == null ? "--" : formatStrike(maxPain), color: colors.warning,
337
337
  detail: maxPain == null ? undefined : `${formatDistance(maxPain, spot)} vs spot` },
338
- { id: "spot", label: "Spot", value: formatLevel(spot) },
338
+ { id: "spot", label: "Spot", value: formatLevel(spot), ...spotStamp },
339
339
  { id: "pc", label: "P/C OI", value: formatRatio(callTotal.putCallRatio) },
340
340
  { id: "calls", label: "Call OI", value: formatCompact(callTotal.callOI), color: colors.positive,
341
341
  detail: callTotal.callChange == null ? undefined : formatCountChange(callTotal.callChange) },
342
342
  { id: "puts", label: "Put OI", value: formatCompact(callTotal.putOI), color: colors.negative,
343
343
  detail: callTotal.putChange == null ? undefined : formatCountChange(callTotal.putChange) },
344
- ], [callTotal, colors.negative, colors.positive, colors.warning, maxPain, spot]);
344
+ ], [callTotal, colors.negative, colors.positive, colors.warning, maxPain, spot, spotStamp]);
345
345
  const selectedStrikeRow = strikes.find((row) => strikeKey(row) === strikeSelectedId) ?? null;
346
346
  const strikeStrip = useMemo<ChartStripSpec | null>(() => strikeWindow.length < 3 ? null : {
347
347
  label: "Open interest by strike",
@@ -404,9 +404,9 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
404
404
  split: [{ id: "calls", value: calls, color: colors.positive }, { id: "puts", value: puts, color: colors.negative }] },
405
405
  { id: "pc", label: "P/C OI", value: formatRatio(calls > 0 ? puts / calls : null) },
406
406
  { id: "largest", label: "Largest expiry", value: expiryLabel(largest.date), detail: formatCompact(expirySize(largest)) },
407
- { id: "spot", label: "Spot", value: formatLevel(spot) },
407
+ { id: "spot", label: "Spot", value: formatLevel(spot), ...spotStamp },
408
408
  ];
409
- }, [changes, colors.negative, colors.positive, expiries, spot]);
409
+ }, [changes, colors.negative, colors.positive, expiries, spot, spotStamp]);
410
410
  const selectedExpiryRow = expiries.find((row) => row.date === expirySelectedId) ?? null;
411
411
  const expiryStrip = useMemo<ChartStripSpec | null>(() => expiries.length < 3 ? null : {
412
412
  label: "Open interest by expiry",
@@ -479,7 +479,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
479
479
  })),
480
480
  ...(band ? [{ id: "band", label: "Dealer range", value: `${formatGamma(band.low)} to ${formatGamma(band.high)}`,
481
481
  detail: `${shares} with dealers${gammaData.boxes?.length ? ", boxes out" : ""}` }] : []),
482
- { id: "spot", label: "Spot", value: formatLevel(gammaSpot) },
482
+ { id: "spot", label: "Spot", value: formatLevel(gammaSpot), ...positioningSpotDetail(gammaSpot, gammaData.spotAsOf) },
483
483
  ], [band, colors.negative, colors.positive, gammaData, gammaSpot, shares, walls]);
484
484
  // The assumption rides in the legend, where the numbers it changes are read; the
485
485
  // dealer range figure names its own share. It gives way before the series name does.
@@ -610,13 +610,11 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
610
610
  }
611
611
 
612
612
  return <Box width={width} height={height} flexDirection="column">
613
+ {tabStrip}
613
614
  <PaneStatusBody loading={openInterest.loading && !data} error={!data ? openInterest.error : null}
614
615
  empty={!!data && !data.expiries.length} subject="open interest"
615
616
  emptyTitle={`No listed options for ${parsePublicTickerKey(symbol).symbol.replace(/^\^/, "")}.`}>
616
- {data?.expiries.length ? <>
617
- {tabStrip}
618
- {content}
619
- </> : null}
617
+ {data?.expiries.length ? content : null}
620
618
  </PaneStatusBody>
621
619
  </Box>;
622
620
  }
@@ -13,7 +13,7 @@ import type { YieldPoint } from "../yield-curve/treasury-data";
13
13
  import { parseLegs, scenarioCurrency, validatePosition, type ScenarioControls, type ScenarioLeg, type ScenarioPosition } from "./model";
14
14
  import { loadModelDividendYield, type ModelDividendYield } from "../../../market-data/model-dividend-yield";
15
15
  import { abortable, abortError } from "../../../utils/async-deadline";
16
- import { extendedSession } from "../options/model-spot";
16
+ import { extendedSession, modelSpotStamp, modelSpotStampVariants, type OptionModelSpot } from "../options/model-spot";
17
17
  import { errorMessage } from "../../../utils/errors";
18
18
 
19
19
  export interface ScenarioMarketSnapshot {
@@ -191,6 +191,24 @@ export function scenarioSpotBasis(
191
191
  return (market?.underlyingQuote ? extendedSession(market.underlyingQuote) : null) ?? "last price";
192
192
  }
193
193
 
194
+ const EXTENDED_BASES: readonly string[] = ["pre-market", "after hours", "overnight"];
195
+
196
+ /**
197
+ * A spot basis as the pane and report name it: an extended-session print with
198
+ * its time, stamped as every options pane stamps its spot (`modelSpotStamp`),
199
+ * `pre-market Wed 21 Oct 09:42 UTC`; any other basis as it is.
200
+ */
201
+ export function scenarioSpotStamp(basis: string, observedAt: number): string {
202
+ if (!EXTENDED_BASES.includes(basis)) return basis;
203
+ return modelSpotStamp({ basis: "last", session: basis as OptionModelSpot["session"], observedAt }) ?? basis;
204
+ }
205
+
206
+ /** That stamp's shorter forms, longest first (`modelSpotStampVariants`): `pre-market 09:42 UTC`, `pre-market`. */
207
+ export function scenarioSpotStampVariants(basis: string, observedAt: number): string[] {
208
+ if (!EXTENDED_BASES.includes(basis)) return [];
209
+ return modelSpotStampVariants({ basis: "last", session: basis as OptionModelSpot["session"], observedAt });
210
+ }
211
+
194
212
  function supplied(value: unknown): boolean { return value != null && value !== ""; }
195
213
 
196
214
  /** A position typed whole: its legs and the spot, rate and dividend yield they are valued at, so no market data is read. */