gloomberb 0.16.3 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -0,0 +1,183 @@
1
+ import type { OptionsChain } from "../../../types/financials";
2
+ import { calculateOptionGreeks, solveChainVolatilities } from "./analytics";
3
+ import { findNearestStrikeIndex, formatStrikeLabel } from "./table";
4
+
5
+ /**
6
+ * Which strikes of an expiry the chain lists. Every strike by default, as
7
+ * layouts saved before the setting existed showed; or a count either side of
8
+ * the money; or the strikes whose call or put delta falls in a band, which
9
+ * reaches deep in-the-money LEAPS without scrolling past the wings.
10
+ */
11
+ export type StrikeWindow =
12
+ | { kind: "all" }
13
+ | { kind: "around"; count: number }
14
+ | { kind: "delta"; min: number; max: number };
15
+
16
+ /** The pane setting, and the `fn OMON --strikes` option that sets it. */
17
+ export const STRIKE_WINDOW_SETTING = "strikes";
18
+ export const ALL_STRIKES = "all";
19
+ const MAX_STRIKES_AROUND = 200;
20
+
21
+ const ALL: StrikeWindow = { kind: "all" };
22
+
23
+ /** Choices the query bar and the settings dialog offer; the CLI takes any count or band. */
24
+ export const STRIKE_WINDOW_PRESETS: ReadonlyArray<{ value: string; label: string; short: string; description: string }> = [
25
+ { value: ALL_STRIKES, label: "All strikes", short: "All", description: "Every listed strike." },
26
+ { value: "10", label: "10 either side of the money", short: "±10", description: "The strike nearest spot and 10 on each side." },
27
+ { value: "20", label: "20 either side of the money", short: "±20", description: "The strike nearest spot and 20 on each side." },
28
+ { value: "40", label: "40 either side of the money", short: "±40", description: "The strike nearest spot and 40 on each side." },
29
+ { value: "0.70-0.90", label: "Delta .70 to .90", short: "Δ .70-.90", description: "In the money: calls below spot, puts above. A stock replacement band." },
30
+ { value: "0.40-0.60", label: "Delta .40 to .60", short: "Δ .40-.60", description: "Near the money." },
31
+ { value: "0.10-0.30", label: "Delta .10 to .30", short: "Δ .10-.30", description: "Out of the money on both wings." },
32
+ ];
33
+
34
+ const BAND = /^(-?\d*\.?\d+)(?:-|to|,|:|\.\.)(-?\d*\.?\d+)$/;
35
+
36
+ /**
37
+ * A band of absolute deltas as typed: `0.70-0.90`, `.7-.9`, `70-90` (read as
38
+ * percent), `-0.9--0.7` (a put's signed deltas). Null when it is not one.
39
+ */
40
+ export function parseDeltaBand(raw: string): { min: number; max: number } | null {
41
+ const band = BAND.exec(raw.trim().toLowerCase().replace(/\s+/g, "").replace(/^(?:δ|delta:?)/, ""));
42
+ if (!band) return null;
43
+ let [low, high] = [Math.abs(Number(band[1])), Math.abs(Number(band[2]))];
44
+ // 70-90 reads as a percent band, the way traders say it.
45
+ if (low > 1 || high > 1) [low, high] = [low / 100, high / 100];
46
+ if (!Number.isFinite(low) || !Number.isFinite(high) || low > 1 || high > 1) return null;
47
+ return low <= high ? { min: low, max: high } : { min: high, max: low };
48
+ }
49
+
50
+ function parseWindowText(raw: string): StrikeWindow | null {
51
+ const text = raw.trim().toLowerCase().replace(/\s+/g, "");
52
+ if (text === ALL_STRIKES) return ALL;
53
+ if (/^\d+$/.test(text)) {
54
+ const count = Number(text);
55
+ return count >= 1 && count <= MAX_STRIKES_AROUND ? { kind: "around", count } : null;
56
+ }
57
+ const band = parseDeltaBand(text);
58
+ return band ? { kind: "delta", ...band } : null;
59
+ }
60
+
61
+ /** The window a pane setting holds; a missing or unreadable one is every strike. */
62
+ export function resolveStrikeWindow(value: unknown): StrikeWindow {
63
+ return typeof value === "string" || typeof value === "number" ? parseWindowText(String(value)) ?? ALL : ALL;
64
+ }
65
+
66
+ /** The setting's stored text: `all`, `10`, `0.70-0.90`. */
67
+ export function strikeWindowValue(window: StrikeWindow): string {
68
+ if (window.kind === "all") return ALL_STRIKES;
69
+ if (window.kind === "around") return String(window.count);
70
+ return `${window.min.toFixed(2)}-${window.max.toFixed(2)}`;
71
+ }
72
+
73
+ /** `fn OMON --strikes`: the window as typed, checked, in the setting's text. */
74
+ export function normalizeStrikeWindowOption(raw: string): string {
75
+ const window = parseWindowText(raw);
76
+ if (!window) {
77
+ throw new Error(
78
+ `Invalid strike window "${raw}". Use all, a count of strikes either side of the money (1 to ${MAX_STRIKES_AROUND}), `
79
+ + "or a call or put delta band such as 0.70-0.90.",
80
+ );
81
+ }
82
+ return strikeWindowValue(window);
83
+ }
84
+
85
+ const deltaText = (value: number) => value.toFixed(2).replace(/^0\./, ".");
86
+
87
+ /** The window on the query bar chip: All, ±10, Δ .70-.90. */
88
+ export function strikeWindowShortLabel(window: StrikeWindow): string {
89
+ if (window.kind === "all") return "All";
90
+ if (window.kind === "around") return `±${window.count}`;
91
+ return `Δ ${deltaText(window.min)}-${deltaText(window.max)}`;
92
+ }
93
+
94
+ /** The absolute call and put delta at each strike, where the chain values them. */
95
+ export type StrikeDeltas = ReadonlyMap<number, { call?: number; put?: number }>;
96
+
97
+ /**
98
+ * Deltas from the same solver and Greeks the chain shows, against one spot.
99
+ * A strike without a two-sided quote on a side has no delta there.
100
+ */
101
+ export function chainDeltasByStrike(
102
+ chain: OptionsChain,
103
+ spot: number | undefined,
104
+ dividendYield: number | undefined,
105
+ now: number = Date.now(),
106
+ ): StrikeDeltas {
107
+ const deltas = new Map<number, { call?: number; put?: number }>();
108
+ if (spot == null) return deltas;
109
+ const volatilities = solveChainVolatilities(chain, spot, dividendYield, now);
110
+ for (const [side, contracts] of [["call", chain.calls], ["put", chain.puts]] as const) {
111
+ for (const contract of contracts) {
112
+ const delta = calculateOptionGreeks(contract, side, spot, dividendYield, volatilities)?.delta;
113
+ if (delta == null || !Number.isFinite(delta)) continue;
114
+ deltas.set(contract.strike, { ...deltas.get(contract.strike), [side]: Math.abs(delta) });
115
+ }
116
+ }
117
+ return deltas;
118
+ }
119
+
120
+ export interface StrikeWindowResult {
121
+ /** The strikes the chain lists, ascending. */
122
+ strikes: number[];
123
+ /** Strikes the expiry has. */
124
+ total: number;
125
+ /** Why the window could not be applied, so every strike is listed instead. */
126
+ fallback: "no-price" | "no-delta" | null;
127
+ }
128
+
129
+ // Displayed deltas round to three places; a strike that reads .700 is in a .70 band.
130
+ const DELTA_TOLERANCE = 5e-4;
131
+
132
+ /**
133
+ * The strikes a window keeps. `center` is the price the money is read at (the
134
+ * held strike, else spot); `keep` is a strike that always stays, the held
135
+ * contract's. A window that cannot be read without a price or deltas lists
136
+ * every strike and says why.
137
+ */
138
+ export function applyStrikeWindow(
139
+ strikes: readonly number[],
140
+ window: StrikeWindow,
141
+ { center = null, deltas = null, keep = null }: {
142
+ center?: number | null;
143
+ deltas?: StrikeDeltas | null;
144
+ keep?: number | null;
145
+ } = {},
146
+ ): StrikeWindowResult {
147
+ const total = strikes.length;
148
+ const every = (fallback: StrikeWindowResult["fallback"]): StrikeWindowResult => ({ strikes: [...strikes], total, fallback });
149
+ if (window.kind === "all" || total === 0) return every(null);
150
+ const kept = new Set<number>();
151
+ if (window.kind === "around") {
152
+ if (center == null || !Number.isFinite(center)) return every("no-price");
153
+ const index = findNearestStrikeIndex([...strikes], center);
154
+ for (const strike of strikes.slice(Math.max(0, index - window.count), index + window.count + 1)) kept.add(strike);
155
+ } else {
156
+ if (!deltas || deltas.size === 0) return every("no-delta");
157
+ const inBand = (value: number | undefined) => value != null
158
+ && value >= window.min - DELTA_TOLERANCE && value <= window.max + DELTA_TOLERANCE;
159
+ for (const strike of strikes) {
160
+ const delta = deltas.get(strike);
161
+ if (inBand(delta?.call) || inBand(delta?.put)) kept.add(strike);
162
+ }
163
+ }
164
+ if (keep != null && strikes.includes(keep)) kept.add(keep);
165
+ return { strikes: strikes.filter((strike) => kept.has(strike)), total, fallback: null };
166
+ }
167
+
168
+ /**
169
+ * What a report says about the strikes it lists: how many of how many, and
170
+ * the window that chose them.
171
+ */
172
+ export function strikeWindowNotice(window: StrikeWindow, result: StrikeWindowResult): string {
173
+ const { strikes, total } = result;
174
+ const range = strikes.length > 0 ? ` (${formatStrikeLabel(strikes[0]!)} to ${formatStrikeLabel(strikes.at(-1)!)})` : "";
175
+ const more = " --strikes all lists every strike.";
176
+ if (result.fallback === "no-price") return `All ${total} strikes${range}: no price to centre the window on.`;
177
+ if (result.fallback === "no-delta") return `All ${total} strikes${range}: a delta band needs a current underlying quote.`;
178
+ if (window.kind === "all") return `All ${total} strikes${range}.`;
179
+ if (window.kind === "around") {
180
+ return `${strikes.length} of ${total} strikes, ${window.count} either side of the money${range}.${more}`;
181
+ }
182
+ return `${strikes.length} of ${total} strikes with a call or put delta of ${window.min.toFixed(2)} to ${window.max.toFixed(2)}${range}.${more}`;
183
+ }
@@ -5,6 +5,7 @@ import { blendHex, colors } from "../../../theme/colors";
5
5
  import { blendForContrast, blendForSeparation, contrastRatio } from "../../../theme/color-utils";
6
6
  import { formatCompact } from "../../../utils/format";
7
7
  import { formatOptionPrice, optionQuoteSide, optionSpread } from "./market-reference";
8
+ import { formatCarryPercent, formatCostOfSpot } from "./carry";
8
9
  import type {
9
10
  OptionColumn,
10
11
  OptionFieldId,
@@ -20,6 +21,8 @@ type OptionFieldDef = {
20
21
  header: string;
21
22
  width: number;
22
23
  description: string;
24
+ /** Drawn on the put side of the strike only; the call side leaves it out. */
25
+ putOnly?: true;
23
26
  };
24
27
 
25
28
  const OPTION_TEXT_MIN_CONTRAST = 4.5;
@@ -36,7 +39,7 @@ const MONEYNESS_MAX_TINT = 0.42;
36
39
  export const OPTION_FIELD_DEFS: OptionFieldDef[] = [
37
40
  { id: "bid", label: "Bid", header: "BID", width: 7, description: "Best bid price." },
38
41
  { id: "ask", label: "Ask", header: "ASK", width: 7, description: "Best ask price." },
39
- { id: "spread", label: "Spread", header: "SPRD", width: 6, description: "Bid/ask width as a share of the midpoint, the comparable liquidity read across strikes." },
42
+ { id: "spread", label: "Spread %", header: "SPRD%", width: 6, description: "Bid/ask width as a percent of the midpoint, the comparable liquidity read across strikes." },
40
43
  { id: "last", label: "Last", header: "LAST", width: 7, description: "Last traded price." },
41
44
  { id: "delta", label: "Delta", header: "Δ", width: 6, description: "Price sensitivity to a $1 move in the underlying." },
42
45
  { id: "gamma", label: "Gamma", header: "Γ", width: 7, description: "Delta sensitivity to a $1 move in the underlying." },
@@ -46,13 +49,57 @@ export const OPTION_FIELD_DEFS: OptionFieldDef[] = [
46
49
  { id: "iv", label: "Implied volatility", header: "IV", width: 6, description: "Volatility implied by the contract price." },
47
50
  { id: "volume", label: "Volume", header: "VOL", width: 6, description: "Contracts traded in the current session." },
48
51
  { id: "openInterest", label: "Open interest", header: "OI", width: 6, description: "Outstanding open contracts." },
52
+ {
53
+ id: "extrinsicPerYear",
54
+ label: "Extrinsic per year",
55
+ header: "EXT/Y",
56
+ width: 7,
57
+ description: "Time value (midpoint less intrinsic) as a percent of spot, divided by years to expiry: the yearly cost of holding the contract instead of the stock.",
58
+ },
59
+ {
60
+ id: "costOfSpot",
61
+ label: "Cost % of spot",
62
+ header: "COST%",
63
+ width: 8,
64
+ description: "Put side only. The put's midpoint as a percent of the underlying's price: what a protective put costs per share it covers.",
65
+ putOnly: true,
66
+ },
49
67
  ];
50
68
 
51
69
  export const DEFAULT_OPTION_FIELD_IDS: OptionFieldId[] = ["bid", "ask", "last", "iv", "delta", "volume", "openInterest"];
52
70
 
53
71
  const OPTION_FIELDS_BY_ID = new Map(OPTION_FIELD_DEFS.map((field) => [field.id, field]));
54
72
 
73
+ function optionFieldToken(value: string): string {
74
+ return value.trim().toLowerCase().replace(/[\s_%/-]+/g, "");
75
+ }
76
+
77
+ /** Each field by its id, label and header as typed, any case: `openInterest`, `oi`, `sprd%`, `ext/y`. */
78
+ const OPTION_FIELDS_BY_TOKEN = new Map(OPTION_FIELD_DEFS.flatMap((field) => [
79
+ [optionFieldToken(field.id), field.id],
80
+ [optionFieldToken(field.label), field.id],
81
+ [optionFieldToken(field.header), field.id],
82
+ ] as const));
83
+
84
+ /**
85
+ * `fn OMON --columns bid,ask,spread,extrinsicPerYear`: the fields mirrored
86
+ * around the strike, in order, as the pane setting stores them (a put-only
87
+ * field such as costOfSpot sits on the put side alone). An unknown field
88
+ * fails, naming the ones there are.
89
+ */
90
+ export function normalizeOptionColumnsOption(raw: string): string {
91
+ const tokens = raw.split(/[\s,]+/).filter(Boolean);
92
+ const unknown = tokens.filter((token) => !OPTION_FIELDS_BY_TOKEN.has(optionFieldToken(token)));
93
+ if (tokens.length === 0 || unknown.length > 0) {
94
+ throw new Error(`${unknown.length > 0 ? `Unknown option column ${unknown.join(", ")}` : "No option columns given"}. `
95
+ + `Use: ${OPTION_FIELD_DEFS.map((field) => field.id).join(", ")}.`);
96
+ }
97
+ return resolveOptionFieldIds(tokens.map((token) => OPTION_FIELDS_BY_TOKEN.get(optionFieldToken(token))!)).join(",");
98
+ }
99
+
55
100
  export function resolveOptionFieldIds(value: unknown): OptionFieldId[] {
101
+ // A report's --columns arrives as the comma-separated text it was typed as.
102
+ if (typeof value === "string") value = value.split(",");
56
103
  if (!Array.isArray(value)) return [...DEFAULT_OPTION_FIELD_IDS];
57
104
  const seen = new Set<OptionFieldId>();
58
105
  const fields = value.filter((id): id is OptionFieldId => {
@@ -80,7 +127,7 @@ function sideColumn(side: OptionSide, field: OptionFieldId): Omit<OptionColumn,
80
127
  export function createOptionColumns(fieldIds: readonly OptionFieldId[]): Array<Omit<OptionColumn, "headerColor">> {
81
128
  const fields = resolveOptionFieldIds(fieldIds);
82
129
  return [
83
- ...fields.map((field) => sideColumn("call", field)),
130
+ ...fields.filter((field) => !OPTION_FIELDS_BY_ID.get(field)!.putOnly).map((field) => sideColumn("call", field)),
84
131
  { id: "strike", field: "strike", side: null, label: "STRIKE", width: 9, align: "right" },
85
132
  ...[...fields].reverse().map((field) => sideColumn("put", field)),
86
133
  ];
@@ -165,7 +212,8 @@ function optionColumnRole(column: Pick<OptionColumn, "field" | "side">): OptionC
165
212
  if (column.field === "strike") return "strike";
166
213
  if (column.field === "iv") return "iv";
167
214
  if (column.field === "volume" || column.field === "openInterest") return "activity";
168
- if (column.field === "bid" || column.field === "ask" || column.field === "spread") return "price";
215
+ if (column.field === "bid" || column.field === "ask" || column.field === "spread" || column.field === "extrinsicPerYear"
216
+ || column.field === "costOfSpot") return "price";
169
217
  return column.side ?? "strike";
170
218
  }
171
219
 
@@ -274,6 +322,10 @@ function formatOptionContractCell(
274
322
  return formatGreek(greeks?.vegaPerPoint);
275
323
  case "rho":
276
324
  return formatGreek(greeks?.rhoPerPoint);
325
+ case "extrinsicPerYear":
326
+ return formatCarryPercent(column.side === "call" ? row.callExtrinsicPerYear : row.putExtrinsicPerYear);
327
+ case "costOfSpot":
328
+ return column.side === "put" ? formatCostOfSpot(row.putCostOfSpot) : "\u2014";
277
329
  case "strike":
278
330
  return formatStrikeLabel(contract.strike);
279
331
  }
@@ -14,7 +14,9 @@ export type OptionFieldId =
14
14
  | "rho"
15
15
  | "iv"
16
16
  | "volume"
17
- | "openInterest";
17
+ | "openInterest"
18
+ | "extrinsicPerYear"
19
+ | "costOfSpot";
18
20
 
19
21
  type OptionColumnId = "strike" | `${OptionSide}${Capitalize<OptionFieldId>}`;
20
22
 
@@ -32,6 +34,11 @@ export interface OptionTableRow {
32
34
  impliedVolatility?: number;
33
35
  callGreeks?: OptionValuation;
34
36
  putGreeks?: OptionValuation;
37
+ /** Time value as a share of spot per year (see `carry.ts`); null when it cannot be read. */
38
+ callExtrinsicPerYear?: number | null;
39
+ putExtrinsicPerYear?: number | null;
40
+ /** The put's midpoint as a share of spot (see `carry.ts`); null without a two-sided quote or a current spot. */
41
+ putCostOfSpot?: number | null;
35
42
  isPositionStrike: boolean;
36
43
  }
37
44
 
@@ -6,9 +6,12 @@ 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
8
  import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
9
+ import { expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryText, readOptionExpiration } from "../../../utils/option-expiry";
9
10
  import { canonicalTickerKey } from "../../../utils/exchanges";
11
+ import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../../market-data/options-alternatives";
10
12
  import { useChartQueries, useOptionsQuery, useResolvedEntryValue, useTickerFinancials } from "../../../market-data/hooks";
11
13
  import {
14
+ Button,
12
15
  DataTableView,
13
16
  EmptyState,
14
17
  usePaneFooter,
@@ -67,6 +70,19 @@ import type { TickerFinancials } from "../../../types/financials";
67
70
  import type { IvStats } from "../iv-history/client";
68
71
  import { useIvRank } from "../iv-history/rank";
69
72
  import { useOptionsSessionOpen, useThrottledValue } from "../shared/volatility/live-session";
73
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
74
+ import { optionCarry, optionCostOfSpot } from "./carry";
75
+ import {
76
+ ALL_STRIKES,
77
+ STRIKE_WINDOW_PRESETS,
78
+ STRIKE_WINDOW_SETTING,
79
+ applyStrikeWindow,
80
+ chainDeltasByStrike,
81
+ resolveStrikeWindow,
82
+ strikeWindowNotice,
83
+ strikeWindowShortLabel,
84
+ strikeWindowValue,
85
+ } from "./strike-window";
70
86
 
71
87
  /** The summary strip and analytics recompute from live quotes at most this often. */
72
88
  const OPTIONS_SUMMARY_THROTTLE_MS = 1_000;
@@ -76,6 +92,10 @@ const EXPIRY_NEXT_KEY = "]";
76
92
  /** Switches the cursor row between its call and its put, as clicking either side does. */
77
93
  const SIDE_KEY = "x";
78
94
 
95
+ function sentenceCase(text: string): string {
96
+ return text.charAt(0).toUpperCase() + text.slice(1);
97
+ }
98
+
79
99
  function formatRatio(value: number | null | undefined): string {
80
100
  return value == null || !Number.isFinite(value) ? "--" : value.toFixed(2);
81
101
  }
@@ -152,7 +172,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
152
172
  [savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
153
173
  const { createPaneFromTemplate } = usePluginAppActions();
154
174
  const liveStreaming = useLiveStreamingSetting();
155
- const [seededExpiration] = usePaneSettingValue<number | undefined>("expiration", undefined);
175
+ // A number from the app, or the date or seconds `fn OMON --expiration` took as text.
176
+ const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
156
177
  const [expirationTargetKey] = usePaneSettingValue<string | null>("expirationTargetKey", null);
157
178
  const updatePaneSettings = useUpdatePaneSettings();
158
179
  const [calcSide, setCalcSide] = useState<OptionSide | null>(null);
@@ -207,11 +228,18 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
207
228
  const [chainRefreshMinutes] = usePaneSettingValue<string>("chainRefreshMinutes", "");
208
229
  const [storedOptionFieldIds] = usePaneSettingValue<OptionFieldId[]>("optionColumnIds", DEFAULT_OPTION_FIELD_IDS);
209
230
  const optionFieldIds = useMemo(() => resolveOptionFieldIds(storedOptionFieldIds), [storedOptionFieldIds]);
231
+ const [storedStrikeWindow] = usePaneSettingValue<unknown>(STRIKE_WINDOW_SETTING, ALL_STRIKES);
232
+ const strikeWindow = useMemo(() => resolveStrikeWindow(storedStrikeWindow), [storedStrikeWindow]);
233
+ const strikeWindowText = strikeWindowValue(strikeWindow);
210
234
  const initialChainEntry = useOptionsQuery(baseRequest);
211
235
  const initialChain = useResolvedEntryValue(initialChainEntry);
212
236
  const initialExpiration = initialChain?.expirationDates.reduce((best, expiration) => (
213
237
  parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
214
238
  ), initialChain.expirationDates[0]!);
239
+ // A typed date names a day; the catalogue's stamp for that day is the expiry.
240
+ const seededDate = readOptionExpiration(seededSetting);
241
+ const seededExpiration = seededDate == null ? undefined
242
+ : findListedExpiry(seededDate, initialChain?.expirationDates ?? []) ?? seededDate;
215
243
  const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
216
244
  ? seededExpiration ?? initialExpiration : initialExpiration;
217
245
  const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
@@ -257,7 +285,24 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
257
285
  const expirationPickRef = useRef<(value: string) => void>(() => {});
258
286
  const { nativePaneChrome } = useUiCapabilities();
259
287
  const selectedExpirationIndex = selectedExpiration == null ? -1 : expirationDates.indexOf(selectedExpiration);
260
- const expirationFilters = useMemo<QueryBarFilter[]>(() => [{
288
+ const strikeWindowOptions = useMemo(() => {
289
+ const presets = STRIKE_WINDOW_PRESETS.map(({ value, label, short, description }) => ({ value, label, short, description }));
290
+ // A window typed on the command line that no preset matches still reads on the chip.
291
+ if (presets.some((preset) => preset.value === strikeWindowText)) return presets;
292
+ const label = strikeWindowShortLabel(resolveStrikeWindow(strikeWindowText));
293
+ return [...presets, { value: strikeWindowText, label, short: label, description: "Set from the command line." }];
294
+ }, [strikeWindowText]);
295
+ // The window chip leads: the expiry strip after it can run past the edge of the bar.
296
+ const queryFilters = useMemo<QueryBarFilter[]>(() => [{
297
+ id: "strikes",
298
+ label: "Strikes",
299
+ title: "Strikes",
300
+ value: strikeWindowText,
301
+ // Every strike is the default, so any window narrows the chain and the chip says so.
302
+ defaultValue: ALL_STRIKES,
303
+ options: strikeWindowOptions,
304
+ onChange: (value: string) => updatePaneSettings({ [STRIKE_WINDOW_SETTING]: value }),
305
+ }, {
261
306
  id: "expiration",
262
307
  label: "Exp",
263
308
  inline: true,
@@ -272,7 +317,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
272
317
  : index === selectedExpirationIndex + 1 ? EXPIRY_NEXT_KEY : undefined,
273
318
  })),
274
319
  onChange: (value: string) => expirationPickRef.current(value),
275
- }], [expirationDates, selectedExpiration, selectedExpirationIndex]);
320
+ }], [expirationDates, selectedExpiration, selectedExpirationIndex, strikeWindowOptions, strikeWindowText, updatePaneSettings]);
276
321
  // A scheduled refresh of a chain already on screen is quiet: the in-session
277
322
  // cadence would otherwise blink the footer every few seconds.
278
323
  const loading = (initialChainEntry?.phase === "loading" || initialChainEntry?.phase === "refreshing") && !chain
@@ -280,7 +325,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
280
325
  || (expirationChainEntry?.phase === "refreshing" && strikeChain === null);
281
326
  // Refresh failures keep a ready entry with last-good data and an error.
282
327
  // Surface that warning even when the cached chain is still usable.
283
- const error = (expirationUnavailable ? "Selected expiration unavailable." : null)
328
+ // The body names the dates nearest the one asked for; the footer only flags it.
329
+ const unavailableExpiry = expirationUnavailable && selectedExpiration != null
330
+ ? sentenceCase(missingExpiryText(selectedExpiration, availableExpirations)) : null;
331
+ const error = (expirationUnavailable ? `Expiry ${expiryIsoDate(selectedExpiration!)} unavailable` : null)
284
332
  ?? initialChainEntry?.error?.message ?? expirationChainEntry?.error?.message
285
333
  ?? (initialChainEntry?.phase === "error" || expirationChainEntry?.phase === "error"
286
334
  ? "Failed to load options" : null);
@@ -316,9 +364,76 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
316
364
 
317
365
  useEffect(() => {
318
366
  userSelectedStrikeRef.current = false;
319
- }, [selectedExpiration]);
367
+ }, [selectedExpiration, strikeWindowText]);
320
368
 
321
- const strikes = useMemo(() => strikeChain ? buildStrikeList(strikeChain) : [], [strikeChain]);
369
+ const allStrikes = useMemo(() => strikeChain ? buildStrikeList(strikeChain) : [], [strikeChain]);
370
+ // The window is read once per chain snapshot, and again when a price first
371
+ // arrives, so streamed quotes never move strikes in or out under the cursor.
372
+ const underlyingPrice = underlying?.quote?.price;
373
+ const windowCenter = useMemo(
374
+ () => resolveDefaultStrikeTarget(parsed?.strike, underlyingPrice),
375
+ [strikeChain, parsed?.strike, underlyingPrice == null],
376
+ );
377
+ const windowSpot = useMemo(() => spot, [strikeChain, spot == null]);
378
+ const windowDeltas = useMemo(
379
+ () => strikeWindow.kind === "delta" && strikeChain ? chainDeltasByStrike(strikeChain, windowSpot, dividendYield) : null,
380
+ [dividendYield, strikeChain, strikeWindow.kind, windowSpot],
381
+ );
382
+ const strikeWindowResult = useMemo(
383
+ () => applyStrikeWindow(allStrikes, strikeWindow, { center: windowCenter, deltas: windowDeltas, keep: parsed?.strike ?? null }),
384
+ [allStrikes, parsed?.strike, strikeWindow, windowCenter, windowDeltas],
385
+ );
386
+ const strikes = strikeWindowResult.strikes;
387
+ usePaneNoticeFooter({ registrationId: "options-strike-window", focused,
388
+ notices: strikeWindowResult.fallback === "no-delta"
389
+ ? ["The delta window needs a current underlying quote; showing every strike."]
390
+ : strikeWindowResult.fallback === "no-price" ? ["No price to centre the strike window on; showing every strike."] : [] });
391
+ // What the chain shows, in every renderer and in its reports: the expiry
392
+ // and its days left from the chain's as-of, beside the chain's own feed.
393
+ // Published ahead of the strike window, so a report names the expiry first.
394
+ const asOfChain = strikeChain ?? chain;
395
+ const chainAsOfMs = asOfChain?.asOf ? Date.parse(asOfChain.asOf) : Number.NaN;
396
+ const expiryAsOfMs = Number.isFinite(chainAsOfMs) ? chainAsOfMs : Date.now();
397
+ const expiryDate = selectedExpiration == null ? "" : expiryIsoDate(selectedExpiration);
398
+ const expiryDays = selectedExpiration == null ? "" : formatDaysToExpiry(selectedExpiration, expiryAsOfMs);
399
+ useRemoteUiNode(expiryDate && chain ? {
400
+ role: "report-notice",
401
+ label: "Expiry",
402
+ getMetadata: () => ({
403
+ text: `Expiry ${expiryDate} (${expiryDays})`,
404
+ key: "expiry",
405
+ value: { date: expiryDate, daysToExpiry: daysToExpiry(selectedExpiration!, expiryAsOfMs) },
406
+ }),
407
+ } : null);
408
+ // A refresh that failed keeps the last chain, which is then stale.
409
+ const asOfEntry = strikeChain == null ? null : strikeChain === expirationChain ? expirationChainEntry : initialChainEntry;
410
+ const chainDelay = asOfChain?.delayMinutes ?? 0;
411
+ const chainStatus = asOfEntry?.error != null ? "stale" as const
412
+ : asOfChain?.dataSource ?? (chainDelay > 0 ? "delayed" as const : undefined);
413
+ useRemoteUiNode(asOfChain ? {
414
+ role: "report-freshness",
415
+ label: "Chain freshness",
416
+ getMetadata: () => ({
417
+ ...(asOfChain.asOf ? { asOf: asOfChain.asOf } : {}),
418
+ ...(chainStatus ? { status: chainStatus } : {}),
419
+ ...(chainDelay > 0 ? { delayMinutes: chainDelay } : {}),
420
+ }),
421
+ } : null);
422
+ // A report of the pane (`gloomberb fn OMON`) says how many strikes it lists of how many the expiry has.
423
+ useRemoteUiNode(strikeChain ? {
424
+ role: "report-notice",
425
+ label: "Strike window",
426
+ getMetadata: () => ({
427
+ text: strikeWindowNotice(strikeWindow, strikeWindowResult),
428
+ key: "strikes",
429
+ value: {
430
+ window: strikeWindowText,
431
+ shown: strikeWindowResult.strikes.length,
432
+ total: strikeWindowResult.total,
433
+ ...(strikeWindowResult.fallback ? { fallback: strikeWindowResult.fallback } : {}),
434
+ },
435
+ }),
436
+ } : null);
322
437
  const selectedStrikeIdx = selectedContract ? strikes.indexOf(selectedContract.strike) : strikeIdx;
323
438
  // The snapshot's contracts decide which symbols stream; they do not change
324
439
  // with the stream itself, so the subscription is stable between refreshes.
@@ -395,6 +510,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
395
510
  impliedVolatility: volatilities?.byStrike.get(strike),
396
511
  callGreeks: volatilities ? calculateOptionGreeks(call, "call", spot, dividendYield, volatilities) : undefined,
397
512
  putGreeks: volatilities ? calculateOptionGreeks(put, "put", spot, dividendYield, volatilities) : undefined,
513
+ callExtrinsicPerYear: call && volatilities ? optionCarry(call, "call", spot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
514
+ putExtrinsicPerYear: put && volatilities ? optionCarry(put, "put", spot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
515
+ putCostOfSpot: put ? optionCostOfSpot(put, spot) : null,
398
516
  isPositionStrike: !!parsed && strike === parsed.strike,
399
517
  };
400
518
  }), [callsByStrike, dividendYield, parsed, putsByStrike, spot, strikes, volatilities]);
@@ -643,9 +761,23 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
643
761
  }, { enabled: focused, phase: "before" });
644
762
 
645
763
  if (!ticker) {
646
- return <EmptyState title="No ticker selected." message="Select a ticker to view options." />;
764
+ return <EmptyState title="Select a ticker." />;
647
765
  }
648
766
  if (loading && !chain) return <Spinner label="Loading options chain..." />;
767
+ if (error && !chain && isOptionsUnavailableError(error)) {
768
+ // A future or commodity with no chain of its own: the listed fund whose options stand in, and OSA on it.
769
+ const alternative = listedOptionsAlternative(effectiveTicker);
770
+ return (
771
+ <EmptyState
772
+ title={optionsUnavailableTitle(effectiveTicker)}
773
+ hint={alternative ? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}).` : "OSA builds strategy payoffs on a listed underlying."}
774
+ actions={alternative ? <>
775
+ <Button label={`${alternative.symbol} options`} onPress={() => createPaneFromTemplate("options-pane", { symbol: alternative.symbol })} />
776
+ <Button label={`OSA ${alternative.symbol}`} onPress={() => createPaneFromTemplate("options-scenario-pane", { symbol: alternative.symbol })} />
777
+ </> : undefined}
778
+ />
779
+ );
780
+ }
649
781
  if (error && !chain) return <EmptyState title="Options chain unavailable." message={error} />;
650
782
  if (!chain || expirationDates.length === 0) {
651
783
  return <EmptyState title={`No options available for ${effectiveTicker}.`} />;
@@ -656,7 +788,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
656
788
  : 0;
657
789
  // The root insets the terminal body, so the band gets the width inside it.
658
790
  const statWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
659
- const statItems = optionsSummaryItems({
791
+ const figures = optionsSummaryItems({
660
792
  spot: spotItem(underlying, isOpt ? undefined : ticker.metadata.assetCategory),
661
793
  summary, enrichment, currency: underlying?.quote?.currency || ticker.metadata.currency || "",
662
794
  ivRank: showIvRank ? { stats: ivRank } : null,
@@ -665,8 +797,23 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
665
797
  // Slope or IVR detail is the widest cell and would otherwise halve the
666
798
  // columns and push the chain down; the detail is cut short instead.
667
799
  const statRowBudget = width >= 110 ? 3 : width >= 65 ? 4 : 6;
668
- const statColumns = Math.max(statGridColumns(statItems, statWidth), Math.ceil(statItems.length / statRowBudget));
800
+ const figureColumns = Math.max(statGridColumns(figures, statWidth), Math.ceil(figures.length / statRowBudget));
801
+ // The expiry leads the band, since the strip above can scroll it out of
802
+ // view. It joins the figures as one more cell where the row budget leaves a
803
+ // cell wide enough for all of it, and otherwise takes a row of its own so no
804
+ // figure is squeezed. Sized for four-digit days, so the band does not
805
+ // reflow as the chain's as-of arrives or the expiry steps.
806
+ const expiryItem: StatItem = { id: "expiry", label: "Expiry", value: expiryDate, detail: `(${expiryDays})` };
807
+ const cellColumns = Math.max(figureColumns, Math.ceil((figures.length + 1) / statRowBudget));
808
+ const expiryCells = Math.max(...figures.map((item) => item.label.length), expiryItem.label.length) + 1
809
+ + expiryDate.length + 2 + "(9999d)".length;
810
+ const expiryInline = Math.floor((statWidth - 2 * (cellColumns - 1)) / cellColumns) >= expiryCells;
811
+ const statColumns = expiryInline ? cellColumns : figureColumns;
812
+ const statItems = [expiryInline ? expiryItem : { ...expiryItem, wide: true }, ...figures];
669
813
  const summaryRowCount = Math.min(statGridRows(statItems, statWidth, statColumns), Math.max(0, height - 6));
814
+ // A short band gives up figures from the end; the expiry stays.
815
+ const shownStatItems = summaryRowCount === 0 ? []
816
+ : [statItems[0]!, ...figures.slice(0, summaryRowCount * statColumns - (expiryInline ? 1 : statColumns))];
670
817
  // No term here follows the selection or the load, so an empty cold-expiry
671
818
  // response cannot resize the table: growing it during loading would turn a
672
819
  // clamped scroll into apparent user navigation.
@@ -677,10 +824,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
677
824
 
678
825
  return (
679
826
  <Box flexDirection="column" flexGrow={1} paddingX={nativePaneChrome ? 0 : 1}>
680
- <QueryBar width={Math.max(1, width - 2)} filters={expirationFilters} />
827
+ <QueryBar width={Math.max(1, width - 2)} filters={queryFilters} />
681
828
 
682
829
  {summaryRowCount > 0 && (
683
- <StatGrid items={statItems.slice(0, summaryRowCount * statColumns)} width={statWidth} columns={statColumns} />
830
+ <StatGrid items={shownStatItems} width={statWidth} columns={statColumns} />
684
831
  )}
685
832
 
686
833
  {isOpt && parsed && (
@@ -721,7 +868,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
721
868
  getItemKey={(row) => String(row.strike)}
722
869
  renderCell={renderCell}
723
870
  selectedTextOverridesCellColor
724
- emptyStateTitle={error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..." : "No strikes available."}
871
+ emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
872
+ : strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
873
+ reportEveryRow
725
874
  rootWidth={Math.max(1, width - 2 + inset * 2)}
726
875
  rootHeight={tableHeight}
727
876
  columnGap={0}
@@ -1,4 +1,5 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
+ import { expirationOptionSeconds, expiryIsoDate, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../../utils/option-expiry";
2
3
  import { loadGamma, loadOpenInterest, positioningSymbol } from "./client";
3
4
  import {
4
5
  ALL_EXPIRIES,
@@ -39,8 +40,9 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
39
40
  options: [
40
41
  { key: "tab", type: "enum", values: POSITIONING_TABS.map((tab) => ({ value: tab.value })), defaultValue: initialTab,
41
42
  settingKey: "tab", description: "View the pane opens on: strikes, expiries or gex" },
42
- { key: "expiry", type: "string", settingKey: "expiry",
43
- description: "Expiry for the strikes and gamma, YYYY-MM-DD; the busiest near expiry when omitted (all expiries for gamma)" },
43
+ { key: "expiry", type: "string", settingKey: "expiry", placeholder: OPTION_EXPIRATION_PLACEHOLDER, example: "--expiry 2027-01-15",
44
+ description: `Expiry for the strikes and gamma, as ${OPTION_EXPIRATION_FORMAT}; the busiest near expiry when omitted (all expiries for gamma)`,
45
+ normalize: (value) => expiryIsoDate(expirationOptionSeconds(value, "expiry")) },
44
46
  ],
45
47
  async load(args, ctx) {
46
48
  const symbol = positioningSymbol(args.symbols[0] ?? "SPY");
@@ -3,7 +3,7 @@ import { StaticChartSurface, type StaticChartOverlay } from "../../../components
3
3
  import { Box, Text } from "../../../ui";
4
4
  import { resolveChartPalette } from "../../../components/chart/core/palette";
5
5
  import { useThemeColors } from "../../../theme/theme-context";
6
- import type { buildScenario } from "./model";
6
+ import { currencyLabel, scenarioValueUnit, type buildScenario } from "./model";
7
7
 
8
8
  export function ScenarioPayoffChart({ scenario, width, height, focused = false }: {
9
9
  scenario: ReturnType<typeof buildScenario>; width: number; height: number; focused?: boolean;
@@ -28,7 +28,7 @@ export function ScenarioPayoffChart({ scenario, width, height, focused = false }
28
28
  <Box height={1} paddingX={1} flexDirection="row" gap={3}>
29
29
  <Text fg={colors.warning}>{`${scenario.expiryRisk.reason ? "First expiry" : "Expiry"} ${date(scenario.expiryDate)}`}</Text>
30
30
  <Text fg={colors.borderFocused}>{`Selected ${date(scenario.controls.date)}`}</Text>
31
- <Text fg={colors.textDim}>{`P&L ${scenario.position.currency}`}</Text>
31
+ <Text fg={colors.textDim}>{`P&L ${currencyLabel(scenario.position.currency)} ${scenarioValueUnit(scenario.position)}`}</Text>
32
32
  </Box>
33
33
  <StaticChartSurface points={points} overlays={overlays} calendarSpaced
34
34
  width={width} height={Math.max(2, height - 1)} colors={{ ...resolveChartPalette(colors), lineColor: colors.warning }}