gloomberb 0.18.1 → 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 (307) 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/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -0,0 +1,505 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { isPlainKey } from "../../../utils/keyboard";
3
+ import { Button, EmptyState, Notice, PaneFooterScope, SelectButton, usePaneFooter, usePaneMenuItems, type SelectControl } from "../../../components";
4
+ import { useShortcut } from "../../../react/input";
5
+ import { useDialogState } from "../../../ui/dialog";
6
+ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
7
+ import { usePaneVisible } from "../../../state/app/activity";
8
+ import { useAppGetState, usePaneAppConfig, usePaneStateValue } from "../../../state/app/context";
9
+ import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
10
+ import type { BrokerAdapter } from "../../../types/broker";
11
+ import type { BrokerInstanceConfig } from "../../../types/config";
12
+ import type { BrokerContractRef } from "../../../types/instrument";
13
+ import type { PaneProps, TickerResearchTabProps } from "../../../types/plugin";
14
+ import type { BrokerAccount, BrokerOrder } from "../../../types/trading";
15
+ import { Box, useUiCapabilities } from "../../../ui";
16
+ import { usePluginAppActions, usePluginBrokerActions } from "../../runtime";
17
+ import { BrokerTradingController, type BrokerOrderOutcome } from "./controller";
18
+ import { takeTradeIntent } from "./command";
19
+ import { BrokerAccountPicker } from "./account-picker";
20
+ import { BrokerOrdersView } from "./orders";
21
+ import { brokerOrderDone, brokerOrderStatus } from "./orders-data";
22
+ import { brokerOrderFeed, orderToastBody, useBrokerOrderFeed, useFeedOrderResult } from "./order-feed";
23
+ import { isStrategyOrder, orderInstrumentLabel } from "./strategy";
24
+ import {
25
+ availableTicketPosition, buildTicketAccountChoices, chooseTicketAccount, editTicketDraft, newStrategyDraft, newTicketDraft, quickTicketQuantity,
26
+ resolveTicketContract, subscribeTradingStatus, ticketAccountChoiceKey, ticketPositionContext, type ProfileAccountChoice, type TicketPositionSnapshot,
27
+ } from "./pane-model";
28
+ import { BrokerTicketView, type TicketAction, type TicketField } from "./ticket";
29
+ import { ticketChartLevels, ticketOrderMatches } from "./chart-levels";
30
+ import { useResearchTrading } from "../shared/research-trading";
31
+ import { useThemeColors } from "../../../theme/theme-context";
32
+
33
+ /**
34
+ * The ticket talks to the selected broker directly, never to the market router:
35
+ * its quotes come from the broker's own stream (or, without one, its quote
36
+ * request every 15 seconds) and its orders from the app's order feed.
37
+ */
38
+ function ProfileTicket({ broker, instance, contract, width, height, focused, ordersFirst = false, accountChoices, selectedAccountId, onChooseProfileAccount, allowAutoAccount = true, onCapture, docked = false, onClose }: {
39
+ broker: BrokerAdapter; instance: BrokerInstanceConfig; contract: BrokerContractRef;
40
+ width: number; height: number; focused: boolean; ordersFirst?: boolean;
41
+ accountChoices?: ProfileAccountChoice[]; selectedAccountId?: string;
42
+ onChooseProfileAccount?: (choice: ProfileAccountChoice) => void; allowAutoAccount?: boolean;
43
+ onCapture?: (capturing: boolean) => void; docked?: boolean; onClose?: () => void;
44
+ }) {
45
+ const trade = useResearchTrading();
46
+ const publish = trade?.publish;
47
+ const colors = useThemeColors();
48
+ const native = useUiCapabilities().nativePaneChrome;
49
+ const [ticketContentHeight, setTicketContentHeight] = useState<number>();
50
+ const [openOrders, setOpenOrders] = useState<BrokerOrder[]>([]);
51
+ const visible = usePaneVisible();
52
+ const getState = useAppGetState();
53
+ const { updateBrokerInstance, getBrokerAdapter } = usePluginBrokerActions();
54
+ const { notify } = usePluginAppActions();
55
+ const [accounts, setAccounts] = useState<BrokerAccount[]>([]);
56
+ const [positions, setPositions] = useState<TicketPositionSnapshot>();
57
+ const [accountId, setAccountId] = useState<string | undefined>(selectedAccountId);
58
+ const [page, setPage] = useState<"ticket" | "enable" | "orders">(ordersFirst ? "orders" : "ticket");
59
+ const [error, setError] = useState<string>();
60
+ const [typed, setTyped] = useState("");
61
+ const [statusVersion, setStatusVersion] = useState(0);
62
+ const [defaultPriceLabel, setDefaultPriceLabel] = useState<string>();
63
+ const [tradeIntent, setTradeIntent] = usePaneStateValue<string | null>("brokerTradeIntent", null);
64
+ const [focusRevision, setFocusRevision] = useState(0);
65
+ const [pendingModify, setPendingModify] = useState<BrokerOrder>();
66
+ const refreshResources = useRef<() => void>(() => {});
67
+ const current = useRef({ broker, instance, accounts, accountId, positions });
68
+ current.current = { broker, instance, accounts, accountId, positions };
69
+ // What a refused change came to, said once: the order feed records it quietly and reads the list again.
70
+ const onOutcome = useRef<(outcome: BrokerOrderOutcome) => void>(() => {});
71
+ onOutcome.current = (outcome) => {
72
+ brokerOrderFeed.note(instance.id, outcome.order);
73
+ brokerOrderFeed.refresh(instance.id);
74
+ notify({ type: outcome.type, title: outcome.title, body: orderToastBody(outcome.order) });
75
+ };
76
+ const controller = useMemo(() => new BrokerTradingController({ onOutcome: (outcome) => onOutcome.current(outcome), getContext: (request) => {
77
+ const values = current.current;
78
+ const latest = getState().config.brokerInstances.find((profile) => profile.id === values.instance.id);
79
+ const adapter = getBrokerAdapter(values.instance.brokerType) ?? values.broker;
80
+ return { adapter, instance: latest ?? { ...values.instance, enabled: false }, accounts: values.accounts, accountId: values.accountId,
81
+ availablePosition: availableTicketPosition(values.positions, latest ?? values.instance, values.accountId, request?.contract ?? contract),
82
+ connection: adapter.getStatus?.(latest ?? values.instance) ?? { state: "disconnected", updatedAt: 0 } };
83
+ } }), [instance.id, getState, getBrokerAdapter]);
84
+ const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
85
+ const draft = state.draft;
86
+ const caps = broker.getTradingCapabilities?.(instance, draft?.contract ?? contract, draft?.orderType);
87
+ const connection = broker.getStatus?.(instance);
88
+ const connected = connection?.state === "connected";
89
+ const reconnecting = connection?.state === "connecting";
90
+ const ticketContract = draft?.contract ?? contract;
91
+ const strategy = !!draft?.legs?.length;
92
+ const contractKey = JSON.stringify([ticketContract, draft?.legs ?? null]);
93
+ const [ordersBusy, setOrdersBusy] = useState(false);
94
+ const ticketBusy = state.phase === "previewing" || state.phase === "submitting" || state.phase === "cancelling";
95
+ const busy = ticketBusy || ordersBusy;
96
+ const reportError = (failure: unknown) => setError(failure instanceof Error ? failure.message : "This broker action could not be completed.");
97
+ useEffect(() => () => controller.dispose(), [controller]);
98
+ useEffect(() => {
99
+ controller.setDraft(newTicketDraft(broker, instance, contract, current.current.accountId));
100
+ }, [controller, instance.id, contract]);
101
+ useEffect(() => {
102
+ if (!tradeIntent || ordersFirst) return;
103
+ const intent = takeTradeIntent(tradeIntent);
104
+ setTradeIntent(null);
105
+ if (!intent) return;
106
+ // An order picked elsewhere (Overview) loads once its account has.
107
+ if (intent.modify) { setPendingModify(intent.modify); return; }
108
+ try {
109
+ if (controller.uncertain()) throw new Error("Refresh and reconcile the unknown order before starting another order.");
110
+ // A strategy from the builder trades as one spread on this underlying.
111
+ const next = intent.legs?.length ? newStrategyDraft(broker, instance, contract, { action: intent.action, quantity: intent.quantity ?? 1, legs: intent.legs }, current.current.accountId)
112
+ : newTicketDraft(broker, instance, contract, current.current.accountId, intent.action);
113
+ if (intent.quantity !== undefined) next.quantity = intent.quantity;
114
+ if (intent.limitPrice !== undefined) {
115
+ if (!broker.getTradingCapabilities?.(instance, contract).orderTypes.includes("LMT")) throw new Error("This broker does not support limit orders for this instrument.");
116
+ Object.assign(next, editTicketDraft(broker, instance, next, "orderType", "LMT"), { limitPrice: intent.limitPrice });
117
+ }
118
+ controller.reset();
119
+ controller.setDraft(next);
120
+ setTyped(""); setError(undefined); setDefaultPriceLabel(undefined); setPage("ticket");
121
+ setFocusRevision((revision) => revision + 1);
122
+ } catch (failure) { reportError(failure); }
123
+ }, [tradeIntent, setTradeIntent, controller, broker, instance, contract, ordersFirst]);
124
+ useEffect(() => { const before = controller.getSnapshot(); controller.syncContext(); if (controller.getSnapshot() !== before) setTyped(""); }, [controller, instance, accounts, accountId, positions, statusVersion]);
125
+ useEffect(() => subscribeTradingStatus(broker, instance, () => setStatusVersion((value) => value + 1)), [broker, instance]);
126
+ const selectAccount = useCallback((id: string | undefined) => {
127
+ if (id !== undefined && !current.current.accounts.some((account) => account.accountId === id)) throw new Error("Choose an available account.");
128
+ const phase = controller.getSnapshot().phase;
129
+ if (phase === "submitting" || phase === "cancelling") {
130
+ if (current.current.accountId === id) return;
131
+ throw new Error("Wait for the pending order action before changing accounts.");
132
+ }
133
+ if (controller.uncertain() && current.current.accountId !== id) throw new Error("Refresh and reconcile the unknown order before changing accounts.");
134
+ // The controller reads this ref synchronously, before React commits the selection.
135
+ const changedAccount = current.current.accountId !== id;
136
+ const before = controller.getSnapshot();
137
+ current.current.accountId = id;
138
+ setAccountId(id);
139
+ controller.syncContext();
140
+ const latestDraft = controller.getSnapshot().draft;
141
+ if (latestDraft && latestDraft.accountId !== id) controller.setDraft({ ...latestDraft, accountId: id });
142
+ if (changedAccount || controller.getSnapshot() !== before) { setTyped(""); setDefaultPriceLabel(undefined); }
143
+ setError(undefined);
144
+ }, [controller]);
145
+ useEffect(() => {
146
+ if (selectedAccountId && current.current.accounts.some((account) => account.accountId === selectedAccountId) && current.current.accountId !== selectedAccountId) {
147
+ try { selectAccount(selectedAccountId); } catch (failure) { reportError(failure); }
148
+ }
149
+ }, [selectedAccountId, accounts, selectAccount]);
150
+ useEffect(() => {
151
+ if (!visible || !connected) return;
152
+ let active = true;
153
+ let loading = false;
154
+ const refresh = () => {
155
+ if (loading) return;
156
+ loading = true;
157
+ void Promise.allSettled([broker.listAccounts?.(instance) ?? Promise.resolve([]), broker.importPositions(instance)]).then(([accountResult, positionResult]) => {
158
+ if (!active) return;
159
+ if (accountResult.status === "fulfilled") {
160
+ const nextAccounts = accountResult.value;
161
+ current.current.accounts = nextAccounts;
162
+ setAccounts(nextAccounts);
163
+ const previous = current.current.accountId;
164
+ selectAccount(allowAutoAccount ? chooseTicketAccount(nextAccounts, previous) : nextAccounts.some((account) => account.accountId === previous) ? previous : undefined);
165
+ } else setError("Broker accounts are unavailable. Reconnect in Brokers to try again.");
166
+ const nextPositions = positionResult.status === "fulfilled" ? { profile: instance, positions: positionResult.value, fetchedAt: Date.now() } : undefined;
167
+ current.current.positions = nextPositions;
168
+ setPositions(nextPositions);
169
+ if (positionResult.status === "rejected") setError("Current positions are unavailable. Check the account in your broker before ordering.");
170
+ }).catch((failure) => { if (active) reportError(failure); }).finally(() => { loading = false; });
171
+ };
172
+ refreshResources.current = refresh;
173
+ refresh();
174
+ const timer = setInterval(refresh, 30_000);
175
+ return () => { active = false; clearInterval(timer); if (refreshResources.current === refresh) refreshResources.current = () => {}; };
176
+ }, [broker, instance, connected, visible, selectAccount, allowAutoAccount, statusVersion]);
177
+ // A new ticket's limit defaults to its side of the quote, from the first quote that has a price, asked for
178
+ // or streamed, until the trader sets one. A stop is the trader's: at the touch it would trigger at once.
179
+ const pricePending = useRef(false);
180
+ const prefillPrice = useCallback(() => {
181
+ const latest = controller.getSnapshot();
182
+ const currentDraft = latest.draft, q = latest.quote?.quote;
183
+ if (!pricePending.current || !currentDraft || !q || latest.phase !== "editing" || latest.modifying) return;
184
+ const source = currentDraft.action === "BUY" && q.ask !== undefined ? "ask" : currentDraft.action === "SELL" && q.bid !== undefined ? "bid" : "last";
185
+ try {
186
+ if ((currentDraft.orderType === "LMT" || currentDraft.orderType === "STP LMT") && currentDraft.limitPrice === undefined) {
187
+ controller.applyPriceDefault(source);
188
+ setDefaultPriceLabel(`Broker ${source}. Editable.`);
189
+ }
190
+ pricePending.current = false;
191
+ } catch { /* No usable price in this quote yet: the next one may have it. */ }
192
+ }, [controller]);
193
+ useEffect(() => { prefillPrice(); }, [state.quote, prefillPrice]);
194
+ const quote = useCallback(async (prefill = false) => {
195
+ if (prefill) pricePending.current = true;
196
+ try {
197
+ await controller.loadQuote();
198
+ prefillPrice();
199
+ } catch { /* The controller owns safe error state. */ }
200
+ }, [controller, prefillPrice]);
201
+ usePaneRefreshKey(() => { refreshResources.current(); if (accountId && ticketContract.symbol) void quote(); }, { focused, enabled: page === "ticket" && !busy });
202
+ // The broker's own quote stream, where it has one, keeps the ticket's bid, ask and last current;
203
+ // a broker without one is asked every 15 seconds.
204
+ const streamsQuotes = connected && !!broker.subscribeQuotes && broker.canStreamQuotes?.(instance) !== false;
205
+ useEffect(() => {
206
+ if (!visible || !connected || page !== "ticket" || !accountId || !ticketContract.symbol || state.phase !== "editing") return;
207
+ void quote(true);
208
+ if (streamsQuotes) return;
209
+ const timer = setInterval(() => { void quote(); }, 15_000);
210
+ return () => clearInterval(timer);
211
+ }, [connected, visible, page, accountId, contractKey, draft?.orderType, state.phase, quote, instance.id, statusVersion, focusRevision, streamsQuotes]);
212
+ useEffect(() => {
213
+ const latestDraft = controller.getSnapshot().draft;
214
+ if (!visible || !streamsQuotes || page !== "ticket" || !latestDraft || !ticketContract.symbol) return;
215
+ const symbols = BrokerTradingController.streamSymbols(latestDraft);
216
+ const contracts = latestDraft.legs?.length ? latestDraft.legs.map((leg) => leg.contract) : [latestDraft.contract];
217
+ const targets = contracts.map((value, index) => ({
218
+ symbol: symbols[index]!, exchange: value.exchange ?? "", route: "broker" as const, surface: "detail" as const, visible: true, selected: true, weight: 100,
219
+ context: { brokerId: broker.id, brokerInstanceId: instance.id, instrument: value },
220
+ }));
221
+ return broker.subscribeQuotes!(instance, targets, (target, value) => controller.applyStreamQuote(target.symbol, value));
222
+ }, [broker, instance, controller, visible, streamsQuotes, page, contractKey]);
223
+ // The app's order feed: the drawer, chart levels and this ticket's result follow it.
224
+ const orderFeed = useBrokerOrderFeed(broker, instance, connected);
225
+ useFeedOrderResult(orderFeed, instance.id, state.result, controller.applyOrderUpdate);
226
+ // Without pushed order changes, or while a change's outcome is unknown, the result is asked for every 10 seconds until it ends.
227
+ const unknownResult = state.result?.status.toUpperCase() === "UNKNOWN" && state.result.orderId !== 0;
228
+ useEffect(() => {
229
+ if (!visible || !connected || (orderFeed.live && !unknownResult) || state.phase !== "result" || !state.result || ["FILLED", "CANCELLED", "CANCELED", "REJECTED", "EXPIRED", "SUPERSEDED"].includes(state.result.status.toUpperCase())) return;
230
+ const timer = setInterval(() => { void controller.refreshResult().catch(() => {}); }, 10_000);
231
+ return () => clearInterval(timer);
232
+ }, [visible, connected, orderFeed.live, unknownResult, state.phase, state.result?.orderId, state.result?.status, controller]);
233
+ // A placed order says so in a toast here; its later fills and its end toast from the order feed, app-wide.
234
+ const lastPhase = useRef(state.phase);
235
+ useEffect(() => {
236
+ const placed = lastPhase.current === "submitting" && state.phase === "result" && state.result;
237
+ lastPhase.current = state.phase;
238
+ if (!placed || !state.result) return;
239
+ const order = state.result;
240
+ brokerOrderFeed.note(instance.id, order);
241
+ const status = brokerOrderStatus(order.status);
242
+ if (status === "UNKNOWN") return;
243
+ notify({ type: status === "Rejected" ? "error" : "success", desktop: "when-inactive",
244
+ title: status === "Rejected" ? "Order rejected" : status === "Filled" ? "Order filled" : state.modifying ? "Order replaced" : "Order placed",
245
+ body: orderToastBody(order) });
246
+ }, [state.phase, state.result, state.modifying, notify, instance.id]);
247
+ const onEdit = (field: TicketField, value: string | boolean) => {
248
+ if (ordersBusy) return;
249
+ if (field === "typedConfirmation") { setTyped(String(value)); return; }
250
+ if (field === "confirmOrders") {
251
+ try { controller.setConfirmOrders(value === true); setError(undefined); } catch (failure) { reportError(failure); }
252
+ return;
253
+ }
254
+ const latestDraft = controller.getSnapshot().draft;
255
+ if (!latestDraft) return;
256
+ try {
257
+ const next = editTicketDraft(broker, instance, latestDraft, field, value);
258
+ // Controlled inputs can echo a programmatic value before React commits the next render.
259
+ if (Object.entries(next).every(([key, nextValue]) => Object.is(nextValue, latestDraft[key as keyof typeof latestDraft]))) return;
260
+ controller.setDraft(next);
261
+ setTyped(""); setError(undefined);
262
+ if (field === "limitPrice" || field === "orderType") setDefaultPriceLabel(undefined);
263
+ if (field === "limitPrice" || field === "stopPrice") pricePending.current = false;
264
+ } catch (failure) { reportError(failure); }
265
+ };
266
+ const onAction = (action: TicketAction) => {
267
+ void (async () => {
268
+ try {
269
+ setError(undefined);
270
+ if (busy) return;
271
+ if (action === "enable") {
272
+ if (page !== "enable") { setPage("enable"); return; }
273
+ const values = broker.getTradingConfigUpdate?.(instance, true);
274
+ if (!values) throw new Error("This broker does not support enabling trading here. Open Brokers.");
275
+ await updateBrokerInstance(instance.id, values);
276
+ setPage("ticket");
277
+ } else if (action === "back") {
278
+ setPage("ticket"); setTyped(""); if (draft) controller.setDraft({ ...draft });
279
+ } else if (action === "submit") await controller.submitFromEntry();
280
+ else if (action === "review") await controller.review();
281
+ else if (action === "confirm") await controller.confirm(typed);
282
+ else if (action === "cancel") await controller.confirmCancel(true);
283
+ else if (action === "refresh") { brokerOrderFeed.refresh(instance.id); await controller.refreshResult(); }
284
+ else if (action === "all" || action === "quantity25" || action === "quantity50") {
285
+ const latestDraft = controller.getSnapshot().draft;
286
+ const held = latestDraft ? availableTicketPosition(current.current.positions, current.current.instance, current.current.accountId, latestDraft.contract) : undefined;
287
+ if (!latestDraft) throw new Error("Enter an order first.");
288
+ const capabilities = broker.getTradingCapabilities?.(instance, latestDraft.contract, latestDraft.orderType);
289
+ if (!capabilities) throw new Error("This broker does not declare quantity constraints.");
290
+ const account = current.current.accounts.find((value) => value.accountId === current.current.accountId);
291
+ const quantity = quickTicketQuantity(latestDraft, capabilities, controller.getSnapshot().quote?.quote, account, held, action === "quantity25" ? 0.25 : action === "quantity50" ? 0.5 : 1);
292
+ controller.setDraft({ ...latestDraft, quantity });
293
+ setTyped("");
294
+ }
295
+ else if (action === "priceBid" || action === "priceMid" || action === "priceAsk" || action === "priceLast") {
296
+ const latestDraft = controller.getSnapshot().draft;
297
+ if (!latestDraft || latestDraft.orderType === "MKT") throw new Error("Choose a limit or stop order before setting a price.");
298
+ const source = ({ priceBid: "bid", priceMid: "mid", priceAsk: "ask", priceLast: "last" } as const)[action];
299
+ controller.applyPriceDefault(source, latestDraft.orderType === "STP" ? "stopPrice" : "limitPrice");
300
+ setDefaultPriceLabel(`Broker ${source}. Editable.`); setTyped("");
301
+ }
302
+ else if (action === "orders") setPage("orders");
303
+ else if (action === "new") {
304
+ controller.reset();
305
+ // After a spread, a new order is for the ticket's own instrument again.
306
+ controller.setDraft(newTicketDraft(broker, instance, isStrategyOrder({ contract: ticketContract }) ? contract : ticketContract, accountId));
307
+ setTyped(""); setDefaultPriceLabel(undefined); setPage("ticket");
308
+ }
309
+ } catch (failure) { reportError(failure); }
310
+ })();
311
+ };
312
+ const onAccountChange = (id: string) => { if (busy) return; try { selectAccount(id); } catch (failure) { reportError(failure); } };
313
+ const onTicketAccountChange = (value: string) => {
314
+ if (!accountChoices) { onAccountChange(value); return; }
315
+ const choice = accountChoices.find((item) => item.value === value);
316
+ if (!choice || busy) return;
317
+ if (choice.profileId === instance.id) onAccountChange(choice.accountId);
318
+ onChooseProfileAccount?.(choice);
319
+ };
320
+ const onModify = (order: BrokerOrder, edit?: { price: number; review: boolean }) => {
321
+ try {
322
+ if (!order.accountId) throw new Error("This order has no account. Refresh it in your broker.");
323
+ selectAccount(order.accountId);
324
+ controller.beginModify(order);
325
+ if (edit) {
326
+ const next = controller.getSnapshot().draft!;
327
+ controller.setDraft({ ...next, ...(next.orderType === "STP" ? { stopPrice: edit.price } : { limitPrice: edit.price }) });
328
+ if (edit.review) void controller.review().catch(reportError);
329
+ }
330
+ setTyped(""); setError(undefined); setPage("ticket");
331
+ setFocusRevision((revision) => revision + 1);
332
+ } catch (failure) { reportError(failure); }
333
+ };
334
+ useEffect(() => {
335
+ if (!pendingModify) return;
336
+ if (pendingModify.brokerInstanceId && pendingModify.brokerInstanceId !== instance.id) {
337
+ setPendingModify(undefined); reportError(new Error("Choose this order's broker profile in the ticket to change it.")); return;
338
+ }
339
+ if (!accounts.some((account) => account.accountId === pendingModify.accountId)) return;
340
+ setPendingModify(undefined);
341
+ onModify(pendingModify);
342
+ });
343
+ // A spread has no position of its own; its legs' positions belong to each contract.
344
+ const position = strategy ? undefined : ticketPositionContext(positions, instance, accountId, ticketContract);
345
+ const mode = accounts.find((account) => account.accountId === accountId)?.tradingMode;
346
+ useEffect(() => {
347
+ // A spread's net premium is not a price on the underlying's chart.
348
+ const orders = openOrders.filter((order) => !isStrategyOrder(order) && ticketOrderMatches(order, ticketContract, instance.id, accountId));
349
+ const latestOrder = state.result && !isStrategyOrder(state.result) && ticketOrderMatches(state.result, ticketContract, instance.id, accountId) ? state.result : undefined;
350
+ const levels = ticketChartLevels({
351
+ draft: strategy ? undefined : draft, editing: state.phase === "editing" && trade?.visible !== false, orders, latestOrder,
352
+ avgCost: position?.avgCost, position: position?.quantity, colors, priceStep: caps?.priceStep, priceDecimals: caps?.priceDecimals,
353
+ });
354
+ publish?.({ mode, busy, contract: ticketContract, levels });
355
+ }, [publish, mode, busy, draft, strategy, trade?.visible, state.phase, state.result, openOrders, ticketContract, instance.id, accountId, position?.avgCost, position?.quantity, colors, caps?.priceStep, caps?.priceDecimals]);
356
+ useEffect(() => () => publish?.({ levels: [] }), [publish]);
357
+ // Docked, the open orders show only while this ticker has some, or when asked
358
+ // for: o from the ticket shows and focuses them, o again (or Hide/Show orders) hides them.
359
+ const tickerOrderCount = openOrders.filter((order) => ticketOrderMatches(order, ticketContract, instance.id, accountId) && !brokerOrderDone(order.status)).length;
360
+ const [ordersPinned, setOrdersPinned] = useState<boolean>();
361
+ // A review needs the room: unless asked for, the orders step aside until it is placed or edited.
362
+ const reviewing = state.phase === "review" || state.phase === "submitting" || state.phase === "cancel-review";
363
+ const ordersShown = !!docked && (page === "orders" || (ordersPinned ?? (tickerOrderCount > 0 && !reviewing)));
364
+ const [drawerRows, setDrawerRows] = useState(1);
365
+ const [drawerConfirming, setDrawerConfirming] = useState(false);
366
+ const toggleOrders = useCallback(() => {
367
+ if (ordersShown) { setOrdersPinned(false); if (page === "orders") setPage("ticket"); } else setOrdersPinned(true);
368
+ }, [ordersShown, page]);
369
+ const dialogOpen = useDialogState((dialog) => dialog.isOpen);
370
+ useShortcut((event) => {
371
+ if (event.targetEditable || event.defaultPrevented || event.propagationStopped || event.repeat || !isPlainKey(event, "o")) return;
372
+ event.preventDefault(); event.stopPropagation(); toggleOrders();
373
+ }, { enabled: focused && !!docked && page === "orders" && !busy && !dialogOpen, phase: "before", scope: "broker-ticket:orders-toggle" });
374
+ usePaneMenuItems("broker-ticket:orders-toggle", () => focused && docked
375
+ ? [{ id: "ticket-orders-toggle", label: ordersShown ? "Hide open orders" : "Show open orders", enabled: !busy,
376
+ // o hides the orders from the orders; from the ticket it moves to them.
377
+ ...(ordersShown && page !== "orders" ? {} : { accelerator: "o" }), onSelect: ordersShown ? toggleOrders : () => setPage("orders") }]
378
+ : null, [focused, docked, ordersShown, page, busy, toggleOrders]);
379
+ // The drawer is as tall as its rows, up to a third of the panel: one order is three lines, not a quarter of the screen.
380
+ const rowHeight = native ? 1.6 : 1;
381
+ const ordersHeight = ordersShown ? Math.min(Math.max(4, Math.floor(height / 3)), (native ? 2.6 : 2) + Math.max(1, drawerRows) * rowHeight) : 0;
382
+ // On the desktop the ticket comes first: the drawer gives up its spare rows, down to its heading and one
383
+ // row, before the ticket's last control is cut off. The terminal measures the ticket and sizes both.
384
+ // A cancel question keeps its buttons in view: the ticket scrolls instead.
385
+ const drawerFloor = native && !drawerConfirming ? Math.min(ordersHeight, 1.2 + rowHeight) : ordersHeight;
386
+ const receipt = state.phase === "result" ? state.receipt : undefined;
387
+ const ticketHeight = Math.min(height - drawerFloor - (ordersShown && !native ? 1 : 0), docked && !native ? ticketContentHeight ?? Infinity : Infinity);
388
+ return <Box flexDirection="column" width={width} height={height}>
389
+ <Box visible={docked || page !== "orders"} height={docked && native ? undefined : ticketHeight} maxHeight={ticketHeight} flexShrink={0}>
390
+ <PaneFooterScope active={page !== "orders"}>
391
+ <BrokerTicketView onContentHeight={setTicketContentHeight} blocked={ordersBusy} asking={drawerConfirming} width={width} height={ticketHeight} focused={focused && page !== "orders"} onCapture={onCapture} docked={docked} onClose={onClose} model={{
392
+ brokerName: broker.name, symbol: orderInstrumentLabel(receipt?.request ?? draft ?? { contract: ticketContract }), accounts, accountId,
393
+ accountChoices, accountChoiceValue: accountChoices && accountId ? ticketAccountChoiceKey(instance.id, accountId) : undefined,
394
+ accountType: accounts.find((account) => account.accountId === accountId)?.accountType,
395
+ quote: state.quote?.quote, legQuotes: state.quote?.legs, quoteData: connection?.quoteData,
396
+ phase: page === "enable" ? "enable" : state.phase, draft: receipt?.request ?? draft, preview: state.review?.preview ?? receipt?.preview, warnings: state.review?.warnings ?? [], result: state.result, resultQuantities: state.resultQuantities,
397
+ error: connection?.state === "error" && connection.message ? connection.message : error ?? state.error, reconnecting,
398
+ position: position?.quantity, avgCost: position?.avgCost, positionPnl: position?.pnl, tradingEnabled: caps?.enabled === true,
399
+ typedConfirmation: typed, connected, capabilities: caps, modifying: state.modifying, original: state.modifying ? controller.modifyingOrder() : undefined, defaultPriceLabel, confirmOrders: state.confirmOrders,
400
+ initialFocus: state.modifying ? draft?.orderType === "STP" ? "stopPrice" : "limitPrice" : focusRevision ? "review" : undefined, focusRevision,
401
+ }} onEdit={onEdit} onAction={onAction} onAccountChange={onTicketAccountChange} />
402
+ </PaneFooterScope>
403
+ </Box>
404
+ <Box visible={ordersShown || !docked && page === "orders"} onMouseDown={() => setPage("orders")} marginTop={ordersShown && !native ? 1 : 0}
405
+ flexShrink={docked && native ? 1 : 0} minHeight={docked && native ? drawerFloor : undefined} height={docked ? ordersHeight : height}><PaneFooterScope active={page === "orders"}>
406
+ <BrokerOrdersView broker={broker} instance={instance} accounts={accounts} accountId={accountId} onAccountChange={onAccountChange} onModify={onModify}
407
+ compact={docked} contract={docked ? ticketContract : undefined} latestOrder={state.result} blocked={ticketBusy} onBusy={setOrdersBusy}
408
+ onHide={docked ? toggleOrders : undefined} onContentRows={setDrawerRows} onConfirming={setDrawerConfirming} onOrders={setOpenOrders} onBack={ticketContract.symbol ? () => setPage("ticket") : undefined}
409
+ width={width} height={docked ? ordersHeight : height} focused={focused && page === "orders"} active={trade?.visible !== false && (docked || page === "orders")} />
410
+ </PaneFooterScope></Box>
411
+ </Box>;
412
+ }
413
+
414
+ export function BrokerTradeTab({ width, height, focused, onCapture, docked, onClose }: TickerResearchTabProps & { docked?: boolean; onClose?: () => void }) {
415
+ const identity = usePaneTickerIdentity();
416
+ const config = usePaneAppConfig();
417
+ const visible = usePaneVisible();
418
+ const { getBrokerAdapter } = usePluginBrokerActions();
419
+ const { showPane } = usePluginAppActions();
420
+ const [selectedBroker, setSelectedBroker] = useState<string>();
421
+ const brokerControl = useRef<SelectControl>(null);
422
+ const [selectedAccount, setSelectedAccount] = useState<ProfileAccountChoice>();
423
+ const [accountChoices, setAccountChoices] = useState<ProfileAccountChoice[]>([]);
424
+ const lastAccounts = useRef(new Map<string, BrokerAccount[]>());
425
+ const [statusVersion, setStatusVersion] = useState(0);
426
+ const profiles = useMemo(() => config.brokerInstances.filter((profile) => profile.enabled !== false && !!getBrokerAdapter(profile.brokerType)?.placeOrder), [config.brokerInstances, getBrokerAdapter]);
427
+ const brokerTypes = useMemo(() => [...new Set(profiles.map((profile) => profile.brokerType))], [profiles]);
428
+ usePaneFooter("broker-ticket-profile", () => focused && brokerTypes.length > 1 ? {
429
+ hints: [{ id: "ticket-broker", key: "d", label: "Broker", onPress: () => brokerControl.current?.open() }],
430
+ } : null, [focused, brokerTypes.length]);
431
+ const brokerType = brokerTypes.includes(selectedBroker ?? "") ? selectedBroker : brokerTypes.length === 1 ? brokerTypes[0] : undefined;
432
+ const groupProfiles = useMemo(() => profiles.filter((profile) => profile.brokerType === brokerType), [profiles, brokerType]);
433
+ const instance = groupProfiles.find((value) => value.id === selectedAccount?.profileId) ?? groupProfiles.find((value) => value.id === identity.contract?.brokerInstanceId) ?? groupProfiles[0];
434
+ const broker = instance ? getBrokerAdapter(instance.brokerType) : null;
435
+ useEffect(() => {
436
+ const dispose = groupProfiles.map((profile) => {
437
+ const adapter = getBrokerAdapter(profile.brokerType);
438
+ return adapter ? subscribeTradingStatus(adapter, profile, () => setStatusVersion((value) => value + 1)) : undefined;
439
+ });
440
+ return () => { for (const unsubscribe of dispose) unsubscribe?.(); };
441
+ }, [groupProfiles, getBrokerAdapter]);
442
+ useEffect(() => {
443
+ if (!visible) return;
444
+ let active = true;
445
+ let pending = false;
446
+ const refresh = () => {
447
+ if (pending) return;
448
+ pending = true;
449
+ void Promise.allSettled(groupProfiles.map(async (profile) => {
450
+ const adapter = getBrokerAdapter(profile.brokerType);
451
+ const state = adapter?.getStatus?.(profile).state;
452
+ // A profile reconnecting keeps its accounts, so the ticket does not ask to choose one again.
453
+ const accounts = state === "connected" ? await adapter!.listAccounts?.(profile) ?? [] : state === "connecting" ? lastAccounts.current.get(profile.id) ?? [] : [];
454
+ lastAccounts.current.set(profile.id, accounts);
455
+ return { profile, accounts };
456
+ })).then((results) => {
457
+ if (active) setAccountChoices(buildTicketAccountChoices(results.flatMap((result) => result.status === "fulfilled" ? [result.value] : [])));
458
+ }).finally(() => { pending = false; });
459
+ };
460
+ refresh();
461
+ const timer = setInterval(refresh, 30_000);
462
+ return () => { active = false; clearInterval(timer); };
463
+ }, [groupProfiles, getBrokerAdapter, visible, statusVersion]);
464
+ const resolution = useMemo(() => broker && instance ? resolveTicketContract(identity, broker, instance) : {}, [identity, instance?.id, broker]);
465
+ // A refreshed ticker record resolves the same contract again; only another contract starts a new draft.
466
+ const contractValue = JSON.stringify(resolution.contract ?? null);
467
+ const contract = useMemo(() => resolution.contract, [contractValue]);
468
+ if (!identity.symbol || identity.error) return <EmptyState title={identity.error ?? "Choose a ticker to trade."} />;
469
+ if (!profiles.length) return <EmptyState title="Connect a broker to trade" message="Add a trading-capable broker profile in Brokers." actions={<Button label="Open Brokers" onPress={() => showPane("brokers")} />} />;
470
+ return <Box flexDirection="column" flexGrow={1} minHeight={0}>
471
+ {brokerTypes.length > 1 ? <Box paddingX={1}><SelectButton label="Broker" controlRef={brokerControl} emphasized={focused && !brokerType} value={brokerType ?? ""}
472
+ options={[{ value: "", label: "Choose a broker", disabled: true }, ...brokerTypes.map((value) => ({ value, label: getBrokerAdapter(value)?.name ?? value }))]}
473
+ onChange={(value) => { setSelectedBroker(value); setSelectedAccount(undefined); }} /></Box> : null}
474
+ {broker && instance && contract
475
+ ? <ProfileTicket key={`${instance.id}:${JSON.stringify(contract)}`} broker={broker} instance={instance} contract={contract} width={width} height={height - (brokerTypes.length > 1 ? 1 : 0)}
476
+ focused={focused} onCapture={onCapture} docked={docked} onClose={onClose} accountChoices={accountChoices} onChooseProfileAccount={setSelectedAccount}
477
+ selectedAccountId={selectedAccount?.profileId === instance.id ? selectedAccount.accountId : undefined} allowAutoAccount={groupProfiles.length === 1} />
478
+ : <Box flexDirection="column">
479
+ {accountChoices.length ? <BrokerAccountPicker accounts={accountChoices} value={selectedAccount?.value}
480
+ onChange={(value) => setSelectedAccount(accountChoices.find((choice) => choice.value === value))} /> : null}
481
+ <Notice tone={resolution.error ? "warning" : "muted"}>{resolution.error ?? "Choose the broker for this order."}</Notice>
482
+ </Box>}
483
+ </Box>;
484
+ }
485
+
486
+ export function BrokerOrdersPane({ width, height, focused }: PaneProps) {
487
+ const config = usePaneAppConfig();
488
+ const { getBrokerAdapter } = usePluginBrokerActions();
489
+ const [selected, setSelected] = useState<string>();
490
+ const profileControl = useRef<SelectControl>(null);
491
+ const profiles = config.brokerInstances.filter((instance) => instance.enabled !== false && !!getBrokerAdapter(instance.brokerType)?.listOpenOrders);
492
+ usePaneFooter("broker-orders-profile", () => focused && profiles.length ? {
493
+ hints: [{ id: "orders-profile", key: "d", label: "Broker profile", onPress: () => profileControl.current?.open() }],
494
+ } : null, [focused, profiles.length]);
495
+ const instance = profiles.find((value) => value.id === selected) ?? (profiles.length === 1 ? profiles[0] : undefined);
496
+ const broker = instance ? getBrokerAdapter(instance.brokerType) : null;
497
+ const contract = useMemo(() => ({ brokerId: broker?.id ?? "", brokerInstanceId: instance?.id, symbol: "", currency: "USD", secType: "STK" }), [broker?.id, instance?.id]);
498
+ return <Box flexGrow={1} flexDirection="column">
499
+ <Box paddingX={1}><SelectButton label="Broker profile" controlRef={profileControl} emphasized={focused && !instance} value={instance?.id ?? ""}
500
+ options={[{ value: "", label: "Choose a profile", disabled: true }, ...profiles.map((value) => ({ value: value.id, label: value.label }))]} onChange={setSelected} /></Box>
501
+ {broker && instance
502
+ ? <ProfileTicket key={instance.id} broker={broker} instance={instance} contract={contract} width={width} height={height - 1} focused={focused} ordersFirst />
503
+ : <EmptyState title="Choose a broker profile" />}
504
+ </Box>;
505
+ }