gloomberb 0.2.0 → 0.3.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 (311) hide show
  1. package/README.md +20 -19
  2. package/bin/gloomberb +3 -0
  3. package/package.json +5 -4
  4. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  5. package/src/app.tsx +1365 -0
  6. package/src/brokers/sync-broker-instance.test.ts +100 -0
  7. package/src/brokers/sync-broker-instance.ts +341 -0
  8. package/src/cli/commands/plugins.ts +173 -0
  9. package/src/cli/commands/portfolio.ts +210 -0
  10. package/src/cli/commands/search.ts +120 -0
  11. package/src/cli/commands/ticker.ts +375 -0
  12. package/src/cli/commands/watchlist.ts +210 -0
  13. package/src/cli/context.ts +62 -0
  14. package/src/cli/errors.ts +13 -0
  15. package/src/cli/helpers.ts +85 -0
  16. package/src/cli/index.ts +143 -0
  17. package/src/cli/ticker-resolution.ts +39 -0
  18. package/src/cli/types.ts +15 -0
  19. package/src/cli.test.ts +361 -0
  20. package/src/components/chart/chart-data.ts +239 -0
  21. package/src/components/chart/chart-renderer.ts +1199 -0
  22. package/src/components/chart/chart-types.ts +82 -0
  23. package/src/components/chart/chart-viewport.ts +21 -0
  24. package/src/components/chart/chart.test.ts +439 -0
  25. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  26. package/src/components/chart/comparison-chart-data.ts +316 -0
  27. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  28. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  29. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  30. package/src/components/chart/cursor-motion.ts +48 -0
  31. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  32. package/src/components/chart/native/kitty-adapter.ts +12 -0
  33. package/src/components/chart/native/kitty-manager.ts +142 -0
  34. package/src/components/chart/native/kitty-protocol.ts +110 -0
  35. package/src/components/chart/native/kitty-support.ts +71 -0
  36. package/src/components/chart/native/kitty.test.ts +620 -0
  37. package/src/components/chart/native/renderer-selection.ts +24 -0
  38. package/src/components/chart/native/surface-manager.ts +225 -0
  39. package/src/components/chart/native/surface-sync.ts +34 -0
  40. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  41. package/src/components/chart/stock-chart.test.ts +107 -0
  42. package/src/components/chart/stock-chart.tsx +1556 -0
  43. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  44. package/src/components/command-bar/command-bar.test.tsx +1546 -0
  45. package/src/components/command-bar/command-bar.tsx +4841 -0
  46. package/src/components/command-bar/command-registry.ts +269 -0
  47. package/src/components/command-bar/helpers.test.ts +224 -0
  48. package/src/components/command-bar/helpers.ts +371 -0
  49. package/src/components/command-bar/root-shortcuts.ts +158 -0
  50. package/src/components/command-bar/view-model.test.ts +152 -0
  51. package/src/components/command-bar/view-model.ts +185 -0
  52. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  53. package/src/components/command-bar/workflow-ops.ts +433 -0
  54. package/src/components/command-bar/workflow-types.ts +127 -0
  55. package/src/components/detail-feed-view.test.tsx +105 -0
  56. package/src/components/detail-feed-view.tsx +295 -0
  57. package/src/components/index.ts +32 -0
  58. package/src/components/layout/floating-pane.tsx +49 -0
  59. package/src/components/layout/header.tsx +151 -0
  60. package/src/components/layout/pane-header.tsx +44 -0
  61. package/src/components/layout/pane.tsx +49 -0
  62. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  63. package/src/components/layout/pixel-mouse.test.ts +40 -0
  64. package/src/components/layout/shell.test.tsx +651 -0
  65. package/src/components/layout/shell.tsx +1200 -0
  66. package/src/components/layout/status-bar.test.tsx +173 -0
  67. package/src/components/layout/status-bar.tsx +138 -0
  68. package/src/components/markdown-editor.tsx +134 -0
  69. package/src/components/markdown-text.tsx +218 -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 +347 -0
  97. package/src/data/config-store.ts +507 -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 +55 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +350 -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 +460 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +270 -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 +224 -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 +162 -0
  259. package/src/state/app-context.tsx +981 -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 +444 -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 +282 -0
  285. package/src/updater.ts +303 -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 -68
@@ -0,0 +1,438 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { useKeyboard } from "@opentui/react";
3
+ import { useDialog } from "@opentui-ui/dialog/react";
4
+ import { useCallback, useEffect } from "react";
5
+ import { resolveTickerFinancialsForInstrument } from "../../market-data/coordinator";
6
+ import { instrumentFromTicker } from "../../market-data/request-types";
7
+ import { useAppState, usePaneCollection, usePaneInstanceId } from "../../state/app-context";
8
+ import { colors, priceColor } from "../../theme/colors";
9
+ import type { PaneProps } from "../../types/plugin";
10
+ import type { Quote } from "../../types/financials";
11
+ import { formatCurrency, padTo } from "../../utils/format";
12
+ import { getBrokerInstance } from "../../utils/broker-instances";
13
+ import { getSharedRegistry } from "../registry";
14
+ import { isGatewayConfigured } from "./config";
15
+ import { ChoiceDialog } from "./dialogs";
16
+ import { useIbkrGatewaySelection } from "./gateway-selection";
17
+ import { refreshGatewayData } from "./gateway-helpers";
18
+ import {
19
+ getTradingPaneState,
20
+ loadOrderIntoDraft,
21
+ setTradingBusy,
22
+ setTradingMessage,
23
+ updateTradingPaneState,
24
+ useTradingPaneState,
25
+ } from "./trading-state";
26
+ import {
27
+ findTickerForOrder,
28
+ getKnownIbkrAccounts,
29
+ inferDraftAccountId,
30
+ isMarketDataWarning,
31
+ } from "./trade-utils";
32
+
33
+ export function TradingPane({ focused, width, height }: PaneProps) {
34
+ const { state, dispatch } = useAppState();
35
+ const paneId = usePaneInstanceId();
36
+ const { collectionId } = usePaneCollection(paneId);
37
+ const dialog = useDialog();
38
+ const tradeState = useTradingPaneState();
39
+ const {
40
+ activePortfolio,
41
+ gatewayInstances,
42
+ lockedBrokerInstanceId,
43
+ selectedInstance,
44
+ gatewaySnapshot,
45
+ gatewayService,
46
+ normalizedConfig,
47
+ isGatewayMode,
48
+ availableAccounts,
49
+ gatewayRequiredMessage,
50
+ } = useIbkrGatewaySelection(
51
+ state.config,
52
+ state.brokerAccounts,
53
+ collectionId,
54
+ tradeState.brokerInstanceId,
55
+ );
56
+ const statusMessage = gatewaySnapshot.status.message || gatewaySnapshot.lastError;
57
+ const displayStatusState = gatewaySnapshot.status.state === "error" && isMarketDataWarning(statusMessage)
58
+ ? "connected"
59
+ : gatewaySnapshot.status.state;
60
+ const selectedOrder = gatewaySnapshot.openOrders[tradeState.selectedOpenOrderIndex] ?? null;
61
+
62
+ useEffect(() => {
63
+ if (!focused) return;
64
+ dispatch({ type: "SET_INPUT_CAPTURED", captured: true });
65
+ return () => {
66
+ dispatch({ type: "SET_INPUT_CAPTURED", captured: false });
67
+ };
68
+ }, [focused, dispatch]);
69
+
70
+ useEffect(() => {
71
+ if (!selectedInstance) return;
72
+ const lockedAccountId = lockedBrokerInstanceId === selectedInstance.id ? activePortfolio?.brokerAccountId : undefined;
73
+ if (
74
+ tradeState.brokerInstanceId !== selectedInstance.id
75
+ || tradeState.brokerLabel !== selectedInstance.label
76
+ || (lockedAccountId && tradeState.accountId !== lockedAccountId)
77
+ ) {
78
+ updateTradingPaneState((current) => ({
79
+ ...current,
80
+ brokerInstanceId: selectedInstance.id,
81
+ brokerLabel: selectedInstance.label,
82
+ accountId: lockedAccountId ?? current.accountId,
83
+ selectedOpenOrderIndex: 0,
84
+ }));
85
+ }
86
+ }, [
87
+ selectedInstance,
88
+ tradeState.brokerInstanceId,
89
+ tradeState.brokerLabel,
90
+ tradeState.accountId,
91
+ lockedBrokerInstanceId,
92
+ activePortfolio?.brokerAccountId,
93
+ ]);
94
+
95
+ useEffect(() => {
96
+ if (!isGatewayMode || availableAccounts.length === 0 || tradeState.accountId || !selectedInstance) return;
97
+ const inferred = inferDraftAccountId(
98
+ state.config,
99
+ collectionId,
100
+ availableAccounts,
101
+ selectedInstance.id,
102
+ tradeState.accountId,
103
+ );
104
+ if (inferred) {
105
+ updateTradingPaneState({ accountId: inferred });
106
+ }
107
+ }, [isGatewayMode, availableAccounts, tradeState.accountId, state.config, collectionId, selectedInstance]);
108
+
109
+ const refresh = useCallback(async () => {
110
+ if (!selectedInstance || !normalizedConfig || !isGatewayMode || !isGatewayConfigured(selectedInstance.config)) {
111
+ setTradingMessage(undefined, gatewayRequiredMessage);
112
+ return;
113
+ }
114
+
115
+ try {
116
+ setTradingBusy(true);
117
+ await refreshGatewayData(selectedInstance);
118
+ const inferred = inferDraftAccountId(
119
+ state.config,
120
+ collectionId,
121
+ availableAccounts,
122
+ selectedInstance.id,
123
+ tradeState.accountId,
124
+ );
125
+ if (inferred && !getTradingPaneState().accountId) {
126
+ updateTradingPaneState({ accountId: inferred });
127
+ }
128
+ setTradingMessage(`Refreshed ${selectedInstance.label}.`, undefined);
129
+ } catch (error: any) {
130
+ setTradingMessage(undefined, error?.message || `Failed to refresh ${selectedInstance.label}.`);
131
+ } finally {
132
+ setTradingBusy(false);
133
+ }
134
+ }, [
135
+ selectedInstance,
136
+ normalizedConfig,
137
+ isGatewayMode,
138
+ state.config,
139
+ collectionId,
140
+ availableAccounts,
141
+ tradeState.accountId,
142
+ gatewayRequiredMessage,
143
+ ]);
144
+
145
+ useEffect(() => {
146
+ if (!selectedInstance || !normalizedConfig || !isGatewayMode || !isGatewayConfigured(selectedInstance.config)) return;
147
+ refresh().catch(() => {});
148
+ }, [selectedInstance?.id, isGatewayMode, normalizedConfig ? JSON.stringify(normalizedConfig.gateway) : ""]);
149
+
150
+ const chooseBrokerInstance = useCallback(async () => {
151
+ if (lockedBrokerInstanceId) {
152
+ setTradingMessage(undefined, "This console is locked to the active broker-managed portfolio.");
153
+ return;
154
+ }
155
+ if (gatewayInstances.length === 0) {
156
+ setTradingMessage(undefined, "Connect a Gateway / TWS IBKR profile first.");
157
+ return;
158
+ }
159
+ const selected = await dialog.prompt<string>({
160
+ content: (ctx) => (
161
+ <ChoiceDialog
162
+ {...ctx}
163
+ title="Choose IBKR Profile"
164
+ choices={gatewayInstances.map((instance) => ({
165
+ id: instance.id,
166
+ label: instance.label,
167
+ desc: "Gateway / TWS",
168
+ }))}
169
+ />
170
+ ),
171
+ });
172
+ if (!selected) return;
173
+ const instance = getBrokerInstance(state.config.brokerInstances, selected);
174
+ if (!instance) return;
175
+ updateTradingPaneState({
176
+ brokerInstanceId: instance.id,
177
+ brokerLabel: instance.label,
178
+ accountId: undefined,
179
+ selectedOpenOrderIndex: 0,
180
+ lastError: undefined,
181
+ lastInfo: undefined,
182
+ });
183
+ }, [dialog, gatewayInstances, lockedBrokerInstanceId, state.config.brokerInstances]);
184
+
185
+ const chooseAccount = useCallback(async () => {
186
+ if (!selectedInstance || !normalizedConfig || !gatewayService || !isGatewayMode) return;
187
+ if (availableAccounts.length === 0) {
188
+ await refresh();
189
+ }
190
+
191
+ const nextAccounts = getKnownIbkrAccounts(
192
+ state.brokerAccounts,
193
+ selectedInstance.id,
194
+ gatewayService.getSnapshot().accounts,
195
+ );
196
+ if (nextAccounts.length === 0) {
197
+ setTradingMessage(undefined, "No IBKR accounts available.");
198
+ return;
199
+ }
200
+
201
+ const selected = await dialog.prompt<string>({
202
+ content: (ctx) => (
203
+ <ChoiceDialog
204
+ {...ctx}
205
+ title="Choose Account"
206
+ choices={nextAccounts.map((account) => ({
207
+ id: account.accountId,
208
+ label: `${selectedInstance.label} → ${account.accountId}`,
209
+ desc: `${formatCurrency(account.netLiquidation || 0, account.currency || "USD")} net liq`,
210
+ }))}
211
+ />
212
+ ),
213
+ });
214
+ if (!selected) return;
215
+ updateTradingPaneState({ accountId: selected });
216
+ }, [dialog, selectedInstance, normalizedConfig, gatewayService, isGatewayMode, availableAccounts, refresh]);
217
+
218
+ const cancelSelectedOrder = useCallback(async () => {
219
+ if (!selectedOrder || !selectedInstance || !normalizedConfig || !gatewayService || !isGatewayMode) return;
220
+ try {
221
+ setTradingBusy(true);
222
+ await gatewayService.cancelOrder(normalizedConfig.gateway, selectedOrder.orderId);
223
+ setTradingMessage(`Cancelled order ${selectedOrder.orderId}.`, undefined);
224
+ await refresh();
225
+ } catch (error: any) {
226
+ setTradingMessage(undefined, error?.message || "Failed to cancel order.");
227
+ } finally {
228
+ setTradingBusy(false);
229
+ }
230
+ }, [selectedOrder, selectedInstance, normalizedConfig, gatewayService, isGatewayMode, refresh]);
231
+
232
+ const openSelectedOrder = useCallback(() => {
233
+ if (!selectedOrder) return;
234
+ const registry = getSharedRegistry();
235
+ const ticker = findTickerForOrder(selectedOrder, state.tickers);
236
+ if (!ticker) {
237
+ setTradingMessage(undefined, `No local ticker exists for ${selectedOrder.contract.localSymbol || selectedOrder.contract.symbol}.`);
238
+ return;
239
+ }
240
+ loadOrderIntoDraft(ticker.metadata.ticker, selectedOrder, ticker);
241
+ updateTradingPaneState({
242
+ brokerInstanceId: selectedOrder.brokerInstanceId ?? selectedOrder.contract.brokerInstanceId,
243
+ accountId: selectedOrder.accountId,
244
+ lastInfo: `Loaded order ${selectedOrder.orderId} into ${ticker.metadata.ticker}.`,
245
+ lastError: undefined,
246
+ });
247
+ registry?.selectTickerFn(ticker.metadata.ticker, paneId);
248
+ registry?.switchTabFn("ibkr-trade", paneId);
249
+ registry?.switchPanelFn("right");
250
+ }, [selectedOrder, state.tickers, paneId]);
251
+
252
+ useKeyboard((event) => {
253
+ if (!focused) return;
254
+ event.stopPropagation?.();
255
+
256
+ if (event.name === "j" || event.name === "down") {
257
+ const nextIndex = Math.min(tradeState.selectedOpenOrderIndex + 1, Math.max(0, gatewaySnapshot.openOrders.length - 1));
258
+ updateTradingPaneState({ selectedOpenOrderIndex: nextIndex });
259
+ return;
260
+ }
261
+
262
+ if (event.name === "k" || event.name === "up") {
263
+ const nextIndex = Math.max(0, tradeState.selectedOpenOrderIndex - 1);
264
+ updateTradingPaneState({ selectedOpenOrderIndex: nextIndex });
265
+ return;
266
+ }
267
+
268
+ switch (event.name) {
269
+ case "r":
270
+ refresh().catch(() => {});
271
+ break;
272
+ case "i":
273
+ chooseBrokerInstance().catch(() => {});
274
+ break;
275
+ case "a":
276
+ chooseAccount().catch(() => {});
277
+ break;
278
+ case "m":
279
+ case "return":
280
+ case "enter":
281
+ openSelectedOrder();
282
+ break;
283
+ case "c":
284
+ cancelSelectedOrder().catch(() => {});
285
+ break;
286
+ }
287
+ });
288
+
289
+ const activeAccount = availableAccounts.find((account) => account.accountId === (tradeState.accountId || ""));
290
+ const orderPanelWidth = Math.max(36, Math.floor(width * 0.6));
291
+ const listPanelWidth = Math.max(24, width - orderPanelWidth - 1);
292
+ const listHeight = Math.max(4, height - 6);
293
+ const getOrderQuote = useCallback((symbol: string): Quote | null => {
294
+ const ticker = state.tickers.get(symbol) ?? null;
295
+ const instrument = instrumentFromTicker(ticker, symbol);
296
+ return instrument ? resolveTickerFinancialsForInstrument(instrument)?.quote ?? null : null;
297
+ }, [state.tickers]);
298
+
299
+ return (
300
+ <box flexDirection="column" flexGrow={1} paddingX={1}>
301
+ <box flexDirection="row" height={1}>
302
+ <box flexGrow={1}>
303
+ <text fg={
304
+ displayStatusState === "connected"
305
+ ? colors.positive
306
+ : displayStatusState === "error"
307
+ ? colors.negative
308
+ : colors.textDim
309
+ }>
310
+ {selectedInstance
311
+ ? `${selectedInstance.label} · ${isGatewayMode ? "Gateway" : "Flex"} · ${displayStatusState}`
312
+ : "IBKR · no profile selected"}
313
+ </text>
314
+ </box>
315
+ {tradeState.busy && <text fg={colors.textDim}>Working…</text>}
316
+ </box>
317
+
318
+ <box height={1}>
319
+ <text fg={colors.textDim}>
320
+ {activeAccount
321
+ ? `${selectedInstance?.label || "IBKR"} → ${activeAccount.accountId} · ${formatCurrency(activeAccount.netLiquidation || 0, activeAccount.currency || "USD")} net liq`
322
+ : isGatewayMode
323
+ ? lockedBrokerInstanceId
324
+ ? `Locked to ${selectedInstance?.label || "IBKR"}`
325
+ : "No account selected"
326
+ : gatewayInstances.length > 0
327
+ ? "Choose a Gateway / TWS profile"
328
+ : "Connect an IBKR profile"}
329
+ </text>
330
+ </box>
331
+
332
+ <box height={1}>
333
+ <text fg={tradeState.lastError ? colors.negative : colors.textDim}>
334
+ {tradeState.lastError
335
+ || gatewaySnapshot.status.message
336
+ || gatewaySnapshot.lastError
337
+ || tradeState.lastInfo
338
+ || "Use this console for profile status, accounts, open orders, and executions."}
339
+ </text>
340
+ </box>
341
+
342
+ <box height={1}>
343
+ <text fg={colors.border}>{"─".repeat(Math.max(1, width - 2))}</text>
344
+ </box>
345
+
346
+ <box flexDirection="row" height={listHeight}>
347
+ <box width={orderPanelWidth} flexDirection="column">
348
+ <text attributes={TextAttributes.BOLD} fg={colors.textBright}>Open Orders</text>
349
+ <scrollbox flexGrow={1} scrollY>
350
+ {gatewaySnapshot.openOrders.length === 0 ? (
351
+ <text fg={colors.textDim}>No open IBKR orders.</text>
352
+ ) : (
353
+ gatewaySnapshot.openOrders.map((order, index) => {
354
+ const selected = index === tradeState.selectedOpenOrderIndex;
355
+ const orderSymbol = order.contract.symbol;
356
+ const orderQuote = getOrderQuote(orderSymbol);
357
+ const bidStr = orderQuote?.bid != null ? orderQuote.bid.toFixed(2) : "---";
358
+ const askStr = orderQuote?.ask != null ? orderQuote.ask.toFixed(2) : "---";
359
+ const orderPrice = order.limitPrice != null
360
+ ? order.limitPrice.toFixed(2)
361
+ : order.stopPrice != null
362
+ ? order.stopPrice.toFixed(2)
363
+ : "MKT";
364
+ return (
365
+ <box
366
+ key={order.orderId}
367
+ backgroundColor={selected ? colors.selected : colors.bg}
368
+ onMouseDown={() => {
369
+ if (selected) {
370
+ openSelectedOrder();
371
+ } else {
372
+ updateTradingPaneState({ selectedOpenOrderIndex: index });
373
+ }
374
+ }}
375
+ >
376
+ <text fg={selected ? colors.text : colors.textDim}>
377
+ {selected ? "▸ " : " "}
378
+ {padTo(String(order.orderId), 6)}
379
+ {padTo(order.action, 5)}
380
+ {padTo(order.contract.localSymbol || order.contract.symbol, 14)}
381
+ {padTo(order.status, 10)}
382
+ {padTo(String(order.remaining), 5, "right")}
383
+ {" "}
384
+ {padTo(orderPrice, 9)}
385
+ {padTo(`B:${bidStr}`, 10)}
386
+ {`A:${askStr}`}
387
+ </text>
388
+ </box>
389
+ );
390
+ })
391
+ )}
392
+ </scrollbox>
393
+ </box>
394
+
395
+ <box width={1}>
396
+ <text fg={colors.border}>│</text>
397
+ </box>
398
+
399
+ <box width={listPanelWidth} flexDirection="column">
400
+ <text attributes={TextAttributes.BOLD} fg={colors.textBright}>Executions</text>
401
+ <scrollbox flexGrow={1} scrollY>
402
+ {gatewaySnapshot.executions.length === 0 ? (
403
+ <text fg={colors.textDim}>No recent executions.</text>
404
+ ) : (
405
+ gatewaySnapshot.executions.slice(0, 20).map((execution) => (
406
+ <box
407
+ key={execution.execId}
408
+ onMouseDown={() => {
409
+ const symbol = execution.contract.symbol;
410
+ if (symbol && state.tickers.has(symbol)) {
411
+ getSharedRegistry()?.selectTickerFn(symbol, paneId);
412
+ }
413
+ }}
414
+ >
415
+ <text fg={priceColor(execution.side.toUpperCase() === "BOT" ? 1 : -1)}>
416
+ {padTo(execution.side, 5)}
417
+ {padTo(execution.contract.localSymbol || execution.contract.symbol, 18)}
418
+ {padTo(String(execution.shares), 6, "right")}
419
+ {" "}
420
+ {execution.price.toFixed(2)}
421
+ </text>
422
+ </box>
423
+ ))
424
+ )}
425
+ </scrollbox>
426
+ </box>
427
+ </box>
428
+
429
+ <box flexDirection="row" height={1}>
430
+ <text fg={colors.textMuted} onMouseDown={() => chooseBrokerInstance().catch(() => {})}>{" [i] Profile "}</text>
431
+ <text fg={colors.textMuted} onMouseDown={() => chooseAccount().catch(() => {})}>{" [a] Account "}</text>
432
+ <text fg={colors.textMuted} onMouseDown={() => openSelectedOrder()}>{" [Enter] Open "}</text>
433
+ <text fg={colors.textMuted} onMouseDown={() => cancelSelectedOrder().catch(() => {})}>{" [c] Cancel "}</text>
434
+ <text fg={colors.textMuted} onMouseDown={() => refresh().catch(() => {})}>{" [r] Refresh "}</text>
435
+ </box>
436
+ </box>
437
+ );
438
+ }
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import type { TickerRecord } from "../../types/ticker";
3
+ import type { BrokerOrder } from "../../types/trading";
4
+ import {
5
+ clearTradingDraft,
6
+ getTradeTicketState,
7
+ loadOrderIntoDraft,
8
+ prefillTradeFromTicker,
9
+ removeBrokerInstanceFromTradingState,
10
+ } from "./trading-state";
11
+
12
+ function createTicker(symbol: string, brokerInstanceId?: string): TickerRecord {
13
+ return {
14
+ metadata: {
15
+ ticker: symbol,
16
+ exchange: "NASDAQ",
17
+ currency: "USD",
18
+ name: symbol,
19
+ assetCategory: "STK",
20
+ portfolios: [],
21
+ watchlists: [],
22
+ positions: [],
23
+ broker_contracts: brokerInstanceId ? [{
24
+ brokerId: "ibkr",
25
+ brokerInstanceId,
26
+ symbol,
27
+ localSymbol: symbol,
28
+ secType: "STK",
29
+ exchange: "SMART",
30
+ currency: "USD",
31
+ }] : [],
32
+ custom: {},
33
+ tags: [],
34
+ },
35
+ };
36
+ }
37
+
38
+ afterEach(() => {
39
+ clearTradingDraft();
40
+ });
41
+
42
+ describe("ibkr trading state", () => {
43
+ test("keeps separate ticket drafts per ticker", () => {
44
+ const amd = createTicker("AMD", "ibkr-paper");
45
+ const aapl = createTicker("AAPL", "ibkr-paper");
46
+
47
+ prefillTradeFromTicker(amd, "BUY");
48
+ prefillTradeFromTicker(aapl, "SELL");
49
+
50
+ expect(getTradeTicketState("AMD", amd).draft.action).toBe("BUY");
51
+ expect(getTradeTicketState("AAPL", aapl).draft.action).toBe("SELL");
52
+ expect(getTradeTicketState("AMD", amd).draft.contract.symbol).toBe("AMD");
53
+ expect(getTradeTicketState("AAPL", aapl).draft.contract.symbol).toBe("AAPL");
54
+ });
55
+
56
+ test("loads an order into only the targeted ticker ticket", () => {
57
+ const amd = createTicker("AMD", "ibkr-paper");
58
+ const aapl = createTicker("AAPL", "ibkr-paper");
59
+
60
+ prefillTradeFromTicker(amd, "BUY");
61
+ prefillTradeFromTicker(aapl, "BUY");
62
+
63
+ const order: BrokerOrder = {
64
+ orderId: 101,
65
+ brokerInstanceId: "ibkr-paper",
66
+ accountId: "DU12345",
67
+ status: "Submitted",
68
+ action: "SELL",
69
+ orderType: "LMT",
70
+ quantity: 4,
71
+ filled: 0,
72
+ remaining: 4,
73
+ limitPrice: 123.45,
74
+ tif: "DAY",
75
+ updatedAt: Date.now(),
76
+ contract: {
77
+ brokerId: "ibkr",
78
+ brokerInstanceId: "ibkr-paper",
79
+ symbol: "AMD",
80
+ localSymbol: "AMD",
81
+ secType: "STK",
82
+ exchange: "SMART",
83
+ currency: "USD",
84
+ },
85
+ };
86
+
87
+ loadOrderIntoDraft("AMD", order, amd);
88
+
89
+ expect(getTradeTicketState("AMD", amd).editingOrderId).toBe(101);
90
+ expect(getTradeTicketState("AMD", amd).draft.action).toBe("SELL");
91
+ expect(getTradeTicketState("AAPL", aapl).editingOrderId).toBeUndefined();
92
+ expect(getTradeTicketState("AAPL", aapl).draft.action).toBe("BUY");
93
+ });
94
+
95
+ test("removes broker-linked ticket metadata only for the removed instance", () => {
96
+ const amd = createTicker("AMD", "ibkr-paper");
97
+ const aapl = createTicker("AAPL", "ibkr-live");
98
+
99
+ prefillTradeFromTicker(amd, "BUY");
100
+ prefillTradeFromTicker(aapl, "SELL");
101
+
102
+ removeBrokerInstanceFromTradingState("ibkr-paper");
103
+
104
+ expect(getTradeTicketState("AMD", amd).brokerInstanceId).toBeUndefined();
105
+ expect(getTradeTicketState("AMD", amd).draft.contract.brokerInstanceId).toBeUndefined();
106
+ expect(getTradeTicketState("AAPL", aapl).brokerInstanceId).toBe("ibkr-live");
107
+ expect(getTradeTicketState("AAPL", aapl).draft.contract.brokerInstanceId).toBe("ibkr-live");
108
+ });
109
+ });