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,182 @@
1
+ import { startTransition, useCallback, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import { PaneFooterScope, usePaneMenuItems } from "../../../components";
3
+ import { usePaneAppConfig, usePaneStateValue } from "../../../state/app/context";
4
+ import { usePluginBrokerActions } from "../../runtime";
5
+ import { useThemeColors } from "../../../theme/theme-context";
6
+ import { Box, Text, useNativeRenderer, useUiCapabilities, type BoxRenderable } from "../../../ui";
7
+ import { capturePointerDrag } from "../../../ui/pointer-drag";
8
+ import { useShortcut } from "../../../react/input";
9
+ import { useDialogState } from "../../../ui/dialog";
10
+ import { ResearchTradingProvider, type ResearchTradeProjection } from "../shared/research-trading";
11
+ import type { BrokerOrder } from "../../../types/trading";
12
+ import { TickerOrdersSection } from "./overview-orders";
13
+ import { createTradeIntent } from "./command";
14
+ import { BrokerTradeTab } from "./pane";
15
+
16
+ const TRADE_TAB = "broker-trade";
17
+ /** Cells one press of `<` or `>` moves the divider, as in a pane sidebar. */
18
+ const RESIZE_STEP = 4;
19
+
20
+ /** Default, narrowest and widest ticket beside the chart; null when the pane is too narrow to share. */
21
+ function tradeDockWidths(width: number, native: boolean, cellWidth = 8): { initial: number; min: number; max: number } | null {
22
+ if (width < (native ? 900 / cellWidth : 100)) return null;
23
+ const initial = native ? 336 / cellWidth : 34;
24
+ const min = native ? 280 / cellWidth : 30;
25
+ // The chart keeps at least half the pane.
26
+ return { initial, min, max: Math.max(min, Math.floor(width / 2)) };
27
+ }
28
+
29
+ interface DividerPointerEvent { x?: number; preciseX?: number; preventDefault?(): void; stopPropagation?(): void }
30
+
31
+ /**
32
+ * Trade is the chart with the ticket beside it; Chart is the chart alone and
33
+ * Overview never carries the ticket. b or s on either opens Trade. Below the
34
+ * width where both fit, Trade is the ticket alone and the Chart tab is the
35
+ * other half. The ticket stays mounted across tabs once opened, so a draft
36
+ * survives a look at another tab.
37
+ */
38
+ export function ResearchTradeWorkspace({ width, height, focused, symbol, tabId, selectTab, onCapture, locked = false, children }: {
39
+ width: number; height: number; focused: boolean; symbol: string; tabId: string;
40
+ selectTab(id: string): void; onCapture(captured: boolean): void;
41
+ /** A pane pinned to one tab (no strip) opens the ticket over that tab instead of switching. */
42
+ locked?: boolean;
43
+ /** Renders a research tab: the selected one, or Chart beside the ticket. */
44
+ children(width: number, focused: boolean, shownTabId?: string): ReactNode;
45
+ }) {
46
+ const c = useThemeColors();
47
+ const { nativePaneChrome, cellWidthPx = 8 } = useUiCapabilities();
48
+ const nativeRenderer = useNativeRenderer();
49
+ const widths = tradeDockWidths(width, !!nativePaneChrome, cellWidthPx);
50
+ const [savedWidth, setSavedWidth] = usePaneStateValue<number | null>("brokerTradePanelWidth", null);
51
+ const [dragWidth, setDragWidth] = useState<number | null>(null);
52
+ const panelWidth = widths ? Math.min(widths.max, Math.max(widths.min, dragWidth ?? savedWidth ?? widths.initial)) : null;
53
+ const [intent, setIntent] = usePaneStateValue<string | null>("brokerTradeIntent", null);
54
+ const [mounted, setMounted] = useState(false);
55
+ const [lockedOpen, setLockedOpen] = useState(false);
56
+ const [panelFocused, setPanelFocused] = useState(true);
57
+ const dialogOpen = useDialogState((state) => state.isOpen);
58
+ const [projection, publish] = useState<ResearchTradeProjection>({ levels: [] });
59
+ // A text field in the ticket owns typed keys; otherwise keys the ticket does
60
+ // not use (zoom, pan, cursor) still reach the chart beside it.
61
+ const [ticketCaptured, setTicketCaptured] = useState(false);
62
+ const captureTicket = useCallback((captured: boolean) => { setTicketCaptured(captured); onCapture(captured); }, [onCapture]);
63
+ const config = usePaneAppConfig();
64
+ const { getBrokerAdapter } = usePluginBrokerActions();
65
+ const available = config.brokerInstances.some((profile) => profile.enabled !== false && !!getBrokerAdapter(profile.brokerType)?.placeOrder);
66
+ const docked = panelWidth !== null;
67
+ const visible = tabId === TRADE_TAB || locked && lockedOpen;
68
+ // Layout effect: the tab switches before the mounted ticket's effect takes the intent.
69
+ useLayoutEffect(() => {
70
+ if (visible || intent) { setMounted(true); setPanelFocused(true); }
71
+ if (intent && !visible) { if (locked) setLockedOpen(true); else selectTab(TRADE_TAB); }
72
+ }, [intent, visible, locked, selectTab]);
73
+ const openTicket = useCallback((side?: "BUY" | "SELL") => {
74
+ if (projection.busy) return;
75
+ if (side) setIntent(createTradeIntent({ action: side }));
76
+ setMounted(true); setPanelFocused(true);
77
+ if (locked) setLockedOpen(true); else selectTab(TRADE_TAB);
78
+ }, [projection.busy, locked, selectTab, setIntent]);
79
+ // Overview's order opens Trade with that order in the ticket, as the drawer's edit button does.
80
+ const modifyOrder = useCallback((order: BrokerOrder) => {
81
+ if (projection.busy) return;
82
+ setIntent(createTradeIntent({ action: order.action, modify: order })); setMounted(true); setPanelFocused(true);
83
+ if (locked) setLockedOpen(true); else selectTab(TRADE_TAB);
84
+ }, [projection.busy, locked, selectTab, setIntent]);
85
+ const orders = useCallback((ordersWidth: number) => <TickerOrdersSection width={ordersWidth} onModify={modifyOrder} />, [modifyOrder]);
86
+ const close = useCallback(() => {
87
+ captureTicket(false); setLockedOpen(false);
88
+ if (tabId === TRADE_TAB) selectTab("chart");
89
+ }, [captureTicket, selectTab, tabId]);
90
+ const split = visible && docked;
91
+ const ticketFocused = split && panelFocused;
92
+ const context = useMemo(() => ({ ...projection, symbol, available, docked, visible, ticketFocused, open: openTicket, close, publish, ...(available ? { orders } : {}) }),
93
+ [projection, symbol, available, docked, visible, ticketFocused, openTicket, close, orders]);
94
+ useShortcut((event) => {
95
+ if (event.name !== "f6" || event.repeat) return;
96
+ event.preventDefault(); event.stopPropagation(); captureTicket(false); setPanelFocused((value) => !value);
97
+ }, { enabled: focused && split && !dialogOpen, allowEditable: true, phase: "before", scope: "research:trade-focus" });
98
+ useShortcut((event) => {
99
+ if (event.name !== "escape" || event.defaultPrevented || event.targetEditable) return;
100
+ event.preventDefault(); event.stopPropagation(); close();
101
+ }, { enabled: focused && split && !panelFocused && !dialogOpen, phase: "after", scope: "research:trade-close" });
102
+
103
+ // The divider between chart and ticket drags, and < > move it from the keyboard.
104
+ const dividerRef = useRef<BoxRenderable>(null);
105
+ const dragOrigin = useRef<{ pointerX: number; width: number } | null>(null);
106
+ const [dividerActive, setDividerActive] = useState(false);
107
+ const clampWidth = (next: number) => widths ? Math.min(widths.max, Math.max(widths.min, next)) : next;
108
+ const dragTo = (event?: DividerPointerEvent) => {
109
+ const origin = dragOrigin.current;
110
+ if (!origin) return null;
111
+ event?.preventDefault?.(); event?.stopPropagation?.();
112
+ const pointerX = event?.preciseX ?? event?.x ?? origin.pointerX;
113
+ // Dragging left widens the ticket on the right.
114
+ const next = clampWidth(origin.width - (pointerX - origin.pointerX));
115
+ return nativePaneChrome ? next : Math.round(next);
116
+ };
117
+ const dividerHandlers = split ? {
118
+ onMouseDown: (event?: DividerPointerEvent) => {
119
+ event?.preventDefault?.(); event?.stopPropagation?.();
120
+ dragOrigin.current = { pointerX: event?.preciseX ?? event?.x ?? 0, width: panelWidth! };
121
+ setDividerActive(true);
122
+ capturePointerDrag(nativeRenderer, dividerRef.current);
123
+ },
124
+ onMouseDrag: (event?: DividerPointerEvent) => {
125
+ const next = dragTo(event);
126
+ if (next !== null) startTransition(() => setDragWidth(next));
127
+ },
128
+ onMouseDragEnd: (event?: DividerPointerEvent) => {
129
+ const next = dragTo(event);
130
+ const origin = dragOrigin.current;
131
+ dragOrigin.current = null;
132
+ setDividerActive(false);
133
+ setDragWidth(null);
134
+ // A click that never moved keeps the width as it was.
135
+ if (next !== null && next !== origin?.width) setSavedWidth(next);
136
+ },
137
+ onMouseOver: () => setDividerActive(true),
138
+ onMouseOut: () => { if (!dragOrigin.current) setDividerActive(false); },
139
+ } : {};
140
+ const stepWidth = useCallback((delta: number) => {
141
+ if (!widths || panelWidth === null) return;
142
+ const next = Math.min(widths.max, Math.max(widths.min, panelWidth + delta));
143
+ if (next !== panelWidth) setSavedWidth(next);
144
+ }, [widths?.min, widths?.max, panelWidth, setSavedWidth]);
145
+ useShortcut((event) => {
146
+ if (event.defaultPrevented || event.targetEditable || event.ctrl || event.meta || event.alt || event.super) return;
147
+ if (event.name !== "<" && event.name !== ">") return;
148
+ event.preventDefault(); event.stopPropagation(); stepWidth(event.name === "<" ? RESIZE_STEP : -RESIZE_STEP);
149
+ }, { enabled: focused && split && !dialogOpen, phase: "before", scope: "research:trade-resize" });
150
+ usePaneMenuItems("research:trade-focus", () => focused && split ? [
151
+ { id: "trade-focus", accelerator: "F6", label: panelFocused ? "Focus chart" : "Focus ticket", onSelect: () => setPanelFocused((value) => !value) },
152
+ { id: "trade-widen", accelerator: "<", label: "Widen trade panel", enabled: panelWidth! < widths!.max, onSelect: () => stepWidth(RESIZE_STEP) },
153
+ { id: "trade-narrow", accelerator: ">", label: "Narrow trade panel", enabled: panelWidth! > widths!.min, onSelect: () => stepWidth(-RESIZE_STEP) },
154
+ ] : null, [focused, split, panelFocused, panelWidth, widths?.min, widths?.max, stepWidth]);
155
+
156
+ const chartWidth = split ? width - panelWidth! : width;
157
+ const contentWidth = (split ? panelWidth! : width) - (split ? nativePaneChrome ? 1 / cellWidthPx : 1 : 0);
158
+ const dividerColor = dividerActive ? c.borderFocused : c.border;
159
+ return <ResearchTradingProvider value={context}>
160
+ <Box flexDirection="row" width={width} height={height} overflow="hidden">
161
+ <Box visible={!visible || split} width={chartWidth} height={height} flexDirection="column" overflow="hidden" onMouseDown={() => setPanelFocused(false)}>
162
+ <PaneFooterScope active={!visible || !panelFocused}>{children(chartWidth, focused && (!visible || !panelFocused || split && !ticketCaptured), split && !locked ? "chart" : undefined)}</PaneFooterScope>
163
+ </Box>
164
+ {mounted ? <Box visible={visible} position="relative" onMouseDown={() => setPanelFocused(true)} width={split ? panelWidth! : width} height={height} flexShrink={0} flexDirection="row">
165
+ {split && !nativePaneChrome ? <Box ref={dividerRef} width={1} height={height} flexDirection="column" flexShrink={0} {...dividerHandlers}>
166
+ {Array.from({ length: height }, (_, index) => <Text key={index} fg={dividerColor} selectable={false}>│</Text>)}
167
+ </Box> : null}
168
+ {split && nativePaneChrome ? <>
169
+ <Box position="absolute" top={0} left={0} width={1} height={height} style={{ width: 1, height: "100%", backgroundColor: dividerColor, pointerEvents: "none" }} />
170
+ {/* A 1px line is too thin to grab, so the handle straddles it. */}
171
+ <Box ref={dividerRef} position="absolute" top={0} left={0} width={1} height={height} title="Resize trade panel (< >)" {...dividerHandlers}
172
+ style={{ width: 7, left: -3, height: "100%", zIndex: 3, cursor: "col-resize" }} />
173
+ </> : null}
174
+ <Box flexDirection="column" width={contentWidth} height={height} flexGrow={1}>
175
+ <PaneFooterScope active={visible && panelFocused}>
176
+ <BrokerTradeTab width={contentWidth} height={height} focused={focused && visible && panelFocused} onCapture={captureTicket} docked={split} onClose={close} />
177
+ </PaneFooterScope>
178
+ </Box>
179
+ </Box> : null}
180
+ </Box>
181
+ </ResearchTradingProvider>;
182
+ }
@@ -0,0 +1,76 @@
1
+ import type { BrokerAdapter } from "../../../types/broker";
2
+ import type { HeadlessPaneContext, HeadlessPaneDefinition, HeadlessPaneColumn } from "../../../types/headless";
3
+ import { brokerOrderStatus, loadBrokerOrdersSnapshot } from "./orders-data";
4
+
5
+ type BrokerResolver = (brokerType: string, context: HeadlessPaneContext) => BrokerAdapter | null;
6
+
7
+ const orderColumns: HeadlessPaneColumn[] = [
8
+ { key: "symbol", header: "Symbol" }, { key: "side", header: "Side" }, { key: "status", header: "Status" },
9
+ { key: "quantity", header: "Quantity", align: "right" }, { key: "filled", header: "Filled", align: "right" },
10
+ { key: "orderType", header: "Type" }, { key: "limitPrice", header: "Limit", align: "right" },
11
+ { key: "stopPrice", header: "Stop", align: "right" }, { key: "updatedAt", header: "Updated" },
12
+ ];
13
+ const activityColumns: HeadlessPaneColumn[] = [
14
+ { key: "symbol", header: "Symbol" }, { key: "side", header: "Side" },
15
+ { key: "quantity", header: "Quantity", align: "right" }, { key: "price", header: "Price", align: "right" },
16
+ { key: "commission", header: "Fees", align: "right" }, { key: "time", header: "Time" },
17
+ ];
18
+ const isoTime = (value: number) => Number.isFinite(value) && value > 0 ? new Date(value).toISOString() : null;
19
+
20
+ function ordinal(value: unknown, count: number, label: string): number {
21
+ if (value === undefined && count === 1) return 0;
22
+ if (value === undefined) throw new Error(`Choose ${label === "account" ? "an" : "a"} ${label} with --${label} (1 to ${count}).`);
23
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > count) throw new Error(`The ${label} must be between 1 and ${count}.`);
24
+ return value - 1;
25
+ }
26
+
27
+ function createBrokerOrdersHeadless(
28
+ resolveBroker: BrokerResolver = (type, context) => context.resolveBroker?.(type) ?? null,
29
+ ): HeadlessPaneDefinition<"bundle"> {
30
+ return {
31
+ shape: "bundle", argument: { kind: "none" },
32
+ description: "Read open orders and recent activity for one connected broker account.",
33
+ discovery: { id: "broker-orders", aliases: ["orders", "executions"], screenshotReadiness: "live-dom" },
34
+ options: [
35
+ { key: "profile", type: "integer", minimum: 1, description: "Broker profile number, required when more than one is available." },
36
+ { key: "account", type: "integer", minimum: 1, description: "Account number, required when the profile has more than one account." },
37
+ { key: "view", type: "enum", values: [{ value: "all" }, { value: "open" }, { value: "activity" }], defaultValue: "all", description: "Orders, activity, or both." },
38
+ ],
39
+ async load(args, context) {
40
+ context.signal.throwIfAborted();
41
+ const profiles = context.config.brokerInstances.filter((profile) => profile.enabled !== false)
42
+ .map((instance) => ({ instance, broker: resolveBroker(instance.brokerType, context) }))
43
+ .filter((row): row is { instance: typeof row.instance; broker: BrokerAdapter } => !!row.broker && !!(row.broker.listOpenOrders || row.broker.listExecutions));
44
+ if (!profiles.length) throw new Error("Connect a broker profile that reports orders in Brokers first.");
45
+ const { instance, broker } = profiles[ordinal(args.options.profile, profiles.length, "profile")]!;
46
+ if (!broker.listAccounts) throw new Error("This broker does not report account identities.");
47
+ const accounts = await broker.listAccounts(instance);
48
+ context.signal.throwIfAborted();
49
+ if (!accounts.length) throw new Error("This broker profile has no available accounts.");
50
+ const account = accounts[ordinal(args.options.account, accounts.length, "account")]!;
51
+ const data = await loadBrokerOrdersSnapshot(broker, instance, account.accountId, context.signal);
52
+ const view = args.options.view ?? "all";
53
+ const mode = account.tradingMode === "simulation" ? "SIMULATION" : account.tradingMode === "live" ? "LIVE" : "UNKNOWN";
54
+ return {
55
+ sections: [
56
+ { title: "Account", entries: [{ label: "Broker", value: broker.name }, { label: "Account", value: account.name }, { label: "Mode", value: mode }] },
57
+ ...(view !== "activity" ? [{ title: "Open orders", columns: orderColumns, rows: data.orders.map((order) => ({
58
+ symbol: order.contract.localSymbol || order.contract.symbol, side: order.action, status: brokerOrderStatus(order.status),
59
+ quantity: order.quantity, filled: order.filled, orderType: order.orderType,
60
+ limitPrice: order.limitPrice ?? null, stopPrice: order.stopPrice ?? null,
61
+ updatedAt: isoTime(order.updatedAt),
62
+ })) }] : []),
63
+ ...(view !== "open" ? [{ title: data.executionKind === "order-summaries" ? "Cumulative order summaries" : "Recent activity", columns: activityColumns,
64
+ rows: data.executions.map((execution) => ({ symbol: execution.contract.localSymbol || execution.contract.symbol,
65
+ side: execution.side, quantity: execution.shares, price: execution.price,
66
+ commission: execution.commission ?? null, commissionCurrency: execution.commissionCurrency ?? null,
67
+ time: isoTime(execution.time) })) }] : []),
68
+ ],
69
+ complete: data.errors.length === 0, errors: data.errors, notes: data.notes,
70
+ freshness: { source: broker.name, status: "not-a-feed", basis: "broker account snapshot", asOf: data.updatedAt, oldest: null },
71
+ };
72
+ },
73
+ };
74
+ }
75
+
76
+ export const brokerOrdersHeadless = createBrokerOrdersHeadless();
@@ -0,0 +1,77 @@
1
+ import type { PluginModule } from "../plugin-module";
2
+ import { brokerOrdersHeadless } from "./headless";
3
+ import { BrokerOrdersPane, BrokerTradeTab } from "./pane";
4
+ import { brokerCanTrade, createTradeCommand, openTradeTicket } from "./command";
5
+ import { brokerOrderFeed } from "./order-feed";
6
+ import { tickerSecType } from "./pane-model";
7
+
8
+ /** Every enabled trading profile's orders, held for as long as the app runs: its fills toast with no ticket open. */
9
+ let releaseHeldOrders: (() => void) | undefined;
10
+
11
+ /** A ticker without a saved asset category trades as its research instrument kind says. */
12
+ const INSTRUMENT_CATEGORY: Record<string, string> = { option: "OPT", future: "FUT", bond: "BOND", crypto: "CRYPTO", currency: "CASH" };
13
+
14
+ /** Trading lives with broker profiles, with one research tab and one orders view. */
15
+ export const brokerTradingModule: PluginModule = {
16
+ panes: [{
17
+ id: "broker-orders", name: "Orders", personalData: "account", icon: "O", component: BrokerOrdersPane,
18
+ defaultPosition: "right", defaultMode: "floating",
19
+ defaultFloatingSize: { width: 100, height: 30 }, headless: brokerOrdersHeadless,
20
+ portableShare: { private: { title: true, params: true, settings: true, state: true } },
21
+ }],
22
+ paneTemplates: [{
23
+ id: "broker-orders-pane", paneId: "broker-orders", label: "Orders",
24
+ description: "Open orders and recent activity for a broker account",
25
+ keywords: ["orders", "trades", "executions", "cancel", "modify"],
26
+ shortcut: { prefix: "ORD" }, headless: brokerOrdersHeadless,
27
+ createInstance: () => ({ placement: "floating" }),
28
+ }],
29
+ setup(ctx) {
30
+ brokerOrderFeed.setNotifier((notification) => { ctx.notify(notification); });
31
+ let held = new Map<string, { key: string; release(): void }>();
32
+ // A broker registered by a later plugin, or a profile added or edited, is held from then on.
33
+ const holdProfiles = () => {
34
+ const next = new Map<string, { key: string; release(): void }>();
35
+ for (const instance of ctx.getConfig().brokerInstances) {
36
+ const broker = instance.enabled === false ? null : ctx.getBrokerAdapter?.(instance.brokerType);
37
+ if (!broker?.listOpenOrders || !broker.placeOrder) continue;
38
+ const key = JSON.stringify([broker.id, instance.config]);
39
+ const current = held.get(instance.id);
40
+ if (current?.key === key) { next.set(instance.id, current); held.delete(instance.id); continue; }
41
+ next.set(instance.id, { key, release: brokerOrderFeed.hold(broker, instance) });
42
+ }
43
+ for (const stale of held.values()) stale.release();
44
+ held = next;
45
+ };
46
+ const unsubscribe = [ctx.on("config:changed", holdProfiles), ctx.on("plugin:registered", holdProfiles)];
47
+ holdProfiles();
48
+ releaseHeldOrders = () => {
49
+ for (const stop of unsubscribe) stop();
50
+ for (const entry of held.values()) entry.release();
51
+ held.clear();
52
+ brokerOrderFeed.setNotifier(undefined);
53
+ brokerOrderFeed.clear();
54
+ };
55
+ const supported = (symbol: string, secType = "STK") => brokerCanTrade(ctx.getConfig().brokerInstances, (type) => ctx.getBrokerAdapter?.(type), symbol, secType);
56
+ ctx.registerTickerResearchTab({
57
+ id: "broker-trade", name: "Trade", order: 20, component: BrokerTradeTab,
58
+ instruments: ["equity", "fund", "option", "future", "bond", "crypto", "currency", "other"],
59
+ isVisible: ({ ticker, instrumentKind }) => supported(ticker?.metadata.ticker ?? "", tickerSecType(ticker?.metadata.assetCategory, INSTRUMENT_CATEGORY[instrumentKind])),
60
+ });
61
+ for (const side of ["BUY", "SELL"] as const) {
62
+ ctx.registerCommand(createTradeCommand(ctx, side, supported));
63
+ ctx.registerTickerAction({
64
+ id: `broker-${side.toLowerCase()}`, label: side === "BUY" ? "Buy" : "Sell",
65
+ keywords: [side.toLowerCase(), "trade", "order", "broker"],
66
+ filter: (ticker) => supported(ticker.metadata.ticker, tickerSecType(ticker.metadata.assetCategory)),
67
+ execute: (ticker) => {
68
+ openTradeTicket(ctx, ticker.metadata.ticker, { action: side });
69
+ },
70
+ });
71
+ }
72
+ },
73
+ dispose() {
74
+ releaseHeldOrders?.();
75
+ releaseHeldOrders = undefined;
76
+ },
77
+ };