gloomberb 0.1.0 → 0.3.1

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 (311) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +68 -121
  3. package/bin/gloomberb +3 -0
  4. package/package.json +13 -4
  5. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  6. package/src/app.tsx +1330 -0
  7. package/src/brokers/sync-broker-instance.test.ts +100 -0
  8. package/src/brokers/sync-broker-instance.ts +341 -0
  9. package/src/cli/commands/plugins.ts +173 -0
  10. package/src/cli/commands/portfolio.ts +210 -0
  11. package/src/cli/commands/search.ts +120 -0
  12. package/src/cli/commands/ticker.ts +375 -0
  13. package/src/cli/commands/watchlist.ts +210 -0
  14. package/src/cli/context.ts +62 -0
  15. package/src/cli/errors.ts +13 -0
  16. package/src/cli/helpers.ts +85 -0
  17. package/src/cli/index.ts +143 -0
  18. package/src/cli/ticker-resolution.ts +39 -0
  19. package/src/cli/types.ts +15 -0
  20. package/src/cli.test.ts +361 -0
  21. package/src/components/chart/chart-data.ts +239 -0
  22. package/src/components/chart/chart-renderer.ts +1199 -0
  23. package/src/components/chart/chart-types.ts +82 -0
  24. package/src/components/chart/chart-viewport.ts +21 -0
  25. package/src/components/chart/chart.test.ts +439 -0
  26. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  27. package/src/components/chart/comparison-chart-data.ts +316 -0
  28. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  29. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  30. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  31. package/src/components/chart/cursor-motion.ts +48 -0
  32. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  33. package/src/components/chart/native/kitty-adapter.ts +12 -0
  34. package/src/components/chart/native/kitty-manager.ts +142 -0
  35. package/src/components/chart/native/kitty-protocol.ts +110 -0
  36. package/src/components/chart/native/kitty-support.ts +71 -0
  37. package/src/components/chart/native/kitty.test.ts +620 -0
  38. package/src/components/chart/native/renderer-selection.ts +24 -0
  39. package/src/components/chart/native/surface-manager.ts +225 -0
  40. package/src/components/chart/native/surface-sync.ts +34 -0
  41. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  42. package/src/components/chart/stock-chart.test.ts +107 -0
  43. package/src/components/chart/stock-chart.tsx +1556 -0
  44. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  45. package/src/components/command-bar/command-bar.test.tsx +1524 -0
  46. package/src/components/command-bar/command-bar.tsx +4823 -0
  47. package/src/components/command-bar/command-registry.ts +261 -0
  48. package/src/components/command-bar/helpers.test.ts +224 -0
  49. package/src/components/command-bar/helpers.ts +371 -0
  50. package/src/components/command-bar/root-shortcuts.ts +158 -0
  51. package/src/components/command-bar/view-model.test.ts +152 -0
  52. package/src/components/command-bar/view-model.ts +185 -0
  53. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  54. package/src/components/command-bar/workflow-ops.ts +433 -0
  55. package/src/components/command-bar/workflow-types.ts +127 -0
  56. package/src/components/detail-feed-view.test.tsx +105 -0
  57. package/src/components/detail-feed-view.tsx +295 -0
  58. package/src/components/index.ts +32 -0
  59. package/src/components/layout/floating-pane.tsx +49 -0
  60. package/src/components/layout/header.tsx +129 -0
  61. package/src/components/layout/pane-header.tsx +44 -0
  62. package/src/components/layout/pane.tsx +49 -0
  63. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  64. package/src/components/layout/pixel-mouse.test.ts +40 -0
  65. package/src/components/layout/shell.test.tsx +625 -0
  66. package/src/components/layout/shell.tsx +1200 -0
  67. package/src/components/layout/status-bar.test.tsx +173 -0
  68. package/src/components/layout/status-bar.tsx +138 -0
  69. package/src/components/markdown-editor.tsx +134 -0
  70. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  71. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  72. package/src/components/pane-settings-dialog.tsx +468 -0
  73. package/src/components/pane-template-wizard.test.tsx +52 -0
  74. package/src/components/pane-template-wizard.tsx +199 -0
  75. package/src/components/price-selector-dialog.tsx +195 -0
  76. package/src/components/spinner.tsx +2 -0
  77. package/src/components/tab-bar.tsx +17 -0
  78. package/src/components/ticker-badge-text.test.tsx +160 -0
  79. package/src/components/ticker-badge-text.tsx +58 -0
  80. package/src/components/ticker-badge.tsx +88 -0
  81. package/src/components/ticker-list-table.tsx +188 -0
  82. package/src/components/toggle-list.tsx +74 -0
  83. package/src/components/ui/button.tsx +77 -0
  84. package/src/components/ui/data-table.tsx +210 -0
  85. package/src/components/ui/external-link.tsx +22 -0
  86. package/src/components/ui/fields.tsx +187 -0
  87. package/src/components/ui/frame.tsx +73 -0
  88. package/src/components/ui/index.ts +53 -0
  89. package/src/components/ui/list-view.tsx +166 -0
  90. package/src/components/ui/loading.tsx +70 -0
  91. package/src/components/ui/status.tsx +83 -0
  92. package/src/components/ui/tabs.tsx +47 -0
  93. package/src/components/ui/toggle.tsx +164 -0
  94. package/src/components/ui/ui.test.tsx +134 -0
  95. package/src/data/app-persistence.ts +25 -0
  96. package/src/data/config-store.test.ts +310 -0
  97. package/src/data/config-store.ts +469 -0
  98. package/src/data/plugin-state-store.ts +102 -0
  99. package/src/data/resource-store.ts +323 -0
  100. package/src/data/session-store.ts +52 -0
  101. package/src/data/sqlite-cache.test.ts +103 -0
  102. package/src/data/sqlite-database.ts +62 -0
  103. package/src/data/sqlite-json.ts +19 -0
  104. package/src/data/ticker-repository.ts +45 -0
  105. package/src/data/ticker-store.ts +46 -0
  106. package/src/index.tsx +67 -0
  107. package/src/market-data/coordinator.test.ts +369 -0
  108. package/src/market-data/coordinator.ts +580 -0
  109. package/src/market-data/hooks.tsx +234 -0
  110. package/src/market-data/query-store.ts +28 -0
  111. package/src/market-data/request-types.test.ts +64 -0
  112. package/src/market-data/request-types.ts +70 -0
  113. package/src/market-data/result-types.ts +56 -0
  114. package/src/market-data/selectors.ts +166 -0
  115. package/src/market-data/trace.ts +7 -0
  116. package/src/pane-settings.test.ts +71 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
  119. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  120. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  121. package/src/plugins/builtin/ai/index.tsx +86 -0
  122. package/src/plugins/builtin/ai/providers.ts +99 -0
  123. package/src/plugins/builtin/ai/runner.ts +82 -0
  124. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  125. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  126. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  127. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  128. package/src/plugins/builtin/ai/settings.ts +79 -0
  129. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  130. package/src/plugins/builtin/ai/utils.ts +71 -0
  131. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  132. package/src/plugins/builtin/ask-ai.tsx +9 -0
  133. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  134. package/src/plugins/builtin/chat-controller.ts +340 -0
  135. package/src/plugins/builtin/chat.test.tsx +451 -0
  136. package/src/plugins/builtin/chat.tsx +620 -0
  137. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  138. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  139. package/src/plugins/builtin/data-providers.ts +16 -0
  140. package/src/plugins/builtin/debug.tsx +351 -0
  141. package/src/plugins/builtin/help.test.tsx +110 -0
  142. package/src/plugins/builtin/help.tsx +281 -0
  143. package/src/plugins/builtin/layout-manager.tsx +264 -0
  144. package/src/plugins/builtin/manual-entry.tsx +25 -0
  145. package/src/plugins/builtin/news.tsx +143 -0
  146. package/src/plugins/builtin/notes-files.ts +58 -0
  147. package/src/plugins/builtin/notes.tsx +396 -0
  148. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  149. package/src/plugins/builtin/options-availability.ts +158 -0
  150. package/src/plugins/builtin/options.tsx +364 -0
  151. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  152. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  153. package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
  156. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  157. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  158. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  159. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  160. package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
  161. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  162. package/src/plugins/builtin/sec.tsx +250 -0
  163. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  164. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  165. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  166. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  167. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  168. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  169. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  170. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  171. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  172. package/src/plugins/builtin/yahoo.ts +18 -0
  173. package/src/plugins/event-bus.ts +37 -0
  174. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  175. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  176. package/src/plugins/ibkr/account-cache.ts +137 -0
  177. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  178. package/src/plugins/ibkr/config.test.ts +68 -0
  179. package/src/plugins/ibkr/config.ts +262 -0
  180. package/src/plugins/ibkr/dialogs.tsx +83 -0
  181. package/src/plugins/ibkr/flex.test.ts +107 -0
  182. package/src/plugins/ibkr/flex.ts +267 -0
  183. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  184. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  185. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  186. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  187. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  188. package/src/plugins/ibkr/index.test.tsx +183 -0
  189. package/src/plugins/ibkr/index.tsx +153 -0
  190. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  191. package/src/plugins/ibkr/instance-selection.ts +36 -0
  192. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  193. package/src/plugins/ibkr/price-normalization.ts +59 -0
  194. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  195. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  196. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  197. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  198. package/src/plugins/ibkr/trade-utils.ts +149 -0
  199. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  200. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  201. package/src/plugins/ibkr/trading-state.ts +339 -0
  202. package/src/plugins/loader.ts +71 -0
  203. package/src/plugins/pane-manager.test.ts +103 -0
  204. package/src/plugins/pane-manager.ts +974 -0
  205. package/src/plugins/plugin-persistence.ts +85 -0
  206. package/src/plugins/plugin-runtime.test.tsx +185 -0
  207. package/src/plugins/plugin-runtime.tsx +175 -0
  208. package/src/plugins/prediction-markets/cache.ts +114 -0
  209. package/src/plugins/prediction-markets/categories.ts +223 -0
  210. package/src/plugins/prediction-markets/chart.tsx +512 -0
  211. package/src/plugins/prediction-markets/columns.ts +133 -0
  212. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  213. package/src/plugins/prediction-markets/controller.ts +725 -0
  214. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  215. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  216. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  217. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  218. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  219. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  220. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  221. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  222. package/src/plugins/prediction-markets/index.tsx +102 -0
  223. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  224. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  225. package/src/plugins/prediction-markets/launch.ts +240 -0
  226. package/src/plugins/prediction-markets/metrics.ts +311 -0
  227. package/src/plugins/prediction-markets/navigation.ts +90 -0
  228. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  229. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  230. package/src/plugins/prediction-markets/pane.tsx +294 -0
  231. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  232. package/src/plugins/prediction-markets/rows.ts +229 -0
  233. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  234. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  235. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  236. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  237. package/src/plugins/prediction-markets/settings.ts +164 -0
  238. package/src/plugins/prediction-markets/status.ts +24 -0
  239. package/src/plugins/prediction-markets/table.tsx +84 -0
  240. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  241. package/src/plugins/prediction-markets/types.ts +186 -0
  242. package/src/plugins/registry.ts +633 -0
  243. package/src/sources/exchange-rates.ts +18 -0
  244. package/src/sources/gloomberb-cloud.test.ts +199 -0
  245. package/src/sources/gloomberb-cloud.ts +326 -0
  246. package/src/sources/provider-errors.ts +19 -0
  247. package/src/sources/provider-router.test.ts +1476 -0
  248. package/src/sources/provider-router.ts +1405 -0
  249. package/src/sources/sec-edgar.test.ts +286 -0
  250. package/src/sources/sec-edgar.ts +526 -0
  251. package/src/sources/yahoo-finance.test.ts +14 -0
  252. package/src/sources/yahoo-finance.ts +1017 -0
  253. package/src/state/app-activity.test.ts +41 -0
  254. package/src/state/app-activity.ts +106 -0
  255. package/src/state/app-bootstrap.test.ts +471 -0
  256. package/src/state/app-bootstrap.ts +308 -0
  257. package/src/state/app-context.test.ts +406 -0
  258. package/src/state/app-context.test.tsx +132 -0
  259. package/src/state/app-context.tsx +969 -0
  260. package/src/state/selectors.ts +67 -0
  261. package/src/state/session-persistence.test.ts +128 -0
  262. package/src/state/session-persistence.ts +161 -0
  263. package/src/state/ticker-refresh-queue.test.ts +46 -0
  264. package/src/state/ticker-refresh-queue.ts +40 -0
  265. package/src/state/use-inline-tickers.ts +174 -0
  266. package/src/state/use-quote-streaming.test.ts +37 -0
  267. package/src/state/use-quote-streaming.test.tsx +70 -0
  268. package/src/state/use-quote-streaming.ts +80 -0
  269. package/src/state/use-session-snapshot.ts +84 -0
  270. package/src/test-support/data-provider.ts +22 -0
  271. package/src/theme/color-utils.ts +67 -0
  272. package/src/theme/colors.ts +128 -0
  273. package/src/theme/themes.test.ts +45 -0
  274. package/src/theme/themes.ts +491 -0
  275. package/src/types/broker.ts +105 -0
  276. package/src/types/config.ts +434 -0
  277. package/src/types/data-provider.ts +81 -0
  278. package/src/types/financials.ts +142 -0
  279. package/src/types/instrument.ts +29 -0
  280. package/src/types/persistence.ts +21 -0
  281. package/src/types/plugin.ts +354 -0
  282. package/src/types/ticker.ts +58 -0
  283. package/src/types/trading.ts +85 -0
  284. package/src/updater.test.ts +162 -0
  285. package/src/updater.ts +247 -0
  286. package/src/utils/api-client.ts +666 -0
  287. package/src/utils/ascii-chart.ts +172 -0
  288. package/src/utils/broker-instances.ts +47 -0
  289. package/src/utils/cli-output.ts +83 -0
  290. package/src/utils/currency-units.test.ts +49 -0
  291. package/src/utils/currency-units.ts +82 -0
  292. package/src/utils/debug-log.ts +133 -0
  293. package/src/utils/exchange-rate-map.ts +8 -0
  294. package/src/utils/format.ts +158 -0
  295. package/src/utils/fuzzy-search.ts +34 -0
  296. package/src/utils/market-status.ts +95 -0
  297. package/src/utils/options.ts +86 -0
  298. package/src/utils/price-history.ts +39 -0
  299. package/src/utils/quote-time.test.ts +22 -0
  300. package/src/utils/quote-time.ts +50 -0
  301. package/src/utils/sec.test.ts +55 -0
  302. package/src/utils/sec.ts +45 -0
  303. package/src/utils/ticker-list.test.ts +22 -0
  304. package/src/utils/ticker-list.ts +35 -0
  305. package/src/utils/ticker-metadata.ts +51 -0
  306. package/src/utils/ticker-search.test.ts +227 -0
  307. package/src/utils/ticker-search.ts +764 -0
  308. package/src/utils/ticker-tokenizer.test.ts +48 -0
  309. package/src/utils/ticker-tokenizer.ts +72 -0
  310. package/src/version.ts +1 -0
  311. package/install.mjs +0 -69
@@ -0,0 +1,1050 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { useKeyboard } from "@opentui/react";
3
+ import { useDialog } from "@opentui-ui/dialog/react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { PriceSelectorDialog } from "../../components";
6
+ import { Button } from "../../components/ui/button";
7
+ import { useAppState, usePaneCollection, usePaneInstanceId, usePaneTicker } from "../../state/app-context";
8
+ import { colors, hoverBg } from "../../theme/colors";
9
+ import type { DetailTabProps } from "../../types/plugin";
10
+ import type { BrokerOrderRequest, BrokerOrderType } from "../../types/trading";
11
+ import { formatCurrency, formatNumber } from "../../utils/format";
12
+ import { getBrokerInstance } from "../../utils/broker-instances";
13
+ import { isGatewayConfigured } from "./config";
14
+ import { ChoiceDialog, InputDialog } from "./dialogs";
15
+ import { useIbkrGatewaySelection } from "./gateway-selection";
16
+ import { refreshGatewayData } from "./gateway-helpers";
17
+ import { TradeBadge } from "./trade-badge";
18
+ import {
19
+ getTradeTicketState,
20
+ normalizeContract,
21
+ setTradeTicketBusy,
22
+ setTradeTicketDraft,
23
+ setTradeTicketInstrument,
24
+ setTradeTicketMessage,
25
+ setTradeTicketPreview,
26
+ updateTradeTicketState,
27
+ updateTradingPaneState,
28
+ useTradingPaneState,
29
+ } from "./trading-state";
30
+ import {
31
+ formatContractLabel,
32
+ formatPreviewMetric,
33
+ formatPreviewSummary,
34
+ formatQuoteSummary,
35
+ getTradeTonePalette,
36
+ getKnownIbkrAccounts,
37
+ inferDraftAccountId,
38
+ isLimitOrder,
39
+ isStopOrder,
40
+ truncateTradeText as truncateText,
41
+ type TradeTone,
42
+ } from "./trade-utils";
43
+
44
+ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
45
+ const { state } = useAppState();
46
+ const paneId = usePaneInstanceId();
47
+ const { collectionId } = usePaneCollection(paneId);
48
+ const { ticker, financials } = usePaneTicker(paneId);
49
+ const dialog = useDialog();
50
+ const tradeState = useTradingPaneState();
51
+ const [interactive, setInteractive] = useState(false);
52
+ const [hoveredField, setHoveredField] = useState<string | null>(null);
53
+ const fieldHoverBg = hoverBg();
54
+
55
+ const symbol = ticker?.metadata.ticker ?? null;
56
+ const ticketState = getTradeTicketState(symbol, ticker);
57
+ const preferredInstanceId = ticketState.brokerInstanceId ?? tradeState.brokerInstanceId;
58
+ const {
59
+ activePortfolio,
60
+ gatewayInstances,
61
+ lockedBrokerInstanceId,
62
+ selectedInstance,
63
+ gatewaySnapshot,
64
+ gatewayService,
65
+ normalizedConfig,
66
+ isGatewayMode,
67
+ availableAccounts,
68
+ gatewayRequiredMessage,
69
+ } = useIbkrGatewaySelection(
70
+ state.config,
71
+ state.brokerAccounts,
72
+ collectionId,
73
+ preferredInstanceId,
74
+ );
75
+ const inferredAccountId = selectedInstance
76
+ ? inferDraftAccountId(
77
+ state.config,
78
+ collectionId,
79
+ availableAccounts,
80
+ selectedInstance.id,
81
+ tradeState.accountId,
82
+ )
83
+ : undefined;
84
+ const currentAccountId = ticketState.draft.accountId || inferredAccountId;
85
+ const activeAccount = availableAccounts.find((account) => account.accountId === currentAccountId);
86
+
87
+ const enterInteractive = useCallback(() => {
88
+ setInteractive(true);
89
+ onCapture(true);
90
+ }, [onCapture]);
91
+
92
+ const exitInteractive = useCallback(() => {
93
+ setInteractive(false);
94
+ onCapture(false);
95
+ }, [onCapture]);
96
+
97
+ useEffect(() => {
98
+ if (!focused) {
99
+ exitInteractive();
100
+ }
101
+ }, [focused, exitInteractive]);
102
+
103
+ useEffect(() => {
104
+ exitInteractive();
105
+ }, [symbol]); // eslint-disable-line react-hooks/exhaustive-deps
106
+
107
+ useEffect(() => {
108
+ if (!symbol || !ticker || !selectedInstance) return;
109
+ const lockedAccountId = lockedBrokerInstanceId === selectedInstance.id ? activePortfolio?.brokerAccountId : undefined;
110
+ if (
111
+ ticketState.brokerInstanceId !== selectedInstance.id
112
+ || ticketState.brokerLabel !== selectedInstance.label
113
+ || (lockedAccountId && ticketState.draft.accountId !== lockedAccountId)
114
+ ) {
115
+ updateTradeTicketState(symbol, ticker, (current) => ({
116
+ ...current,
117
+ brokerInstanceId: selectedInstance.id,
118
+ brokerLabel: selectedInstance.label,
119
+ draft: {
120
+ ...current.draft,
121
+ brokerInstanceId: selectedInstance.id,
122
+ accountId: lockedAccountId ?? current.draft.accountId,
123
+ },
124
+ }));
125
+ }
126
+ }, [
127
+ symbol,
128
+ ticker,
129
+ selectedInstance,
130
+ ticketState.brokerInstanceId,
131
+ ticketState.brokerLabel,
132
+ ticketState.draft.accountId,
133
+ lockedBrokerInstanceId,
134
+ activePortfolio?.brokerAccountId,
135
+ ]);
136
+
137
+ useEffect(() => {
138
+ if (!symbol || !ticker || !isGatewayMode || availableAccounts.length === 0 || ticketState.draft.accountId || !selectedInstance) return;
139
+ const inferred = inferDraftAccountId(
140
+ state.config,
141
+ collectionId,
142
+ availableAccounts,
143
+ selectedInstance.id,
144
+ tradeState.accountId,
145
+ );
146
+ if (inferred) {
147
+ setTradeTicketDraft(symbol, { brokerInstanceId: selectedInstance.id, accountId: inferred }, ticker);
148
+ }
149
+ }, [
150
+ symbol,
151
+ ticker,
152
+ isGatewayMode,
153
+ availableAccounts,
154
+ ticketState.draft.accountId,
155
+ selectedInstance,
156
+ state.config,
157
+ collectionId,
158
+ tradeState.accountId,
159
+ ]);
160
+
161
+ const refresh = useCallback(async () => {
162
+ if (!symbol || !ticker) return;
163
+ if (!selectedInstance || !normalizedConfig || !isGatewayMode || !isGatewayConfigured(selectedInstance.config)) {
164
+ setTradeTicketMessage(symbol, undefined, gatewayRequiredMessage, ticker);
165
+ return;
166
+ }
167
+
168
+ try {
169
+ setTradeTicketBusy(symbol, true, ticker);
170
+ await refreshGatewayData(selectedInstance);
171
+ const inferred = inferDraftAccountId(
172
+ state.config,
173
+ collectionId,
174
+ availableAccounts,
175
+ selectedInstance.id,
176
+ tradeState.accountId,
177
+ );
178
+ if (inferred && !getTradeTicketState(symbol, ticker).draft.accountId) {
179
+ setTradeTicketDraft(symbol, { brokerInstanceId: selectedInstance.id, accountId: inferred }, ticker);
180
+ }
181
+ setTradeTicketMessage(symbol, `Refreshed ${selectedInstance.label}.`, undefined, ticker);
182
+ } catch (error: any) {
183
+ setTradeTicketMessage(symbol, undefined, error?.message || `Failed to refresh ${selectedInstance.label}.`, ticker);
184
+ } finally {
185
+ setTradeTicketBusy(symbol, false, ticker);
186
+ }
187
+ }, [
188
+ symbol,
189
+ ticker,
190
+ selectedInstance,
191
+ normalizedConfig,
192
+ isGatewayMode,
193
+ gatewayRequiredMessage,
194
+ state.config,
195
+ collectionId,
196
+ availableAccounts,
197
+ tradeState.accountId,
198
+ ]);
199
+
200
+ useEffect(() => {
201
+ if (!selectedInstance || !normalizedConfig || !isGatewayMode || !isGatewayConfigured(selectedInstance.config) || !symbol) return;
202
+ refresh().catch(() => {});
203
+ }, [symbol, selectedInstance?.id, isGatewayMode, normalizedConfig ? JSON.stringify(normalizedConfig.gateway) : ""]);
204
+
205
+ const chooseBrokerInstance = useCallback(async () => {
206
+ if (!symbol || !ticker) return;
207
+ if (lockedBrokerInstanceId) {
208
+ setTradeTicketMessage(symbol, undefined, "This ticket is locked to the active broker-managed portfolio.", ticker);
209
+ return;
210
+ }
211
+ if (gatewayInstances.length === 0) {
212
+ setTradeTicketMessage(symbol, undefined, "Connect a Gateway / TWS IBKR profile first.", ticker);
213
+ return;
214
+ }
215
+
216
+ const selected = await dialog.prompt<string>({
217
+ content: (ctx) => (
218
+ <ChoiceDialog
219
+ {...ctx}
220
+ title="Choose IBKR Profile"
221
+ choices={gatewayInstances.map((instance) => ({
222
+ id: instance.id,
223
+ label: instance.label,
224
+ desc: "Gateway / TWS",
225
+ }))}
226
+ />
227
+ ),
228
+ });
229
+ if (!selected) return;
230
+
231
+ const instance = getBrokerInstance(state.config.brokerInstances, selected);
232
+ if (!instance) return;
233
+ updateTradingPaneState({
234
+ brokerInstanceId: instance.id,
235
+ brokerLabel: instance.label,
236
+ accountId: undefined,
237
+ selectedOpenOrderIndex: 0,
238
+ lastError: undefined,
239
+ lastInfo: undefined,
240
+ });
241
+ updateTradeTicketState(symbol, ticker, (current) => ({
242
+ ...current,
243
+ brokerInstanceId: instance.id,
244
+ brokerLabel: instance.label,
245
+ draft: {
246
+ ...current.draft,
247
+ brokerInstanceId: instance.id,
248
+ accountId: undefined,
249
+ },
250
+ preview: null,
251
+ editingOrderId: undefined,
252
+ lastError: undefined,
253
+ lastInfo: undefined,
254
+ }));
255
+ }, [symbol, ticker, lockedBrokerInstanceId, gatewayInstances, dialog, state.config.brokerInstances]);
256
+
257
+ const chooseInstrument = useCallback(async () => {
258
+ if (!symbol || !ticker) return;
259
+ if (!selectedInstance || !normalizedConfig || !gatewayService || !isGatewayMode || !isGatewayConfigured(selectedInstance.config)) {
260
+ setTradeTicketMessage(symbol, undefined, gatewayRequiredMessage, ticker);
261
+ return;
262
+ }
263
+
264
+ const search = await dialog.prompt<string>({
265
+ content: (ctx) => (
266
+ <InputDialog
267
+ {...ctx}
268
+ step={{
269
+ key: "query",
270
+ type: "text",
271
+ label: "Override Ticker Contract",
272
+ placeholder: "AAPL, ES, SPY 260619C00500000",
273
+ body: ["The current ticker is preloaded. Search only if you need a different listing, future, or options contract."],
274
+ }}
275
+ />
276
+ ),
277
+ });
278
+ if (!search) return;
279
+
280
+ try {
281
+ setTradeTicketBusy(symbol, true, ticker);
282
+ await gatewayService.connect(normalizedConfig.gateway);
283
+ const results = (await gatewayService.searchInstruments(search, normalizedConfig.gateway)).map((result) => ({
284
+ ...result,
285
+ brokerInstanceId: result.brokerInstanceId ?? selectedInstance.id,
286
+ brokerLabel: result.brokerLabel ?? selectedInstance.label,
287
+ brokerContract: result.brokerContract
288
+ ? { ...result.brokerContract, brokerInstanceId: result.brokerContract.brokerInstanceId ?? selectedInstance.id }
289
+ : undefined,
290
+ }));
291
+ const choices = results.slice(0, 12).map((result, index) => ({
292
+ id: String(index),
293
+ label: `${result.symbol} ${result.type}`.trim(),
294
+ desc: `${selectedInstance.label} · ${result.name} · ${result.exchange || "SMART"}`,
295
+ }));
296
+ if (choices.length === 0) {
297
+ setTradeTicketMessage(symbol, undefined, `No IBKR contracts matched "${search}".`, ticker);
298
+ return;
299
+ }
300
+
301
+ const selected = await dialog.prompt<string>({
302
+ content: (ctx) => <ChoiceDialog {...ctx} title="Choose Contract" choices={choices} />,
303
+ });
304
+ if (!selected) return;
305
+
306
+ const result = results[Number(selected)];
307
+ if (!result) return;
308
+ setTradeTicketInstrument(symbol, result, ticker);
309
+ setTradeTicketMessage(symbol, `Loaded ${result.symbol} into the ticket.`, undefined, ticker);
310
+ } catch (error: any) {
311
+ setTradeTicketMessage(symbol, undefined, error?.message || "Failed to search IBKR contracts.", ticker);
312
+ } finally {
313
+ setTradeTicketBusy(symbol, false, ticker);
314
+ }
315
+ }, [symbol, ticker, selectedInstance, normalizedConfig, gatewayService, isGatewayMode, gatewayRequiredMessage, dialog]);
316
+
317
+ const chooseAccount = useCallback(async () => {
318
+ if (!symbol || !ticker || !selectedInstance || !normalizedConfig || !gatewayService || !isGatewayMode) return;
319
+ if (availableAccounts.length === 0) {
320
+ await refresh();
321
+ }
322
+
323
+ const nextAccounts = getKnownIbkrAccounts(
324
+ state.brokerAccounts,
325
+ selectedInstance.id,
326
+ gatewayService.getSnapshot().accounts,
327
+ );
328
+ if (nextAccounts.length === 0) {
329
+ setTradeTicketMessage(symbol, undefined, "No IBKR accounts available.", ticker);
330
+ return;
331
+ }
332
+
333
+ const selected = await dialog.prompt<string>({
334
+ content: (ctx) => (
335
+ <ChoiceDialog
336
+ {...ctx}
337
+ title="Choose Account"
338
+ choices={nextAccounts.map((account) => ({
339
+ id: account.accountId,
340
+ label: `${selectedInstance.label} → ${account.accountId}`,
341
+ desc: `${formatCurrency(account.netLiquidation || 0, account.currency || "USD")} net liq`,
342
+ }))}
343
+ />
344
+ ),
345
+ });
346
+ if (!selected) return;
347
+ updateTradingPaneState({ accountId: selected });
348
+ setTradeTicketDraft(symbol, { brokerInstanceId: selectedInstance.id, accountId: selected }, ticker);
349
+ }, [
350
+ symbol,
351
+ ticker,
352
+ selectedInstance,
353
+ normalizedConfig,
354
+ gatewayService,
355
+ isGatewayMode,
356
+ availableAccounts,
357
+ refresh,
358
+ dialog,
359
+ ]);
360
+
361
+ const editNumericField = useCallback(async (
362
+ label: string,
363
+ currentValue: number | undefined,
364
+ onCommit: (value: number | undefined) => void,
365
+ ) => {
366
+ const response = await dialog.prompt<string>({
367
+ content: (ctx) => (
368
+ <InputDialog
369
+ {...ctx}
370
+ step={{
371
+ key: label,
372
+ type: "number",
373
+ label,
374
+ placeholder: currentValue != null ? String(currentValue) : "",
375
+ }}
376
+ />
377
+ ),
378
+ });
379
+ if (response === undefined) return;
380
+ if (!response.trim()) {
381
+ onCommit(undefined);
382
+ return;
383
+ }
384
+ const numeric = Number(response);
385
+ if (!Number.isFinite(numeric)) {
386
+ if (symbol && ticker) setTradeTicketMessage(symbol, undefined, `${label} must be numeric.`, ticker);
387
+ return;
388
+ }
389
+ onCommit(numeric);
390
+ }, [dialog, symbol, ticker]);
391
+
392
+ const editPriceField = useCallback(async (
393
+ label: string,
394
+ currentValue: number | undefined,
395
+ onCommit: (value: number | undefined) => void,
396
+ ) => {
397
+ const response = await dialog.prompt<string>({
398
+ content: (ctx) => (
399
+ <PriceSelectorDialog
400
+ {...ctx}
401
+ label={label}
402
+ currentValue={currentValue}
403
+ quote={financials?.quote}
404
+ />
405
+ ),
406
+ });
407
+ if (response === undefined) return;
408
+ if (!response.trim()) {
409
+ onCommit(undefined);
410
+ return;
411
+ }
412
+ const numeric = Number(response);
413
+ if (!Number.isFinite(numeric)) {
414
+ if (symbol && ticker) setTradeTicketMessage(symbol, undefined, `${label} must be numeric.`, ticker);
415
+ return;
416
+ }
417
+ onCommit(numeric);
418
+ }, [dialog, symbol, ticker, financials]);
419
+
420
+ const editOrderType = useCallback(async () => {
421
+ if (!symbol || !ticker) return;
422
+ const choice = await dialog.prompt<string>({
423
+ content: (ctx) => (
424
+ <ChoiceDialog
425
+ {...ctx}
426
+ title="Order Type"
427
+ choices={[
428
+ { id: "MKT", label: "Market", desc: "Execute at market price" },
429
+ { id: "LMT", label: "Limit", desc: "Set a limit price" },
430
+ { id: "STP", label: "Stop", desc: "Trigger at a stop price" },
431
+ { id: "STP LMT", label: "Stop Limit", desc: "Trigger a limit order at a stop price" },
432
+ ]}
433
+ />
434
+ ),
435
+ });
436
+ if (!choice) return;
437
+ const orderType = choice as BrokerOrderType;
438
+ setTradeTicketDraft(symbol, {
439
+ orderType,
440
+ limitPrice: isLimitOrder(orderType) ? ticketState.draft.limitPrice : undefined,
441
+ stopPrice: isStopOrder(orderType) ? ticketState.draft.stopPrice : undefined,
442
+ }, ticker);
443
+ }, [dialog, symbol, ticker, ticketState.draft.limitPrice, ticketState.draft.stopPrice]);
444
+
445
+ const draftRequest = useCallback((): BrokerOrderRequest | null => {
446
+ if (!symbol || !ticker) return null;
447
+ if (!selectedInstance || !normalizedConfig || !isGatewayMode || !isGatewayConfigured(selectedInstance.config)) {
448
+ setTradeTicketMessage(symbol, undefined, gatewayRequiredMessage, ticker);
449
+ return null;
450
+ }
451
+ if (!ticketState.draft.contract.symbol) {
452
+ setTradeTicketMessage(symbol, undefined, "Choose a contract before previewing an order.", ticker);
453
+ return null;
454
+ }
455
+ if (!currentAccountId) {
456
+ setTradeTicketMessage(symbol, undefined, "Select an IBKR account before trading.", ticker);
457
+ return null;
458
+ }
459
+
460
+ const request: BrokerOrderRequest = {
461
+ ...ticketState.draft,
462
+ brokerInstanceId: selectedInstance.id,
463
+ accountId: currentAccountId,
464
+ contract: {
465
+ ...ticketState.draft.contract,
466
+ brokerId: "ibkr",
467
+ brokerInstanceId: ticketState.draft.contract.brokerInstanceId ?? selectedInstance.id,
468
+ },
469
+ };
470
+ if (request.quantity <= 0) {
471
+ setTradeTicketMessage(symbol, undefined, "Quantity must be greater than zero.", ticker);
472
+ return null;
473
+ }
474
+ if (isLimitOrder(request.orderType) && request.limitPrice == null) {
475
+ setTradeTicketMessage(symbol, undefined, "Limit orders require a limit price.", ticker);
476
+ return null;
477
+ }
478
+ if (isStopOrder(request.orderType) && request.stopPrice == null) {
479
+ setTradeTicketMessage(symbol, undefined, "Stop orders require a stop price.", ticker);
480
+ return null;
481
+ }
482
+ return request;
483
+ }, [symbol, ticker, selectedInstance, normalizedConfig, isGatewayMode, gatewayRequiredMessage, ticketState.draft, currentAccountId]);
484
+
485
+ const previewOrder = useCallback(async () => {
486
+ const request = draftRequest();
487
+ if (!request || !symbol || !ticker || !selectedInstance || !normalizedConfig || !gatewayService) return;
488
+
489
+ try {
490
+ setTradeTicketBusy(symbol, true, ticker);
491
+ await gatewayService.connect(normalizedConfig.gateway);
492
+ const preview = await gatewayService.previewOrder(normalizedConfig.gateway, request);
493
+ updateTradingPaneState({ accountId: request.accountId });
494
+ setTradeTicketDraft(symbol, { brokerInstanceId: selectedInstance.id, accountId: request.accountId }, ticker);
495
+ setTradeTicketPreview(symbol, preview, ticker);
496
+ setTradeTicketMessage(symbol, "Review the what-if preview, then press Enter again to submit.", undefined, ticker);
497
+ } catch (error: any) {
498
+ const message = error?.message || "Failed to preview order.";
499
+ setTradeTicketMessage(symbol, undefined, message.replace("Timeout has occurred", "Preview timed out — try again."), ticker);
500
+ } finally {
501
+ setTradeTicketBusy(symbol, false, ticker);
502
+ }
503
+ }, [draftRequest, symbol, ticker, selectedInstance, normalizedConfig, gatewayService]);
504
+
505
+ const submitOrder = useCallback(async () => {
506
+ const request = draftRequest();
507
+ if (!request || !symbol || !ticker || !selectedInstance || !normalizedConfig || !gatewayService) return;
508
+ if (!ticketState.preview) {
509
+ await previewOrder();
510
+ return;
511
+ }
512
+
513
+ try {
514
+ setTradeTicketBusy(symbol, true, ticker);
515
+ await gatewayService.connect(normalizedConfig.gateway);
516
+ let successMessage: string;
517
+ if (ticketState.editingOrderId) {
518
+ await gatewayService.modifyOrder(normalizedConfig.gateway, ticketState.editingOrderId, request);
519
+ successMessage = `Modified order ${ticketState.editingOrderId}.`;
520
+ } else {
521
+ const order = await gatewayService.placeOrder(normalizedConfig.gateway, request);
522
+ successMessage = `Submitted order ${order.orderId}.`;
523
+ }
524
+ setTradeTicketPreview(symbol, null, ticker);
525
+ await refresh();
526
+ setTradeTicketMessage(symbol, successMessage, undefined, ticker, true);
527
+ } catch (error: any) {
528
+ const message = error?.message || "Failed to submit order.";
529
+ setTradeTicketMessage(symbol, undefined, message.replace("Timeout has occurred", "Order timed out — check open orders to verify status."), ticker);
530
+ } finally {
531
+ setTradeTicketBusy(symbol, false, ticker);
532
+ }
533
+ }, [draftRequest, symbol, ticker, selectedInstance, normalizedConfig, gatewayService, ticketState.preview, ticketState.editingOrderId, previewOrder, refresh]);
534
+
535
+ useKeyboard((event) => {
536
+ if (!focused || !symbol || !ticker) return;
537
+
538
+ const isEnter = event.name === "enter" || event.name === "return";
539
+ if (event.name === "escape" && interactive) {
540
+ event.stopPropagation?.();
541
+ exitInteractive();
542
+ return;
543
+ }
544
+ if (isEnter && !interactive) {
545
+ event.stopPropagation?.();
546
+ enterInteractive();
547
+ return;
548
+ }
549
+ if (!interactive) return;
550
+ event.stopPropagation?.();
551
+
552
+ if (ticketState.busy) return;
553
+
554
+ switch (event.name) {
555
+ case "r":
556
+ refresh().catch(() => {});
557
+ break;
558
+ case "i":
559
+ chooseBrokerInstance().catch(() => {});
560
+ break;
561
+ case "s":
562
+ case "/":
563
+ chooseInstrument().catch(() => {});
564
+ break;
565
+ case "a":
566
+ chooseAccount().catch(() => {});
567
+ break;
568
+ case "b":
569
+ setTradeTicketDraft(symbol, { action: "BUY" }, ticker);
570
+ break;
571
+ case "v":
572
+ setTradeTicketDraft(symbol, { action: "SELL" }, ticker);
573
+ break;
574
+ case "q":
575
+ editNumericField("Quantity", ticketState.draft.quantity, (value) => {
576
+ if (value != null) setTradeTicketDraft(symbol, { quantity: value }, ticker);
577
+ }).catch(() => {});
578
+ break;
579
+ case "t":
580
+ editOrderType().catch(() => {});
581
+ break;
582
+ case "l":
583
+ if (isLimitOrder(ticketState.draft.orderType)) {
584
+ editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => {
585
+ setTradeTicketDraft(symbol, { limitPrice: value }, ticker);
586
+ }).catch(() => {});
587
+ }
588
+ break;
589
+ case "x":
590
+ if (isStopOrder(ticketState.draft.orderType)) {
591
+ editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => {
592
+ setTradeTicketDraft(symbol, { stopPrice: value }, ticker);
593
+ }).catch(() => {});
594
+ }
595
+ break;
596
+ case "p":
597
+ previewOrder().catch(() => {});
598
+ break;
599
+ case "return":
600
+ case "enter":
601
+ submitOrder().catch(() => {});
602
+ break;
603
+ }
604
+ });
605
+
606
+ if (!ticker || !symbol) {
607
+ return (
608
+ <box flexGrow={1} alignItems="center" justifyContent="center">
609
+ <text fg={colors.textDim}>Select a ticker to draft an IBKR trade.</text>
610
+ </box>
611
+ );
612
+ }
613
+
614
+ const showLimit = isLimitOrder(ticketState.draft.orderType);
615
+ const showStop = isStopOrder(ticketState.draft.orderType);
616
+ const hasProfile = Boolean(selectedInstance);
617
+ const wideLayout = width >= 112;
618
+ const previewPanelWidth = wideLayout ? Math.min(38, Math.max(30, Math.floor(width * 0.28))) : undefined;
619
+ const ticketPanelWidth = wideLayout ? Math.max(52, width - (previewPanelWidth ?? 0) - 5) : Math.max(34, width - 4);
620
+ const fieldsPerRow = ticketPanelWidth >= 96 ? 4 : ticketPanelWidth >= 70 ? 3 : 2;
621
+ const fieldWidth = Math.max(16, Math.floor((ticketPanelWidth - fieldsPerRow) / fieldsPerRow));
622
+ const coreFieldWidth = ticketPanelWidth >= 76
623
+ ? Math.max(16, Math.floor((ticketPanelWidth - 4) / 3))
624
+ : fieldWidth;
625
+ const orderFieldWidth = ticketPanelWidth >= 76
626
+ ? Math.max(16, Math.floor((ticketPanelWidth - 5) / 4))
627
+ : fieldWidth;
628
+ const fieldTextWidth = Math.max(8, fieldWidth - 3);
629
+ const activeContract = ticketState.draft.contract.symbol ? ticketState.draft.contract : normalizeContract(ticker);
630
+ const hasContract = Boolean(activeContract.symbol);
631
+ const hasAccount = Boolean(currentAccountId);
632
+ const hasPreview = Boolean(ticketState.preview);
633
+ const previewTextWidth = Math.max(18, (previewPanelWidth ?? Math.max(34, width - 6)) - 4);
634
+ const contractValue = truncateText(formatContractLabel(activeContract), fieldTextWidth);
635
+ const contractMeta = truncateText(
636
+ ticketState.contractName || ticker.metadata.name || "Using current ticker",
637
+ Math.max(fieldTextWidth * Math.max(2, fieldsPerRow), 18),
638
+ );
639
+ const connectionTone: TradeTone = gatewaySnapshot.status.state === "connected"
640
+ ? "positive"
641
+ : gatewaySnapshot.status.state === "error"
642
+ ? "negative"
643
+ : "neutral";
644
+ const statusTone: TradeTone = ticketState.lastError
645
+ ? "negative"
646
+ : ticketState.isSuccess
647
+ ? "positive"
648
+ : "accent";
649
+ const statusText = ticketState.busy
650
+ ? "Working…"
651
+ : ticketState.lastError
652
+ || ticketState.lastInfo
653
+ || gatewaySnapshot.status.message
654
+ || gatewaySnapshot.lastError
655
+ || "Click a workflow step or field to activate the ticket, then preview before submit.";
656
+ const previewTone: TradeTone = !ticketState.preview
657
+ ? "neutral"
658
+ : ticketState.preview.warningText
659
+ ? "negative"
660
+ : "positive";
661
+ const previewHeading = !ticketState.preview
662
+ ? "Preview required"
663
+ : ticketState.preview.warningText
664
+ ? "Preview warning"
665
+ : "Preview ready";
666
+ const previewMetricWidth = Math.max(14, Math.floor(((previewPanelWidth ?? Math.max(34, width - 6)) - 6) / 2));
667
+ const nextStep = !hasProfile
668
+ ? "Choose profile"
669
+ : !hasContract
670
+ ? "Confirm ticker"
671
+ : !hasAccount
672
+ ? "Choose account"
673
+ : !hasPreview
674
+ ? "Run preview"
675
+ : ticketState.editingOrderId
676
+ ? "Submit change"
677
+ : "Submit order";
678
+ const workflowTone: TradeTone = hasPreview
679
+ ? "positive"
680
+ : hasProfile && hasContract && hasAccount
681
+ ? "accent"
682
+ : "neutral";
683
+ const ticketHint = interactive
684
+ ? "Esc releases the ticket. Field shortcuts stay active while captured."
685
+ : "Click a field or press Enter to edit. Shortcuts are shown inline.";
686
+
687
+ const renderFieldPill = ({
688
+ id,
689
+ label,
690
+ value,
691
+ valueColor,
692
+ valueAttributes = 0,
693
+ disabled = false,
694
+ active = false,
695
+ widthOverride,
696
+ onPress,
697
+ }: {
698
+ id: string;
699
+ label: string;
700
+ value: string;
701
+ valueColor?: string;
702
+ valueAttributes?: number;
703
+ disabled?: boolean;
704
+ active?: boolean;
705
+ widthOverride?: number;
706
+ onPress?: () => void;
707
+ }) => {
708
+ const itemWidth = widthOverride ?? fieldWidth;
709
+ const valueWidth = Math.max(4, itemWidth - label.length - 3);
710
+ const hovered = hoveredField === id;
711
+ const backgroundColor = disabled
712
+ ? colors.panel
713
+ : active
714
+ ? colors.selected
715
+ : hovered
716
+ ? fieldHoverBg
717
+ : colors.panel;
718
+ const labelColor = disabled
719
+ ? colors.textMuted
720
+ : active
721
+ ? colors.selectedText
722
+ : hovered
723
+ ? colors.textBright
724
+ : colors.textDim;
725
+ const resolvedValueColor = active
726
+ ? colors.selectedText
727
+ : hovered
728
+ ? colors.textBright
729
+ : valueColor ?? (disabled ? colors.textMuted : colors.text);
730
+
731
+ return (
732
+ <box
733
+ key={id}
734
+ width={itemWidth}
735
+ minWidth={16}
736
+ height={1}
737
+ flexDirection="row"
738
+ backgroundColor={backgroundColor}
739
+ paddingX={1}
740
+ marginRight={1}
741
+ onMouseMove={() => {
742
+ if (!disabled) setHoveredField(id);
743
+ }}
744
+ onMouseDown={disabled ? undefined : () => {
745
+ enterInteractive();
746
+ onPress?.();
747
+ }}
748
+ >
749
+ <text fg={labelColor}>{label}</text>
750
+ <text fg={resolvedValueColor} attributes={valueAttributes}>
751
+ {` ${truncateText(value, valueWidth)}`}
752
+ </text>
753
+ </box>
754
+ );
755
+ };
756
+
757
+ const renderSummaryPill = ({
758
+ id,
759
+ label,
760
+ value,
761
+ tone = "neutral",
762
+ onPress,
763
+ }: {
764
+ id: string;
765
+ label: string;
766
+ value: string;
767
+ tone?: TradeTone;
768
+ onPress?: () => void;
769
+ }) => {
770
+ const palette = getTradeTonePalette(tone);
771
+
772
+ return (
773
+ <box
774
+ key={id}
775
+ height={1}
776
+ flexDirection="row"
777
+ backgroundColor={palette.background}
778
+ paddingX={1}
779
+ marginRight={1}
780
+ onMouseDown={onPress}
781
+ >
782
+ <text fg={tone === "neutral" ? colors.textDim : palette.text}>{label}</text>
783
+ <text fg={palette.text} attributes={TextAttributes.BOLD}>{` ${value}`}</text>
784
+ </box>
785
+ );
786
+ };
787
+
788
+ const renderPreviewMetric = (label: string, value: string, tone: TradeTone = "neutral") => (
789
+ <box
790
+ key={label}
791
+ width={previewMetricWidth}
792
+ height={1}
793
+ backgroundColor={colors.panel}
794
+ paddingX={1}
795
+ marginRight={1}
796
+ >
797
+ <text fg={tone === "negative" ? colors.negative : tone === "positive" ? colors.positive : colors.text}>
798
+ {truncateText(`${label} ${value}`, Math.max(6, previewMetricWidth - 2))}
799
+ </text>
800
+ </box>
801
+ );
802
+
803
+ return (
804
+ <scrollbox flexGrow={1} scrollY>
805
+ <box
806
+ flexDirection="column"
807
+ paddingX={1}
808
+ paddingBottom={1}
809
+ gap={1}
810
+ onMouseDown={!interactive ? enterInteractive : undefined}
811
+ >
812
+ <box flexDirection="row" flexWrap="wrap" justifyContent="space-between">
813
+ <box flexDirection="column" marginBottom={1}>
814
+ <box height={1} flexDirection="row">
815
+ <text attributes={TextAttributes.BOLD} fg={colors.textBright}>{`Trade ${ticker.metadata.ticker}`}</text>
816
+ {ticker.metadata.name && ticker.metadata.name !== ticker.metadata.ticker && (
817
+ <text fg={colors.textDim}>{` · ${ticker.metadata.name}`}</text>
818
+ )}
819
+ </box>
820
+ <box height={1}>
821
+ <text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote)}</text>
822
+ </box>
823
+ </box>
824
+
825
+ <box flexDirection="row" flexWrap="wrap" justifyContent="flex-end">
826
+ <TradeBadge
827
+ label="Broker"
828
+ value={selectedInstance ? `${selectedInstance.label} ${isGatewayMode ? "Gateway" : "Flex"}` : "Select profile"}
829
+ tone={connectionTone}
830
+ onPress={() => {
831
+ enterInteractive();
832
+ chooseBrokerInstance().catch(() => {});
833
+ }}
834
+ />
835
+ <TradeBadge
836
+ label="Account"
837
+ value={currentAccountId || (lockedBrokerInstanceId ? "Locked" : "Select")}
838
+ tone={hasAccount ? "accent" : "neutral"}
839
+ onPress={() => {
840
+ enterInteractive();
841
+ chooseAccount().catch(() => {});
842
+ }}
843
+ />
844
+ <TradeBadge
845
+ label="Net Liq"
846
+ value={activeAccount ? formatCurrency(activeAccount.netLiquidation || 0, activeAccount.currency || "USD") : "—"}
847
+ tone="neutral"
848
+ onPress={activeAccount ? undefined : () => {
849
+ enterInteractive();
850
+ chooseAccount().catch(() => {});
851
+ }}
852
+ />
853
+ </box>
854
+ </box>
855
+
856
+ <box flexDirection="row" flexWrap="wrap">
857
+ {renderSummaryPill({ id: "next", label: "Next", value: nextStep, tone: workflowTone })}
858
+ {renderSummaryPill({
859
+ id: "ticket",
860
+ label: "Ticket",
861
+ value: interactive ? "Captured" : "Standby",
862
+ tone: interactive ? "accent" : "neutral",
863
+ onPress: () => (interactive ? exitInteractive() : enterInteractive()),
864
+ })}
865
+ <Button
866
+ label="Refresh"
867
+ shortcut="r"
868
+ variant="ghost"
869
+ disabled={ticketState.busy}
870
+ onPress={() => refresh().catch(() => {})}
871
+ />
872
+ </box>
873
+
874
+ <box backgroundColor={getTradeTonePalette(statusTone).background} paddingX={1}>
875
+ <text fg={ticketState.lastError ? colors.negative : ticketState.isSuccess ? colors.positive : colors.text}>
876
+ {statusText}
877
+ </text>
878
+ </box>
879
+
880
+ <box flexDirection={wideLayout ? "row" : "column"} alignItems="stretch" gap={1}>
881
+ <box
882
+ flexDirection="column"
883
+ flexGrow={1}
884
+ width={wideLayout ? ticketPanelWidth : undefined}
885
+ border
886
+ borderStyle="rounded"
887
+ borderColor={interactive ? colors.borderFocused : colors.border}
888
+ paddingX={1}
889
+ >
890
+ <box height={1} flexDirection="row">
891
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>Ticket</text>
892
+ <box flexGrow={1} />
893
+ <text fg={interactive ? colors.positive : colors.textMuted}>
894
+ {interactive ? "Captured" : "Click field / Enter"}
895
+ </text>
896
+ </box>
897
+ <text fg={colors.textMuted}>{truncateText(ticketHint, Math.max(ticketPanelWidth - 4, 24))}</text>
898
+ <box height={1} />
899
+
900
+ <box flexDirection="row" flexWrap="wrap">
901
+ {renderFieldPill({
902
+ id: "profile",
903
+ label: "Profile",
904
+ value: selectedInstance ? selectedInstance.label : "Choose profile",
905
+ active: hasProfile,
906
+ widthOverride: coreFieldWidth,
907
+ onPress: () => chooseBrokerInstance().catch(() => {}),
908
+ })}
909
+ {renderFieldPill({
910
+ id: "contract",
911
+ label: "Ticker",
912
+ value: contractValue,
913
+ active: hasContract,
914
+ widthOverride: coreFieldWidth,
915
+ onPress: () => chooseInstrument().catch(() => {}),
916
+ })}
917
+ {renderFieldPill({
918
+ id: "account",
919
+ label: "Account",
920
+ value: currentAccountId || "Select account",
921
+ active: hasAccount,
922
+ widthOverride: coreFieldWidth,
923
+ onPress: () => chooseAccount().catch(() => {}),
924
+ })}
925
+ </box>
926
+ <box height={1} />
927
+ <box flexDirection="row" flexWrap="wrap">
928
+ {renderFieldPill({
929
+ id: "action",
930
+ label: "Side [b/v]",
931
+ value: ticketState.draft.action,
932
+ valueColor: ticketState.draft.action === "BUY" ? colors.positive : colors.negative,
933
+ valueAttributes: TextAttributes.BOLD,
934
+ widthOverride: orderFieldWidth,
935
+ onPress: () => {
936
+ setTradeTicketDraft(symbol, { action: ticketState.draft.action === "BUY" ? "SELL" : "BUY" }, ticker);
937
+ },
938
+ })}
939
+ {renderFieldPill({
940
+ id: "orderType",
941
+ label: "Type [t]",
942
+ value: ticketState.draft.orderType,
943
+ widthOverride: orderFieldWidth,
944
+ onPress: () => editOrderType().catch(() => {}),
945
+ })}
946
+ {renderFieldPill({
947
+ id: "quantity",
948
+ label: "Qty [q]",
949
+ value: formatNumber(ticketState.draft.quantity, 0),
950
+ widthOverride: orderFieldWidth,
951
+ onPress: () => {
952
+ editNumericField("Quantity", ticketState.draft.quantity, (value) => {
953
+ if (value != null && symbol && ticker) setTradeTicketDraft(symbol, { quantity: value }, ticker);
954
+ }).catch(() => {});
955
+ },
956
+ })}
957
+ {showLimit && renderFieldPill({
958
+ id: "limitPrice",
959
+ label: "Limit [l]",
960
+ value: ticketState.draft.limitPrice != null ? ticketState.draft.limitPrice.toFixed(2) : "—",
961
+ widthOverride: orderFieldWidth,
962
+ onPress: () => {
963
+ editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => {
964
+ if (symbol && ticker) setTradeTicketDraft(symbol, { limitPrice: value }, ticker);
965
+ }).catch(() => {});
966
+ },
967
+ })}
968
+ {showStop && renderFieldPill({
969
+ id: "stopPrice",
970
+ label: "Stop [x]",
971
+ value: ticketState.draft.stopPrice != null ? ticketState.draft.stopPrice.toFixed(2) : "—",
972
+ widthOverride: orderFieldWidth,
973
+ onPress: () => {
974
+ editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => {
975
+ if (symbol && ticker) setTradeTicketDraft(symbol, { stopPrice: value }, ticker);
976
+ }).catch(() => {});
977
+ },
978
+ })}
979
+ {renderFieldPill({
980
+ id: "tif",
981
+ label: "TIF",
982
+ value: ticketState.draft.tif || "DAY",
983
+ widthOverride: orderFieldWidth,
984
+ })}
985
+ {ticketState.editingOrderId && renderFieldPill({
986
+ id: "editing",
987
+ label: "Mode",
988
+ value: `Edit #${ticketState.editingOrderId}`,
989
+ valueColor: colors.textBright,
990
+ widthOverride: orderFieldWidth,
991
+ })}
992
+ </box>
993
+ <box height={1} />
994
+ <text fg={colors.textMuted}>{contractMeta}</text>
995
+ </box>
996
+
997
+ <box
998
+ flexDirection="column"
999
+ width={previewPanelWidth}
1000
+ minWidth={34}
1001
+ border
1002
+ borderStyle="rounded"
1003
+ borderColor={getTradeTonePalette(previewTone).border}
1004
+ paddingX={1}
1005
+ >
1006
+ <box height={1} flexDirection="row">
1007
+ <text fg={colors.textBright} attributes={TextAttributes.BOLD}>Preview</text>
1008
+ <box flexGrow={1} />
1009
+ <text fg={getTradeTonePalette(previewTone).text}>{previewHeading}</text>
1010
+ </box>
1011
+ <text fg={ticketState.preview?.warningText ? colors.negative : colors.textMuted}>
1012
+ {truncateText(formatPreviewSummary(ticketState.preview), previewTextWidth)}
1013
+ </text>
1014
+
1015
+ <box flexDirection="row" flexWrap="wrap">
1016
+ {renderPreviewMetric(
1017
+ "Fee",
1018
+ ticketState.preview?.commission != null
1019
+ ? formatCurrency(ticketState.preview.commission, ticketState.preview.commissionCurrency || "USD")
1020
+ : "—",
1021
+ )}
1022
+ {renderPreviewMetric("Init", formatPreviewMetric(ticketState.preview?.initMarginBefore, ticketState.preview?.initMarginAfter))}
1023
+ {renderPreviewMetric("Maint", formatPreviewMetric(ticketState.preview?.maintMarginBefore, ticketState.preview?.maintMarginAfter))}
1024
+ {renderPreviewMetric("Equity", formatPreviewMetric(ticketState.preview?.equityWithLoanBefore, ticketState.preview?.equityWithLoanAfter))}
1025
+ {ticketState.preview?.warningText && renderPreviewMetric("Warn", ticketState.preview.warningText, "negative")}
1026
+ </box>
1027
+
1028
+ <box flexDirection="row" flexWrap="wrap">
1029
+ <Button
1030
+ label="Preview"
1031
+ shortcut="p"
1032
+ variant="secondary"
1033
+ disabled={ticketState.busy}
1034
+ onPress={() => previewOrder().catch(() => {})}
1035
+ />
1036
+ <box width={1} />
1037
+ <Button
1038
+ label={ticketState.editingOrderId ? "Submit Change" : "Submit Order"}
1039
+ shortcut="Enter"
1040
+ variant="primary"
1041
+ disabled={!ticketState.preview || ticketState.busy}
1042
+ onPress={() => submitOrder().catch(() => {})}
1043
+ />
1044
+ </box>
1045
+ </box>
1046
+ </box>
1047
+ </box>
1048
+ </scrollbox>
1049
+ );
1050
+ }