gloomberb 0.18.0 → 0.18.2

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 (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -59,6 +59,9 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
59
59
  const lastSavedTextRef = useRef<Map<string, string>>(new Map());
60
60
  const loadedTabIdRef = useRef<string | null>(null);
61
61
  const loadedRef = useRef(false);
62
+ // Owners whose index exists but could not be read: nothing is saved over it.
63
+ const unreadableIndexRef = useRef(new Set<string>());
64
+ const [indexError, setIndexError] = useState<{ message: string; owners: NoteOwner[] } | null>(null);
62
65
  const activeTab = tabs.find((tab) => tab.id === activeTabId);
63
66
 
64
67
  const saveQuickNotesIndex = useCallback((entries: OwnedQuickNote[]) => {
@@ -73,7 +76,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
73
76
  for (const owner of owners) {
74
77
  const bucket = byOwner.get(noteOwnerKey(owner));
75
78
  const store = storeFor(owner);
76
- if (store.readOnly) continue;
79
+ if (store.readOnly || unreadableIndexRef.current.has(noteOwnerKey(owner))) continue;
77
80
  store.saveQuickNotesIndex(bucket?.entries ?? []).catch((error) => {
78
81
  notesLog.error("Failed to save notes index", { error: error instanceof Error ? error.message : String(error) });
79
82
  notify({ body: error instanceof Error ? error.message : "Failed to save notes index.", type: "error" });
@@ -143,17 +146,24 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
143
146
  useEffect(() => {
144
147
  // Mine first, then each team's notes; reloads when the team list changes.
145
148
  let cancelled = false;
149
+ const unreadable = new Map<string, { owner: NoteOwner; message: string }>();
146
150
  void Promise.all(owners.map(async (owner) => {
147
151
  try {
148
- return (await storeFor(owner).loadQuickNotesIndex()).map((entry) => ({ ...entry, owner }));
152
+ return (await storeFor(owner).loadQuickNotesIndex({ strict: true })).map((entry) => ({ ...entry, owner }));
149
153
  } catch (error) {
154
+ unreadable.set(noteOwnerKey(owner), { owner, message: error instanceof Error ? error.message : String(error) });
150
155
  return logFailure("notes.quick-notes", "Load a quick notes index failed", [] as OwnedQuickNote[])(error);
151
156
  }
152
157
  })).then((lists) => {
153
158
  if (cancelled) return;
159
+ unreadableIndexRef.current = new Set(unreadable.keys());
160
+ const failed = [...unreadable.values()];
161
+ setIndexError(failed.length > 0 ? { message: failed[0]!.message, owners: failed.map((entry) => entry.owner) } : null);
154
162
  const entries = lists.flat();
155
163
  if (entries.length === 0) {
156
- if (loadedRef.current) return;
164
+ // Only a first run, with no index yet, gets a first tab; an index
165
+ // that failed to load must not have a fresh one saved over it.
166
+ if (loadedRef.current || unreadable.size > 0) return;
157
167
  const id = generateNoteId();
158
168
  const initial: OwnedQuickNote[] = [{ id, title: "New", owner: { kind: "user" } }];
159
169
  lastSavedTextRef.current.set(id, "");
@@ -224,6 +234,10 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
224
234
  notify({ body: `${ownerLabel(owner, teams)} notes are read-only right now.`, type: "info" });
225
235
  return;
226
236
  }
237
+ if (unreadableIndexRef.current.has(noteOwnerKey(owner))) {
238
+ notify({ body: `${ownerLabel(owner, teams)} quick notes could not be read, so a new one is not added yet.`, type: "info" });
239
+ return;
240
+ }
227
241
  const id = generateNoteId();
228
242
  const entry: OwnedQuickNote = { id, title: "New", owner };
229
243
  lastSavedTextRef.current.set(id, "");
@@ -243,6 +257,14 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
243
257
  const owner = tabOwner(id, tabsRef.current);
244
258
  setTabs((prev) => {
245
259
  const next = prev.filter((t) => t.id !== id);
260
+ // With my index unreadable, a fresh tab of mine could not be saved in it.
261
+ if (next.length === 0 && unreadableIndexRef.current.has(noteOwnerKey({ kind: "user" }))) {
262
+ saveQuickNotesIndex(next);
263
+ setActiveTabId(null);
264
+ setNoteText("");
265
+ prevTabRef.current = null;
266
+ return next;
267
+ }
246
268
  if (next.length === 0) {
247
269
  const newId = generateNoteId();
248
270
  const fresh: OwnedQuickNote[] = [{ id: newId, title: "New", owner: { kind: "user" } }];
@@ -332,6 +354,13 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
332
354
  if (activeTabId) void requestRemoveTab(activeTabId);
333
355
  }, [activeTabId, requestRemoveTab]);
334
356
 
357
+ // With no tab to show, an index that failed to load is the pane's state;
358
+ // beside other owners' tabs it is a footer warning.
359
+ const indexUnreadable = tabs.length === 0 && !!indexError;
360
+ const readError = loadError ?? (indexUnreadable ? indexError!.message : null);
361
+ const footerWarning = readError
362
+ ?? (indexError ? `${indexError.owners.map((owner) => ownerLabel(owner, teams)).join(", ")} quick notes could not be read` : null);
363
+
335
364
  // n, t and d are footer hints, which bind them without modifiers, so the
336
365
  // shell's Ctrl+W or Cmd+N never adds or deletes a note.
337
366
  useShortcut((event) => {
@@ -351,7 +380,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
351
380
  }
352
381
 
353
382
  // A note that failed to load stays read-only, as it does for a click.
354
- if (isPlainKey(event, "enter", "return") && !editing && !loadError) {
383
+ if (isPlainKey(event, "enter", "return") && !editing && !readError) {
355
384
  event.preventDefault();
356
385
  setEditing(true);
357
386
  return;
@@ -375,11 +404,11 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
375
404
 
376
405
  // Deleting the only note when it is empty just swaps in another empty one,
377
406
  // and a note that failed to load reads as empty, so it would go unconfirmed.
378
- const canDelete = !!activeTabId && !loadError && (tabs.length > 1 || noteText.trim().length > 0);
407
+ const canDelete = !!activeTabId && !readError && (tabs.length > 1 || noteText.trim().length > 0);
379
408
 
380
409
  usePaneFooter("quick-notes", () => ({
381
- info: loadError
382
- ? [{ id: "load-error", parts: [{ text: loadError, tone: "warning" as const }] }]
410
+ info: footerWarning
411
+ ? [{ id: "load-error", parts: [{ text: footerWarning, tone: "warning" as const }] }]
383
412
  : [
384
413
  { id: "edited", parts: [{ text: editing || renaming ? "editing" : formatLastEdited(activeTab?.updatedAt), tone: "muted" as const }] },
385
414
  ],
@@ -391,7 +420,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
391
420
  { id: "title", key: "t", label: "itle", title: "Rename Note", onPress: startRename, disabled: !activeTabId },
392
421
  { id: "delete", key: "d", label: "elete", title: "Delete Note", onPress: deleteActiveNote, disabled: !canDelete },
393
422
  ],
394
- }), [activeTab, activeTabId, addTab, canDelete, deleteActiveNote, editing, loadError, renaming, startRename]);
423
+ }), [activeTab, activeTabId, addTab, canDelete, deleteActiveNote, editing, footerWarning, renaming, startRename]);
395
424
 
396
425
  const noteTabs = tabs.map((tab) => ({
397
426
  label: tab.owner.kind === "team" ? `${ownerLabel(tab.owner, teams)} ${tab.title}` : tab.title,
@@ -437,13 +466,19 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
437
466
  />
438
467
  </Box>
439
468
  )}
440
- <Box flexGrow={1} minHeight={0} paddingX={1} onMouseDown={() => { if (!editing && !renaming && !loadError) setEditing(true); }}>
469
+ <Box flexGrow={1} minHeight={0} paddingX={1} onMouseDown={() => { if (!editing && !renaming && !readError) setEditing(true); }}>
441
470
  {loadError ? (
442
471
  <EmptyState
443
472
  title="This note could not be read."
444
473
  message={loadError}
445
474
  hint="Editing is disabled so the saved note is not overwritten. Fix the file, then reopen the pane."
446
475
  />
476
+ ) : indexUnreadable ? (
477
+ <EmptyState
478
+ title="Quick notes could not be read."
479
+ message={indexError!.message}
480
+ hint="Nothing is saved over them until they load. Reopen the pane to try again."
481
+ />
447
482
  ) : editing && !renaming ? (
448
483
  <MarkdownEditor
449
484
  textareaKey="editing"
@@ -458,7 +493,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
458
493
  text={noteText}
459
494
  width={width}
460
495
  placeholder="Write notes..."
461
- onActivate={() => { if (!renaming && !loadError) setEditing(true); }}
496
+ onActivate={() => { if (!renaming && !readError) setEditing(true); }}
462
497
  />
463
498
  )}
464
499
  </Box>
@@ -7,7 +7,7 @@ import {
7
7
  type NoteKind,
8
8
  } from "../../../api-client";
9
9
  import type { PluginPersistence } from "../../../types/plugin";
10
- import type { NoteFileEntry, QuickNoteEntry } from "./files";
10
+ import type { NoteFileEntry, NotesListOptions, QuickNoteEntry } from "./files";
11
11
 
12
12
  export { NoteConflictError };
13
13
 
@@ -27,8 +27,18 @@ export interface NotesStore {
27
27
  load(key: string): Promise<string>;
28
28
  save(key: string, text: string, options?: { title?: string | null }): Promise<void>;
29
29
  delete(key: string): Promise<void>;
30
- list(): Promise<NoteFileEntry[]>;
31
- loadQuickNotesIndex(): Promise<QuickNoteEntry[]>;
30
+ /**
31
+ * Every note, newest first. A note that cannot be read is left out and
32
+ * handed to `skipped`; with `strict`, notes that cannot be listed at all
33
+ * reject instead of coming back as none.
34
+ */
35
+ list(options?: NotesListOptions): Promise<NoteFileEntry[]>;
36
+ /**
37
+ * The quick notes' ids and titles. With `strict`, an index that exists but
38
+ * cannot be read rejects instead of reading as empty, for a caller that
39
+ * would otherwise save a replacement over it.
40
+ */
41
+ loadQuickNotesIndex(options?: { strict?: boolean }): Promise<QuickNoteEntry[]>;
32
42
  saveQuickNotesIndex(entries: QuickNoteEntry[]): Promise<void>;
33
43
  quickNoteKey(id: string): string;
34
44
  /** Called after a server-side change so the next load sees it. */
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import type { PaneFooterSegment, PaneHint } from "../../../components";
3
+ import { delayedFooterSegment } from "../../../components/layout/pane/footer/model";
3
4
  import { t, tf } from "../../../i18n";
4
5
  import type { OptionsChain } from "../../../types/financials";
5
6
  import { formatRelativeTime } from "../../../utils/datetime-format";
@@ -9,18 +10,18 @@ import { CLOUD_QUOTE_DELAY_MINUTES } from "../../../api-client/plan-access";
9
10
  import type { OptionQuoteCoverage, OptionQuoteCoverageStatus } from "./live-quotes";
10
11
  import { formatOptionPrice, optionSpread, type OptionMarketReference } from "./market-reference";
11
12
 
12
- interface OptionsCoverageState {
13
- text: string;
14
- tone: "muted" | "positive" | "warning";
15
- }
16
-
17
- /** Stream coverage the pane reports once the account is entitled to real-time options. */
18
- function resolveOptionsCoverageState(status: OptionQuoteCoverageStatus): OptionsCoverageState | null {
19
- if (status === "live") return { text: t("real-time options"), tone: "positive" };
20
- if (status === "mixed") return { text: t("mixed real-time and delayed options"), tone: "warning" };
13
+ /**
14
+ * Stream coverage the pane reports once the account is entitled to real-time
15
+ * options. Delayed quotes, all or some, are the footer's clock, its detail on
16
+ * hover.
17
+ */
18
+ export function optionsCoverageSegment(status: OptionQuoteCoverageStatus, delayLabel?: string): PaneFooterSegment | null {
19
+ const id = "options-access";
20
+ if (status === "live") return { id, parts: [{ text: t("real-time options"), tone: "positive" }] };
21
+ if (status === "mixed") return delayedFooterSegment({ id, detail: t("mixed real-time and delayed options") });
21
22
  // The stream connects in the background; nothing to report until it answers.
22
23
  if (status === "connecting") return null;
23
- return { text: t("options delayed fallback"), tone: "warning" };
24
+ return delayedFooterSegment({ id, detail: t("Delayed options quotes, real-time stream unavailable"), delay: delayLabel });
24
25
  }
25
26
 
26
27
  /**
@@ -82,6 +83,7 @@ export function resolveOptionsDelayLabel(chain: OptionsChain | null | undefined)
82
83
  }
83
84
 
84
85
  export function useOptionsAccessFooter({
86
+ broker = false,
85
87
  chain,
86
88
  error,
87
89
  focused,
@@ -91,6 +93,8 @@ export function useOptionsAccessFooter({
91
93
  reference,
92
94
  spreadColumnVisible,
93
95
  }: {
96
+ /** The chain is a broker profile's, quoted by that broker: it has no Gloom Cloud delay to report or upgrade from. */
97
+ broker?: boolean;
94
98
  chain: OptionsChain | null | undefined;
95
99
  error?: string | null;
96
100
  focused: boolean;
@@ -102,24 +106,25 @@ export function useOptionsAccessFooter({
102
106
  /** Set while the chain prints a SPRD column, so the status bar stops repeating it. */
103
107
  spreadColumnVisible?: boolean;
104
108
  }): void {
109
+ const delayLabel = resolveOptionsDelayLabel(chain);
105
110
  const { access, hint: upgradeHint, segment } = useCloudAccessFooter({
106
- delayLabel: resolveOptionsDelayLabel(chain),
111
+ delayLabel,
112
+ degraded: !broker,
107
113
  focused,
108
114
  segmentId: "options-access",
109
115
  placement: "omon-footer",
110
116
  shortcutScope: "options:upgrade",
111
117
  });
112
- // Trial accounts stream real-time too, but the countdown is the status worth the row.
113
- const coverage = access.hasProAccess && !access.isTrialActive
114
- ? resolveOptionsCoverageState(quoteCoverage.status)
115
- : null;
116
-
118
+ // Trial accounts stream real-time too, but the countdown is the status worth the row. A broker's
119
+ // chain streams from the broker whatever the plan, so its coverage is the status, and its delay
120
+ // is the chain's own, not Gloom Cloud's.
121
+ const proCoverage = broker || (access.hasProAccess && !access.isTrialActive);
122
+ const coverageDelay = broker && !(chain?.delayMinutes && chain.delayMinutes > 0) ? undefined : delayLabel;
117
123
  const accessInfo = useMemo<PaneFooterSegment[]>(() => {
118
- if (coverage) {
119
- return [{ id: "options-access", parts: [{ text: coverage.text, tone: coverage.tone }] }];
120
- }
124
+ const coverage = proCoverage ? optionsCoverageSegment(quoteCoverage.status, coverageDelay) : null;
125
+ if (coverage) return [coverage];
121
126
  return segment ? [segment] : [];
122
- }, [coverage?.text, coverage?.tone, segment]);
127
+ }, [coverageDelay, proCoverage, quoteCoverage.status, segment]);
123
128
 
124
129
  // Rebuilt every render so the quote and trade ages keep counting with the
125
130
  // stream; the registration compares by value, so only a changed label
@@ -1,6 +1,6 @@
1
1
  import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
2
2
  import type { OptionContract, OptionsChain, Quote } from "../../../types/financials";
3
- import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
3
+ import { buildQuoteKey, instrumentFromQuoteTarget, resolveEntryData } from "../../../market-data/selectors";
4
4
  import type { QueryEntry } from "../../../market-data/result-types";
5
5
  import type { OptionTableRow } from "./types";
6
6
  import { isFiniteNumber } from "../../../utils/guards";
@@ -75,9 +75,20 @@ function normalizeVisibleRange(
75
75
  return { start, end };
76
76
  }
77
77
 
78
+ /**
79
+ * The broker profile a chain came from. Its contracts' quotes are asked of the
80
+ * same profile, so a broker that streams options (and no cloud stream) keeps
81
+ * the chain it served current, rather than leaving it to a slower source.
82
+ */
83
+ export interface OptionQuoteBroker {
84
+ brokerId?: string;
85
+ brokerInstanceId?: string;
86
+ }
87
+
78
88
  export function buildOptionQuoteTargets(
79
89
  rows: readonly OptionTableRow[],
80
90
  window: OptionQuoteTargetWindow,
91
+ broker?: OptionQuoteBroker,
81
92
  ): QuoteSubscriptionTarget[] {
82
93
  if (rows.length === 0) return [];
83
94
  const visibleRange = normalizeVisibleRange(rows.length, window);
@@ -101,6 +112,7 @@ export function buildOptionQuoteTargets(
101
112
  targets.set(symbol, {
102
113
  symbol,
103
114
  exchange: OPTIONS_QUOTE_EXCHANGE,
115
+ ...(broker ? { context: { brokerId: broker.brokerId, brokerInstanceId: broker.brokerInstanceId } } : {}),
104
116
  surface: "options",
105
117
  visible,
106
118
  selected,
@@ -119,10 +131,12 @@ export function buildOptionQuoteTargets(
119
131
  return [...targets.values()];
120
132
  }
121
133
 
122
- export function buildOptionQuoteKey(contractSymbol: string): string {
134
+ export function buildOptionQuoteKey(contractSymbol: string, broker?: OptionQuoteBroker): string {
123
135
  return buildQuoteKey({
124
136
  symbol: contractSymbol,
125
137
  exchange: OPTIONS_QUOTE_EXCHANGE,
138
+ brokerId: broker?.brokerId,
139
+ brokerInstanceId: broker?.brokerInstanceId,
126
140
  });
127
141
  }
128
142
 
@@ -179,17 +193,20 @@ export interface LiveOptionsChain {
179
193
  }
180
194
 
181
195
  const EMPTY_STRIKES: ReadonlySet<number> = new Set();
182
- const contractKeyIndexes = new WeakMap<readonly OptionContract[], Map<string, number[]>>();
196
+ const contractKeyIndexes = new WeakMap<readonly OptionContract[], Map<string, Map<string, number[]>>>();
183
197
 
184
- function contractKeyIndex(contracts: readonly OptionContract[]): Map<string, number[]> {
185
- let index = contractKeyIndexes.get(contracts);
198
+ function contractKeyIndex(contracts: readonly OptionContract[], broker?: OptionQuoteBroker): Map<string, number[]> {
199
+ const byBroker = contractKeyIndexes.get(contracts) ?? new Map<string, Map<string, number[]>>();
200
+ contractKeyIndexes.set(contracts, byBroker);
201
+ const brokerKey = `${broker?.brokerId ?? ""}|${broker?.brokerInstanceId ?? ""}`;
202
+ let index = byBroker.get(brokerKey);
186
203
  if (!index) {
187
204
  index = new Map();
188
205
  contracts.forEach((contract, position) => {
189
- const key = buildOptionQuoteKey(contract.contractSymbol);
206
+ const key = buildOptionQuoteKey(contract.contractSymbol, broker);
190
207
  index!.set(key, [...(index!.get(key) ?? []), position]);
191
208
  });
192
- contractKeyIndexes.set(contracts, index);
209
+ byBroker.set(brokerKey, index);
193
210
  }
194
211
  return index;
195
212
  }
@@ -204,12 +221,13 @@ export function overlayOptionChainQuotes(
204
221
  chain: OptionsChain,
205
222
  quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
206
223
  freshness: OptionsQuoteFreshness,
224
+ broker?: OptionQuoteBroker,
207
225
  ): LiveOptionsChain {
208
226
  if (quoteEntries.size === 0) return { chain, streamedStrikes: EMPTY_STRIKES, quotedAt: null };
209
227
  const streamedStrikes = new Set<number>();
210
228
  let quotedAt: number | null = null;
211
229
  const overlay = (contracts: OptionContract[]): OptionContract[] => {
212
- const index = contractKeyIndex(contracts);
230
+ const index = contractKeyIndex(contracts, broker);
213
231
  let next: OptionContract[] | null = null;
214
232
  for (const [key, entry] of quoteEntries) {
215
233
  const positions = index.get(key);
@@ -242,19 +260,16 @@ export function resolveOptionQuoteCoverage(
242
260
  quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
243
261
  freshness: OptionsQuoteFreshness,
244
262
  ): OptionQuoteCoverage {
245
- const visibleSymbols = new Set(
263
+ // Each target's own key: a chain's broker profile is part of it.
264
+ const visibleKeys = new Set(
246
265
  targets
247
- .filter((target) => target.visible === true)
248
- .map((target) => target.symbol.trim().toUpperCase())
249
- .filter(Boolean),
266
+ .filter((target) => target.visible === true && target.symbol.trim())
267
+ .map((target) => buildQuoteKey(instrumentFromQuoteTarget(target))),
250
268
  );
251
269
  let liveCount = 0;
252
270
  let fallbackCount = 0;
253
- for (const symbol of visibleSymbols) {
254
- const quote = freshOptionQuote(
255
- quoteEntries.get(buildOptionQuoteKey(symbol)),
256
- freshness,
257
- );
271
+ for (const key of visibleKeys) {
272
+ const quote = freshOptionQuote(quoteEntries.get(key), freshness);
258
273
  if (
259
274
  quote?.dataSource === "live"
260
275
  && quote.delivery === "stream"
@@ -266,7 +281,7 @@ export function resolveOptionQuoteCoverage(
266
281
  }
267
282
  }
268
283
 
269
- const totalCount = visibleSymbols.size;
284
+ const totalCount = visibleKeys.size;
270
285
  const status: OptionQuoteCoverageStatus = totalCount > 0 && liveCount === totalCount
271
286
  ? "live"
272
287
  : liveCount > 0
@@ -1,11 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useUiCapabilities } from "../../../ui";
3
- import { usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
3
+ import { usePaneAppConfig, usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
4
4
  import { colors, priceColor } from "../../../theme/colors";
5
5
  import { isPlainKey } from "../../../utils/keyboard";
6
6
  import { formatCompact, formatPercentRaw } from "../../../utils/format";
7
7
  import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
8
- import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
8
+ import { chainOptionContract, formatExpDate, optionContractName, resolveOptionsTarget } from "../../../utils/options";
9
9
  import {
10
10
  expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
11
11
  } from "../../../utils/option-expiry";
@@ -34,7 +34,9 @@ import { useLiveQuoteEntries, useQuoteUpdates } from "../../../state/hooks/quote
34
34
  import { buildChartKey } from "../../../market-data/selectors";
35
35
  import type { ChartRequest } from "../../../market-data/request-types";
36
36
  import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
37
- import { usePluginAppActions } from "../../runtime";
37
+ import { usePluginAppActions, usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
38
+ import { brokerCanTrade, openTradeTicket } from "../broker-trading/command";
39
+ import type { OptionContract } from "../../../types/financials";
38
40
  import { OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
39
41
  import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
40
42
  import { useOptionsCatalogue } from "./expiry-catalogue";
@@ -66,7 +68,7 @@ import { signedPositionDirection } from "../../../portfolio/position-metrics";
66
68
  import { optionMarketReference } from "./market-reference";
67
69
  import { useOptionsEnrichment } from "./enrichment";
68
70
  import type { OptionsEnrichmentSnapshot } from "./enrichment-model";
69
- import { optionMid, type OptionSide } from "../shared/volatility";
71
+ import { daysToExpiryFrom, optionMid, type OptionSide } from "../shared/volatility";
70
72
  import type { TickerRecord } from "../../../types/ticker";
71
73
  import type { TickerFinancials } from "../../../types/financials";
72
74
  import type { IvStats } from "../iv-history/client";
@@ -176,6 +178,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
176
178
  const ticker = useMemo(() => savedTicker ?? (boundSymbol ? transientTicker(boundSymbol, fallbackExchange, fallbackCurrency) : null),
177
179
  [savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
178
180
  const { createPaneFromTemplate } = usePluginAppActions();
181
+ const { pinTicker } = usePluginTickerActions();
182
+ const { getBrokerAdapter } = usePluginBrokerActions();
183
+ const appConfig = usePaneAppConfig();
179
184
  const liveStreaming = useLiveStreamingSetting();
180
185
  // A number from the app, or the date or seconds `fn OMON --expiration` took as text.
181
186
  const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
@@ -238,13 +243,20 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
238
243
  const strikeWindowText = strikeWindowValue(strikeWindow);
239
244
  const initialChainEntry = useOptionsQuery(baseRequest);
240
245
  const initialChain = useResolvedEntryValue(initialChainEntry);
241
- const initialExpiration = initialChain?.expirationDates.reduce((best, expiration) => (
246
+ // A listing can still carry a date whose 16:00 ET close has passed. The chain
247
+ // never opens there, nor on a saved date that has since passed: it takes the nearest live one.
248
+ const now = Date.now();
249
+ const liveExpiration = (expiration: number) => daysToExpiryFrom(expiration, now) > 0;
250
+ const listedExpirations = initialChain?.expirationDates ?? [];
251
+ const openingExpirations = listedExpirations.some(liveExpiration) ? listedExpirations.filter(liveExpiration) : listedExpirations;
252
+ const initialExpiration = openingExpirations.length === 0 ? undefined : openingExpirations.reduce((best, expiration) => (
242
253
  parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
243
- ), initialChain.expirationDates[0]!);
254
+ ), openingExpirations[0]!);
244
255
  // A typed date names a day; the catalogue's stamp for that day is the expiry.
245
256
  const seededDate = readOptionExpiration(seededSetting);
246
- const seededExpiration = seededDate == null ? undefined
247
- : findListedExpiry(seededDate, initialChain?.expirationDates ?? []) ?? seededDate;
257
+ const seededListed = seededDate == null ? undefined : findListedExpiry(seededDate, listedExpirations) ?? seededDate;
258
+ const seededExpiration = seededListed != null && (liveExpiration(seededListed) || !openingExpirations.some(liveExpiration))
259
+ ? seededListed : undefined;
248
260
  const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
249
261
  ? seededExpiration ?? initialExpiration : initialExpiration;
250
262
  const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
@@ -281,8 +293,11 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
281
293
  ?? (matchesSelectedExpiration(initialChain) ? initialChain : null);
282
294
  const strikesLoading = strikeChain === null && !expirationUnavailable;
283
295
  const expirationDates = useMemo(() => {
284
- return selectedExpirationMissing
296
+ const listed = selectedExpirationMissing
285
297
  ? [...availableExpirations, selectedExpiration!].sort((left, right) => left - right) : availableExpirations;
298
+ // A date that has ended has no contracts left to show; the strip leaves it out, unless every date has.
299
+ const current = (expiration: number) => daysToExpiry(expiration, Date.now()) >= 0;
300
+ return listed.some(current) ? listed.filter((expiration) => expiration === selectedExpiration || current(expiration)) : listed;
286
301
  }, [availableExpirations, selectedExpiration, selectedExpirationMissing]);
287
302
  // Keep the date strip stable while a mouse press focuses this pane. A new
288
303
  // tab list asks the web host to reveal the active tab and can move the date
@@ -468,13 +483,17 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
468
483
  : { key: viewportKey, range }
469
484
  ));
470
485
  }, [viewportKey]);
486
+ // A chain read from a broker profile has its contracts quoted by that profile too.
487
+ const quoteBrokerId = instrument?.brokerId, quoteBrokerInstanceId = instrument?.brokerInstanceId;
488
+ const optionQuoteBroker = useMemo(() => quoteBrokerId || quoteBrokerInstanceId
489
+ ? { brokerId: quoteBrokerId, brokerInstanceId: quoteBrokerInstanceId } : undefined, [quoteBrokerId, quoteBrokerInstanceId]);
471
490
  const optionQuoteTargets = useMemo(
472
491
  () => buildOptionQuoteTargets(quoteRows, {
473
492
  fallbackHeight: height,
474
493
  selectedIndex: selectedStrikeIdx,
475
494
  visibleRange: visibleStrikeRange,
476
- }),
477
- [height, quoteRows, selectedStrikeIdx, visibleStrikeRange],
495
+ }, optionQuoteBroker),
496
+ [height, quoteRows, selectedStrikeIdx, visibleStrikeRange, optionQuoteBroker],
478
497
  );
479
498
  const {
480
499
  entries: optionQuoteEntries,
@@ -497,8 +516,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
497
516
  // The snapshot object itself comes back while nothing streamed, so the
498
517
  // memos below only recompute when a quote actually landed.
499
518
  const liveChain = useMemo(
500
- () => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness).chain : null,
501
- [optionQuoteEntries, optionQuoteFreshness, strikeChain],
519
+ () => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness, optionQuoteBroker).chain : null,
520
+ [optionQuoteEntries, optionQuoteFreshness, strikeChain, optionQuoteBroker],
502
521
  );
503
522
  const callsByStrike = useMemo(
504
523
  () => new Map(liveChain?.calls.map((c) => [c.strike, c]) ?? []),
@@ -672,6 +691,28 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
672
691
  }, [scenarioContract, scenarioAvailable, scenarioMid, scenarioVolatility, createPaneFromTemplate, dividendYield, effectiveTicker,
673
692
  effectiveExchange, selectedSide, spot, underlying?.quote?.lastUpdated]);
674
693
 
694
+ // A contract opens as its own listing: quote, chart and Trade. It stays on
695
+ // the chain's broker while that profile is on, else it opens by its symbol.
696
+ const contractListing = useCallback((contract: OptionContract) => {
697
+ const symbol = contract.contractSymbol.trim().toUpperCase();
698
+ const currency = contract.currency || underlying?.quote?.currency || "USD";
699
+ const brokerOn = !!instrument && appConfig.brokerInstances.some((profile) => profile.id === instrument.brokerInstanceId && profile.enabled !== false);
700
+ return { symbol, selection: { instrument: brokerOn ? chainOptionContract(symbol, instrument, currency) : null,
701
+ listing: { name: optionContractName(symbol) ?? symbol, exchange: "", currency, type: "OPT" } } };
702
+ }, [appConfig.brokerInstances, instrument, underlying?.quote?.currency]);
703
+ const openContract = useCallback((contract: OptionContract | null | undefined) => {
704
+ if (!contract) return;
705
+ const { symbol, selection } = contractListing(contract);
706
+ pinTicker(symbol, selection);
707
+ }, [contractListing, pinTicker]);
708
+ const tradable = !!scenarioContract && brokerCanTrade(appConfig.brokerInstances, getBrokerAdapter, scenarioContract.contractSymbol.trim().toUpperCase(), "OPT");
709
+ const tradeContract = useCallback(() => {
710
+ if (!scenarioContract || !tradable) return;
711
+ const { symbol, selection } = contractListing(scenarioContract);
712
+ // A new contract's ticket starts on Buy; b and s choose the side in it.
713
+ openTradeTicket({ pinTicker }, symbol, { action: "BUY" }, selection);
714
+ }, [contractListing, pinTicker, scenarioContract, tradable]);
715
+
675
716
  const openSurface = useCallback(() => {
676
717
  if (!ticker || selectedExpiration == null) return;
677
718
  createPaneFromTemplate("vol-surface-pane", { symbol: ticker.metadata.ticker, ticker, instrument,
@@ -703,12 +744,14 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
703
744
  setCalcSide(sideTarget);
704
745
  }, [selectContract, selectedRow, selectedStrikeIdx, sideTarget]);
705
746
 
747
+ // With a broker that trades the contract, [t]rade opens its ticket on Buy, and Sell is chosen there.
706
748
  const footerHints = useMemo(() => [
707
749
  ...(sideTarget ? [{ id: "side", key: SIDE_KEY, label: ` ${sideTarget}`, title: sideTarget === "put" ? "Select Put" : "Select Call", onPress: switchSide }] : []),
750
+ ...(tradable ? [{ id: "trade-contract", key: "t", label: "rade", title: "Trade Contract", onPress: tradeContract }] : []),
708
751
  ...(calcParams ? [{ id: "calc", key: "c", label: "alc", onPress: openCalculator }] : []),
709
752
  ...(scenarioAvailable ? [{ id: "scenario", key: "a", label: "dd to OSA", onPress: openScenario }] : []),
710
753
  ...(ticker && selectedExpiration != null ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
711
- ], [sideTarget, switchSide, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
754
+ ], [sideTarget, switchSide, tradable, tradeContract, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
712
755
 
713
756
  // Back to where the chain opens: the held contract's strike, else the money.
714
757
  const defaultStrikeIndex = strikes.length === 0 ? -1 : (() => {
@@ -732,8 +775,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
732
775
  { id: "default-strike", label: parsed ? "Go to Held Strike" : "Go to ATM Strike",
733
776
  enabled: defaultStrikeIndex >= 0, onSelect: goToDefaultStrike },
734
777
  ...(enrichment?.asOf && dialog ? [{ id: "analytics-as-of", label: "Analytics As Of…", onSelect: showAnalyticsAsOf }] : []),
778
+ { id: "open-contract", label: "Open Contract", accelerator: "Enter", enabled: !!scenarioContract, onSelect: () => openContract(scenarioContract) },
779
+ ...(tradable ? [{ id: "trade-contract", label: "Trade Contract", accelerator: "t", onSelect: tradeContract }] : []),
735
780
  ], [defaultStrikeIndex, dialog, enrichment?.asOf, expirationDates.length, goToDefaultStrike, parsed,
736
- selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf]);
781
+ selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf, scenarioContract, openContract, tradable, tradeContract]);
737
782
 
738
783
  const renderCell = useCallback((
739
784
  row: OptionTableRow,
@@ -756,6 +801,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
756
801
  }, [selectContract, selectedSide]);
757
802
 
758
803
  useOptionsAccessFooter({
804
+ broker: !!optionQuoteBroker,
759
805
  chain,
760
806
  error: [error, underlyingStale ? "Underlying quote stale: Greeks and calculator unavailable" : null,
761
807
  selectedContract && strikeChain && !selectedContractAvailable
@@ -789,7 +835,21 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
789
835
  // the arrows for it; in a research tab those stay with the tab strip.
790
836
  useShortcut((event) => {
791
837
  if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
792
- if (event.ctrl || event.meta || event.alt || event.shift) return;
838
+ if (event.ctrl || event.meta || event.alt) return;
839
+ const surface = isPlainKey(event, "s");
840
+ if (surface && ticker && selectedExpiration != null) {
841
+ event.preventDefault();
842
+ event.stopPropagation();
843
+ openSurface();
844
+ return;
845
+ }
846
+ if (event.shift) return;
847
+ if (tradable && isPlainKey(event, "t") && !event.repeat) {
848
+ event.preventDefault();
849
+ event.stopPropagation();
850
+ tradeContract();
851
+ return;
852
+ }
793
853
 
794
854
  const expiryStep = isPlainKey(event, EXPIRY_PREVIOUS_KEY) || (!nestedInTabs && isPlainKey(event, "h", "left")) ? -1
795
855
  : isPlainKey(event, EXPIRY_NEXT_KEY) || (!nestedInTabs && isPlainKey(event, "l", "right")) ? 1 : 0;
@@ -799,12 +859,6 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
799
859
  selectAdjacentExpiration(expiryStep);
800
860
  return;
801
861
  }
802
- if (isPlainKey(event, "s") && ticker && selectedExpiration != null) {
803
- event.preventDefault();
804
- event.stopPropagation();
805
- openSurface();
806
- return;
807
- }
808
862
  if (isPlainKey(event, "c") && calcParams) {
809
863
  event.preventDefault();
810
864
  event.stopPropagation();
@@ -925,6 +979,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
925
979
  onVisibleRangeChange={handleVisibleStrikeRangeChange}
926
980
  getItemKey={(row) => String(row.strike)}
927
981
  renderCell={renderCell}
982
+ onActivate={(row) => openContract(selectedSide === "put" ? row.put : selectedSide === "call" ? row.call : null)}
928
983
  selectedTextOverridesCellColor
929
984
  emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
930
985
  : strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
2
+ import { delayedFooterSegment, FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
3
3
  import { CompositeChart } from "../../../components/chart/composite/composite-chart";
4
4
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
5
5
  import { useAsyncResource, useInputCapture } from "../../../public/react";
@@ -279,7 +279,9 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
279
279
  ...(surfaceSource && surfaceResource.loading ? [{ id: "loading", parts: [{ text: "loading surface", tone: "muted" as const }] }] : []),
280
280
  ...(surfaceSource && surface?.asOf ? [{ id: "surface-asof", parts: [{ text: `surface · ${surface.asOf}`, tone: "muted" as const }] }] : []),
281
281
  ...(effectiveSteps && effectiveSteps !== (draft.steps ?? 400) ? [{ id: "refined", parts: [{ text: `tree refined to ${effectiveSteps} steps`, tone: "muted" as const }] }] : []),
282
- ...(liveInputs ? [{ id: "market", parts: [{ text: liveInputs.delayed ? "delayed market" : "real-time market", tone: "muted" as const }] }] : []),
282
+ ...(liveInputs ? [liveInputs.delayed
283
+ ? delayedFooterSegment({ id: "market", detail: "Delayed market inputs" })
284
+ : { id: "market", parts: [{ text: "real-time market", tone: "muted" as const }] }] : []),
283
285
  ],
284
286
  hints: activeFieldId ? [] : [
285
287
  // Names the cell Enter or `e` edits, which the grid does not mark.