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,437 @@
1
+ import { useCallback, useEffect, useId, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
2
+ import {
3
+ Badge, Button, DataTableView, IconButton, PaneStatusBody, RatioBar, SegmentedControl, Notice, SectionHeading, confirmDialog, usePaneFooter, usePaneNoticeFooter,
4
+ type DataTableCell, type DataTableColumn, type SelectControl,
5
+ } from "../../../components";
6
+ import { loadingErrorFooterInfo, usePaneRefreshKey } from "../../../components/data-table/table-pane";
7
+ import { useAsyncResource } from "../../../react/async-resource";
8
+ import { useShortcut } from "../../../react/input";
9
+ import { usePaneVisible } from "../../../state/app/activity";
10
+ import { colors } from "../../../theme/colors";
11
+ import type { BrokerAdapter } from "../../../types/broker";
12
+ import type { BrokerInstanceConfig } from "../../../types/config";
13
+ import type { BrokerAccount, BrokerExecution, BrokerOrder, BrokerOrderType } from "../../../types/trading";
14
+ import { Box, ScrollBox, Text, TextAttributes, useUiCapabilities } from "../../../ui";
15
+ import { useDialog, useDialogState } from "../../../ui/dialog";
16
+ import { BrokerTradingController, type BrokerOrderOutcome, type BrokerTradingContext } from "./controller";
17
+ import { usePluginAppActions } from "../../runtime";
18
+ import { BrokerAccountPicker } from "./account-picker";
19
+ import { brokerOrderDone, brokerOrderKey, brokerOrderStatus, canChangeBrokerOrder, ticketEditingPriceStep, withLatestOrder, formatOrderPrice as price, formatOrderQuantity as quantity, isBuySide, loadBrokerOrdersSnapshot, type BrokerOrdersSnapshot } from "./orders-data";
20
+ import { accountModeLabel } from "./account-picker";
21
+ import type { BrokerContractRef } from "../../../types/instrument";
22
+ import { ticketOrderMatches } from "./chart-levels";
23
+ import { brokerOrderFeed, orderToastBody, useBrokerOrderFeed, useFeedOrderResult } from "./order-feed";
24
+ import { orderInstrumentLabel, strategyShape } from "./strategy";
25
+ import { stepNumber } from "../../../components/ui/number-step";
26
+ import { ticketTypeName } from "./ticket-parts";
27
+ import { wrapTextLines } from "../../../utils/text-wrap";
28
+
29
+ export interface BrokerOrdersViewProps {
30
+ broker: BrokerAdapter;
31
+ instance: BrokerInstanceConfig;
32
+ accounts: BrokerAccount[];
33
+ accountId?: string;
34
+ onAccountChange: (accountId: string) => void;
35
+ onModify: (order: BrokerOrder, edit?: { price: number; review: boolean }) => void;
36
+ compact?: boolean;
37
+ /** Docked: collapse the orders section beside the ticket. */
38
+ onHide?: () => void;
39
+ blocked?: boolean;
40
+ onBusy?(busy: boolean): void;
41
+ /** A cancel question is open in the drawer, whose buttons must stay in view. */
42
+ onConfirming?(confirming: boolean): void;
43
+ contract?: BrokerContractRef;
44
+ onOrders?: (orders: BrokerOrder[]) => void;
45
+ /** Compact: the rows it has to show, so the drawer can be as tall as its content. */
46
+ onContentRows?: (rows: number) => void;
47
+ latestOrder?: BrokerOrder;
48
+ onBack?: () => void;
49
+ width: number;
50
+ height: number;
51
+ focused: boolean;
52
+ active?: boolean;
53
+ initialSnapshot?: BrokerOrdersSnapshot;
54
+ }
55
+
56
+ const orderColumns: DataTableColumn[] = [
57
+ { id: "symbol", label: "Symbol", width: 8, align: "left" }, { id: "side", label: "Side", width: 6, align: "left" },
58
+ { id: "status", label: "Status", width: 16, align: "left" }, { id: "quantity", label: "Filled / Qty", width: 14, align: "right" },
59
+ { id: "type", label: "Type", width: 4, align: "left" }, { id: "price", label: "Price", width: 8, align: "right" },
60
+ { id: "age", label: "Age", width: 4, align: "right" }, { id: "actions", label: "", width: 14, align: "right" },
61
+ ];
62
+ const activityColumns: DataTableColumn[] = [
63
+ { id: "symbol", label: "Symbol", width: 18, align: "left" }, { id: "side", label: "Side", width: 5, align: "left" },
64
+ { id: "quantity", label: "Quantity", width: 12, align: "right" }, { id: "price", label: "Price", width: 12, align: "right" },
65
+ { id: "fees", label: "Fees", width: 12, align: "right" }, { id: "age", label: "Age", width: 5, align: "right" },
66
+ ];
67
+ /** An order's type, prices and time in force, as a cancellation names them: "Limit 336.50 · DAY". */
68
+ function orderTerms(order: BrokerOrder): string {
69
+ const prices = order.orderType === "MKT" ? "" : order.orderType === "STP LMT" ? ` ${price(order.stopPrice)}, limit ${price(order.limitPrice)}` : ` ${price(order.limitPrice ?? order.stopPrice)}`;
70
+ return `${ticketTypeName[order.orderType] ?? order.orderType}${prices} · ${order.tif ?? "DAY"}`;
71
+ }
72
+
73
+ function age(timestamp: number, now: number): string {
74
+ if (!Number.isFinite(timestamp) || timestamp <= 0) return "--";
75
+ const seconds = Math.max(0, Math.floor((now - timestamp) / 1_000));
76
+ if (seconds < 60) return `${seconds}s`;
77
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
78
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
79
+ return `${Math.floor(seconds / 86400)}d`;
80
+ }
81
+
82
+ function renderOrderCell(order: BrokerOrder, column: DataTableColumn, now: number): DataTableCell {
83
+ const status = brokerOrderStatus(order.status);
84
+ switch (column.id) {
85
+ case "symbol": return { text: orderInstrumentLabel(order), attributes: TextAttributes.BOLD };
86
+ case "status": {
87
+ const tone = status === "Rejected" ? "negative" : status === "Filled" ? "positive" : ["UNKNOWN", "Pending cancel", "Partially filled"].includes(status) ? "warning" : "accent";
88
+ return { text: status, content: <Badge label={status === "Partially filled" ? "Partial fill" : status} variant={status === "Working" ? "solid" : "subtle"} tone={tone} /> };
89
+ }
90
+ case "side": return { text: order.action, content: <Badge label={order.action} tone={order.action === "BUY" ? "positive" : "negative"} /> };
91
+ case "quantity": return { text: `${quantity(order.filled)} / ${quantity(order.quantity)}`, content: <Box flexDirection="row" gap={1} alignItems="center" justifyContent="flex-end" width={column.width}>
92
+ <RatioBar ratio={order.quantity > 0 ? order.filled / order.quantity : 0} width={4} color={order.filled > 0 ? colors.positive : colors.textMuted} track thickness={5} />
93
+ <Text fg={colors.text} truncate wrapMode="none">{`${quantity(order.filled)} / ${quantity(order.quantity)}`}</Text>
94
+ </Box> };
95
+ case "type": return { text: order.orderType };
96
+ case "price": return { text: order.orderType === "MKT" ? "Market" : price(order.limitPrice ?? order.stopPrice) };
97
+ default: return { text: age(order.updatedAt, now), color: colors.textDim };
98
+ }
99
+ }
100
+
101
+ const CANCEL_NOTE = "The order can still fill before cancellation is confirmed.";
102
+ /**
103
+ * "Cancel Buy 1 SPY?" over "Limit 759.50 · DAY": which order, when two of the same size wait at different prices.
104
+ * The terms keep a line of their own, so a narrow drawer never breaks a price across lines.
105
+ */
106
+ function cancelLines(order: BrokerOrder | undefined): string[] {
107
+ return order ? [`Cancel ${order.action === "BUY" ? "Buy" : "Sell"} ${quantity(order.quantity)} ${orderInstrumentLabel(order)}?`, orderTerms(order)] : ["Cancel this order?"];
108
+ }
109
+
110
+ /** Until the broker takes a change, the order can be cancelled but not changed again. */
111
+ /** Cancel, not change: the broker has not taken the last change, or not yet acknowledged the order. */
112
+ const unsettled = (order: BrokerOrder) => ["Replacing", "Submitted"].includes(brokerOrderStatus(order.status));
113
+
114
+ /** A drawer row's state in a word: working is what an open order is, so it says nothing. */
115
+ function compactOrderState(order: BrokerOrder): string {
116
+ const status = brokerOrderStatus(order.status);
117
+ if (status === "Partially filled") return `${quantity(order.filled)}/${quantity(order.quantity)}`;
118
+ return ({ Working: "", Submitted: "Sent", "Pending cancel": "Cancelling", UNKNOWN: "Unknown" } as Record<string, string>)[status] ?? status;
119
+ }
120
+
121
+ function renderExecutionCell(execution: BrokerExecution, column: DataTableColumn, now: number): DataTableCell {
122
+ switch (column.id) {
123
+ case "symbol": return { text: execution.contract.localSymbol || execution.contract.symbol };
124
+ case "side": return { text: execution.side, content: <Badge label={execution.side} tone={isBuySide(execution.side) ? "positive" : "negative"} /> };
125
+ case "quantity": return { text: quantity(execution.shares), value: execution.shares };
126
+ case "price": return { text: price(execution.price), value: execution.price };
127
+ case "fees": return { text: execution.commission === undefined ? "--" : `${price(execution.commission)} ${execution.commissionCurrency ?? ""}`.trim() };
128
+ default: return { text: age(execution.time, now), color: colors.textDim };
129
+ }
130
+ }
131
+
132
+ export function BrokerOrdersView({
133
+ broker, instance, accounts, accountId, onAccountChange, onModify, onBack, onHide, width, height, focused,
134
+ active = true, initialSnapshot, compact = false, contract, onOrders, onContentRows, blocked = false, onBusy, onConfirming, latestOrder,
135
+ }: BrokerOrdersViewProps) {
136
+ const [view, setView] = useState<"open" | "activity">("open");
137
+ const [selectedKey, setSelectedKey] = useState<string | null>(null);
138
+ const selectedKeyRef = useRef<string | null>(null);
139
+ const [priceEdit, setPriceEdit] = useState<{ key: string; value: string }>();
140
+ const priceEditRef = useRef(priceEdit); priceEditRef.current = priceEdit;
141
+ const [keepOrder, setKeepOrder] = useState(false);
142
+ const [inlineCancel, setInlineCancel] = useState(false);
143
+ const [, connectionChanged] = useReducer((value: number) => value + 1, 0);
144
+ const native = useUiCapabilities().nativePaneChrome;
145
+ const dialog = useDialog();
146
+ const dialogOpen = useDialogState((state) => state.isOpen);
147
+ const accountControl = useRef<SelectControl>(null);
148
+ const keyboardScope = `broker-orders:${useId()}`;
149
+ const visible = usePaneVisible();
150
+ const account = accounts.find((item) => item.accountId === accountId);
151
+ const connection = broker.getStatus?.(instance) ?? { state: "disconnected" as const, updatedAt: 0 };
152
+ const context = useRef<BrokerTradingContext>({ adapter: broker, instance, accounts, accountId, connection });
153
+ context.current = { adapter: broker, instance, accounts, accountId, connection };
154
+ const { notify } = usePluginAppActions();
155
+ // What a refused cancel came to, said once: the order feed records it quietly and reads the list again.
156
+ const onOutcome = useRef<(outcome: BrokerOrderOutcome) => void>(() => {});
157
+ onOutcome.current = (outcome) => {
158
+ brokerOrderFeed.note(instance.id, outcome.order);
159
+ brokerOrderFeed.refresh(instance.id);
160
+ notify({ type: outcome.type, title: outcome.title, body: orderToastBody(outcome.order) });
161
+ };
162
+ const controller = useMemo(() => new BrokerTradingController({ getContext: () => context.current, onOutcome: (outcome) => onOutcome.current(outcome) }), [broker, instance.id]);
163
+ const failed = useCallback((error: unknown, fallback: string) => notify({ type: "error", body: error instanceof Error && error.message ? error.message : fallback }), [notify]);
164
+ const trading = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
165
+ useEffect(() => () => controller.dispose(), [controller]);
166
+ useEffect(() => { controller.syncContext(); }, [controller, instance, accountId, accounts, connection.state]);
167
+ useEffect(() => broker.subscribeStatus?.(instance, connectionChanged), [broker, instance]);
168
+
169
+ const canRead = active && !!account && connection.state === "connected";
170
+ // Open orders come from the app's order feed, which the broker's pushes or polling keep current; activity is read here.
171
+ const feed = useBrokerOrderFeed(broker, instance, canRead);
172
+ const loader = useCallback(() => loadBrokerOrdersSnapshot(broker, instance, accountId!, undefined, { orders: [], failed: false }), [broker, instance, accountId]);
173
+ const resource = useAsyncResource(canRead ? loader : null, { initialData: () => initialSnapshot && initialSnapshot.accountId === accountId && initialSnapshot.brokerInstanceId === instance.id ? initialSnapshot : null });
174
+ const reload = resource.reload;
175
+ // A new fill is new activity.
176
+ const fillsSeen = useRef(feed.fills);
177
+ useEffect(() => {
178
+ if (feed.fills === fillsSeen.current) return;
179
+ fillsSeen.current = feed.fills;
180
+ if (canRead) void reload();
181
+ }, [feed.fills, canRead, reload]);
182
+ // A cancel this view sent follows the order feed, and is asked for every 15 seconds without pushed changes or while
183
+ // its outcome is unknown, which only the broker's answer settles. The fills and the end toast from the feed.
184
+ useFeedOrderResult(feed, instance.id, trading.result, controller.applyOrderUpdate);
185
+ const unknown = trading.result?.status.toUpperCase() === "UNKNOWN";
186
+ useEffect(() => {
187
+ if (!canRead || !visible || (feed.live && !unknown) || !trading.result || brokerOrderDone(trading.result.status)) return;
188
+ const timer = setInterval(() => { void controller.refreshResult().catch(() => {}); }, 15_000);
189
+ return () => clearInterval(timer);
190
+ }, [canRead, visible, feed.live, unknown, trading.result, controller]);
191
+ const refresh = useCallback(() => {
192
+ void controller.refreshResult().catch(() => {});
193
+ brokerOrderFeed.refresh(instance.id);
194
+ void reload();
195
+ }, [controller, reload, instance.id]);
196
+ usePaneRefreshKey(refresh, { focused, enabled: active && !!account });
197
+
198
+ const snapshot = useMemo((): BrokerOrdersSnapshot | null => {
199
+ const data = resource.data;
200
+ if (!data) return null;
201
+ // Orders that ended moments ago stay in: they say what became of an order shown as working.
202
+ const orders = feed.orders.filter((order) => order.accountId === accountId
203
+ && (!order.brokerInstanceId || order.brokerInstanceId === instance.id) && (!order.contract.brokerInstanceId || order.contract.brokerInstanceId === instance.id));
204
+ return { ...data, orders, errors: [...(feed.failed ? ["Open orders are unavailable. Refresh to try again."] : []), ...data.errors] };
205
+ }, [resource.data, feed.orders, feed.failed, accountId, instance.id]);
206
+ const reconciledRows = useMemo(() => {
207
+ let source = snapshot && snapshot.accountId === accountId && snapshot.brokerInstanceId === instance.id
208
+ ? snapshot.orders.filter((order) => !contract || ticketOrderMatches(order, contract, instance.id, accountId)) : [];
209
+ if (latestOrder && latestOrder.accountId === accountId && (!contract || ticketOrderMatches(latestOrder, contract, instance.id, accountId))) source = withLatestOrder(source, latestOrder);
210
+ const result = trading.result;
211
+ if (!result || result.accountId !== accountId || contract && !ticketOrderMatches(result, contract, instance.id, accountId)) return source;
212
+ const status = brokerOrderStatus(result.status);
213
+ const pending = ["UNKNOWN", "Pending cancel"].includes(status);
214
+ if (!pending && !brokerOrderDone(status)) return source;
215
+ const key = brokerOrderKey(result);
216
+ return source.some((order) => brokerOrderKey(order) === key)
217
+ ? source.map((order) => brokerOrderKey(order) === key ? result : order) : pending ? [result, ...source] : source;
218
+ }, [snapshot, trading.result, accountId, instance.id, contract, latestOrder]);
219
+ // Keep terminal acknowledgements in the projection even after hiding their rows.
220
+ // The full list shows an order that ended only when it is the one this view just acted on.
221
+ const rows = useMemo(() => reconciledRows.filter((order) => !brokerOrderDone(order.status)
222
+ || (!compact && !!trading.result && brokerOrderKey(order) === brokerOrderKey(trading.result))), [compact, reconciledRows, trading.result]);
223
+ useEffect(() => { onOrders?.(reconciledRows); }, [reconciledRows, onOrders]);
224
+ // Only an order the trader chose is selected: c, m and Enter never fall back to another row.
225
+ const selected = rows.find((order) => brokerOrderKey(order) === selectedKey);
226
+ // A selected order that leaves the list (filled, cancelled, gone on a refresh) clears the selection.
227
+ useEffect(() => {
228
+ if (selectedKeyRef.current === null || rows.some((order) => brokerOrderKey(order) === selectedKeyRef.current)) return;
229
+ selectedKeyRef.current = null; setSelectedKey(null); setPriceEdit(undefined);
230
+ }, [rows]);
231
+ const capabilities = broker.getTradingCapabilities?.(instance, selected?.contract);
232
+ const ownBusy = trading.phase === "cancelling" || trading.phase === "cancel-review";
233
+ useEffect(() => { onBusy?.(ownBusy); }, [ownBusy, onBusy]);
234
+ useEffect(() => { onConfirming?.(trading.phase === "cancel-review"); }, [trading.phase, onConfirming]);
235
+ const busy = ownBusy || blocked;
236
+ const changeable = !!selected && canRead && capabilities?.enabled === true && canChangeBrokerOrder(selected) && !busy;
237
+ const canModify = changeable && !unsettled(selected) && !!broker.modifyOrder && capabilities?.modify === true;
238
+ const canCancel = changeable && !!broker.cancelOrder && capabilities?.cancel === true;
239
+ const modifyOrder = useCallback((order: BrokerOrder) => {
240
+ const capability = broker.getTradingCapabilities?.(instance, order.contract);
241
+ if (canRead && !busy && broker.modifyOrder && capability?.enabled && capability.modify && canChangeBrokerOrder(order)) onModify(order);
242
+ }, [broker, instance, canRead, busy, onModify]);
243
+ const modify = useCallback(() => { const order = rows.find((row) => brokerOrderKey(row) === selectedKeyRef.current); if (order) modifyOrder(order); }, [rows, modifyOrder]);
244
+ const cancelOrder = useCallback(async (order: BrokerOrder) => {
245
+ const capability = broker.getTradingCapabilities?.(instance, order.contract);
246
+ if (!account || !canRead || busy || !broker.cancelOrder || !capability?.enabled || !capability.cancel || !canChangeBrokerOrder(order)) return;
247
+ try {
248
+ controller.requestCancel(order);
249
+ if (compact) { setInlineCancel(true); setKeepOrder(false); return; }
250
+ const confirmed = await confirmDialog(dialog, {
251
+ title: `Cancel ${orderInstrumentLabel(order)} order?`,
252
+ body: [`${order.action} ${quantity(order.quantity)} ${orderInstrumentLabel(order)}, ${orderTerms(order)}. ${quantity(order.remaining)} remaining.`, `${broker.name} ${accountModeLabel(account?.tradingMode)} · ${account.name}`],
253
+ confirmLabel: "Cancel order", cancelLabel: "Keep order", confirmVariant: "danger",
254
+ });
255
+ await controller.confirmCancel(confirmed);
256
+ if (confirmed) {
257
+ if (controller.getSnapshot().result?.status === "PENDING_CANCEL") await controller.refreshResult().catch(() => {});
258
+ brokerOrderFeed.refresh(instance.id);
259
+ }
260
+ } catch (error) {
261
+ failed(error, "Cancellation could not be confirmed.");
262
+ }
263
+ }, [account, canRead, busy, broker, instance, controller, dialog, compact, failed]);
264
+ const cancel = useCallback(() => { const order = rows.find((row) => brokerOrderKey(row) === selectedKeyRef.current); return order ? cancelOrder(order) : Promise.resolve(); }, [rows, cancelOrder]);
265
+ const confirmInline = async (confirmed: boolean) => {
266
+ if (controller.getSnapshot().phase !== "cancel-review") return;
267
+ try { await controller.confirmCancel(confirmed); setInlineCancel(false); if (confirmed) brokerOrderFeed.refresh(instance.id); }
268
+ catch (failure) { failed(failure, "Cancellation could not be confirmed."); }
269
+ };
270
+ useShortcut((event) => {
271
+ if (event.defaultPrevented || event.propagationStopped || event.targetEditable || event.ctrl || event.meta || event.alt || event.super) return;
272
+ if (inlineCancel && trading.phase === "cancel-review") {
273
+ if (event.name === "tab") { event.preventDefault(); event.stopPropagation(); setKeepOrder((value) => !value); }
274
+ else if (event.name === "escape" || event.name === "return" || event.name === "enter") {
275
+ event.preventDefault(); event.stopPropagation();
276
+ if (!event.repeat) void confirmInline(event.name !== "escape" && !keepOrder);
277
+ }
278
+ return;
279
+ }
280
+ if (event.shift || busy) return;
281
+ // With nothing chosen, j starts at the first order and k at the last (the arrows too, in the table).
282
+ const starts = event.name === "j" || event.name === "k" || !compact && (event.name === "down" || event.name === "up");
283
+ if (view === "open" && rows.length && starts && !rows.some((row) => brokerOrderKey(row) === selectedKeyRef.current)) {
284
+ event.preventDefault(); event.stopPropagation();
285
+ selectedKeyRef.current = brokerOrderKey(rows[event.name === "j" || event.name === "down" ? 0 : rows.length - 1]!);
286
+ setSelectedKey(selectedKeyRef.current); setPriceEdit(undefined);
287
+ return;
288
+ }
289
+ if (compact && view === "open" && rows.length && ["j", "k", "up", "down", "return", "enter"].includes(event.name ?? "")) {
290
+ event.preventDefault(); event.stopPropagation();
291
+ const index = rows.findIndex((row) => brokerOrderKey(row) === selectedKeyRef.current);
292
+ const order = rows[index];
293
+ if (event.name === "j" || event.name === "k") {
294
+ const next = rows[(index + (event.name === "j" ? 1 : rows.length - 1)) % rows.length]!;
295
+ selectedKeyRef.current = brokerOrderKey(next); setSelectedKey(selectedKeyRef.current); setPriceEdit(undefined);
296
+ } else if (!order) {
297
+ // The arrows and Enter act on a chosen order only.
298
+ } else if ((event.name === "up" || event.name === "down") && canModify && order.orderType !== "MKT") {
299
+ const key = brokerOrderKey(order);
300
+ const current = priceEditRef.current?.key === key ? priceEditRef.current.value : String(order.limitPrice ?? order.stopPrice ?? "");
301
+ const caps = broker.getTradingCapabilities?.(instance, order.contract, order.orderType as BrokerOrderType);
302
+ const step = caps?.priceStep ?? ticketEditingPriceStep(Number(current) || undefined, caps?.priceDecimals);
303
+ const value = stepNumber(current, event.name === "up" ? 1 : -1, step, step);
304
+ if (value !== undefined) { priceEditRef.current = { key, value }; setPriceEdit(priceEditRef.current); }
305
+ } else if ((event.name === "enter" || event.name === "return") && !event.repeat && canModify) {
306
+ const edit = priceEditRef.current;
307
+ if (edit?.key === brokerOrderKey(order)) { onModify(order, { price: Number(edit.value), review: true }); setPriceEdit(undefined); }
308
+ else modifyOrder(order);
309
+ }
310
+ return;
311
+ }
312
+ const invoke = event.name === "a" && !compact ? () => accountControl.current?.open()
313
+ : event.name === "v" && account ? () => setView(view === "open" ? "activity" : "open")
314
+ : event.name === "m" && view === "open" && canModify ? modify
315
+ : event.name === "c" && view === "open" && canCancel ? () => { void cancel(); }
316
+ : event.name === "r" && account ? refresh
317
+ : event.name === "escape" && onBack ? onBack : undefined;
318
+ if (!invoke) return;
319
+ event.preventDefault(); event.stopPropagation();
320
+ // A held key acts once, so a repeat never opens a second cancel or change.
321
+ if (!event.repeat) invoke();
322
+ }, { enabled: focused && active && !dialogOpen, phase: "before", scope: keyboardScope });
323
+
324
+ const status = connection.state !== "connected" ? "Broker disconnected" : trading.error ?? resource.error
325
+ ?? (capabilities?.enabled === false ? capabilities.disabledReason ?? "Trading is off" : null);
326
+ usePaneNoticeFooter({ registrationId: "broker-orders-notices", focused, enabled: active, notices: [...(snapshot?.errors ?? []), ...(snapshot?.notes ?? [])] });
327
+ usePaneFooter("broker-orders", () => active ? {
328
+ info: loadingErrorFooterInfo(resource.loading || trading.phase === "cancelling", status),
329
+ hints: busy ? inlineCancel && trading.phase === "cancel-review" ? [
330
+ { id: "cancel-confirm", key: "Enter", label: keepOrder ? "Keep order" : "Cancel order" },
331
+ { id: "cancel-back", key: "Esc", label: "Back" },
332
+ ] : [] : [
333
+ ...(view === "open" && selected ? [
334
+ ...(canModify ? [{ id: "modify-order", key: "m", label: "odify", onPress: modify }] : []),
335
+ ...(canCancel ? [{ id: "cancel-order", key: "c", label: "ancel", onPress: () => { void cancel(); } }] : []),
336
+ ] : []),
337
+ ...(compact ? [] : [{ id: "account", key: "a", label: "ccount", onPress: () => accountControl.current?.open() }]),
338
+ ...(account ? [
339
+ { id: "view", key: "v", label: view === "open" ? "Activity" : "Open orders", onPress: () => setView(view === "open" ? "activity" : "open") },
340
+ { id: "refresh", key: "r", label: "efresh", onPress: refresh },
341
+ ...(view === "open" ? [{ id: "navigate", key: compact ? "j/k" : "↑↓", label: compact ? "Rows · ↑↓ Price · Enter Review" : "j/k Rows" }] : []),
342
+ ] : []),
343
+ ...(onBack ? [{ id: "back", key: "Esc", label: "Ticket", onPress: onBack }] : []),
344
+ ],
345
+ } : null, [active, resource.loading, trading.phase, status, view, selected, canModify, canCancel, modify, cancel, busy, account, onBack, refresh, compact, keepOrder, inlineCancel]);
346
+ const now = resource.updatedAt ?? Date.now();
347
+ const summaries = snapshot?.executionKind === "order-summaries";
348
+ const executions = (snapshot?.executions ?? []).filter((execution) => !compact || !contract || ticketOrderMatches(execution, contract, instance.id, accountId));
349
+ // Confirming a cancel takes a question, its wrapped note and the two buttons (the note's width is allowed some slack).
350
+ const contentRows = trading.phase === "cancel-review" ? 1 + [...cancelLines(trading.cancelOrder), CANCEL_NOTE]
351
+ .reduce((rows, text) => rows + Math.max(1, wrapTextLines(text, Math.max(12, width - 6)).length), 0)
352
+ : (view === "open" ? rows.length : executions.length) + (snapshot?.errors?.length ?? 0);
353
+ useEffect(() => { if (compact) onContentRows?.(contentRows); }, [compact, contentRows, onContentRows]);
354
+
355
+
356
+ // Docked on the desktop, the drawer fills what the ticket above leaves it.
357
+ if (compact || inlineCancel && trading.phase === "cancel-review") return <Box width={width} height={compact && native ? "100%" : height} flexDirection="column" overflow="hidden"
358
+ border={native ? undefined : ["top"]} borderColor={colors.border} style={native ? { borderTop: `1px solid ${colors.border}` } : undefined}>
359
+ <Box flexDirection="row" paddingX={1} alignItems="center" justifyContent="space-between" flexShrink={0}>
360
+ <SectionHeading title={view === "open" ? `Open orders (${rows.length})` : summaries ? "Order summaries" : "Activity"} />
361
+ <Box flexDirection="row" alignItems="center" gap={native ? 0.5 : 0}>
362
+ <Button label={view === "open" ? "Show activity" : "Show open orders"} displayLabel={view === "open" ? "Activity" : "Open"} variant="plain" compact disabled={busy} onPress={() => setView(view === "open" ? "activity" : "open")} />
363
+ {onHide ? <IconButton icon="close" label="Hide open orders" shortcut="o" onPress={onHide} /> : null}
364
+ </Box>
365
+ </Box>
366
+ {trading.phase === "cancel-review" ? <Box flexDirection="column" paddingX={1} gap={native ? 0.5 : 0}>
367
+ {cancelLines(trading.cancelOrder).map((line, index) => <Text key={index} fg={index ? colors.text : colors.textBright} wrapText>{line}</Text>)}
368
+ <Text fg={colors.textDim} wrapText>{CANCEL_NOTE}</Text>
369
+ <Box flexDirection="row" gap={1}>
370
+ <Button label="Cancel order" variant="primary" tone="negative" active={!keepOrder} onPress={() => { void confirmInline(true); }} />
371
+ <Button label="Keep order" variant="quiet" active={keepOrder} onPress={() => { void confirmInline(false); }} />
372
+ </Box>
373
+ </Box> : <ScrollBox flexGrow={1} scrollY>
374
+ {(snapshot?.errors ?? []).map((error) => <Notice key={error} tone="negative">{error}</Notice>)}
375
+ {view === "open" ? rows.map((order) => {
376
+ const key = brokerOrderKey(order), picked = selected === order;
377
+ const allowed = broker.getTradingCapabilities?.(instance, order.contract);
378
+ const disabled = !canRead || busy || !allowed?.enabled || !canChangeBrokerOrder(order);
379
+ const state = brokerOrderStatus(order.status);
380
+ // One line: side and size, price, state, then edit and cancel as icons.
381
+ return <Box key={key} position="relative" flexDirection="row" alignItems="center" gap={1} paddingLeft={1} height={native ? 1.6 : 1} backgroundColor={picked ? colors.panel : undefined}>
382
+ {picked ? <Box position="absolute" left={0} top={0} width={1} height="100%" backgroundColor={focused ? colors.borderFocused : colors.border} style={native ? { width: 2 } : undefined} /> : null}
383
+ <Box flexGrow={1} minWidth={0}>
384
+ <Button label={`${order.action} ${quantity(order.quantity)} ${orderInstrumentLabel(order)}, ${state}`} width={native ? undefined : Math.max(10, width - 9)}
385
+ active={focused && picked} variant="quiet" flush onPress={() => { selectedKeyRef.current = key; setSelectedKey(key); setPriceEdit(undefined); }}>
386
+ <Box flexDirection="row" gap={1} alignItems="center" width="100%">
387
+ <Text fg={order.action === "BUY" ? colors.positive : colors.negative} attributes={TextAttributes.BOLD} flexShrink={0}>{`${order.action === "BUY" ? "Buy" : "Sell"} ${quantity(order.quantity)}`}</Text>
388
+ {/* The drawer is the ticker's: a spread names its shape, the stock needs no name. */}
389
+ {order.legs?.length ? <Text fg={colors.text} truncate wrapMode="none" flexShrink={1} minWidth={0}>{strategyShape(order.legs)}</Text> : null}
390
+ <Text fg={colors.text} selectable flexShrink={0}>{priceEdit?.key === key ? `→ ${priceEdit.value}` : order.orderType === "MKT" ? "Mkt" : price(order.limitPrice ?? order.stopPrice)}</Text>
391
+ <Text fg={colors.warning} truncate wrapMode="none" flexShrink={1} minWidth={0}>{compactOrderState(order)}</Text>
392
+ </Box>
393
+ </Button>
394
+ </Box>
395
+ <Box flexDirection="row" flexShrink={0}>
396
+ <IconButton icon="edit" label="Modify order" shortcut="Enter" disabled={disabled || unsettled(order) || !broker.modifyOrder || !allowed?.modify} onPress={() => modifyOrder(order)} />
397
+ <IconButton icon="close" label="Cancel order" shortcut="c" color={colors.negative} disabled={disabled || !broker.cancelOrder || !allowed?.cancel} onPress={() => { void cancelOrder(order); }} />
398
+ </Box>
399
+ </Box>;
400
+ }) : executions.map((execution) => <Box key={execution.execId} paddingX={1} height={native ? 1.6 : 1} flexDirection="row" alignItems="center">
401
+ <Text selectable>{`${execution.side} ${quantity(execution.shares)} @ ${price(execution.price)}`}</Text>
402
+ </Box>)}
403
+ {!resource.loading && (view === "open" ? feed.loaded && !rows.length : !executions.length) ? <Box paddingX={1}><Text fg={colors.textDim}>{view === "open" ? "No open orders for this ticker." : "No recent activity."}</Text></Box> : null}
404
+ </ScrollBox>}
405
+ </Box>;
406
+ return <Box width={width} height={height} flexDirection="column" overflow="hidden">
407
+ <Box flexDirection="row" alignItems="center" gap={1} paddingX={1} height={2}>
408
+ <Box flexGrow={1} minWidth={0}>
409
+ <BrokerAccountPicker accounts={accounts.map((item) => ({ value: item.accountId, label: item.name, profileLabel: instance.label || broker.name, tradingMode: item.tradingMode }))}
410
+ value={accountId} onChange={onAccountChange} controlRef={accountControl} disabled={busy} width={Math.max(16, Math.min(42, width - (summaries ? 49 : 43)))} />
411
+ </Box>
412
+ {account && <Badge label={accountModeLabel(account?.tradingMode)} tone={account.tradingMode === "simulation" ? "accent" : "negative"} />}
413
+ <SegmentedControl value={view} options={[{ value: "open", label: "Open orders" }, { value: "activity", label: summaries ? "Order summaries" : "Activity" }]} onChange={(next) => setView(next === "activity" ? "activity" : "open")} />
414
+ </Box>
415
+ <PaneStatusBody subject="orders" loading={canRead && resource.loading && !snapshot} error={!snapshot ? resource.error : null}
416
+ empty={!account || !canRead} emptyTitle={!account ? "Choose an account to see its orders." : "Connect this broker in Brokers to read orders."}>
417
+ {view === "open" ? <DataTableView<BrokerOrder> rootWidth={width} rootHeight={Math.max(2, height - 2)} focused={focused && !busy && !dialogOpen}
418
+ columns={orderColumns} items={rows} sortColumnId={null} sortDirection="desc" getItemKey={brokerOrderKey} renderCell={(order, column) => {
419
+ if (column.id !== "actions") return renderOrderCell(order, column, now);
420
+ if (brokerOrderDone(order.status)) return { text: "" };
421
+ const allowed = broker.getTradingCapabilities?.(instance, order.contract);
422
+ const disabled = !canRead || busy || !allowed?.enabled || !canChangeBrokerOrder(order);
423
+ return { text: "Modify Cancel", content: <Box flexDirection="row" gap={1} justifyContent="flex-end" width={column.width}>
424
+ <Button label="Modify" compact variant="secondary" disabled={disabled || unsettled(order) || !broker.modifyOrder || !allowed?.modify} onPress={() => modifyOrder(order)} stopPropagation />
425
+ <Button label="Cancel" compact variant="secondary" disabled={disabled || !broker.cancelOrder || !allowed?.cancel} onPress={() => { void cancelOrder(order); }} stopPropagation />
426
+ </Box> };
427
+ }}
428
+ selection={{ kind: "id", selectedId: selected ? brokerOrderKey(selected) : "", getId: brokerOrderKey, onChange: (key) => { selectedKeyRef.current = key; setSelectedKey(key); } }}
429
+ onActivate={modifyOrder} selectedTextOverridesCellColor emptyStateTitle="No open orders in this account." />
430
+ : <DataTableView<BrokerExecution> rootWidth={width} rootHeight={Math.max(2, height - 2)} focused={focused && !dialogOpen}
431
+ columns={activityColumns.map((column) => column.id === "quantity" && summaries ? { ...column, label: "Cumulative qty" } : column)}
432
+ items={snapshot?.executions ?? []} sortColumnId={null} sortDirection="desc" getItemKey={(execution) => execution.execId}
433
+ renderCell={(execution, column) => renderExecutionCell(execution, column, now)} selection={{ kind: "none" }}
434
+ emptyStateTitle="No recent activity in this account." />}
435
+ </PaneStatusBody>
436
+ </Box>;
437
+ }
@@ -0,0 +1,121 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { ActionRow, SectionHeading } from "../../../components";
3
+ import { useAsyncResource } from "../../../react/async-resource";
4
+ import { usePaneVisible } from "../../../state/app/activity";
5
+ import { usePaneAppConfig } from "../../../state/app/context";
6
+ import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
7
+ import { useThemeColors } from "../../../theme/theme-context";
8
+ import type { BrokerAdapter } from "../../../types/broker";
9
+ import type { BrokerInstanceConfig } from "../../../types/config";
10
+ import type { BrokerContractRef } from "../../../types/instrument";
11
+ import type { BrokerExecution, BrokerOrder } from "../../../types/trading";
12
+ import { Box, Text } from "../../../ui";
13
+ import { usePluginBrokerActions } from "../../runtime";
14
+ import { ticketOrderMatches } from "./chart-levels";
15
+ import { strategyShape } from "./strategy";
16
+ import { brokerOrderDone, brokerOrderKey, brokerOrderStatus, canChangeBrokerOrder, formatOrderPrice as money, formatOrderQuantity as amount, isBuySide } from "./orders-data";
17
+ import { useBrokerOrderFeeds } from "./order-feed";
18
+ import { ticketTypeName } from "./ticket-parts";
19
+ import { resolveTicketContract, subscribeTradingStatus } from "./pane-model";
20
+
21
+ export interface TickerOrderSource {
22
+ broker: BrokerAdapter;
23
+ instance: BrokerInstanceConfig;
24
+ contract: BrokerContractRef;
25
+ }
26
+
27
+ export interface TickerActivity {
28
+ recent: BrokerExecution[];
29
+ failed: boolean;
30
+ }
31
+
32
+ const RECENT_LIMIT = 5;
33
+
34
+ /**
35
+ * This ticker's latest fills across connected trading profiles, newest
36
+ * first. One profile failing leaves the others shown. Working orders come
37
+ * from the app's order feed.
38
+ */
39
+ async function loadTickerActivity(sources: readonly TickerOrderSource[]): Promise<TickerActivity> {
40
+ let failed = false;
41
+ const results = await Promise.all(sources.map(async ({ broker, instance, contract }) => {
42
+ if (broker.getStatus?.(instance).state !== "connected") return [];
43
+ try {
44
+ const recent = await Promise.resolve().then(() => broker.listExecutions?.(instance) ?? []);
45
+ return recent.filter((row) => ticketOrderMatches(row, contract, instance.id, row.accountId));
46
+ } catch { failed = true; return []; }
47
+ }));
48
+ return { recent: results.flat().sort((a, b) => b.time - a.time).slice(0, RECENT_LIMIT), failed };
49
+ }
50
+
51
+ function ago(timestamp: number, now: number): string {
52
+ const minutes = Math.max(0, Math.floor((now - timestamp) / 60_000));
53
+ if (minutes < 1) return "now";
54
+ if (minutes < 60) return `${minutes}m ago`;
55
+ if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`;
56
+ return `${Math.floor(minutes / 1440)}d ago`;
57
+ }
58
+
59
+ function orderSummary(order: BrokerOrder): string {
60
+ const status = brokerOrderStatus(order.status);
61
+ const type = ticketTypeName[order.orderType] ?? order.orderType;
62
+ const priced = order.orderType === "MKT" ? type : `${type} ${money(order.limitPrice ?? order.stopPrice)}`;
63
+ const state = status === "Partially filled" ? `${amount(order.filled)} of ${amount(order.quantity)} filled` : status === "Working" ? "" : status;
64
+ return [`${order.action === "BUY" ? "Buy" : "Sell"} ${amount(order.quantity)}${order.legs?.length ? ` ${strategyShape(order.legs)}` : ""}`, priced, state].filter(Boolean).join(" · ");
65
+ }
66
+
67
+ function fillSummary(execution: BrokerExecution): string {
68
+ return `${isBuySide(execution.side) ? "Bought" : "Sold"} ${amount(execution.shares)} at ${money(execution.price)}`;
69
+ }
70
+
71
+ /**
72
+ * Overview's orders for its ticker: what is working and what last filled,
73
+ * without the ticket. An open order opens Trade to change it there.
74
+ */
75
+ export function TickerOrdersSection({ width, onModify }: { width: number; onModify(order: BrokerOrder): void }) {
76
+ const colors = useThemeColors();
77
+ const identity = usePaneTickerIdentity();
78
+ const config = usePaneAppConfig();
79
+ const visible = usePaneVisible();
80
+ const { getBrokerAdapter } = usePluginBrokerActions();
81
+ const [statusVersion, setStatusVersion] = useState(0);
82
+ const sources = useMemo(() => config.brokerInstances.flatMap((instance): TickerOrderSource[] => {
83
+ if (instance.enabled === false) return [];
84
+ const broker = getBrokerAdapter(instance.brokerType);
85
+ if (!broker?.listOpenOrders || !broker.placeOrder) return [];
86
+ const contract = resolveTicketContract(identity, broker, instance).contract;
87
+ return contract ? [{ broker, instance, contract }] : [];
88
+ }), [config.brokerInstances, getBrokerAdapter, identity]);
89
+ useEffect(() => {
90
+ const unsubscribe = sources.map(({ broker, instance }) => subscribeTradingStatus(broker, instance, () => setStatusVersion((value) => value + 1)));
91
+ return () => { for (const dispose of unsubscribe) dispose(); };
92
+ }, [sources]);
93
+ // Working orders from the app's order feed; fills are read again whenever the feed sees a new one.
94
+ const feeds = useBrokerOrderFeeds(sources, visible);
95
+ const open = useMemo(() => sources.flatMap(({ instance, contract }, index) => (feeds[index]?.orders ?? [])
96
+ .filter((order) => !brokerOrderDone(order.status) && ticketOrderMatches(order, contract, instance.id, order.accountId)))
97
+ .sort((a, b) => b.updatedAt - a.updatedAt), [sources, feeds]);
98
+ const fills = feeds.reduce((sum, feed) => sum + feed.fills, 0);
99
+ const loader = useCallback(() => loadTickerActivity(sources), [sources, statusVersion, fills]);
100
+ const resource = useAsyncResource(visible && sources.length ? loader : null);
101
+ const activity = resource.data;
102
+ const failed = !!activity?.failed || feeds.some((feed) => feed.failed);
103
+ const recent = activity?.recent ?? [];
104
+ if (!open.length && !recent.length && !failed) return null;
105
+ const now = resource.updatedAt ?? Date.now();
106
+ const accounts = new Set([...open, ...recent].map((row) => `${row.brokerInstanceId ?? row.contract.brokerInstanceId ?? ""}:${row.accountId ?? ""}`)).size;
107
+ const account = (row: { accountId?: string }) => accounts > 1 && row.accountId ? ` · ${row.accountId}` : "";
108
+ return <Box flexDirection="column" width={width}>
109
+ <SectionHeading title="Orders" />
110
+ {open.map((order) => {
111
+ const changeable = canChangeBrokerOrder(order);
112
+ return <ActionRow key={brokerOrderKey(order)} label={`${orderSummary(order)}${account(order)}`} disabled={!changeable} width={width}
113
+ fg={order.action === "BUY" ? colors.positive : colors.negative} onPress={() => { if (changeable) onModify(order); }} />;
114
+ })}
115
+ {recent.map((execution) => <Box key={execution.execId} flexDirection="row" gap={1} data-gloom-selectable="true">
116
+ <Text fg={colors.text} selectable>{`${fillSummary(execution)}${account(execution)}`}</Text>
117
+ <Text fg={colors.textDim}>{ago(execution.time, now)}</Text>
118
+ </Box>)}
119
+ {failed ? <Text fg={colors.warning}>Some orders could not be read. They are read again shortly.</Text> : null}
120
+ </Box>;
121
+ }