gloomberb 0.18.4 → 0.19.0

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 (600) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +31 -6
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -34,10 +34,12 @@ import type { ResolvedSeries } from "../../../time-series/types";
34
34
  import type { PaneProps } from "../../../types/plugin";
35
35
  import { Box, Text } from "../../../ui";
36
36
  import { formatCompact } from "../../../utils/format";
37
+ import { formatStrikeLabel } from "../../../utils/options";
37
38
  import { parsePublicTickerKey } from "../../../utils/exchanges";
38
39
  import { isPlainKey } from "../../../utils/keyboard";
39
40
  import { nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
40
41
  import { useCloudAccessFooter } from "../shared/cloud-upgrade";
42
+ import { COIN_OPTIONS_HINT, isCoinWithoutOptions, optionsUnavailableTitle } from "../../../market-data/options-alternatives";
41
43
  import {
42
44
  loadGamma,
43
45
  loadOpenInterest,
@@ -49,18 +51,23 @@ import {
49
51
  } from "./client";
50
52
  import {
51
53
  ALL_EXPIRIES,
54
+ axisRatioOfDate,
52
55
  chartWindow,
53
56
  expiryAxis,
54
57
  expiryLabel,
55
- formatCount,
58
+ expiryWithTimeLabel,
59
+ formatContracts,
60
+ isCoinContract,
61
+ settlementTime,
62
+ wallsByExpiry,
56
63
  formatCountChange,
57
64
  formatDistance,
58
65
  formatGamma,
59
66
  formatLevel,
67
+ gammaTimesFigure,
60
68
  positioningSpotDetail,
61
69
  formatPayout,
62
70
  formatRatio,
63
- formatStrike,
64
71
  gammaWalls,
65
72
  netGammaByExpiry,
66
73
  openInterestTotal,
@@ -92,26 +99,30 @@ const expirySize = (row: { callOI: number; putOI: number }) => row.callOI + row.
92
99
  const reportedOpenInterest = (row: { callOI: number | null; putOI: number | null }) =>
93
100
  (row.callOI == null ? 0 : row.callOI) + (row.putOI == null ? 0 : row.putOI);
94
101
 
95
- function strikeColumns(changes: boolean): DataTableColumn[] {
102
+ function strikeColumns(changes: boolean, unit = ""): DataTableColumn[] {
96
103
  return [
97
104
  { id: "strike", label: "Strike", width: 9, align: "left" },
98
- { id: "callOI", label: "Call OI", width: 10, align: "right" },
105
+ { id: "callOI", label: `Call OI${unit}`, width: 10 + unit.length, align: "right" },
99
106
  ...(changes ? [{ id: "callChange", label: "Call chg", width: 9, align: "right" as const }] : []),
100
- { id: "putOI", label: "Put OI", width: 10, align: "right" },
107
+ { id: "putOI", label: `Put OI${unit}`, width: 10 + unit.length, align: "right" },
101
108
  ...(changes ? [{ id: "putChange", label: "Put chg", width: 9, align: "right" as const }] : []),
102
109
  { id: "payout", label: "Payout $", width: 10, align: "right" },
103
110
  ];
104
111
  }
105
- function expiryColumns(changes: boolean, gamma: boolean): DataTableColumn[] {
112
+ function expiryColumns(changes: boolean, gamma: boolean, options: { settles?: boolean; walls?: boolean; unit?: string } = {}): DataTableColumn[] {
113
+ const unit = options.unit ?? "";
106
114
  return [
107
- { id: "date", label: "Expiry", width: 11, align: "left" },
115
+ // An expiry that settles at a stated time (Deribit's 08:00 UTC) names it.
116
+ { id: "date", label: "Expiry", width: options.settles ? 21 : 11, align: "left" },
108
117
  { id: "days", label: "Days", width: 5, align: "right" },
109
- { id: "callOI", label: "Call OI", width: 11, align: "right" },
110
- { id: "putOI", label: "Put OI", width: 11, align: "right" },
118
+ { id: "callOI", label: `Call OI${unit}`, width: 11 + unit.length, align: "right" },
119
+ { id: "putOI", label: `Put OI${unit}`, width: 11 + unit.length, align: "right" },
111
120
  { id: "putCallRatio", label: "P/C", width: 6, align: "right" },
112
121
  ...(changes ? [{ id: "change", label: "OI chg", width: 10, align: "right" as const }] : []),
113
122
  { id: "maxPain", label: "Max pain", width: 9, align: "right" },
114
123
  { id: "distance", label: "Vs spot", width: 8, align: "right" },
124
+ ...(options.walls ? [{ id: "callWall", label: "Call wall", width: 10, align: "right" as const },
125
+ { id: "putWall", label: "Put wall", width: 9, align: "right" as const }] : []),
115
126
  ...(gamma ? [{ id: "netGamma", label: "Net GEX $", width: 11, align: "right" as const }] : []),
116
127
  ];
117
128
  }
@@ -151,13 +162,18 @@ interface PositioningChartProps {
151
162
 
152
163
  /** Columns across a numeric axis (strikes, or expiries one slot apart), with spot and the other levels as guide lines. */
153
164
  function PositioningChart(props: PositioningChartProps) {
165
+ // The legend's values are one column's, under the cursor (the last column's
166
+ // without one), not the totals the figures above show: it names the column.
167
+ const column = props.formatCursor(props.selection.cursorDate ? axisRatioOfDate(props.selection.cursorDate) : 1);
168
+ const legendSeries = useMemo(() => column
169
+ ? props.series.map((entry) => ({ ...entry, label: `${entry.label} at ${column}` })) : undefined, [column, props.series]);
154
170
  const xAxis = useMemo(() => ({
155
171
  ticks: props.ticks(Math.max(1, props.width - VALUE_GUTTER)),
156
172
  markers: props.markers.flatMap((marker) => marker.ratio != null && marker.ratio >= 0 && marker.ratio <= 1
157
173
  ? [{ id: marker.id, xRatio: marker.ratio, label: marker.label, color: marker.color }] : []),
158
174
  formatCursor: props.formatCursor,
159
175
  }), [props.formatCursor, props.markers, props.ticks, props.width]);
160
- return <CompositeChart series={props.series} panels={PANELS} width={props.width} height={props.height}
176
+ return <CompositeChart series={props.series} legendSeries={legendSeries} panels={PANELS} width={props.width} height={props.height}
161
177
  focused={false} navigable={false} showLegend showTimeAxis xAxis={xAxis}
162
178
  formatValue={(value) => props.formatValue(value)} formatAxisValue={props.formatAxisValue}
163
179
  cursorDate={props.selection.cursorDate} onCursorDateChange={props.selection.onCursorDateChange}
@@ -170,6 +186,8 @@ const UNDERLYING_STATE_KEY = /^opx:(expiry|gammaExpiry|strike|expiryRow|gammaStr
170
186
 
171
187
  export function OptionsPositioningPane(props: PaneProps) {
172
188
  const { symbol } = usePaneTickerIdentity();
189
+ // A coin nothing lists options on (SOL-USD) says which coins have them, before any request.
190
+ if (symbol && isCoinWithoutOptions(symbol)) return <EmptyState title={optionsUnavailableTitle(parsePublicTickerKey(symbol).symbol)} hint={COIN_OPTIONS_HINT} />;
173
191
  // A new underlying starts over rather than relabelling the last one's data.
174
192
  return symbol ? <OptionsPositioningView key={symbol} {...props} symbol={symbol} />
175
193
  : <EmptyState title="Choose an option underlying." />;
@@ -205,7 +223,13 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
205
223
  const openInterest = useAsyncResource<OpenInterestPayload>(openInterestLoader, { keepPreviousData: true });
206
224
  const data = openInterest.data;
207
225
  const spot = data?.spot ?? null;
208
- const spotStamp = useMemo(() => positioningSpotDetail(spot, data?.spotAsOf), [spot, data?.spotAsOf]);
226
+ const venue = data?.venue;
227
+ const spotStamp = useMemo(() => positioningSpotDetail(spot, data?.spotAsOf, venue, data?.spotBasis),
228
+ [spot, data?.spotAsOf, data?.spotBasis, venue]);
229
+ // A coin's options (Deribit's) count open interest in coins, to tenths.
230
+ const coin = isCoinContract(data?.contract);
231
+ const unit = coin ? ` ${data!.contract!.unit}` : "";
232
+ const count = useCallback((value: number | null | undefined) => formatContracts(value, coin), [coin]);
209
233
  const expiries = data?.expiries ?? [];
210
234
  const shownExpiry = data?.expiry ?? requestedExpiry;
211
235
  const changes = data?.previousOiDate != null;
@@ -276,9 +300,12 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
276
300
  });
277
301
  const info = useMemo<PaneFooterSegment[]>(() => [
278
302
  ...(accessSegment ? [accessSegment] : []),
279
- ...(data?.oiDate ? [{ id: "oi-date", parts: [{ text: `OI as of ${shortDate(data.oiDate)}`, tone: "muted" as const }] }] : []),
303
+ ...(venue ? [{ id: "venue", parts: [{ text: venue, tone: "muted" as const }] }] : []),
304
+ // A venue that publishes open interest live (Deribit) says when it was read.
305
+ ...(venue && data?.oiAsOf ? [{ id: "oi-date", parts: [{ text: `OI live ${data.oiAsOf.slice(11, 16)} UTC`, tone: "muted" as const }] }]
306
+ : data?.oiDate ? [{ id: "oi-date", parts: [{ text: `OI as of ${shortDate(data.oiDate)}`, tone: "muted" as const }] }] : []),
280
307
  ...(changes && tab !== "gex" ? [{ id: "oi-change", parts: [{ text: `chg since ${shortDate(data!.previousOiDate!)}`, tone: "muted" as const }] }] : []),
281
- ], [accessSegment, changes, data?.oiDate, data?.previousOiDate, tab]);
308
+ ], [accessSegment, changes, data?.oiAsOf, data?.oiDate, data?.previousOiDate, tab, venue]);
282
309
  const activeResource = tab === "gex" ? gamma : openInterest;
283
310
  // Expiries' Net GEX column loads beside the open interest it sits with.
284
311
  const columnGamma = tab === "expiries" && gammaReady ? gamma : null;
@@ -305,7 +332,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
305
332
  : index === options.findIndex((entry) => entry.value === value) + 1 ? EXPIRY_NEXT_KEY : undefined,
306
333
  })),
307
334
  });
308
- const expiryOptions = useMemo(() => expiries.map((row) => ({ value: row.date, label: expiryLabel(row.date) })), [expiries]);
335
+ const expiryOptions = useMemo(() => expiries.map((row) => ({ value: row.date, label: expiryWithTimeLabel(row) })), [expiries]);
309
336
 
310
337
  // ---- Strikes ----
311
338
  const strikes = data?.strikes ?? [];
@@ -333,15 +360,17 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
333
360
  ] : [], [colors.negative, colors.positive, strikeChart, strikeWindow]);
334
361
  const callTotal = expiries.find((row) => row.date === shownExpiry);
335
362
  const strikeFigures = useMemo<StatItem[]>(() => !callTotal ? [] : [
336
- { id: "max-pain", label: "Max pain", value: maxPain == null ? "--" : formatStrike(maxPain), color: colors.warning,
363
+ { id: "max-pain", label: "Max pain", value: maxPain == null ? "--" : formatStrikeLabel(maxPain), color: colors.warning,
337
364
  detail: maxPain == null ? undefined : `${formatDistance(maxPain, spot)} vs spot` },
338
365
  { id: "spot", label: "Spot", value: formatLevel(spot), ...spotStamp },
339
366
  { id: "pc", label: "P/C OI", value: formatRatio(callTotal.putCallRatio) },
340
- { id: "calls", label: "Call OI", value: formatCompact(callTotal.callOI), color: colors.positive,
367
+ { id: "calls", label: `Call OI${unit}`, value: formatCompact(callTotal.callOI), color: colors.positive,
341
368
  detail: callTotal.callChange == null ? undefined : formatCountChange(callTotal.callChange) },
342
- { id: "puts", label: "Put OI", value: formatCompact(callTotal.putOI), color: colors.negative,
369
+ { id: "puts", label: `Put OI${unit}`, value: formatCompact(callTotal.putOI), color: colors.negative,
343
370
  detail: callTotal.putChange == null ? undefined : formatCountChange(callTotal.putChange) },
344
- ], [callTotal, colors.negative, colors.positive, colors.warning, maxPain, spot, spotStamp]);
371
+ ...(settlementTime(callTotal.expiresAt) ? [{ id: "settles", label: "Settles", value: settlementTime(callTotal.expiresAt)!,
372
+ detail: expiryLabel(callTotal.date) }] : []),
373
+ ], [callTotal, colors.negative, colors.positive, colors.warning, maxPain, spot, spotStamp, unit]);
345
374
  const selectedStrikeRow = strikes.find((row) => strikeKey(row) === strikeSelectedId) ?? null;
346
375
  const strikeStrip = useMemo<ChartStripSpec | null>(() => strikeWindow.length < 3 ? null : {
347
376
  label: "Open interest by strike",
@@ -349,17 +378,17 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
349
378
  const value = openInterestTotal(row);
350
379
  return value == null ? [] : [value];
351
380
  }),
352
- value: selectedStrikeRow ? `${formatStrike(selectedStrikeRow.strike)} ${formatCompact(openInterestTotal(selectedStrikeRow) ?? undefined)}` : "",
381
+ value: selectedStrikeRow ? `${formatStrikeLabel(selectedStrikeRow.strike)} ${formatCompact(openInterestTotal(selectedStrikeRow) ?? undefined)}` : "",
353
382
  color: colors.textMuted,
354
383
  }, [colors.textMuted, selectedStrikeRow, strikeWindow]);
355
384
  const renderStrike = useCallback((row: StrikeOpenInterest, column: DataTableColumn): DataTableCell => {
356
385
  switch (column.id) {
357
386
  case "strike": return {
358
- text: formatStrike(row.strike), value: row.strike,
387
+ text: formatStrikeLabel(row.strike), value: row.strike,
359
388
  color: row.strike === maxPain ? colors.warning : colors.textBright,
360
389
  };
361
- case "callOI": return { text: formatCount(row.callOI), value: row.callOI, color: colors.positive };
362
- case "putOI": return { text: formatCount(row.putOI), value: row.putOI, color: colors.negative };
390
+ case "callOI": return { text: count(row.callOI), value: row.callOI, color: colors.positive };
391
+ case "putOI": return { text: count(row.putOI), value: row.putOI, color: colors.negative };
363
392
  case "callChange":
364
393
  case "putChange": {
365
394
  const change = row[column.id];
@@ -369,16 +398,18 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
369
398
  case "payout": return { text: formatPayout(row.payout), value: row.payout, color: colors.textMuted };
370
399
  default: return { text: "" };
371
400
  }
372
- }, [colors, maxPain]);
401
+ }, [colors, count, maxPain]);
373
402
 
374
403
  // ---- Expiries ----
375
404
  // Older servers send no per-expiry gamma; the column then reads --.
376
405
  const netGammas = useMemo(() => netGammaByExpiry(gamma.data), [gamma.data]);
406
+ const expiryWalls = useMemo(() => wallsByExpiry(gamma.data), [gamma.data]);
377
407
  const expiryRows = useMemo(() => sortRows(expiries, expirySort, (row, id) =>
378
408
  id === "date" ? row.date : id === "distance" ? (row.maxPain == null || spot == null ? null : row.maxPain / spot)
379
409
  : id === "change" ? (row.callChange == null ? null : row.callChange + (row.putChange ?? 0))
380
410
  : id === "netGamma" ? netGammas?.get(row.date) ?? null
381
- : (row[id as keyof ExpiryOpenInterest] as number | null)), [expiries, expirySort, netGammas, spot]);
411
+ : id === "callWall" || id === "putWall" ? expiryWalls?.get(row.date)?.[id] ?? null
412
+ : (row[id as keyof ExpiryOpenInterest] as number | null)), [expiries, expiryWalls, expirySort, netGammas, spot]);
382
413
  const expiryChart = useMemo(() => expiries.length >= 3 ? expiryAxis(expiryDates) : null, [expiries.length, expiryDates]);
383
414
  const expirySelectedId = expiryRows.some((row) => row.date === selectedExpiry) ? selectedExpiry!
384
415
  : shownExpiry && expiryRows.some((row) => row.date === shownExpiry) ? shownExpiry : expiryRows[0]?.date ?? null;
@@ -416,17 +447,22 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
416
447
  }, [colors.textMuted, expiries, selectedExpiryRow]);
417
448
  const renderExpiry = useCallback((row: ExpiryOpenInterest, column: DataTableColumn): DataTableCell => {
418
449
  switch (column.id) {
419
- case "date": return { text: expiryLabel(row.date), value: row.date, color: colors.textBright };
450
+ case "date": return { text: expiryWithTimeLabel(row), value: row.date, color: colors.textBright };
420
451
  case "days": return { text: String(row.days), value: row.days, color: colors.textMuted };
421
- case "callOI": return { text: formatCount(row.callOI), value: row.callOI, color: colors.positive };
422
- case "putOI": return { text: formatCount(row.putOI), value: row.putOI, color: colors.negative };
452
+ case "callOI": return { text: count(row.callOI), value: row.callOI, color: colors.positive };
453
+ case "putOI": return { text: count(row.putOI), value: row.putOI, color: colors.negative };
454
+ case "callWall":
455
+ case "putWall": {
456
+ const strike = expiryWalls?.get(row.date)?.[column.id] ?? null;
457
+ return { text: strike == null ? "--" : formatStrikeLabel(strike), value: strike, color: colors.textMuted };
458
+ }
423
459
  case "putCallRatio": return { text: formatRatio(row.putCallRatio), value: row.putCallRatio };
424
460
  case "change": {
425
461
  const change = row.callChange == null ? null : row.callChange + (row.putChange ?? 0);
426
462
  return { text: formatCountChange(change), value: change,
427
463
  color: change == null || change === 0 ? colors.textMuted : change > 0 ? colors.positive : colors.negative };
428
464
  }
429
- case "maxPain": return { text: row.maxPain == null ? "--" : formatStrike(row.maxPain), value: row.maxPain, color: colors.warning };
465
+ case "maxPain": return { text: row.maxPain == null ? "--" : formatStrikeLabel(row.maxPain), value: row.maxPain, color: colors.warning };
430
466
  case "distance": return {
431
467
  text: formatDistance(row.maxPain, spot),
432
468
  value: row.maxPain == null || spot == null ? null : Number(((row.maxPain / spot - 1) * 100).toFixed(4)),
@@ -439,7 +475,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
439
475
  }
440
476
  default: return { text: "" };
441
477
  }
442
- }, [colors, netGammas, spot]);
478
+ }, [colors, count, expiryWalls, netGammas, spot]);
443
479
 
444
480
  // ---- GEX ----
445
481
  const gammaData = gamma.data;
@@ -468,19 +504,24 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
468
504
  const band = gammaData?.band;
469
505
  const shares = band ? `${Math.round(band.shareLow * 100)}-${Math.round(band.shareHigh * 100)}%` : "";
470
506
  const walls = useMemo(() => gammaWalls(gammaStrikes), [gammaStrikes]);
507
+ const gammaTimes = useMemo(() => gammaTimesFigure(gammaData?.gammaAsOf, gammaData?.quotesAsOf),
508
+ [gammaData?.gammaAsOf, gammaData?.quotesAsOf]);
471
509
  const gammaFigures = useMemo<StatItem[]>(() => !gammaData?.total ? [] : [
472
510
  { id: "net", label: "Net GEX", value: formatGamma(gammaData.total.net), detail: "$ per 1% move",
473
511
  color: gammaData.total.net >= 0 ? colors.positive : colors.negative },
474
512
  { id: "flip", label: "Flip", value: formatLevel(gammaData.flip),
475
513
  detail: gammaData.flip == null ? "none within 15%" : `${formatDistance(gammaData.flip, gammaSpot)} vs spot` },
476
514
  ...([["call-wall", "Call wall", walls.callWall], ["put-wall", "Put wall", walls.putWall]] as const).map(([id, label, strike]) => ({
477
- id, label, value: strike == null ? "--" : formatStrike(strike),
515
+ id, label, value: strike == null ? "--" : formatStrikeLabel(strike),
478
516
  detail: strike == null ? undefined : `${formatDistance(strike, gammaSpot)} vs spot`,
479
517
  })),
480
518
  ...(band ? [{ id: "band", label: "Dealer range", value: `${formatGamma(band.low)} to ${formatGamma(band.high)}`,
481
519
  detail: `${shares} with dealers${gammaData.boxes?.length ? ", boxes out" : ""}` }] : []),
482
- { id: "spot", label: "Spot", value: formatLevel(gammaSpot), ...positioningSpotDetail(gammaSpot, gammaData.spotAsOf) },
483
- ], [band, colors.negative, colors.positive, gammaData, gammaSpot, shares, walls]);
520
+ { id: "spot", label: "Spot", value: formatLevel(gammaSpot),
521
+ ...positioningSpotDetail(gammaSpot, gammaData.spotAsOf, gammaData.venue, gammaData.spotBasis) },
522
+ // Gamma is evaluated at the spot's time; before the open its volatilities are the last session's.
523
+ ...(gammaTimes ? [{ id: "gamma-at", ...gammaTimes }] : []),
524
+ ], [band, colors.negative, colors.positive, gammaData, gammaSpot, gammaTimes, shares, walls]);
484
525
  // The assumption rides in the legend, where the numbers it changes are read; the
485
526
  // dealer range figure names its own share. It gives way before the series name does.
486
527
  const assumption = width >= 60 ? "dealers long calls, short puts" : width >= 48 ? "long calls, short puts" : null;
@@ -488,12 +529,12 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
488
529
  const gammaStrip = useMemo<ChartStripSpec | null>(() => gammaWindow.length < 3 ? null : {
489
530
  label: "Net GEX by strike",
490
531
  values: gammaWindow.map((row) => row.net),
491
- value: selectedGammaRow ? `${formatStrike(selectedGammaRow.strike)} ${formatGamma(selectedGammaRow.net)}` : "",
532
+ value: selectedGammaRow ? `${formatStrikeLabel(selectedGammaRow.strike)} ${formatGamma(selectedGammaRow.net)}` : "",
492
533
  color: colors.textMuted,
493
534
  }, [colors.textMuted, gammaWindow, selectedGammaRow]);
494
535
  const renderGamma = useCallback((row: GammaStrike, column: DataTableColumn): DataTableCell => {
495
536
  switch (column.id) {
496
- case "strike": return { text: formatStrike(row.strike), value: row.strike, color: colors.textBright };
537
+ case "strike": return { text: formatStrikeLabel(row.strike), value: row.strike, color: colors.textBright };
497
538
  case "calls": return { text: formatGamma(row.calls), value: row.calls, color: colors.positive };
498
539
  case "puts": return { text: formatGamma(-row.puts), value: -row.puts, color: colors.negative };
499
540
  case "net": return { text: formatGamma(row.net), value: row.net, color: row.net >= 0 ? colors.positive : colors.negative };
@@ -506,8 +547,10 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
506
547
  }
507
548
  }, [colors, gammaSpot]);
508
549
 
509
- const strikeColumnList = useMemo(() => strikeColumns(changes), [changes]);
510
- const expiryColumnList = useMemo(() => expiryColumns(changes, hasGamma), [changes, hasGamma]);
550
+ const strikeColumnList = useMemo(() => strikeColumns(changes, unit), [changes, unit]);
551
+ const settles = expiries.some((row) => settlementTime(row.expiresAt));
552
+ const expiryColumnList = useMemo(() => expiryColumns(changes, hasGamma, { settles, walls: !!expiryWalls, unit }),
553
+ [changes, expiryWalls, hasGamma, settles, unit]);
511
554
  const markerColor = colors.textBright;
512
555
 
513
556
  let content: ReactNode = null;
@@ -532,8 +575,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
532
575
  { id: "spot", ratio: spot == null ? null : strikeChart.ratio(spot), label: "spot", color: markerColor },
533
576
  { id: "max-pain", ratio: maxPain == null ? null : strikeChart.ratio(maxPain), label: "max pain", color: colors.warning },
534
577
  ]}
535
- formatCursor={(ratio) => formatStrike(nearestStrike(strikeWindow, strikeChart.at(ratio)))}
536
- formatValue={(value) => formatCount(Math.abs(value))}
578
+ formatCursor={(ratio) => formatStrikeLabel(nearestStrike(strikeWindow, strikeChart.at(ratio)))}
579
+ formatValue={(value) => count(Math.abs(value))}
537
580
  formatAxisValue={(value, domain) => formatCompactAxis(Math.abs(value), domain)}
538
581
  selection={strikeSelection} remoteKind="opx-strikes" />,
539
582
  minRows: CHART_MIN_ROWS,
@@ -550,7 +593,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
550
593
  onHeaderClick={(id) => setExpirySort((current) => nextHeaderSort(current, id))}
551
594
  emptyStateTitle="No listed expiries."
552
595
  getExportMetadata={() => [["open interest as of", data?.oiDate ?? ""], ["change since", data?.previousOiDate ?? ""], ["spot", spot ?? ""],
553
- ...(hasGamma ? [["net GEX unit", "dollars of dealer delta per 1% move"], ["quotes as of", netGammas ? gamma.data?.quotesAsOf ?? "" : ""]] : [])]}
596
+ ...(hasGamma ? [["net GEX unit", "dollars of dealer delta per 1% move"], ["gamma at", netGammas ? gamma.data?.gammaAsOf ?? "" : ""],
597
+ ["vols from", netGammas ? gamma.data?.quotesAsOf ?? "" : ""]] : [])]}
554
598
  rootBefore={<ChartTableHeader width={width} height={bodyHeight} figures={ladderFigures}
555
599
  tableRows={expiryRows.length} tableChromeRows={chartTableChromeRows(expiryColumnList, width)}
556
600
  chart={expiryChart ? {
@@ -560,7 +604,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
560
604
  const date = expiryChart.at(ratio);
561
605
  return date ? expiryLabel(date) : "";
562
606
  }}
563
- formatValue={(value) => formatCount(Math.abs(value))}
607
+ formatValue={(value) => count(Math.abs(value))}
564
608
  formatAxisValue={(value, domain) => formatCompactAxis(Math.abs(value), domain)}
565
609
  selection={expirySelection} remoteKind="opx-expiries" />,
566
610
  minRows: CHART_MIN_ROWS - 1,
@@ -589,7 +633,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
589
633
  ["range with boxes high", gammaData.bandWithBoxes.high],
590
634
  ["box legs left out", (gammaData.boxes ?? []).map((leg) => `${leg.expiry} ${leg.strike} x${leg.contracts}`).join("; ")]] : []),
591
635
  ["flip", gammaData.flip ?? ""], ["call wall", walls.callWall ?? ""], ["put wall", walls.putWall ?? ""],
592
- ["open interest as of", gammaData.oiDate ?? ""], ["quotes as of", gammaData.quotesAsOf ?? ""]]}
636
+ ["open interest as of", gammaData.oiDate ?? ""], ["gamma at", gammaData.gammaAsOf ?? ""],
637
+ ["vols from", gammaData.quotesAsOf ?? ""]]}
593
638
  rootBefore={<ChartTableHeader width={width} height={bodyHeight} query={query} figures={gammaFigures}
594
639
  tableRows={gammaRows.length} tableChromeRows={chartTableChromeRows(GAMMA_COLUMNS, width)}
595
640
  chart={gammaChart ? {
@@ -599,7 +644,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
599
644
  { id: "spot", ratio: gammaSpot == null ? null : gammaChart.ratio(gammaSpot), label: "spot", color: markerColor },
600
645
  { id: "flip", ratio: gammaData.flip == null ? null : gammaChart.ratio(gammaData.flip), label: "flip", color: colors.warning },
601
646
  ]}
602
- formatCursor={(ratio) => formatStrike(nearestStrike(gammaWindow, gammaChart.at(ratio)))}
647
+ formatCursor={(ratio) => formatStrikeLabel(nearestStrike(gammaWindow, gammaChart.at(ratio)))}
603
648
  formatValue={formatGamma} formatAxisValue={formatCompactAxis}
604
649
  legendAccessory={assumption ? <Text fg={colors.textMuted}>{assumption}</Text> : undefined}
605
650
  legendAccessoryWidth={assumption?.length}
@@ -384,11 +384,52 @@ export const SCENARIO_STRATEGIES = ["vertical", "straddle", "put", "csp", "colla
384
384
  type ScenarioStrategy = typeof SCENARIO_STRATEGIES[number];
385
385
  const isScenarioStrategy = (value: unknown): value is ScenarioStrategy => SCENARIO_STRATEGIES.includes(value as ScenarioStrategy);
386
386
 
387
- interface StrategyPricing { spot: number; asOf: number; rate: number; dividendYield: number }
387
+ export interface StrategyPricing { spot: number; asOf: number; rate: number; dividendYield: number }
388
388
 
389
389
  const usableVolatility = (value: unknown): value is number => positive(value) && value <= 5;
390
390
 
391
- interface StrategyParity { forward: number; daysToExpiry: number }
391
+ export interface StrategyParity { forward: number; daysToExpiry: number }
392
+
393
+ /**
394
+ * The contracts a strategy seed picks from: every two-sided quote with the IV
395
+ * its midpoint implies against its expiry's parity forward (or a usable
396
+ * provider IV), each side nearest `spot` first. `parityFor` is the forward an
397
+ * expiry's IVs were solved on, null without a rate to solve them by.
398
+ */
399
+ export interface QuotedChain {
400
+ calls: QuotedContract[];
401
+ puts: QuotedContract[];
402
+ parityFor(expiration: number): StrategyParity | null;
403
+ }
404
+
405
+ /** The quoted chain every seed reads, as `scenarioStrategyLegs` describes it. */
406
+ function quoteStrategyChain(chain: OptionsChain, spot: number, pricing: StrategyPricing | null): QuotedChain {
407
+ const parity = new Map<number, StrategyParity | null>();
408
+ const parityFor = (expiration: number) => {
409
+ if (!pricing) return null;
410
+ if (!parity.has(expiration)) {
411
+ const daysToExpiry = daysToExpiryFrom(expiration, pricing.asOf);
412
+ const { forward } = extractImpliedForward(chain.calls.filter((call) => call.expiration === expiration),
413
+ chain.puts.filter((put) => put.expiration === expiration), pricing.spot, daysToExpiry / 365, pricing.rate,
414
+ chain.underlyingSymbol);
415
+ parity.set(expiration, forward == null ? null : { forward, daysToExpiry });
416
+ }
417
+ return parity.get(expiration)!;
418
+ };
419
+ const quoted = (contracts: OptionsChain["calls"], side: ScenarioLeg["side"]) => contracts.flatMap((contract): QuotedContract[] => {
420
+ const mid = optionMid(contract);
421
+ if (!positive(contract.strike) || mid == null) return [];
422
+ const expiry = parityFor(contract.expiration);
423
+ // q=r turns the spot pricer into discounted forward pricing.
424
+ const volatility = expiry ? solveImpliedVolatility({ side, spot: expiry.forward, strike: contract.strike,
425
+ daysToExpiry: expiry.daysToExpiry, rate: pricing!.rate, dividendYield: pricing!.rate }, mid).volatility : null;
426
+ if (usableVolatility(volatility)) return [{ contract, side, mid, volatility, solved: true }];
427
+ return usableVolatility(contract.impliedVolatility) ? [{ contract, side, mid, volatility: contract.impliedVolatility, solved: false }] : [];
428
+ });
429
+ const nearest = (a: { contract: { strike: number } }, b: { contract: { strike: number } }) =>
430
+ Math.abs(a.contract.strike - spot) - Math.abs(b.contract.strike - spot);
431
+ return { calls: quoted(chain.calls, "call").sort(nearest), puts: quoted(chain.puts, "put").sort(nearest), parityFor };
432
+ }
392
433
 
393
434
  /**
394
435
  * An explicit strategy request uses only two-sided quotes and enters each leg
@@ -418,33 +459,8 @@ function scenarioStrategyLegs(
418
459
  chain: OptionsChain, spot: number, strategy: ScenarioStrategy, pricing: StrategyPricing | null, strike?: number,
419
460
  collar: { put?: number; call?: number } = {}, delta?: number, unusable: UnusableChainText = defaultUnusableChainText,
420
461
  ): { legs: ScenarioLeg[]; parity: StrategyParity | null } {
421
- const parity = new Map<number, StrategyParity | null>();
422
- const parityFor = (expiration: number) => {
423
- if (!pricing) return null;
424
- if (!parity.has(expiration)) {
425
- const daysToExpiry = daysToExpiryFrom(expiration, pricing.asOf);
426
- const { forward } = extractImpliedForward(chain.calls.filter((call) => call.expiration === expiration),
427
- chain.puts.filter((put) => put.expiration === expiration), pricing.spot, daysToExpiry / 365, pricing.rate,
428
- chain.underlyingSymbol);
429
- parity.set(expiration, forward == null ? null : { forward, daysToExpiry });
430
- }
431
- return parity.get(expiration)!;
432
- };
433
- const quoted = (contracts: OptionsChain["calls"], side: ScenarioLeg["side"]) => contracts.flatMap((contract) => {
434
- const mid = optionMid(contract);
435
- if (!positive(contract.strike) || mid == null) return [];
436
- const expiry = parityFor(contract.expiration);
437
- // q=r turns the spot pricer into discounted forward pricing.
438
- const volatility = expiry ? solveImpliedVolatility({ side, spot: expiry.forward, strike: contract.strike,
439
- daysToExpiry: expiry.daysToExpiry, rate: pricing!.rate, dividendYield: pricing!.rate }, mid).volatility : null;
440
- if (usableVolatility(volatility)) return [{ contract, mid, volatility, solved: true }];
441
- return usableVolatility(contract.impliedVolatility) ? [{ contract, mid, volatility: contract.impliedVolatility, solved: false }] : [];
442
- });
443
- const nearest = (a: { contract: { strike: number } }, b: { contract: { strike: number } }) =>
444
- Math.abs(a.contract.strike - spot) - Math.abs(b.contract.strike - spot);
462
+ const { calls, puts, parityFor } = quoteStrategyChain(chain, spot, pricing);
445
463
  const anchored = ({ contract }: { contract: { strike: number } }) => strike == null || Math.abs(contract.strike - strike) < 1e-9;
446
- const calls = quoted(chain.calls, "call").sort(nearest);
447
- const puts = quoted(chain.puts, "put").sort(nearest);
448
464
  const missing = (candidates: { contract: { strike: number } }[], at = strike, what: string = strategy === "csp" ? "put to sell" : strategy) => {
449
465
  if (at == null) return new Error(unusable("strategy"));
450
466
  const listed = [...new Set(candidates.map(({ contract }) => contract.strike))]
@@ -455,7 +471,7 @@ function scenarioStrategyLegs(
455
471
  };
456
472
  if (strategy === "collar") return collarLegs(calls, puts, spot, collar, missing, parityFor, unusable);
457
473
  if (strategy === "put" || strategy === "csp") {
458
- const put = delta == null ? puts.find(anchored) : nearestDelta(puts, -delta, spot, pricing, parityFor);
474
+ const put = delta == null ? puts.find(anchored) : nearestDeltaContract(puts, -delta, spot, pricing, parityFor);
459
475
  if (!put) throw missing(puts);
460
476
  return { legs: [{ id: put.contract.contractSymbol, side: "put", quantity: strategy === "csp" ? -1 : 1, strike: put.contract.strike,
461
477
  expiration: put.contract.expiration, price: put.mid, priceSource: "mid", volatility: put.volatility, multiplier: 100,
@@ -480,10 +496,10 @@ function scenarioStrategyLegs(
480
496
  return { legs, parity: first.solved || second.solved ? parityFor(first.contract.expiration) : null };
481
497
  }
482
498
 
483
- interface QuotedContract { contract: OptionsChain["calls"][number]; mid: number; volatility: number; solved: boolean }
499
+ export interface QuotedContract { contract: OptionsChain["calls"][number]; side: ScenarioLeg["side"]; mid: number; volatility: number; solved: boolean }
484
500
 
485
501
  /** Why a chain seeds nothing: `what` it has no usable quote for, then the next live expiry to try unless `next` is false. */
486
- type UnusableChainText = (what: string, next?: boolean) => string;
502
+ export type UnusableChainText = (what: string, next?: boolean) => string;
487
503
  const defaultUnusableChainText: UnusableChainText = (what) => `The selected chain has no complete quoted ${what} with usable IV`;
488
504
 
489
505
  /**
@@ -502,38 +518,91 @@ function unusableChainText(market: ScenarioMarketSnapshot, exchange: string | un
502
518
  }
503
519
 
504
520
  /**
505
- * A put's spot delta at the IV it trades at: against the parity forward its
506
- * IV was solved on when it was (forward delta, carried to spot), else at the
507
- * seed's spot. Null when it cannot be valued.
521
+ * An option's spot delta at the IV it trades at: against the parity forward
522
+ * its IV was solved on when it was (forward delta, carried to spot), else at
523
+ * the seed's spot. Null when it cannot be valued.
508
524
  */
509
- function quotedPutDelta(quoted: QuotedContract, spot: number, pricing: StrategyPricing | null, parity: StrategyParity | null): number | null {
510
- const { contract, volatility, solved } = quoted;
525
+ function quotedDelta(quoted: QuotedContract, spot: number, pricing: StrategyPricing | null, parity: StrategyParity | null): number | null {
526
+ const { contract, side, volatility, solved } = quoted;
511
527
  if (!pricing) return null;
512
528
  if (solved && parity) {
513
529
  // On the forward, with the dividend yield set to the rate so it carries no drift, as the IV was solved.
514
- const forwardDelta = valueOption({ side: "put", spot: parity.forward, strike: contract.strike, daysToExpiry: parity.daysToExpiry,
530
+ const forwardDelta = valueOption({ side, spot: parity.forward, strike: contract.strike, daysToExpiry: parity.daysToExpiry,
515
531
  rate: pricing.rate, dividendYield: pricing.rate, volatility }).delta;
516
532
  return Number.isFinite(forwardDelta) ? forwardDelta * Math.exp((pricing.rate - pricing.dividendYield) * parity.daysToExpiry / 365) : null;
517
533
  }
518
534
  const years = daysToExpiryFrom(contract.expiration, pricing.asOf) / 365;
519
- return optionDelta({ spot, years, volatility, rate: pricing.rate, dividendYield: pricing.dividendYield }, contract.strike, "put");
535
+ return optionDelta({ spot, years, volatility, rate: pricing.rate, dividendYield: pricing.dividendYield }, contract.strike, side);
520
536
  }
521
537
 
522
- /** The put whose delta is nearest `target` (negative, as a put's delta reads), the lower strike on a tie; null when none can be valued. */
523
- function nearestDelta(
524
- puts: readonly QuotedContract[], target: number, spot: number, pricing: StrategyPricing | null,
538
+ /**
539
+ * The contract whose delta is nearest `target` (negative for a put, as its
540
+ * delta reads), the one further out of the money on a tie (the lower strike
541
+ * of two puts, the higher of two calls); null when none can be valued.
542
+ */
543
+ export function nearestDeltaContract(
544
+ candidates: readonly QuotedContract[], target: number, spot: number, pricing: StrategyPricing | null,
525
545
  parityFor: (expiration: number) => StrategyParity | null,
526
546
  ): QuotedContract | null {
527
- let best: { put: QuotedContract; distance: number } | null = null;
528
- for (const put of puts) {
529
- const delta = quotedPutDelta(put, spot, pricing, put.solved ? parityFor(put.contract.expiration) : null);
547
+ let best: { quoted: QuotedContract; distance: number } | null = null;
548
+ for (const quoted of candidates) {
549
+ const delta = quotedDelta(quoted, spot, pricing, quoted.solved ? parityFor(quoted.contract.expiration) : null);
530
550
  if (delta == null) continue;
531
551
  const distance = Math.abs(delta - target);
532
- if (!best || distance < best.distance - 1e-12 || (Math.abs(distance - best.distance) <= 1e-12 && put.contract.strike < best.put.contract.strike)) {
533
- best = { put, distance };
552
+ const further = quoted.side === "put" ? quoted.contract.strike < (best?.quoted.contract.strike ?? Infinity)
553
+ : quoted.contract.strike > (best?.quoted.contract.strike ?? -Infinity);
554
+ if (!best || distance < best.distance - 1e-12 || (Math.abs(distance - best.distance) <= 1e-12 && further)) {
555
+ best = { quoted, distance };
534
556
  }
535
557
  }
536
- return best?.put ?? null;
558
+ return best?.quoted ?? null;
559
+ }
560
+
561
+ /** A seeded leg: `quantity` contracts of a quoted option, entered at its midpoint with the IV that midpoint implies, as every seed enters it. */
562
+ export function seededLeg({ contract, side, mid, volatility, solved }: QuotedContract, quantity: number): ScenarioLeg {
563
+ return { id: contract.contractSymbol, side, quantity, strike: contract.strike, expiration: contract.expiration, price: mid,
564
+ priceSource: "mid", volatility, multiplier: 100, ...(solved ? { volatilitySource: "mid" as const } : {}) };
565
+ }
566
+
567
+ /** What a strategy screen picks from a quoted chain: its legs, and the shares it holds with them (a covered call's). */
568
+ export interface ScreenSeed { legs: ScenarioLeg[]; shares?: number }
569
+
570
+ /**
571
+ * A position a strategy screen seeds as OSA seeds `--strategy`: legs picked
572
+ * from the market's chain (or `merged`, a calendar's two expiries together) at
573
+ * their quote midpoints, valued at the market's rate and dividend yield from
574
+ * the spot the first expiry's parity forward implies when a leg's IV was solved
575
+ * against it. Shares are bought at that spot. `pick` reads the quoted chain
576
+ * with its pricing, so a leg can be chosen by delta.
577
+ */
578
+ export function seedScreenPosition(
579
+ market: ScenarioMarketSnapshot,
580
+ pick: (quoted: QuotedChain, pricing: StrategyPricing, unusable: UnusableChainText) => ScreenSeed,
581
+ merged?: OptionsChain,
582
+ ): ScenarioPosition {
583
+ if (!market.chain || market.warnings.includes("Options chain is stale")) {
584
+ throw new Error(market.missingExpiry ?? "A current options chain is required to seed a strategy");
585
+ }
586
+ const chain = merged ?? market.chain;
587
+ if (market.spot == null) throw new Error("A current underlying price is required");
588
+ if (market.rate == null) throw new Error("Choosing legs by delta needs the Treasury rate");
589
+ if (market.dividendYield == null) throw new Error("Dividend yield unavailable");
590
+ const pricing = { spot: market.spot, asOf: market.asOf, rate: market.rate, dividendYield: market.dividendYield };
591
+ const quoted = quoteStrategyChain(chain, market.spot, pricing);
592
+ const { legs, shares } = pick(quoted, pricing, unusableChainText(market, market.exchange));
593
+ const first = Math.min(...legs.map((leg) => leg.expiration));
594
+ const parity = legs.some((leg) => leg.expiration === first && leg.volatilitySource === "mid") ? quoted.parityFor(first) : null;
595
+ const spot = parity ? parity.forward * Math.exp(-(pricing.rate - pricing.dividendYield) * parity.daysToExpiry / 365) : market.spot;
596
+ const position: ScenarioPosition = {
597
+ symbol: market.symbol, exchange: market.exchange, currency: scenarioCurrency(undefined, market.currency, market.exchange),
598
+ spot, rate: pricing.rate, dividendYield: pricing.dividendYield, asOf: market.asOf, legs,
599
+ ...(shares ? { underlying: { quantity: shares, price: spot } } : {}),
600
+ };
601
+ const error = validatePosition(position);
602
+ if (error) throw new Error(error);
603
+ const contracts = [...chain.calls, ...chain.puts].filter((contract) => legs.some((leg) => leg.id === contract.contractSymbol));
604
+ if (contracts.some((contract) => contract.currency && contract.currency !== position.currency)) throw new Error("Strategy currency differs from the underlying quote currency");
605
+ return position;
537
606
  }
538
607
 
539
608
  /** How far out of the money a collar's put and call sit when their strikes are not given. */