gloomberb 0.5.1 → 0.6.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 (506) hide show
  1. package/README.md +91 -11
  2. package/package.json +13 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +318 -158
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +53 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/native/surface-visibility.ts +48 -0
  53. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  54. package/src/components/chart/static-chart-surface.tsx +191 -0
  55. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  56. package/src/components/chart/stock-chart.tsx +452 -191
  57. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  58. package/src/components/command-bar/command-bar.test.tsx +269 -20
  59. package/src/components/command-bar/command-bar.tsx +830 -353
  60. package/src/components/command-bar/command-registry.ts +10 -20
  61. package/src/components/command-bar/helpers.test.ts +1 -1
  62. package/src/components/command-bar/helpers.ts +9 -11
  63. package/src/components/command-bar/pane-template-display.ts +4 -8
  64. package/src/components/command-bar/root-shortcuts.ts +12 -11
  65. package/src/components/command-bar/view-model.test.ts +8 -42
  66. package/src/components/command-bar/view-model.ts +8 -36
  67. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  68. package/src/components/command-bar/workflow-ops.ts +40 -9
  69. package/src/components/command-bar/workflow-types.ts +2 -1
  70. package/src/components/data-table-stack-view.test.tsx +206 -0
  71. package/src/components/data-table-stack-view.tsx +152 -0
  72. package/src/components/data-table-view.test.tsx +188 -0
  73. package/src/components/data-table-view.tsx +279 -0
  74. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  75. package/src/components/feed-data-table-stack-view.tsx +365 -0
  76. package/src/components/index.ts +26 -31
  77. package/src/components/layout/detached-pane-shell.tsx +185 -0
  78. package/src/components/layout/floating-pane.tsx +87 -14
  79. package/src/components/layout/header.tsx +139 -49
  80. package/src/components/layout/pane-content.tsx +42 -0
  81. package/src/components/layout/pane-footer.test.tsx +195 -0
  82. package/src/components/layout/pane-footer.tsx +425 -0
  83. package/src/components/layout/pane-header.test.tsx +39 -0
  84. package/src/components/layout/pane-header.tsx +192 -14
  85. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  86. package/src/components/layout/pane-sizing.ts +9 -10
  87. package/src/components/layout/pane-title.ts +35 -0
  88. package/src/components/layout/pane.tsx +52 -11
  89. package/src/components/layout/shell.test.tsx +315 -107
  90. package/src/components/layout/shell.tsx +1010 -368
  91. package/src/components/layout/status-bar.test.tsx +17 -10
  92. package/src/components/layout/status-bar.tsx +367 -100
  93. package/src/components/layout/titlebar-overlay.ts +2 -0
  94. package/src/components/markdown-editor.tsx +29 -14
  95. package/src/components/markdown-text.tsx +25 -24
  96. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  97. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  98. package/src/components/pane-settings-dialog.tsx +521 -89
  99. package/src/components/pane-template-wizard.test.tsx +1 -1
  100. package/src/components/pane-template-wizard.tsx +38 -37
  101. package/src/components/price-selector-dialog.tsx +22 -21
  102. package/src/components/ticker-badge-list.tsx +53 -0
  103. package/src/components/ticker-badge-text.test.tsx +44 -1
  104. package/src/components/ticker-badge-text.tsx +78 -38
  105. package/src/components/ticker-badge.tsx +28 -7
  106. package/src/components/ticker-list-table-view.tsx +248 -0
  107. package/src/components/ticker-list-table.test.tsx +47 -1
  108. package/src/components/ticker-list-table.tsx +212 -41
  109. package/src/components/toggle-list.tsx +114 -8
  110. package/src/components/ui/button.tsx +24 -16
  111. package/src/components/ui/choice-dialog.tsx +89 -0
  112. package/src/components/ui/data-table.tsx +313 -114
  113. package/src/components/ui/external-link.tsx +52 -13
  114. package/src/components/ui/fields.tsx +44 -30
  115. package/src/components/ui/frame.tsx +24 -57
  116. package/src/components/ui/index.ts +15 -50
  117. package/src/components/ui/list-view.tsx +65 -25
  118. package/src/components/ui/loading.tsx +5 -60
  119. package/src/components/ui/message-composer.test.tsx +81 -0
  120. package/src/components/ui/message-composer.tsx +139 -0
  121. package/src/components/ui/multi-select-dialog.tsx +100 -32
  122. package/src/components/ui/multi-select.ts +112 -0
  123. package/src/components/ui/native-select.tsx +120 -0
  124. package/src/components/ui/page-stack-view.tsx +49 -19
  125. package/src/components/ui/remote-image.tsx +50 -0
  126. package/src/components/ui/row-value-cache.test.ts +17 -0
  127. package/src/components/ui/row-value-cache.ts +41 -0
  128. package/src/components/ui/shortcut-hint.tsx +55 -0
  129. package/src/components/ui/status.tsx +12 -67
  130. package/src/components/ui/table-layout.ts +59 -0
  131. package/src/components/ui/tabs.tsx +249 -27
  132. package/src/components/ui/toggle.tsx +19 -127
  133. package/src/components/ui/ui.test.tsx +266 -53
  134. package/src/components/use-double-click-activation.ts +7 -2
  135. package/src/components/use-external-link-footer.ts +79 -0
  136. package/src/core/app-services.ts +95 -0
  137. package/src/core/state/app-state.ts +886 -0
  138. package/src/{state → core/state}/session-persistence.ts +7 -6
  139. package/src/data/config-layout.ts +225 -0
  140. package/src/data/config-store-node.ts +390 -0
  141. package/src/data/config-store.test.ts +72 -0
  142. package/src/data/config-store.ts +35 -478
  143. package/src/data/sqlite-json.ts +2 -5
  144. package/src/data/sqlite-retry.ts +1 -0
  145. package/src/index.tsx +2 -68
  146. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  147. package/src/market-data/coordinator.test.ts +73 -4
  148. package/src/market-data/coordinator.ts +190 -24
  149. package/src/market-data/hooks.test.tsx +1 -1
  150. package/src/market-data/hooks.tsx +109 -87
  151. package/src/market-data/query-store.ts +3 -3
  152. package/src/market-data/request-types.test.ts +0 -14
  153. package/src/market-data/result-types.ts +1 -0
  154. package/src/news/aggregator.test.ts +267 -0
  155. package/src/news/aggregator.ts +452 -0
  156. package/src/news/hooks.ts +39 -0
  157. package/src/news/ticker-symbols.ts +21 -0
  158. package/src/news/types.ts +77 -0
  159. package/src/notifications/app-notifier.ts +21 -10
  160. package/src/pane-settings.test.ts +0 -2
  161. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  162. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  163. package/src/plugins/builtin/ai/providers.ts +1 -9
  164. package/src/plugins/builtin/ai/runner.ts +41 -5
  165. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  166. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  167. package/src/plugins/builtin/ai/settings.ts +0 -1
  168. package/src/plugins/builtin/ai/utils.ts +1 -71
  169. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  170. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  171. package/src/plugins/builtin/alerts/index.tsx +263 -155
  172. package/src/plugins/builtin/alerts/types.ts +7 -0
  173. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  174. package/src/plugins/builtin/analytics/index.tsx +78 -120
  175. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  176. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  177. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  178. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  179. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  180. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  181. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  182. package/src/plugins/builtin/chat-controller.ts +85 -14
  183. package/src/plugins/builtin/chat.test.tsx +283 -36
  184. package/src/plugins/builtin/chat.tsx +363 -170
  185. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  186. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  187. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  188. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  189. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  190. package/src/plugins/builtin/correlation/index.tsx +51 -46
  191. package/src/plugins/builtin/correlation/settings.ts +1 -2
  192. package/src/plugins/builtin/debug.tsx +99 -70
  193. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  194. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  195. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  196. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  197. package/src/plugins/builtin/earnings/index.tsx +187 -94
  198. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  199. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  200. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  201. package/src/plugins/builtin/econ/index.tsx +236 -421
  202. package/src/plugins/builtin/econ/types.ts +1 -0
  203. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  204. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  205. package/src/plugins/builtin/help.test.tsx +120 -60
  206. package/src/plugins/builtin/help.tsx +266 -82
  207. package/src/plugins/builtin/holders/index.test.ts +45 -0
  208. package/src/plugins/builtin/holders/index.tsx +949 -0
  209. package/src/plugins/builtin/insider/index.tsx +207 -253
  210. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  211. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  212. package/src/plugins/builtin/layout-manager.tsx +12 -40
  213. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  214. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  215. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  216. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  217. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  218. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  219. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  220. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  221. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  222. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  223. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  224. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  225. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  226. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  227. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  228. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  229. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  230. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  231. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  232. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  233. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  234. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  235. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  236. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  237. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  238. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  239. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  240. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  241. package/src/plugins/builtin/news.tsx +98 -25
  242. package/src/plugins/builtin/notes-files.ts +51 -10
  243. package/src/plugins/builtin/notes.tsx +332 -131
  244. package/src/plugins/builtin/options.test.tsx +185 -0
  245. package/src/plugins/builtin/options.tsx +346 -206
  246. package/src/plugins/builtin/plugin-groups.ts +92 -0
  247. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  248. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  249. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  250. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  251. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  252. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  253. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  254. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  255. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  256. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  257. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  258. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  259. package/src/plugins/builtin/research/index.tsx +724 -0
  260. package/src/plugins/builtin/sec.tsx +67 -19
  261. package/src/plugins/builtin/sectors/index.tsx +42 -80
  262. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  263. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  264. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  265. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  266. package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
  267. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  268. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  269. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
  270. package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
  271. package/src/plugins/builtin/ticker-surface.ts +68 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  273. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  274. package/src/plugins/builtin/yahoo.ts +55 -1
  275. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  276. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  277. package/src/plugins/catalog-ui.test.ts +24 -0
  278. package/src/plugins/catalog-ui.ts +44 -0
  279. package/src/plugins/catalog.ts +2 -50
  280. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  281. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  282. package/src/plugins/ibkr/account-cache.ts +15 -120
  283. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  284. package/src/plugins/ibkr/config.test.ts +0 -18
  285. package/src/plugins/ibkr/dialogs.tsx +14 -56
  286. package/src/plugins/ibkr/flex.test.ts +57 -2
  287. package/src/plugins/ibkr/flex.ts +116 -12
  288. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  289. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  290. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  291. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  292. package/src/plugins/ibkr/gateway-types.ts +27 -0
  293. package/src/plugins/ibkr/index.test.tsx +3 -11
  294. package/src/plugins/ibkr/index.tsx +1 -0
  295. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  296. package/src/plugins/ibkr/price-normalization.ts +1 -0
  297. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  298. package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
  299. package/src/plugins/ibkr/trade-tab.tsx +166 -117
  300. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  301. package/src/plugins/pane-manager.test.ts +3 -1
  302. package/src/plugins/pane-manager.ts +93 -19
  303. package/src/plugins/plugin-runtime.test.tsx +176 -74
  304. package/src/plugins/plugin-runtime.tsx +228 -10
  305. package/src/plugins/prediction-markets/cache.ts +49 -0
  306. package/src/plugins/prediction-markets/categories.ts +0 -10
  307. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  308. package/src/plugins/prediction-markets/chart.tsx +21 -20
  309. package/src/plugins/prediction-markets/cli.ts +1 -0
  310. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  311. package/src/plugins/prediction-markets/controller.ts +37 -108
  312. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  313. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  314. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  315. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  316. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  317. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  318. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  319. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  320. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  321. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  322. package/src/plugins/prediction-markets/launch.ts +1 -1
  323. package/src/plugins/prediction-markets/metrics.ts +25 -12
  324. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  325. package/src/plugins/prediction-markets/pane.tsx +180 -82
  326. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  327. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  328. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  329. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  330. package/src/plugins/prediction-markets/settings.ts +0 -5
  331. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  332. package/src/plugins/registry.test.ts +307 -0
  333. package/src/plugins/registry.ts +293 -82
  334. package/src/react/input.ts +61 -0
  335. package/src/react/plugins/plugin-slot.tsx +21 -0
  336. package/src/react/use-raf-callback.ts +44 -0
  337. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  338. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  339. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  340. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  341. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  342. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  343. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  344. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  345. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  346. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  347. package/src/renderers/electrobun/bun/index.ts +1149 -0
  348. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  349. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  350. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  351. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  352. package/src/renderers/electrobun/view/app-services.ts +348 -0
  353. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  354. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  355. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  356. package/src/renderers/electrobun/view/config-host.ts +35 -0
  357. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  358. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  359. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  360. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  361. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  362. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  363. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  364. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  365. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  366. package/src/renderers/electrobun/view/input-host.tsx +112 -0
  367. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  368. package/src/renderers/electrobun/view/key-event.ts +134 -0
  369. package/src/renderers/electrobun/view/main.tsx +104 -0
  370. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  372. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  373. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  374. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  375. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  376. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  377. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  378. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  379. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  380. package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
  381. package/src/renderers/electrobun/view/update-host.ts +38 -0
  382. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  383. package/src/renderers/opentui/dialog-host.tsx +56 -0
  384. package/src/renderers/opentui/host.tsx +140 -0
  385. package/src/renderers/opentui/image-loader.test.ts +25 -0
  386. package/src/renderers/opentui/image-loader.ts +105 -0
  387. package/src/renderers/opentui/image-surface.tsx +266 -0
  388. package/src/renderers/opentui/input-host.tsx +63 -0
  389. package/src/renderers/opentui/start.tsx +71 -0
  390. package/src/renderers/opentui/test-utils.tsx +76 -0
  391. package/src/renderers/opentui/toast-host.tsx +10 -0
  392. package/src/renderers/opentui/ui-host.tsx +98 -0
  393. package/src/sources/gloomberb-cloud.test.ts +288 -17
  394. package/src/sources/gloomberb-cloud.ts +176 -9
  395. package/src/sources/provider-errors.ts +1 -0
  396. package/src/sources/provider-router.test.ts +220 -81
  397. package/src/sources/provider-router.ts +294 -111
  398. package/src/sources/sec-edgar.ts +19 -6
  399. package/src/sources/yahoo-finance.test.ts +0 -5
  400. package/src/sources/yahoo-finance.ts +420 -11
  401. package/src/state/app-activity.ts +7 -7
  402. package/src/state/app-bootstrap.ts +114 -40
  403. package/src/state/app-context-selector.test.tsx +70 -1
  404. package/src/state/app-context.test.ts +71 -3
  405. package/src/state/app-context.test.tsx +14 -7
  406. package/src/state/app-context.tsx +209 -869
  407. package/src/state/config-save-scheduler.ts +35 -0
  408. package/src/state/pane-scroll-registry.tsx +254 -0
  409. package/src/state/persist-scheduler.test.ts +67 -0
  410. package/src/state/persist-scheduler.ts +69 -0
  411. package/src/state/selectors-ui.ts +55 -0
  412. package/src/state/selectors.ts +15 -71
  413. package/src/state/session-persistence.test.ts +1 -1
  414. package/src/state/ticker-refresh-queue.ts +52 -3
  415. package/src/state/use-inline-tickers.ts +18 -16
  416. package/src/state/use-quote-streaming.test.ts +0 -6
  417. package/src/state/use-quote-streaming.test.tsx +1 -1
  418. package/src/state/use-quote-streaming.ts +3 -2
  419. package/src/state/use-session-snapshot.ts +32 -17
  420. package/src/test-support/plugin-runtime.ts +149 -0
  421. package/src/theme/color-utils.ts +1 -0
  422. package/src/theme/colors.ts +16 -0
  423. package/src/types/broker.ts +16 -1
  424. package/src/types/capability-route-source.ts +23 -0
  425. package/src/types/config.test.ts +23 -0
  426. package/src/types/config.ts +145 -10
  427. package/src/types/context-menu.ts +95 -0
  428. package/src/types/data-provider.ts +34 -3
  429. package/src/types/desktop-menu.ts +13 -0
  430. package/src/types/desktop-window.ts +29 -0
  431. package/src/types/financials.ts +123 -2
  432. package/src/types/instrument.ts +1 -0
  433. package/src/types/news-source.ts +9 -0
  434. package/src/types/persistence.ts +1 -0
  435. package/src/types/plugin.ts +32 -8
  436. package/src/ui/ascii-font.ts +85 -0
  437. package/src/ui/context-menu.test.ts +94 -0
  438. package/src/ui/context-menu.tsx +433 -0
  439. package/src/ui/dialog.tsx +55 -0
  440. package/src/ui/host.tsx +364 -0
  441. package/src/ui/index.tsx +134 -0
  442. package/src/ui/toast.tsx +40 -0
  443. package/src/updater.test.ts +69 -0
  444. package/src/updater.ts +61 -18
  445. package/src/utils/api-client.test.ts +99 -9
  446. package/src/utils/api-client.ts +378 -18
  447. package/src/utils/back-navigation.ts +6 -0
  448. package/src/utils/cli-output.ts +1 -0
  449. package/src/utils/currency-units.test.ts +1 -6
  450. package/src/utils/currency-units.ts +1 -0
  451. package/src/utils/debug-log.ts +68 -0
  452. package/src/utils/dom-screenshot.ts +326 -0
  453. package/src/utils/exchange-rate-map.ts +1 -0
  454. package/src/utils/exchanges.ts +214 -0
  455. package/src/utils/format.test.ts +7 -0
  456. package/src/utils/format.ts +1 -0
  457. package/src/utils/fuzzy-search.ts +1 -0
  458. package/src/utils/html-entities.test.ts +8 -0
  459. package/src/utils/html-entities.ts +32 -0
  460. package/src/utils/http-transport.ts +11 -0
  461. package/src/utils/main-thread-monitor.ts +127 -0
  462. package/src/utils/perf-marks.ts +54 -0
  463. package/src/utils/quote-freshness.ts +27 -50
  464. package/src/utils/quote-resolution.test.ts +5 -5
  465. package/src/utils/quote-resolution.ts +0 -1
  466. package/src/utils/quote-time.ts +1 -5
  467. package/src/utils/sec.test.ts +0 -12
  468. package/src/utils/selection-clipboard.ts +14 -6
  469. package/src/utils/shortcut-labels.test.ts +20 -0
  470. package/src/utils/shortcut-labels.ts +67 -0
  471. package/src/utils/slugify.ts +1 -0
  472. package/src/utils/terminal-input-reset.ts +1 -0
  473. package/src/utils/text-wrap.ts +82 -0
  474. package/src/utils/throttled-fetch.test.ts +19 -0
  475. package/src/utils/throttled-fetch.ts +11 -9
  476. package/src/utils/ticker-list.test.ts +1 -7
  477. package/src/utils/ticker-list.ts +1 -0
  478. package/src/utils/timestamp.ts +29 -0
  479. package/src/utils/tweet-text.test.ts +13 -0
  480. package/src/utils/tweet-text.ts +18 -0
  481. package/src/utils/url.ts +11 -0
  482. package/src/version.ts +1 -1
  483. package/src/components/chart/chart-control-hint.tsx +0 -39
  484. package/src/components/detail-feed-view.test.tsx +0 -180
  485. package/src/components/detail-feed-view.tsx +0 -531
  486. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  487. package/src/components/layout/pixel-mouse.test.ts +0 -40
  488. package/src/components/spinner.tsx +0 -2
  489. package/src/components/tab-bar.tsx +0 -17
  490. package/src/components/ui/multi-select-chips.tsx +0 -69
  491. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  492. package/src/plugins/builtin/ask-ai.tsx +0 -9
  493. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  494. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  495. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  496. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  497. package/src/plugins/builtin/fred-settings.ts +0 -10
  498. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  499. package/src/plugins/builtin/options-availability.ts +0 -158
  500. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  501. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  502. package/src/plugins/prediction-markets/status.ts +0 -24
  503. package/src/plugins/prediction-markets/table.tsx +0 -87
  504. package/src/utils/quote-time.test.ts +0 -22
  505. package/src/utils/selection-clipboard.test.ts +0 -43
  506. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,110 +1,40 @@
1
- import {
2
- BarSizeSetting,
3
- ConnectionState,
4
- EventName,
5
- IBApiTickType,
6
- IBApiNext,
7
- MarketDataType,
8
- OptionType,
9
- OrderAction,
10
- OrderType,
11
- SecType,
12
- TimeInForce,
13
- WhatToShow,
14
- type Contract,
15
- type ContractDescription,
16
- type ContractDetails,
17
- type MarketDataUpdate,
18
- type Order,
19
- type OrderState,
20
- type TickByTickAllLast,
21
- } from "@stoqey/ib";
22
- import { mkdir, open, readFile, rm } from "fs/promises";
23
- import { createConnection } from "net";
24
- import { tmpdir } from "os";
25
- import { join } from "path";
26
- import { firstValueFrom, filter, take, timeout } from "rxjs";
27
1
  import type { TimeRange } from "../../components/chart/chart-types";
28
- import {
29
- normalizeChartResolutionSupport,
30
- type ChartResolutionSupport,
31
- type ManualChartResolution,
32
- } from "../../components/chart/chart-resolution";
2
+ import type { ChartResolutionSupport, ManualChartResolution } from "../../components/chart/chart-resolution";
3
+ import { getBrokerRemoteClient } from "../../brokers/remote-broker-adapter";
33
4
  import type { BrokerConnectionStatus, BrokerPosition } from "../../types/broker";
34
5
  import type { QuoteSubscriptionTarget } from "../../types/data-provider";
35
- import type { Fundamentals, FinancialStatement, Quote, PricePoint, TickerFinancials } from "../../types/financials";
6
+ import type { Quote, PricePoint, TickerFinancials } from "../../types/financials";
36
7
  import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
37
8
  import type {
38
9
  BrokerAccount,
39
- BrokerCashBalance,
40
10
  BrokerExecution,
41
11
  BrokerOrder,
42
12
  BrokerOrderPreview,
43
13
  BrokerOrderRequest,
44
14
  } from "../../types/trading";
45
- import { debugLog } from "../../utils/debug-log";
46
- import { parseReportSnapshot, parseFinStatements } from "./fundamental-parser";
47
- import { getIbkrPriceDivisor, normalizeIbkrPriceValue } from "./price-normalization";
48
-
49
- export interface IbkrGatewayConfig {
50
- host: string;
51
- port?: number;
52
- clientId?: number;
53
- lastSuccessfulPort?: number;
54
- lastSuccessfulClientId?: number;
55
- marketDataType?: "auto" | "live" | "frozen" | "delayed" | "delayed-frozen";
56
- }
57
-
58
- export interface ResolvedIbkrGatewayConnection {
59
- host: string;
60
- port: number;
61
- clientId: number;
62
- requestedPort?: number;
63
- requestedClientId: number;
64
- }
65
-
66
- export interface IbkrSnapshot {
67
- status: BrokerConnectionStatus;
68
- accounts: BrokerAccount[];
69
- openOrders: BrokerOrder[];
70
- executions: BrokerExecution[];
71
- lastError?: string;
72
- }
73
-
74
- interface ClientLockMetadata {
75
- pid: number;
76
- ownerToken: string;
77
- requestedClientId: number;
78
- clientId: number;
79
- host: string;
80
- port: number;
81
- instanceId?: string;
82
- cwd: string;
83
- timestamp: number;
84
- }
85
-
86
- interface ClaimedClientLock {
87
- clientId: number;
88
- requestedClientId: number;
89
- path: string;
90
- }
91
-
92
- type QuoteStreamListener = (target: QuoteSubscriptionTarget, quote: Quote) => void;
93
-
94
- interface ActiveQuoteStream {
95
- target: QuoteSubscriptionTarget;
96
- listeners: Map<QuoteStreamListener, QuoteSubscriptionTarget>;
97
- stop: () => void;
98
- lastQuote?: Quote;
99
- }
100
-
101
- interface AccountPortfolioSnapshotPosition {
102
- contract: Contract;
103
- avgCost?: number;
104
- marketPrice?: number;
105
- marketValue?: number;
106
- unrealizedPNL?: number;
107
- }
15
+ import type {
16
+ IbkrGatewayConfig,
17
+ IbkrSnapshot,
18
+ ResolvedIbkrGatewayConnection,
19
+ } from "./gateway-types";
20
+
21
+ export type {
22
+ IbkrGatewayConfig,
23
+ IbkrSnapshot,
24
+ ResolvedIbkrGatewayConnection,
25
+ } from "./gateway-types";
26
+
27
+ type NativeGatewayService = any;
28
+ type NativeGatewayManager = any;
29
+ type NativeGatewayModule = {
30
+ ibkrGatewayManager: NativeGatewayManager;
31
+ setResolvedIbkrGatewayListener(listener: ResolvedGatewayListener | null): void;
32
+ };
33
+ type Listener = () => void;
34
+ type ResolvedGatewayListener = (
35
+ instanceId: string | undefined,
36
+ connection: ResolvedIbkrGatewayConnection,
37
+ ) => void | Promise<void>;
108
38
 
109
39
  const DEFAULT_SNAPSHOT: IbkrSnapshot = {
110
40
  status: { state: "disconnected", updatedAt: Date.now() },
@@ -113,928 +43,218 @@ const DEFAULT_SNAPSHOT: IbkrSnapshot = {
113
43
  executions: [],
114
44
  };
115
45
 
116
- const gatewayLog = debugLog.createLogger("ibkr-gateway");
117
- const IBKR_CLIENT_LOCK_DIR = join(tmpdir(), "gloomberb-ibkr-client-locks");
118
- const IBKR_CLIENT_ID_SEARCH_SPAN = 64;
119
- const LOCAL_IBKR_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
120
- const COMMON_LOCAL_IBKR_PORTS = [4001, 4002, 7496, 7497] as const;
121
- const LOCAL_TCP_PROBE_TIMEOUT_MS = 250;
122
- const DEFAULT_AUTO_CLIENT_ID = 1;
123
-
124
- type ResolvedGatewayListener = (
125
- instanceId: string | undefined,
126
- connection: ResolvedIbkrGatewayConnection,
127
- ) => void | Promise<void>;
128
-
129
46
  let resolvedGatewayListener: ResolvedGatewayListener | null = null;
47
+ let loadedNativeGatewayModule: NativeGatewayModule | null = null;
48
+ let nativeGatewayModulePromise: Promise<NativeGatewayModule> | null = null;
49
+ let nativeGatewayManager: NativeGatewayManager | null = null;
50
+ let nativeGatewayModuleLoader: () => Promise<NativeGatewayModule>;
51
+ const importNativeGatewayModule = new Function("specifier", "return import(specifier)") as (
52
+ specifier: string,
53
+ ) => Promise<NativeGatewayModule>;
130
54
 
131
- export function setResolvedIbkrGatewayListener(listener: ResolvedGatewayListener | null): void {
132
- resolvedGatewayListener = listener;
133
- }
134
-
135
- type AccountSummaryValueMap = ReadonlyMap<string, { value: string }>;
136
- type AccountSummaryTags = ReadonlyMap<string, AccountSummaryValueMap>;
137
-
138
- function getAccountSummaryEntry(
139
- values: AccountSummaryValueMap | undefined,
140
- preferredCurrency?: string,
141
- ): { currency: string; value: number } | null {
142
- if (!values) return null;
143
-
144
- if (preferredCurrency) {
145
- const preferred = values.get(preferredCurrency);
146
- if (preferred?.value) {
147
- const numeric = parseFloat(preferred.value);
148
- if (Number.isFinite(numeric)) {
149
- return { currency: preferredCurrency, value: numeric };
150
- }
151
- }
152
- }
153
-
154
- for (const [currency, entry] of values.entries()) {
155
- const numeric = parseFloat(entry?.value ?? "");
156
- if (Number.isFinite(numeric)) {
157
- return { currency, value: numeric };
158
- }
159
- }
160
- return null;
161
- }
162
-
163
- function getAccountSummaryNumber(
164
- tags: AccountSummaryTags | undefined,
165
- tagName: string,
166
- preferredCurrency?: string,
167
- ): number | undefined {
168
- return getAccountSummaryEntry(tags?.get(tagName), preferredCurrency)?.value;
169
- }
170
-
171
- function getAccountSummaryNumberWithAggregateFallback(
172
- tags: AccountSummaryTags | undefined,
173
- aggregateTags: AccountSummaryTags | undefined,
174
- tagName: string,
175
- preferredCurrency: string | undefined,
176
- allowAggregateFallback: boolean,
177
- ): number | undefined {
178
- const direct = getAccountSummaryNumber(tags, tagName, preferredCurrency);
179
- if (direct != null) return direct;
180
- if (!allowAggregateFallback) return undefined;
181
- return getAccountSummaryNumber(aggregateTags, tagName, preferredCurrency);
182
- }
183
-
184
- function inferAccountCurrency(tags: AccountSummaryTags | undefined): string | undefined {
185
- if (!tags) return undefined;
186
- for (const tagName of ["NetLiquidation", "TotalCashValue", "SettledCash", "AvailableFunds"]) {
187
- const entry = getAccountSummaryEntry(tags.get(tagName));
188
- if (entry?.currency) return entry.currency;
189
- }
190
- return undefined;
191
- }
192
-
193
- function getSummaryMap(
194
- tags: AccountSummaryTags | undefined,
195
- tagName: string,
196
- ): AccountSummaryValueMap | undefined {
197
- const values = tags?.get(tagName);
198
- return values && values.size > 0 ? values : undefined;
199
- }
200
-
201
- function buildCashBalancesFromSummary(
202
- cashBalanceMap: AccountSummaryValueMap | undefined,
203
- exchangeRateMap: AccountSummaryValueMap | undefined,
204
- baseCurrency?: string,
205
- ): BrokerCashBalance[] | undefined {
206
- const balances: BrokerCashBalance[] = [];
207
- for (const [currency, entry] of cashBalanceMap?.entries() ?? []) {
208
- if (!currency || currency === "BASE") continue;
209
- const numeric = parseFloat(entry?.value ?? "");
210
- if (!Number.isFinite(numeric)) continue;
211
-
212
- const exchangeRate = currency === baseCurrency
213
- ? 1
214
- : parseFloat(exchangeRateMap?.get(currency)?.value ?? "");
215
- const baseValue = Number.isFinite(exchangeRate) ? numeric * exchangeRate : undefined;
216
-
217
- balances.push({
218
- currency,
219
- quantity: numeric,
220
- baseValue,
221
- baseCurrency,
222
- });
223
- }
224
-
225
- return balances.length > 0 ? balances : undefined;
226
- }
227
-
228
- function buildCashBalances(
229
- tags: AccountSummaryTags | undefined,
230
- aggregateTags: AccountSummaryTags | undefined,
231
- baseCurrency: string | undefined,
232
- allowAggregateFallback: boolean,
233
- ): BrokerCashBalance[] | undefined {
234
- const directLedger = buildCashBalancesFromSummary(
235
- getSummaryMap(tags, "$LEDGER:ALL"),
236
- undefined,
237
- baseCurrency,
238
- );
239
- if (directLedger) return directLedger;
240
-
241
- const directSummary = buildCashBalancesFromSummary(
242
- getSummaryMap(tags, "CashBalance") ?? getSummaryMap(tags, "TotalCashBalance"),
243
- getSummaryMap(tags, "ExchangeRate"),
244
- baseCurrency,
245
- );
246
- if (directSummary) return directSummary;
247
-
248
- if (!allowAggregateFallback) return undefined;
249
-
250
- return buildCashBalancesFromSummary(
251
- getSummaryMap(aggregateTags, "CashBalance") ?? getSummaryMap(aggregateTags, "TotalCashBalance"),
252
- getSummaryMap(aggregateTags, "ExchangeRate"),
253
- baseCurrency,
254
- );
255
- }
256
-
257
- export function summarizeBrokerAccount(
258
- accountId: string,
259
- tags: AccountSummaryTags | undefined,
260
- updatedAt: number,
261
- aggregateTags?: AccountSummaryTags,
262
- allowAggregateCashBalances = false,
263
- ): BrokerAccount {
264
- const currency = inferAccountCurrency(tags) ?? (allowAggregateCashBalances ? inferAccountCurrency(aggregateTags) : undefined);
265
- return {
266
- accountId,
267
- name: accountId,
268
- currency,
269
- source: "gateway",
270
- updatedAt,
271
- netLiquidation: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "NetLiquidation", currency, allowAggregateCashBalances),
272
- totalCashValue: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "TotalCashValue", currency, allowAggregateCashBalances),
273
- settledCash: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "SettledCash", currency, allowAggregateCashBalances),
274
- availableFunds: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "AvailableFunds", currency, allowAggregateCashBalances),
275
- buyingPower: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "BuyingPower", currency, allowAggregateCashBalances),
276
- excessLiquidity: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "ExcessLiquidity", currency, allowAggregateCashBalances),
277
- initMarginReq: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "InitMarginReq", currency, allowAggregateCashBalances),
278
- maintMarginReq: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "MaintMarginReq", currency, allowAggregateCashBalances),
279
- cashBalances: buildCashBalances(tags, aggregateTags, currency, allowAggregateCashBalances),
280
- };
281
- }
282
-
283
- /** Wrap a promise with a timeout so that IBKR calls don't hang indefinitely. */
284
- function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
285
- return new Promise<T>((resolve, reject) => {
286
- const timer = setTimeout(() => reject(new Error(`IBKR ${label} timed out after ${ms}ms`)), ms);
287
- promise.then(
288
- (value) => { clearTimeout(timer); resolve(value); },
289
- (error) => { clearTimeout(timer); reject(error); },
290
- );
291
- });
292
- }
293
-
294
- const IBKR_DATA_TIMEOUT = 8_000;
295
- const IBKR_QUOTE_STREAM_TICKS = "165,221,233";
296
-
297
- const HISTORY_PARAMS: Record<TimeRange, { duration: string; size: BarSizeSetting }> = {
298
- "1D": { duration: "1 D", size: BarSizeSetting.MINUTES_FIVE },
299
- "1W": { duration: "7 D", size: BarSizeSetting.HOURS_ONE },
300
- "1M": { duration: "1 M", size: BarSizeSetting.HOURS_ONE },
301
- "3M": { duration: "3 M", size: BarSizeSetting.DAYS_ONE },
302
- "6M": { duration: "6 M", size: BarSizeSetting.DAYS_ONE },
303
- "1Y": { duration: "1 Y", size: BarSizeSetting.DAYS_ONE },
304
- "5Y": { duration: "5 Y", size: BarSizeSetting.WEEKS_ONE },
305
- "ALL": { duration: "10 Y", size: BarSizeSetting.MONTHS_ONE },
306
- };
307
-
308
- const IBKR_RESOLUTION_SUPPORT = normalizeChartResolutionSupport([
309
- { resolution: "1m", maxRange: "1W" },
310
- { resolution: "5m", maxRange: "1M" },
311
- { resolution: "15m", maxRange: "3M" },
312
- { resolution: "30m", maxRange: "6M" },
313
- { resolution: "1h", maxRange: "1Y" },
314
- { resolution: "1d", maxRange: "ALL" },
315
- { resolution: "1wk", maxRange: "ALL" },
316
- { resolution: "1mo", maxRange: "ALL" },
317
- ]);
318
-
319
- /** Map generic bar size labels to IBKR BarSizeSetting values. */
320
- const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
321
- "1m": BarSizeSetting.MINUTES_ONE,
322
- "5m": BarSizeSetting.MINUTES_FIVE,
323
- "15m": BarSizeSetting.MINUTES_FIFTEEN,
324
- "30m": BarSizeSetting.MINUTES_THIRTY,
325
- "1h": BarSizeSetting.HOURS_ONE,
326
- "1d": BarSizeSetting.DAYS_ONE,
327
- "1wk": BarSizeSetting.WEEKS_ONE,
328
- "1mo": BarSizeSetting.MONTHS_ONE,
329
- };
330
-
331
- export function parseIbkrHistoricalBarTime(value: string | number): Date {
332
- if (typeof value === "number") {
333
- if (!Number.isFinite(value)) return new Date(Number.NaN);
334
-
335
- const compactDate = String(Math.trunc(value));
336
- if (/^\d{8}$/.test(compactDate)) {
337
- return parseIbkrHistoricalBarTime(compactDate);
338
- }
339
-
340
- return new Date(value > 10_000_000_000 ? value : value * 1000);
341
- }
342
-
343
- const trimmed = value.trim();
344
- if (!trimmed) return new Date(Number.NaN);
345
-
346
- const parsed = Date.parse(trimmed);
347
- if (Number.isFinite(parsed)) {
348
- return new Date(parsed);
349
- }
350
-
351
- const compactMatch = trimmed.match(/^(\d{4})(\d{2})(\d{2})(?:\D+(\d{2}):(\d{2}):(\d{2}))?/);
352
- if (!compactMatch) {
353
- return new Date(Number.NaN);
354
- }
355
-
356
- const [, yearText, monthText, dayText, hourText, minuteText, secondText] = compactMatch;
357
- const year = Number(yearText);
358
- const month = Number(monthText) - 1;
359
- const day = Number(dayText);
360
- const hour = Number(hourText ?? "0");
361
- const minute = Number(minuteText ?? "0");
362
- const second = Number(secondText ?? "0");
363
-
364
- return new Date(year, month, day, hour, minute, second);
365
- }
366
-
367
- function normalizeQuoteStreamTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
368
- const symbol = target.symbol.trim().toUpperCase();
369
- if (!symbol) return null;
370
- return {
371
- ...target,
372
- symbol,
373
- exchange: (target.exchange ?? "").trim().toUpperCase(),
374
- };
375
- }
376
-
377
- function buildQuoteStreamKey(target: QuoteSubscriptionTarget): string {
378
- const contractKey = target.context?.instrument?.conId
379
- ?? target.context?.instrument?.localSymbol
380
- ?? target.context?.instrument?.symbol
381
- ?? "";
382
- return [target.symbol, target.exchange ?? "", contractKey].join("|");
383
- }
55
+ const snapshots = new Map<string, IbkrSnapshot>();
56
+ const listeners = new Map<string, Set<Listener>>();
384
57
 
385
- function hasDelayedMarketData(
386
- marketData: ReadonlyMap<number, { value?: number; ingressTm: number }>,
387
- ): boolean {
388
- return [
389
- IBApiTickType.DELAYED_BID,
390
- IBApiTickType.DELAYED_ASK,
391
- IBApiTickType.DELAYED_LAST,
392
- IBApiTickType.DELAYED_BID_SIZE,
393
- IBApiTickType.DELAYED_ASK_SIZE,
394
- IBApiTickType.DELAYED_LAST_SIZE,
395
- IBApiTickType.DELAYED_HIGH,
396
- IBApiTickType.DELAYED_LOW,
397
- IBApiTickType.DELAYED_VOLUME,
398
- IBApiTickType.DELAYED_CLOSE,
399
- IBApiTickType.DELAYED_OPEN,
400
- IBApiTickType.DELAYED_LAST_TIMESTAMP,
401
- ].some((tickType) => marketData.has(tickType));
58
+ function getSnapshotEntry(instanceId?: string): IbkrSnapshot {
59
+ if (!instanceId) return DEFAULT_SNAPSHOT;
60
+ return snapshots.get(instanceId) ?? DEFAULT_SNAPSHOT;
402
61
  }
403
62
 
404
- function firstNonSmartExchange(value?: string): string | undefined {
405
- return value
406
- ?.split(",")
407
- .map((entry) => entry.trim())
408
- .find((entry) => entry.length > 0 && entry !== "SMART");
63
+ function defaultNativeGatewayModuleLoader(): Promise<NativeGatewayModule> {
64
+ return importNativeGatewayModule("./gateway-service-native");
409
65
  }
410
66
 
411
- function resolveIbkrListingExchange(
412
- current: Quote | undefined,
413
- contract: Contract,
414
- details: ContractDetails,
415
- ): string | undefined {
416
- return current?.listingExchangeName
417
- ?? contract.primaryExch
418
- ?? firstNonSmartExchange(details.validExchanges)
419
- ?? (current?.exchangeName !== "SMART" ? current?.exchangeName : undefined)
420
- ?? (contract.exchange !== "SMART" ? contract.exchange : undefined);
421
- }
422
-
423
- function resolveIbkrRoutingExchange(current: Quote | undefined, contract: Contract): string | undefined {
424
- return current?.routingExchangeName
425
- ?? contract.exchange
426
- ?? "SMART";
427
- }
428
-
429
- function ibkrVenueFields(
430
- current: Quote | undefined,
431
- contract: Contract,
432
- details: ContractDetails,
433
- ): Pick<
434
- Quote,
435
- | "exchangeName"
436
- | "fullExchangeName"
437
- | "listingExchangeName"
438
- | "listingExchangeFullName"
439
- | "routingExchangeName"
440
- | "routingExchangeFullName"
441
- | "marketState"
442
- | "sessionConfidence"
443
- > {
444
- const listingExchangeName = resolveIbkrListingExchange(current, contract, details);
445
- const listingExchangeFullName = current?.listingExchangeFullName
446
- ?? current?.fullExchangeName
447
- ?? listingExchangeName;
448
- const routingExchangeName = resolveIbkrRoutingExchange(current, contract);
449
- const routingExchangeFullName = current?.routingExchangeFullName ?? routingExchangeName;
67
+ nativeGatewayModuleLoader = defaultNativeGatewayModuleLoader;
450
68
 
451
- return {
452
- exchangeName: listingExchangeName,
453
- fullExchangeName: listingExchangeFullName,
454
- listingExchangeName,
455
- listingExchangeFullName,
456
- routingExchangeName,
457
- routingExchangeFullName,
458
- marketState: current?.marketState,
459
- sessionConfidence: current?.sessionConfidence ?? "unknown",
460
- };
461
- }
462
-
463
- interface TickByTickBidAskUpdate {
464
- time: number;
465
- bidPrice: number;
466
- askPrice: number;
467
- bidSize: number;
468
- askSize: number;
69
+ export function setNativeIbkrGatewayModuleLoader(loader: (() => Promise<NativeGatewayModule>) | null): void {
70
+ nativeGatewayModuleLoader = loader ?? defaultNativeGatewayModuleLoader;
469
71
  }
470
72
 
471
- export function applyTickByTickAllLastToQuote(
472
- current: Quote | undefined,
473
- contract: Contract,
474
- details: ContractDetails,
475
- tick: TickByTickAllLast,
476
- priceDivisor: number,
477
- dataSource: Quote["dataSource"],
478
- ): Quote | null {
479
- const nextPrice = normalizeIbkrPriceValue(tick.price, priceDivisor);
480
- if (nextPrice == null || nextPrice <= 0) return null;
481
-
482
- const previousClose = current?.previousClose ?? current?.price ?? nextPrice;
483
- const change = nextPrice - previousClose;
484
- return {
485
- symbol: current?.symbol ?? contract.localSymbol ?? contract.symbol ?? "",
486
- providerId: "ibkr",
487
- price: nextPrice,
488
- currency: current?.currency ?? contract.currency ?? "USD",
489
- change,
490
- changePercent: previousClose ? (change / previousClose) * 100 : 0,
491
- previousClose,
492
- high52w: current?.high52w,
493
- low52w: current?.low52w,
494
- volume: current?.volume,
495
- name: current?.name ?? details.longName ?? details.marketName ?? contract.symbol,
496
- lastUpdated: tick.time ? tick.time * 1000 : Date.now(),
497
- ...ibkrVenueFields(current, contract, details),
498
- preMarketPrice: current?.preMarketPrice,
499
- preMarketChange: current?.preMarketChange,
500
- preMarketChangePercent: current?.preMarketChangePercent,
501
- postMarketPrice: current?.postMarketPrice,
502
- postMarketChange: current?.postMarketChange,
503
- postMarketChangePercent: current?.postMarketChangePercent,
504
- bid: current?.bid,
505
- ask: current?.ask,
506
- bidSize: current?.bidSize,
507
- askSize: current?.askSize,
508
- open: current?.open,
509
- high: current?.high,
510
- low: current?.low,
511
- mark: current?.mark,
512
- dataSource,
513
- };
514
- }
515
-
516
- export function applyTickByTickBidAskToQuote(
517
- current: Quote | undefined,
518
- update: TickByTickBidAskUpdate,
519
- priceDivisor: number,
520
- ): Quote | null {
521
- if (!current) return null;
522
- const bid = normalizeIbkrPriceValue(update.bidPrice, priceDivisor);
523
- const ask = normalizeIbkrPriceValue(update.askPrice, priceDivisor);
524
- return {
73
+ function applySnapshot(instanceId: string, patch: Partial<IbkrSnapshot>, shouldNotify: boolean): IbkrSnapshot {
74
+ const current = getSnapshotEntry(instanceId);
75
+ const next = {
525
76
  ...current,
526
- bid: bid ?? current.bid,
527
- ask: ask ?? current.ask,
528
- bidSize: Number.isFinite(update.bidSize) && update.bidSize >= 0 ? update.bidSize : current.bidSize,
529
- askSize: Number.isFinite(update.askSize) && update.askSize >= 0 ? update.askSize : current.askSize,
530
- lastUpdated: update.time ? update.time * 1000 : current.lastUpdated,
77
+ ...patch,
78
+ status: patch.status ?? current.status,
79
+ accounts: patch.accounts ?? current.accounts,
80
+ openOrders: patch.openOrders ?? current.openOrders,
81
+ executions: patch.executions ?? current.executions,
531
82
  };
532
- }
533
-
534
- function marketDataTypeFromConfig(config?: IbkrGatewayConfig): MarketDataType {
535
- switch (config?.marketDataType) {
536
- case "frozen":
537
- return MarketDataType.FROZEN;
538
- case "delayed":
539
- return MarketDataType.DELAYED;
540
- case "delayed-frozen":
541
- return MarketDataType.DELAYED_FROZEN;
542
- case "live":
543
- return MarketDataType.REALTIME;
544
- case "auto":
545
- default:
546
- // Start with realtime; withMarketDataFallback will downgrade to delayed
547
- // if the account lacks live market data subscriptions.
548
- return MarketDataType.REALTIME;
83
+ if (
84
+ next.status === current.status
85
+ && next.accounts === current.accounts
86
+ && next.openOrders === current.openOrders
87
+ && next.executions === current.executions
88
+ ) {
89
+ return current;
549
90
  }
91
+ snapshots.set(instanceId, next);
92
+ if (shouldNotify) notify(instanceId);
93
+ return next;
550
94
  }
551
95
 
552
- function isMarketDataPermissionError(code: number | undefined, message: string | undefined): boolean {
553
- if (code === 354 || code === 10167) return true;
554
- const text = (message || "").toLowerCase();
555
- return text.includes("displaying delayed market data")
556
- || text.includes("delayed market data is available")
557
- || text.includes("market data connections")
558
- || text.includes("requested market data is not subscribed")
559
- || text.includes("requested market data requires additional subscription")
560
- || text.includes("market data subscription");
96
+ function updateSnapshot(instanceId: string, patch: Partial<IbkrSnapshot>): void {
97
+ applySnapshot(instanceId, patch, true);
561
98
  }
562
99
 
563
- function isIbInformationalWarning(code: number | undefined, message: string | undefined): boolean {
564
- if (code != null && [1102, 2104, 2106, 2107, 2108, 2158].includes(code)) {
565
- return true;
566
- }
567
-
568
- const text = (message || "").toLowerCase();
569
- return text.includes("connection is ok")
570
- || text.includes("connectivity between ib and trader workstation has been restored")
571
- || text.includes("inactive but should be available upon demand")
572
- || text.includes("sec-def data farm connection is ok")
573
- || text.includes("market data farm connection is ok")
574
- || text.includes("hmds data farm connection is ok");
100
+ function cacheSnapshot(instanceId: string, patch: Partial<IbkrSnapshot>): IbkrSnapshot {
101
+ return applySnapshot(instanceId, patch, false);
575
102
  }
576
103
 
577
- function getIbErrorCode(error: any): number | undefined {
578
- const candidates = [
579
- error?.errorCode,
580
- error?.code,
581
- error?.error?.errorCode,
582
- error?.error?.code,
583
- error?.id,
584
- ];
585
- for (const candidate of candidates) {
586
- if (typeof candidate === "number" && Number.isFinite(candidate)) {
587
- return candidate;
588
- }
104
+ function notify(instanceId: string): void {
105
+ for (const listener of listeners.get(instanceId) ?? []) {
106
+ listener();
589
107
  }
590
- return undefined;
591
108
  }
592
109
 
593
- function getIbErrorMessage(error: any): string | undefined {
594
- const candidates = [
595
- error?.error?.message,
596
- error?.message,
597
- error?.error?.toString?.(),
598
- ];
599
- for (const candidate of candidates) {
600
- if (typeof candidate === "string" && candidate.trim()) {
601
- return candidate;
602
- }
110
+ async function loadNativeGatewayModule(): Promise<NativeGatewayModule> {
111
+ if (!nativeGatewayModulePromise) {
112
+ nativeGatewayModulePromise = nativeGatewayModuleLoader().catch((error) => {
113
+ nativeGatewayModulePromise = null;
114
+ throw error;
115
+ });
603
116
  }
604
- return undefined;
605
- }
606
-
607
- function isClientIdInUseError(code: number | undefined, message: string | undefined): boolean {
608
- if (code === 326) return true;
609
- const text = (message || "").toLowerCase();
610
- return text.includes("client id is already in use")
611
- || text.includes("clientid is already in use")
612
- || text.includes("client id already in use");
613
- }
614
-
615
- function buildClientLockPath(config: IbkrGatewayConfig, clientId: number): string {
616
- const host = (config.host || "localhost").replace(/[^a-z0-9_.-]+/gi, "_");
617
- return join(IBKR_CLIENT_LOCK_DIR, `${host}-${config.port}-${clientId}.lock`);
117
+ const module = await nativeGatewayModulePromise;
118
+ loadedNativeGatewayModule = module;
119
+ nativeGatewayManager = module.ibkrGatewayManager;
120
+ module.setResolvedIbkrGatewayListener(resolvedGatewayListener);
121
+ return module;
618
122
  }
619
123
 
620
- function buildPortfolioPositionKey(accountId: string, contract: Contract): string {
621
- return [
622
- accountId.trim(),
623
- contract.conId ?? "",
624
- contract.localSymbol ?? "",
625
- contract.symbol ?? "",
626
- contract.secType ?? "",
627
- ].join("|");
124
+ async function getNativeService(instanceId: string): Promise<NativeGatewayService> {
125
+ const module = await loadNativeGatewayModule();
126
+ return module.ibkrGatewayManager.getService(instanceId);
628
127
  }
629
128
 
630
- function isProcessAlive(pid: number | undefined): boolean {
631
- if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
632
- try {
633
- process.kill(pid, 0);
634
- return true;
635
- } catch {
636
- return false;
637
- }
638
- }
639
-
640
- async function readClientLock(path: string): Promise<ClientLockMetadata | null> {
641
- try {
642
- const raw = await readFile(path, "utf-8");
643
- const parsed = JSON.parse(raw) as Partial<ClientLockMetadata>;
644
- if (
645
- typeof parsed.pid !== "number"
646
- || typeof parsed.ownerToken !== "string"
647
- || typeof parsed.clientId !== "number"
648
- || typeof parsed.requestedClientId !== "number"
649
- ) {
650
- return null;
651
- }
652
- return {
653
- pid: parsed.pid,
654
- ownerToken: parsed.ownerToken,
655
- clientId: parsed.clientId,
656
- requestedClientId: parsed.requestedClientId,
657
- host: typeof parsed.host === "string" ? parsed.host : "",
658
- port: typeof parsed.port === "number" ? parsed.port : 0,
659
- instanceId: typeof parsed.instanceId === "string" ? parsed.instanceId : undefined,
660
- cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
661
- timestamp: typeof parsed.timestamp === "number" ? parsed.timestamp : 0,
662
- };
663
- } catch {
664
- return null;
665
- }
666
- }
667
-
668
- function buildConnectionNote(
669
- requestedClientId: number,
670
- actualClientId: number,
671
- requestedPort: number | undefined,
672
- actualPort: number,
673
- ): string | undefined {
674
- const notes: string[] = [];
675
- if (requestedPort == null || requestedPort !== actualPort) {
676
- notes.push(`Detected IBKR API on port ${actualPort}.`);
129
+ export function setResolvedIbkrGatewayListener(listener: ResolvedGatewayListener | null): void {
130
+ resolvedGatewayListener = listener;
131
+ if (getBrokerRemoteClient()) return;
132
+ if (loadedNativeGatewayModule) {
133
+ loadedNativeGatewayModule.setResolvedIbkrGatewayListener(listener);
134
+ return;
677
135
  }
678
- if (requestedClientId !== actualClientId) {
679
- notes.push(`Using client ID ${actualClientId} because ${requestedClientId} is already in use.`);
136
+ if (!nativeGatewayModulePromise) {
137
+ return;
680
138
  }
681
- return notes.length > 0 ? notes.join(" ") : undefined;
139
+ void nativeGatewayModulePromise.then((module) => {
140
+ module.setResolvedIbkrGatewayListener(listener);
141
+ }).catch(() => {});
682
142
  }
683
143
 
684
- function isRetryableErrorCode(code: number): boolean {
685
- return ![200, 201, 202, 321, 354].includes(code);
686
- }
687
-
688
- function isLoopbackHost(host: string): boolean {
689
- return LOCAL_IBKR_HOSTS.has(host.trim().toLowerCase());
690
- }
144
+ class IbkrGatewayServiceFacade {
145
+ constructor(private readonly instanceId: string) {}
691
146
 
692
- async function probeTcpPort(host: string, port: number, timeoutMs = LOCAL_TCP_PROBE_TIMEOUT_MS): Promise<boolean> {
693
- return new Promise<boolean>((resolve) => {
694
- let settled = false;
695
- const finish = (result: boolean) => {
696
- if (settled) return;
697
- settled = true;
698
- socket.removeAllListeners();
699
- socket.destroy();
700
- resolve(result);
701
- };
702
-
703
- const socket = createConnection({ host, port });
704
- socket.once("connect", () => finish(true));
705
- socket.once("error", () => finish(false));
706
- socket.setTimeout(timeoutMs, () => finish(false));
707
- });
708
- }
709
-
710
- function uniquePorts(...values: Array<number | undefined | readonly number[]>): number[] {
711
- const unique = new Set<number>();
712
- for (const value of values) {
713
- if (Array.isArray(value)) {
714
- for (const candidate of value) {
715
- if (Number.isFinite(candidate)) unique.add(candidate);
716
- }
717
- continue;
147
+ getSnapshot(): IbkrSnapshot {
148
+ const remoteStatus = getBrokerRemoteClient()?.getStatus(this.instanceId);
149
+ if (remoteStatus) {
150
+ return cacheSnapshot(this.instanceId, { status: remoteStatus });
718
151
  }
719
- if (typeof value === "number" && Number.isFinite(value)) {
720
- unique.add(value);
152
+ if (nativeGatewayManager) {
153
+ return nativeGatewayManager.getService(this.instanceId).getSnapshot();
721
154
  }
722
- }
723
- return [...unique];
724
- }
725
-
726
- interface IbkrPortDiagnosticOptions {
727
- candidatePorts?: readonly number[];
728
- probePort?: (host: string, port: number) => Promise<boolean>;
729
- }
730
-
731
- export async function diagnoseLocalIbkrPortIssue(
732
- config: IbkrGatewayConfig,
733
- options: IbkrPortDiagnosticOptions = {},
734
- ): Promise<string | null> {
735
- if (typeof config.port !== "number" || !Number.isFinite(config.port)) return null;
736
- if (!isLoopbackHost(config.host)) return null;
737
-
738
- const candidatePorts = options.candidatePorts ?? COMMON_LOCAL_IBKR_PORTS;
739
- const probePort = options.probePort ?? ((host: string, port: number) => probeTcpPort(host, port));
740
- if (await probePort(config.host, config.port)) return null;
741
-
742
- const openAlternatives: number[] = [];
743
- for (const port of candidatePorts) {
744
- if (port === config.port) continue;
745
- if (await probePort(config.host, port)) openAlternatives.push(port);
746
- }
747
-
748
- if (openAlternatives.length === 0) {
749
- return `IBKR is not listening on ${config.host}:${config.port}. Start Gateway/TWS and confirm the API socket port in the IBKR API settings.`;
155
+ return getSnapshotEntry(this.instanceId);
750
156
  }
751
157
 
752
- const detectedTargets = openAlternatives.map((port) => `${config.host}:${port}`).join(", ");
753
- return `IBKR is not listening on ${config.host}:${config.port}. Detected a local IBKR API listener on ${detectedTargets} instead. Update this profile's port to match Gateway/TWS.`;
754
- }
755
-
756
- export async function resolveGatewayConnection(
757
- config: IbkrGatewayConfig,
758
- options: IbkrPortDiagnosticOptions = {},
759
- ): Promise<ResolvedIbkrGatewayConnection> {
760
- const host = (config.host || "127.0.0.1").trim() || "127.0.0.1";
761
- const requestedPort = typeof config.port === "number" && Number.isFinite(config.port) ? config.port : undefined;
762
- const requestedClientId = typeof config.clientId === "number" && Number.isFinite(config.clientId)
763
- ? config.clientId
764
- : typeof config.lastSuccessfulClientId === "number" && Number.isFinite(config.lastSuccessfulClientId)
765
- ? config.lastSuccessfulClientId
766
- : DEFAULT_AUTO_CLIENT_ID;
767
- const probePort = options.probePort ?? ((candidateHost: string, candidatePort: number) => probeTcpPort(candidateHost, candidatePort));
768
-
769
- if (requestedPort != null) {
770
- const localPortIssue = await diagnoseLocalIbkrPortIssue({
771
- host,
772
- port: requestedPort,
773
- clientId: requestedClientId,
774
- marketDataType: config.marketDataType,
775
- }, options);
776
- if (localPortIssue) throw new Error(localPortIssue);
777
- return {
778
- host,
779
- port: requestedPort,
780
- clientId: requestedClientId,
781
- requestedPort,
782
- requestedClientId,
783
- };
784
- }
785
-
786
- if (isLoopbackHost(host)) {
787
- const candidatePorts = uniquePorts(config.lastSuccessfulPort, options.candidatePorts ?? COMMON_LOCAL_IBKR_PORTS);
788
- for (const port of candidatePorts) {
789
- if (await probePort(host, port)) {
790
- return {
791
- host,
792
- port,
793
- clientId: requestedClientId,
794
- requestedPort: typeof config.lastSuccessfulPort === "number" ? config.lastSuccessfulPort : undefined,
795
- requestedClientId,
796
- };
797
- }
158
+ getResolvedConnection(): ResolvedIbkrGatewayConnection | null {
159
+ if (nativeGatewayManager) {
160
+ return nativeGatewayManager.getService(this.instanceId).getResolvedConnection();
798
161
  }
799
-
800
- throw new Error(
801
- `No local IBKR API listeners were detected on ${host}. Checked ports ${candidatePorts.join(", ")}. Start Gateway/TWS and enable socket clients in the IBKR API settings.`,
802
- );
162
+ return null;
803
163
  }
804
164
 
805
- const fallbackPort = typeof config.lastSuccessfulPort === "number" && Number.isFinite(config.lastSuccessfulPort)
806
- ? config.lastSuccessfulPort
807
- : COMMON_LOCAL_IBKR_PORTS[0];
808
- return {
809
- host,
810
- port: fallbackPort,
811
- clientId: requestedClientId,
812
- requestedPort: typeof config.lastSuccessfulPort === "number" ? config.lastSuccessfulPort : undefined,
813
- requestedClientId,
814
- };
815
- }
816
-
817
- export class IbkrGatewayService {
818
- private api: IBApiNext | null = null;
819
- private configKey: string | null = null;
820
- private connecting: Promise<void> | null = null;
821
- /** Account IDs captured from the managedAccounts event on initial connection. */
822
- private cachedAccountIds: string[] = [];
823
- private snapshot: IbkrSnapshot = DEFAULT_SNAPSHOT;
824
- private listeners = new Set<() => void>();
825
- private activeMarketDataType: MarketDataType = MarketDataType.REALTIME;
826
- private autoMarketData = true;
827
- /** Dedup in-flight requests: reuse pending promises for the same key. */
828
- private pendingRequests = new Map<string, Promise<any>>();
829
- private readonly clientLockOwner = `${process.pid}:${Math.random().toString(36).slice(2, 10)}`;
830
- private claimedClientLock: ClaimedClientLock | null = null;
831
- private connectionNote?: string;
832
- private resolvedConnection: ResolvedIbkrGatewayConnection | null = null;
833
- private readonly quoteStreams = new Map<string, ActiveQuoteStream>();
834
- private readonly quoteStreamStarts = new Map<string, Promise<void>>();
835
-
836
- constructor(private readonly instanceId?: string) {}
837
-
838
- /** Dedup concurrent calls with the same key — returns cached promise if one is in flight. */
839
- private dedup<T>(key: string, fn: () => Promise<T>): Promise<T> {
840
- const existing = this.pendingRequests.get(key);
841
- if (existing) return existing as Promise<T>;
842
- const promise = fn().finally(() => this.pendingRequests.delete(key));
843
- this.pendingRequests.set(key, promise);
844
- return promise;
845
- }
165
+ subscribe(listener: Listener): () => void {
166
+ if (!listeners.has(this.instanceId)) listeners.set(this.instanceId, new Set());
167
+ listeners.get(this.instanceId)!.add(listener);
846
168
 
847
- subscribe(listener: () => void): () => void {
848
- this.listeners.add(listener);
849
- return () => this.listeners.delete(listener);
850
- }
169
+ const remoteDispose = getBrokerRemoteClient()?.subscribeStatus(this.instanceId, () => {
170
+ const status = getBrokerRemoteClient()?.getStatus(this.instanceId);
171
+ if (status) {
172
+ updateSnapshot(this.instanceId, { status });
173
+ } else {
174
+ listener();
175
+ }
176
+ });
851
177
 
852
- getSnapshot(): IbkrSnapshot {
853
- return this.snapshot;
854
- }
178
+ if (remoteDispose) {
179
+ return () => {
180
+ remoteDispose();
181
+ listeners.get(this.instanceId)?.delete(listener);
182
+ };
183
+ }
855
184
 
856
- getStatus(): BrokerConnectionStatus {
857
- return this.snapshot.status;
858
- }
185
+ let nativeDispose: (() => void) | null = null;
186
+ if (nativeGatewayManager) {
187
+ const service = nativeGatewayManager.getService(this.instanceId);
188
+ nativeDispose = service.subscribe(() => {
189
+ updateSnapshot(this.instanceId, service.getSnapshot());
190
+ });
191
+ }
192
+ listener();
859
193
 
860
- getResolvedConnection(): ResolvedIbkrGatewayConnection | null {
861
- return this.resolvedConnection;
194
+ return () => {
195
+ nativeDispose?.();
196
+ listeners.get(this.instanceId)?.delete(listener);
197
+ };
862
198
  }
863
199
 
864
200
  async connect(config: IbkrGatewayConfig): Promise<void> {
865
- const nextKey = JSON.stringify({
866
- host: config.host,
867
- port: config.port ?? null,
868
- clientId: config.clientId ?? null,
869
- marketDataType: config.marketDataType ?? "auto",
870
- });
871
- if (this.api?.isConnected && this.configKey === nextKey) {
201
+ const remote = getBrokerRemoteClient();
202
+ if (remote) {
203
+ await remote.invoke<void>(this.instanceId, "connect");
872
204
  return;
873
205
  }
874
- if (this.connecting) {
875
- return this.connecting;
876
- }
877
-
878
- this.connecting = this.connectWithClientFallback(config, nextKey);
879
- try {
880
- await this.connecting;
881
- } finally {
882
- this.connecting = null;
883
- }
206
+ const service = await getNativeService(this.instanceId);
207
+ await service.connect(config);
208
+ updateSnapshot(this.instanceId, service.getSnapshot());
884
209
  }
885
210
 
886
211
  async disconnect(): Promise<void> {
887
- this.teardownQuoteStreams();
888
- this.api?.disconnect();
889
- this.api = null;
890
- this.configKey = null;
891
- this.autoMarketData = true;
892
- this.activeMarketDataType = MarketDataType.REALTIME;
893
- this.connectionNote = undefined;
894
- this.resolvedConnection = null;
895
- await this.releaseClientLock();
896
- this.updateSnapshot({
897
- ...this.snapshot,
898
- status: { state: "disconnected", updatedAt: Date.now() },
899
- accounts: [],
900
- openOrders: [],
901
- executions: [],
902
- });
903
- }
904
-
905
- async searchInstruments(query: string, config: IbkrGatewayConfig): Promise<InstrumentSearchResult[]> {
906
- await this.connect(config);
907
- const [descriptions, directMatches] = await Promise.all([
908
- withTimeout(this.api!.getMatchingSymbols(query), IBKR_DATA_TIMEOUT, "getMatchingSymbols").catch(() => [] as ContractDescription[]),
909
- withTimeout(this.getDirectContractMatches(query), IBKR_DATA_TIMEOUT, "getDirectContractMatches").catch(() => [] as InstrumentSearchResult[]),
910
- ]);
911
- const merged = new Map<string, InstrumentSearchResult>();
912
-
913
- for (const result of descriptions
914
- .map((description) => this.contractDescriptionToSearchResult(description))
915
- .filter((value): value is InstrumentSearchResult => value != null)) {
916
- const key = `${result.symbol}|${result.exchange}|${result.type}`;
917
- merged.set(key, result);
918
- }
919
-
920
- for (const result of directMatches) {
921
- const key = `${result.symbol}|${result.exchange}|${result.type}`;
922
- merged.set(key, result);
923
- }
924
-
925
- return [...merged.values()];
212
+ const remote = getBrokerRemoteClient();
213
+ if (remote) return remote.invoke<void>(this.instanceId, "disconnect");
214
+ const service = await getNativeService(this.instanceId);
215
+ await service.disconnect();
216
+ updateSnapshot(this.instanceId, service.getSnapshot());
926
217
  }
927
218
 
928
219
  async getAccounts(config: IbkrGatewayConfig): Promise<BrokerAccount[]> {
929
- await this.connect(config);
930
- return this.loadAccountsAndUpdateSnapshot();
220
+ const remote = getBrokerRemoteClient();
221
+ const accounts = remote
222
+ ? await remote.invoke<BrokerAccount[]>(this.instanceId, "listAccounts")
223
+ : await (await getNativeService(this.instanceId)).getAccounts(config);
224
+ updateSnapshot(this.instanceId, { accounts });
225
+ return accounts;
931
226
  }
932
227
 
933
228
  async getPositions(config: IbkrGatewayConfig): Promise<BrokerPosition[]> {
934
- await this.connect(config);
935
- const update = await firstValueFrom(this.api!.getPositions().pipe(take(1), timeout(10_000)));
936
- const portfolioSnapshots = await this.loadPortfolioSnapshotsForAccounts([...update.all.keys()]);
937
- const positions: BrokerPosition[] = [];
938
- for (const [accountId, accountPositions] of update.all) {
939
- for (const position of accountPositions) {
940
- if (!position.contract.symbol) continue;
941
- const portfolioSnapshot = portfolioSnapshots.get(buildPortfolioPositionKey(accountId, position.contract));
942
- const priceDivisor = getIbkrPriceDivisor(position.contract);
943
- positions.push({
944
- ticker: position.contract.localSymbol || position.contract.symbol,
945
- exchange: position.contract.primaryExch || position.contract.exchange || "",
946
- shares: Math.abs(position.pos),
947
- avgCost: normalizeIbkrPriceValue(portfolioSnapshot?.avgCost ?? position.avgCost, priceDivisor),
948
- currency: position.contract.currency || "USD",
949
- accountId,
950
- name: position.contract.description || position.contract.localSymbol || position.contract.symbol,
951
- assetCategory: position.contract.secType,
952
- markPrice: normalizeIbkrPriceValue(portfolioSnapshot?.marketPrice ?? position.marketPrice, priceDivisor),
953
- marketValue: portfolioSnapshot?.marketValue ?? position.marketValue,
954
- unrealizedPnl: portfolioSnapshot?.unrealizedPNL ?? position.unrealizedPNL,
955
- side: position.pos < 0 ? "short" : "long",
956
- multiplier: position.contract.multiplier,
957
- brokerContract: this.contractToRef(position.contract),
958
- });
959
- }
960
- }
961
- return positions;
229
+ const remote = getBrokerRemoteClient();
230
+ return remote
231
+ ? remote.invoke(this.instanceId, "importPositions")
232
+ : (await getNativeService(this.instanceId)).getPositions(config);
962
233
  }
963
234
 
964
- async getQuote(
965
- ticker: string,
966
- config: IbkrGatewayConfig,
967
- exchange = "",
968
- instrument?: BrokerContractRef | null,
969
- ): Promise<Quote> {
970
- return this.dedup(`quote:${ticker}:${exchange}`, async () => {
971
- await this.connect(config);
972
- const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
973
- const details = await withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails");
974
- const marketData = await this.withMarketDataFallback(
975
- config,
976
- () => withTimeout(this.api!.getMarketDataSnapshot(contract, "", false), IBKR_DATA_TIMEOUT, "getMarketDataSnapshot"),
977
- );
978
- const quote = this.marketDataToQuote(contract, details, marketData);
979
- quote.dataSource = hasDelayedMarketData(marketData) || this.activeMarketDataType !== MarketDataType.REALTIME
980
- ? "delayed"
981
- : "live";
982
- return quote;
983
- });
235
+ async listOpenOrders(config: IbkrGatewayConfig): Promise<BrokerOrder[]> {
236
+ const remote = getBrokerRemoteClient();
237
+ const openOrders = remote
238
+ ? await remote.invoke<BrokerOrder[]>(this.instanceId, "listOpenOrders")
239
+ : await (await getNativeService(this.instanceId)).listOpenOrders(config);
240
+ updateSnapshot(this.instanceId, { openOrders });
241
+ return openOrders;
984
242
  }
985
243
 
986
- subscribeQuotes(
987
- config: IbkrGatewayConfig,
988
- targets: QuoteSubscriptionTarget[],
989
- onQuote: QuoteStreamListener,
990
- ): () => void {
991
- const uniqueTargets = [...new Map(
992
- targets
993
- .map((target) => normalizeQuoteStreamTarget(target))
994
- .filter((target): target is QuoteSubscriptionTarget => target != null)
995
- .map((target) => [buildQuoteStreamKey(target), target] as const),
996
- ).values()];
997
-
998
- for (const target of uniqueTargets) {
999
- const key = buildQuoteStreamKey(target);
1000
- const stream = this.quoteStreams.get(key) ?? {
1001
- target,
1002
- listeners: new Map<QuoteStreamListener, QuoteSubscriptionTarget>(),
1003
- stop: () => {},
1004
- };
1005
- stream.target = target;
1006
- stream.listeners.set(onQuote, target);
1007
- this.quoteStreams.set(key, stream);
1008
-
1009
- if (!this.quoteStreamStarts.has(key) && stream.listeners.size === 1) {
1010
- const startPromise = this.ensureQuoteStream(key, target, config)
1011
- .catch((error) => {
1012
- gatewayLog.warn("Quote stream setup failed", {
1013
- instanceId: this.instanceId,
1014
- symbol: target.symbol,
1015
- exchange: target.exchange ?? "",
1016
- error: error instanceof Error ? error.message : String(error ?? ""),
1017
- });
1018
- })
1019
- .finally(() => {
1020
- this.quoteStreamStarts.delete(key);
1021
- });
1022
- this.quoteStreamStarts.set(key, startPromise);
1023
- }
1024
- }
244
+ async listExecutions(config: IbkrGatewayConfig): Promise<BrokerExecution[]> {
245
+ const remote = getBrokerRemoteClient();
246
+ const executions = remote
247
+ ? await remote.invoke<BrokerExecution[]>(this.instanceId, "listExecutions")
248
+ : await (await getNativeService(this.instanceId)).listExecutions(config);
249
+ updateSnapshot(this.instanceId, { executions });
250
+ return executions;
251
+ }
1025
252
 
1026
- return () => {
1027
- for (const target of uniqueTargets) {
1028
- const key = buildQuoteStreamKey(target);
1029
- const stream = this.quoteStreams.get(key);
1030
- if (!stream) continue;
1031
- stream.listeners.delete(onQuote);
1032
- if (stream.listeners.size === 0) {
1033
- stream.stop();
1034
- this.quoteStreams.delete(key);
1035
- }
1036
- }
1037
- };
253
+ async searchInstruments(query: string, config: IbkrGatewayConfig): Promise<InstrumentSearchResult[]> {
254
+ const remote = getBrokerRemoteClient();
255
+ return remote
256
+ ? remote.invoke(this.instanceId, "searchInstruments", [query])
257
+ : (await getNativeService(this.instanceId)).searchInstruments(query, config);
1038
258
  }
1039
259
 
1040
260
  async getTickerFinancials(
@@ -1043,49 +263,22 @@ export class IbkrGatewayService {
1043
263
  exchange = "",
1044
264
  instrument?: BrokerContractRef | null,
1045
265
  ): Promise<TickerFinancials> {
1046
- return this.dedup(`financials:${ticker}:${exchange}`, async () => {
1047
- await this.connect(config);
1048
- const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
1049
-
1050
- const [quote, priceHistory, snapshotXml, statementsXml] = await Promise.all([
1051
- this.getQuote(ticker, config, exchange, instrument),
1052
- this.getPriceHistory(ticker, config, exchange, "1Y", instrument),
1053
- this.fetchFundamentalData(contract, "ReportSnapshot"),
1054
- this.fetchFundamentalData(contract, "ReportsFinStatements"),
1055
- ]);
1056
-
1057
- const fundamentals = snapshotXml ? parseReportSnapshot(snapshotXml) : {};
1058
- const statements = statementsXml ? parseFinStatements(statementsXml) : { annual: [], quarterly: [] };
1059
-
1060
- // Compute 1Y return from price history
1061
- if (priceHistory.length >= 2) {
1062
- const oldest = priceHistory[0]!.close;
1063
- const newest = priceHistory[priceHistory.length - 1]!.close;
1064
- if (oldest > 0) {
1065
- fundamentals.return1Y = (newest - oldest) / oldest;
1066
- }
1067
- }
1068
-
1069
- return {
1070
- quote,
1071
- fundamentals,
1072
- annualStatements: statements.annual,
1073
- quarterlyStatements: statements.quarterly,
1074
- priceHistory,
1075
- };
1076
- });
266
+ const remote = getBrokerRemoteClient();
267
+ return remote
268
+ ? remote.invoke(this.instanceId, "getTickerFinancials", [ticker, exchange, instrument])
269
+ : (await getNativeService(this.instanceId)).getTickerFinancials(ticker, config, exchange, instrument);
1077
270
  }
1078
271
 
1079
- private async fetchFundamentalData(contract: Contract, reportType: string): Promise<string | null> {
1080
- try {
1081
- return await withTimeout(
1082
- this.api!.getFundamentalData(contract, reportType),
1083
- IBKR_DATA_TIMEOUT,
1084
- `getFundamentalData(${reportType})`,
1085
- );
1086
- } catch {
1087
- return null; // Paper account or no Reuters subscription — Yahoo fallback handles it
1088
- }
272
+ async getQuote(
273
+ ticker: string,
274
+ config: IbkrGatewayConfig,
275
+ exchange = "",
276
+ instrument?: BrokerContractRef | null,
277
+ ): Promise<Quote> {
278
+ const remote = getBrokerRemoteClient();
279
+ return remote
280
+ ? remote.invoke(this.instanceId, "getQuote", [ticker, exchange, instrument])
281
+ : (await getNativeService(this.instanceId)).getQuote(ticker, config, exchange, instrument);
1089
282
  }
1090
283
 
1091
284
  async getPriceHistory(
@@ -1095,54 +288,22 @@ export class IbkrGatewayService {
1095
288
  range: TimeRange,
1096
289
  instrument?: BrokerContractRef | null,
1097
290
  ): Promise<PricePoint[]> {
1098
- return this.dedup(`history:${ticker}:${exchange}:${range}`, async () => {
1099
- await this.connect(config);
1100
- const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
1101
- const params = HISTORY_PARAMS[range];
1102
- const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
1103
- .catch(() => undefined);
1104
- const bars = await this.withMarketDataFallback(
1105
- config,
1106
- () => withTimeout(this.api!.getHistoricalData(
1107
- contract,
1108
- "",
1109
- params.duration,
1110
- params.size,
1111
- WhatToShow.TRADES,
1112
- 1,
1113
- 1,
1114
- ), IBKR_DATA_TIMEOUT, "getHistoricalData"),
1115
- );
1116
- const details = await detailsPromise;
1117
- const priceDivisor = getIbkrPriceDivisor(contract, details);
1118
-
1119
- return bars.map((bar) => ({
1120
- date: parseIbkrHistoricalBarTime(bar.time),
1121
- open: normalizeIbkrPriceValue(bar.open, priceDivisor),
1122
- high: normalizeIbkrPriceValue(bar.high, priceDivisor),
1123
- low: normalizeIbkrPriceValue(bar.low, priceDivisor),
1124
- close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
1125
- volume: bar.volume,
1126
- }));
1127
- });
291
+ const remote = getBrokerRemoteClient();
292
+ return remote
293
+ ? remote.invoke(this.instanceId, "getPriceHistory", [ticker, exchange, range, instrument])
294
+ : (await getNativeService(this.instanceId)).getPriceHistory(ticker, config, exchange, range, instrument);
1128
295
  }
1129
296
 
1130
297
  getChartResolutionSupport(
1131
- _ticker: string,
1132
- _config: IbkrGatewayConfig,
1133
- _exchange?: string,
1134
- _instrument?: BrokerContractRef | null,
1135
- ): ChartResolutionSupport[] {
1136
- return IBKR_RESOLUTION_SUPPORT;
1137
- }
1138
-
1139
- getChartResolutionCapabilities(
1140
- _ticker: string,
1141
- _config: IbkrGatewayConfig,
1142
- _exchange?: string,
1143
- _instrument?: BrokerContractRef | null,
1144
- ): ManualChartResolution[] {
1145
- return IBKR_RESOLUTION_SUPPORT.map((entry) => entry.resolution);
298
+ ticker: string,
299
+ config: IbkrGatewayConfig,
300
+ exchange = "",
301
+ instrument?: BrokerContractRef | null,
302
+ ): Promise<ChartResolutionSupport[]> | ChartResolutionSupport[] {
303
+ const remote = getBrokerRemoteClient();
304
+ return remote
305
+ ? remote.invoke(this.instanceId, "getChartResolutionSupport", [ticker, exchange, instrument])
306
+ : getNativeService(this.instanceId).then((service) => service.getChartResolutionSupport(ticker, config, exchange, instrument));
1146
307
  }
1147
308
 
1148
309
  async getPriceHistoryForResolution(
@@ -1153,39 +314,10 @@ export class IbkrGatewayService {
1153
314
  resolution: ManualChartResolution,
1154
315
  instrument?: BrokerContractRef | null,
1155
316
  ): Promise<PricePoint[]> {
1156
- const ibkrBarSize = GENERIC_BAR_SIZE_MAP[resolution];
1157
- if (!ibkrBarSize) return [];
1158
-
1159
- return this.dedup(`history:${ticker}:${exchange}:${bufferRange}:${resolution}`, async () => {
1160
- await this.connect(config);
1161
- const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
1162
- const params = HISTORY_PARAMS[bufferRange];
1163
- const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
1164
- .catch(() => undefined);
1165
- const bars = await this.withMarketDataFallback(
1166
- config,
1167
- () => withTimeout(this.api!.getHistoricalData(
1168
- contract,
1169
- "",
1170
- params.duration,
1171
- ibkrBarSize,
1172
- WhatToShow.TRADES,
1173
- 1,
1174
- 1,
1175
- ), IBKR_DATA_TIMEOUT, "getHistoricalData"),
1176
- );
1177
- const details = await detailsPromise;
1178
- const priceDivisor = getIbkrPriceDivisor(contract, details);
1179
-
1180
- return bars.map((bar) => ({
1181
- date: parseIbkrHistoricalBarTime(bar.time),
1182
- open: normalizeIbkrPriceValue(bar.open, priceDivisor),
1183
- high: normalizeIbkrPriceValue(bar.high, priceDivisor),
1184
- low: normalizeIbkrPriceValue(bar.low, priceDivisor),
1185
- close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
1186
- volume: bar.volume,
1187
- }));
1188
- });
317
+ const remote = getBrokerRemoteClient();
318
+ return remote
319
+ ? remote.invoke(this.instanceId, "getPriceHistoryForResolution", [ticker, exchange, bufferRange, resolution, instrument])
320
+ : (await getNativeService(this.instanceId)).getPriceHistoryForResolution(ticker, config, exchange, bufferRange, resolution, instrument);
1189
321
  }
1190
322
 
1191
323
  async getDetailedPriceHistory(
@@ -1197,1181 +329,68 @@ export class IbkrGatewayService {
1197
329
  barSize: string,
1198
330
  instrument?: BrokerContractRef | null,
1199
331
  ): Promise<PricePoint[]> {
1200
- const ibkrBarSize = GENERIC_BAR_SIZE_MAP[barSize];
1201
- if (!ibkrBarSize) return [];
1202
-
1203
- const key = `detail:${ticker}:${exchange}:${barSize}:${startDate.getTime()}:${endDate.getTime()}`;
1204
- return this.dedup(key, async () => {
1205
- await this.connect(config);
1206
- const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
1207
- const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
1208
- .catch(() => undefined);
1209
-
1210
- // Format endDateTime as "yyyyMMdd HH:mm:ss" for IBKR
1211
- const pad = (n: number) => String(n).padStart(2, "0");
1212
- const endDateTime = `${endDate.getFullYear()}${pad(endDate.getMonth() + 1)}${pad(endDate.getDate())} ${pad(endDate.getHours())}:${pad(endDate.getMinutes())}:${pad(endDate.getSeconds())}`;
1213
-
1214
- // Compute duration from date range
1215
- const spanMs = endDate.getTime() - startDate.getTime();
1216
- const spanDays = Math.ceil(spanMs / (1000 * 60 * 60 * 24));
1217
- let durationStr: string;
1218
- if (spanDays <= 1) durationStr = `${Math.max(Math.ceil(spanMs / (1000 * 60 * 60)), 1)} hours`;
1219
- else if (spanDays <= 30) durationStr = `${spanDays} D`;
1220
- else if (spanDays <= 365) durationStr = `${Math.ceil(spanDays / 30)} M`;
1221
- else durationStr = `${Math.ceil(spanDays / 365)} Y`;
1222
-
1223
- const bars = await this.withMarketDataFallback(
1224
- config,
1225
- () => withTimeout(this.api!.getHistoricalData(
1226
- contract,
1227
- endDateTime,
1228
- durationStr,
1229
- ibkrBarSize,
1230
- WhatToShow.TRADES,
1231
- 1,
1232
- 1,
1233
- ), IBKR_DATA_TIMEOUT, "getDetailedHistoricalData"),
1234
- );
1235
- const details = await detailsPromise;
1236
- const priceDivisor = getIbkrPriceDivisor(contract, details);
1237
-
1238
- return bars.map((bar) => ({
1239
- date: parseIbkrHistoricalBarTime(bar.time),
1240
- open: normalizeIbkrPriceValue(bar.open, priceDivisor),
1241
- high: normalizeIbkrPriceValue(bar.high, priceDivisor),
1242
- low: normalizeIbkrPriceValue(bar.low, priceDivisor),
1243
- close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
1244
- volume: bar.volume,
1245
- }));
1246
- });
1247
- }
1248
-
1249
- async listOpenOrders(config: IbkrGatewayConfig): Promise<BrokerOrder[]> {
1250
- await this.connect(config);
1251
- return this.loadOpenOrders();
1252
- }
1253
-
1254
- async listExecutions(config: IbkrGatewayConfig): Promise<BrokerExecution[]> {
1255
- await this.connect(config);
1256
- return this.loadExecutions();
1257
- }
1258
-
1259
- async previewOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrderPreview> {
1260
- await this.connect(config);
1261
- const contract = await this.resolveContract(request.contract.symbol, request.contract.exchange || "", request.contract);
1262
- const rawApi = this.getRawApi();
1263
- const orderId = await this.api!.getNextValidOrderId();
1264
- const order = this.buildOrder(request, true);
1265
-
1266
- return new Promise<BrokerOrderPreview>((resolve, reject) => {
1267
- const timeoutId = setTimeout(() => {
1268
- cleanup();
1269
- reject(new Error("Timed out while previewing order"));
1270
- }, 10_000);
1271
-
1272
- const cleanup = () => {
1273
- clearTimeout(timeoutId);
1274
- rawApi.off(EventName.openOrder, onOpenOrder);
1275
- rawApi.off(EventName.error, onError);
1276
- };
1277
-
1278
- const onOpenOrder = (incomingOrderId: number, _contract: Contract, _order: Order, orderState: OrderState) => {
1279
- if (incomingOrderId !== orderId) return;
1280
- cleanup();
1281
- resolve({
1282
- initMarginBefore: orderState.initMarginBefore,
1283
- initMarginAfter: orderState.initMarginAfter,
1284
- maintMarginBefore: orderState.maintMarginBefore,
1285
- maintMarginAfter: orderState.maintMarginAfter,
1286
- equityWithLoanBefore: orderState.equityWithLoanBefore,
1287
- equityWithLoanAfter: orderState.equityWithLoanAfter,
1288
- commission: orderState.commission,
1289
- commissionCurrency: orderState.commissionCurrency,
1290
- warningText: orderState.warningText,
1291
- });
1292
- };
1293
-
1294
- const onError = (error: Error, code: number, reqId: number) => {
1295
- if (reqId !== orderId) return;
1296
- cleanup();
1297
- reject(new Error(error.message || `IBKR error ${code}`));
1298
- };
1299
-
1300
- rawApi.on(EventName.openOrder, onOpenOrder);
1301
- rawApi.on(EventName.error, onError);
1302
- rawApi.placeOrder(orderId, contract, order);
1303
- });
1304
- }
1305
-
1306
- async placeOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrder> {
1307
- await this.connect(config);
1308
- const contract = await this.resolveContract(request.contract.symbol, request.contract.exchange || "", request.contract);
1309
- const order = this.buildOrder(request, false);
1310
- const rawApi = this.getRawApi();
1311
- const orderId = await this.api!.getNextValidOrderId();
1312
-
1313
- await new Promise<void>((resolve, reject) => {
1314
- const timeoutId = setTimeout(() => {
1315
- cleanup();
1316
- reject(new Error("Timed out waiting for order acknowledgement"));
1317
- }, 10_000);
1318
-
1319
- const cleanup = () => {
1320
- clearTimeout(timeoutId);
1321
- rawApi.off(EventName.openOrder, onOpenOrder);
1322
- rawApi.off(EventName.orderStatus, onOrderStatus);
1323
- rawApi.off(EventName.error, onError);
1324
- };
1325
-
1326
- const onOpenOrder = (incomingOrderId: number) => {
1327
- if (incomingOrderId !== orderId) return;
1328
- cleanup();
1329
- resolve();
1330
- };
1331
-
1332
- const onOrderStatus = (incomingOrderId: number, status: string) => {
1333
- if (incomingOrderId !== orderId) return;
1334
- cleanup();
1335
- resolve();
1336
- };
1337
-
1338
- const onError = (error: Error, code: number, reqId: number) => {
1339
- if (reqId !== orderId) return;
1340
- cleanup();
1341
- reject(new Error(error.message || `IBKR error ${code}`));
1342
- };
1343
-
1344
- rawApi.on(EventName.openOrder, onOpenOrder);
1345
- rawApi.on(EventName.orderStatus, onOrderStatus);
1346
- rawApi.on(EventName.error, onError);
1347
- rawApi.placeOrder(orderId, contract, order);
1348
- });
1349
-
1350
- const openOrders = await this.listOpenOrders(config);
1351
- return openOrders.find((openOrder) => openOrder.orderId === orderId) ?? {
1352
- orderId,
1353
- brokerInstanceId: this.instanceId,
1354
- accountId: request.accountId,
1355
- status: "Submitted",
1356
- action: request.action,
1357
- orderType: request.orderType,
1358
- quantity: request.quantity,
1359
- filled: 0,
1360
- remaining: request.quantity,
1361
- limitPrice: request.limitPrice,
1362
- stopPrice: request.stopPrice,
1363
- tif: request.tif,
1364
- updatedAt: Date.now(),
1365
- contract: request.contract,
1366
- };
1367
- }
1368
-
1369
- async modifyOrder(config: IbkrGatewayConfig, orderId: number, request: BrokerOrderRequest): Promise<BrokerOrder> {
1370
- await this.connect(config);
1371
- const contract = await this.resolveContract(request.contract.symbol, request.contract.exchange || "", request.contract);
1372
- const rawApi = this.getRawApi();
1373
- const order = this.buildOrder({ ...request }, false);
1374
-
1375
- await new Promise<void>((resolve, reject) => {
1376
- const timeoutId = setTimeout(() => {
1377
- cleanup();
1378
- reject(new Error("Timed out waiting for order modification acknowledgement"));
1379
- }, 10_000);
1380
-
1381
- const cleanup = () => {
1382
- clearTimeout(timeoutId);
1383
- rawApi.off(EventName.openOrder, onOpenOrder);
1384
- rawApi.off(EventName.orderStatus, onOrderStatus);
1385
- rawApi.off(EventName.error, onError);
1386
- };
1387
-
1388
- const onOpenOrder = (incomingOrderId: number) => {
1389
- if (incomingOrderId !== orderId) return;
1390
- cleanup();
1391
- resolve();
1392
- };
1393
-
1394
- const onOrderStatus = (incomingOrderId: number, status: string) => {
1395
- if (incomingOrderId !== orderId) return;
1396
- cleanup();
1397
- resolve();
1398
- };
1399
-
1400
- const onError = (error: Error, code: number, reqId: number) => {
1401
- if (reqId !== orderId) return;
1402
- cleanup();
1403
- reject(new Error(error.message || `IBKR error ${code}`));
1404
- };
1405
-
1406
- rawApi.on(EventName.openOrder, onOpenOrder);
1407
- rawApi.on(EventName.orderStatus, onOrderStatus);
1408
- rawApi.on(EventName.error, onError);
1409
- rawApi.placeOrder(orderId, contract, order);
1410
- });
1411
-
1412
- const openOrders = await this.listOpenOrders(config);
1413
- return openOrders.find((openOrder) => openOrder.orderId === orderId) ?? {
1414
- orderId,
1415
- brokerInstanceId: this.instanceId,
1416
- accountId: request.accountId,
1417
- status: "Submitted",
1418
- action: request.action,
1419
- orderType: request.orderType,
1420
- quantity: request.quantity,
1421
- filled: 0,
1422
- remaining: request.quantity,
1423
- limitPrice: request.limitPrice,
1424
- stopPrice: request.stopPrice,
1425
- tif: request.tif,
1426
- updatedAt: Date.now(),
1427
- contract: request.contract,
1428
- };
1429
- }
1430
-
1431
- async cancelOrder(config: IbkrGatewayConfig, orderId: number): Promise<void> {
1432
- await this.connect(config);
1433
- this.api!.cancelOrder(orderId);
1434
- await this.listOpenOrders(config);
1435
- }
1436
-
1437
- private async connectWithClientFallback(config: IbkrGatewayConfig, configKey: string): Promise<void> {
1438
- let resolvedConfig: ResolvedIbkrGatewayConnection;
1439
- try {
1440
- resolvedConfig = await resolveGatewayConnection(config);
1441
- } catch (error: any) {
1442
- const message = error?.message || String(error || "");
1443
- this.updateSnapshot({
1444
- ...this.snapshot,
1445
- status: {
1446
- state: "error",
1447
- updatedAt: Date.now(),
1448
- mode: "gateway",
1449
- message,
1450
- },
1451
- lastError: message,
1452
- });
1453
- throw error;
1454
- }
1455
-
1456
- const candidates = this.getClientIdCandidates(resolvedConfig.requestedClientId);
1457
- let lastConflict: Error | null = null;
1458
-
1459
- for (const clientId of candidates) {
1460
- const claimed = await this.tryClaimClientLock(resolvedConfig, clientId, resolvedConfig.requestedClientId);
1461
- if (!claimed) continue;
1462
-
1463
- try {
1464
- await this.connectInternal(
1465
- { ...config, host: resolvedConfig.host, port: resolvedConfig.port, clientId },
1466
- configKey,
1467
- buildConnectionNote(resolvedConfig.requestedClientId, clientId, resolvedConfig.requestedPort, resolvedConfig.port),
1468
- );
1469
- this.resolvedConnection = {
1470
- ...resolvedConfig,
1471
- clientId,
1472
- };
1473
- void resolvedGatewayListener?.(this.instanceId, this.resolvedConnection);
1474
- return;
1475
- } catch (error: any) {
1476
- const code = getIbErrorCode(error);
1477
- const message = getIbErrorMessage(error) || error?.message || String(error || "");
1478
- await this.disconnect();
1479
- if (!isClientIdInUseError(code, message)) {
1480
- throw error;
1481
- }
1482
- lastConflict = new Error(message || `IBKR client ID ${clientId} is already in use.`);
1483
- }
1484
- }
1485
-
1486
- throw lastConflict ?? new Error(
1487
- `No IBKR client IDs are available near ${resolvedConfig.requestedClientId}. Close other sessions or choose a different client ID.`,
1488
- );
1489
- }
1490
-
1491
- private async connectInternal(
1492
- config: IbkrGatewayConfig,
1493
- configKey: string,
1494
- connectionNote?: string,
1495
- ): Promise<void> {
1496
- if (this.api && this.configKey !== configKey) {
1497
- await this.disconnect();
1498
- }
1499
-
1500
- this.connectionNote = connectionNote;
1501
- this.updateSnapshot({
1502
- ...this.snapshot,
1503
- status: {
1504
- state: "connecting",
1505
- updatedAt: Date.now(),
1506
- mode: "gateway",
1507
- message: this.connectionNote,
1508
- },
1509
- lastError: undefined,
1510
- });
1511
-
1512
- if (!this.api) {
1513
- this.api = new IBApiNext({
1514
- host: config.host,
1515
- port: config.port,
1516
- reconnectInterval: 2_000,
1517
- });
1518
- this.bindConnectionEvents(this.api);
1519
- }
1520
-
1521
- let unsubscribeConflict = () => {};
1522
- const conflictPromise = new Promise<never>((_, reject) => {
1523
- const subscription = this.api!.error.subscribe((err) => {
1524
- const code = getIbErrorCode(err);
1525
- const message = getIbErrorMessage(err);
1526
- if (!isClientIdInUseError(code, message)) return;
1527
- subscription.unsubscribe();
1528
- reject(new Error(message || `IBKR client ID ${config.clientId} is already in use.`));
1529
- });
1530
- unsubscribeConflict = () => subscription.unsubscribe();
1531
- });
1532
-
1533
- this.api.connect(config.clientId);
1534
- try {
1535
- await Promise.race([
1536
- firstValueFrom(this.api.connectionState.pipe(
1537
- filter((state) => state === ConnectionState.Connected),
1538
- take(1),
1539
- timeout(10_000),
1540
- )),
1541
- conflictPromise,
1542
- ]);
1543
- } finally {
1544
- unsubscribeConflict();
1545
- }
1546
-
1547
- gatewayLog.info("Connected to IBKR gateway", {
1548
- instanceId: this.instanceId,
1549
- requestedClientId: this.claimedClientLock?.requestedClientId ?? config.clientId,
1550
- actualClientId: config.clientId,
1551
- host: config.host,
1552
- port: config.port,
1553
- });
1554
- this.autoMarketData = (config.marketDataType ?? "auto") === "auto";
1555
- this.activeMarketDataType = marketDataTypeFromConfig(config);
1556
- this.api.setMarketDataType(this.activeMarketDataType);
1557
- this.configKey = configKey;
1558
- await Promise.allSettled([
1559
- this.loadAccountsAndUpdateSnapshot(),
1560
- this.loadOpenOrders(),
1561
- this.loadExecutions(),
1562
- ]);
1563
- this.updateSnapshot({
1564
- ...this.snapshot,
1565
- status: {
1566
- state: "connected",
1567
- updatedAt: Date.now(),
1568
- mode: "gateway",
1569
- message: this.connectionNote,
1570
- },
1571
- });
1572
- }
1573
-
1574
- private bindConnectionEvents(api: IBApiNext): void {
1575
- api.connectionState.subscribe((state) => {
1576
- if (state === ConnectionState.Connected) {
1577
- this.updateSnapshot({
1578
- ...this.snapshot,
1579
- status: {
1580
- state: "connected",
1581
- updatedAt: Date.now(),
1582
- mode: "gateway",
1583
- message: this.connectionNote,
1584
- },
1585
- });
1586
- } else if (state === ConnectionState.Connecting) {
1587
- this.updateSnapshot({
1588
- ...this.snapshot,
1589
- status: {
1590
- state: "connecting",
1591
- updatedAt: Date.now(),
1592
- mode: "gateway",
1593
- message: this.connectionNote,
1594
- },
1595
- });
1596
- } else {
1597
- this.updateSnapshot({
1598
- ...this.snapshot,
1599
- status: { state: "disconnected", updatedAt: Date.now(), mode: "gateway" },
1600
- });
1601
- }
1602
- });
1603
-
1604
- // Capture account IDs as they arrive from the managedAccounts event on connection.
1605
- const rawApi = this.getRawApi();
1606
- rawApi.on(EventName.managedAccounts, (accountsList: string) => {
1607
- this.cachedAccountIds = accountsList.split(",").map((s: string) => s.trim()).filter(Boolean);
1608
- });
1609
-
1610
- api.error.subscribe((err) => {
1611
- const code = getIbErrorCode(err);
1612
- const message = getIbErrorMessage(err);
1613
- if (isMarketDataPermissionError(code, message)) {
1614
- this.updateSnapshot({
1615
- ...this.snapshot,
1616
- status: {
1617
- state: "connected",
1618
- updatedAt: Date.now(),
1619
- mode: "gateway",
1620
- message: this.autoMarketData
1621
- ? "Live API market data unavailable; delayed quotes will be used when IBKR allows them."
1622
- : message,
1623
- },
1624
- lastError: message,
1625
- });
1626
- return;
1627
- }
1628
- if (isIbInformationalWarning(code, message)) {
1629
- this.updateSnapshot({
1630
- ...this.snapshot,
1631
- status: {
1632
- ...this.snapshot.status,
1633
- updatedAt: Date.now(),
1634
- mode: "gateway",
1635
- message,
1636
- },
1637
- });
1638
- return;
1639
- }
1640
-
1641
- const keepConnectionState = this.snapshot.status.state === "connected" || this.snapshot.status.state === "connecting";
1642
- this.updateSnapshot({
1643
- ...this.snapshot,
1644
- status: keepConnectionState
1645
- ? {
1646
- ...this.snapshot.status,
1647
- updatedAt: Date.now(),
1648
- mode: "gateway",
1649
- message,
1650
- }
1651
- : { state: "error", updatedAt: Date.now(), mode: "gateway", message },
1652
- lastError: message,
1653
- });
1654
- });
1655
- }
1656
-
1657
- private getClientIdCandidates(requestedClientId: number): number[] {
1658
- const preferred = this.claimedClientLock?.clientId;
1659
- const candidates = new Set<number>();
1660
- if (preferred && Number.isFinite(preferred) && preferred > 0) {
1661
- candidates.add(preferred);
1662
- }
1663
- for (let offset = 0; offset < IBKR_CLIENT_ID_SEARCH_SPAN; offset += 1) {
1664
- candidates.add(requestedClientId + offset);
1665
- }
1666
- return [...candidates];
1667
- }
1668
-
1669
- private async tryClaimClientLock(
1670
- config: IbkrGatewayConfig,
1671
- clientId: number,
1672
- requestedClientId: number,
1673
- ): Promise<boolean> {
1674
- const path = buildClientLockPath(config, clientId);
1675
- const existing = this.claimedClientLock;
1676
- if (
1677
- existing
1678
- && existing.clientId === clientId
1679
- && existing.requestedClientId === requestedClientId
1680
- ) {
1681
- return true;
1682
- }
1683
-
1684
- await mkdir(IBKR_CLIENT_LOCK_DIR, { recursive: true });
1685
- const metadata: ClientLockMetadata = {
1686
- pid: process.pid,
1687
- ownerToken: this.clientLockOwner,
1688
- requestedClientId,
1689
- clientId,
1690
- host: config.host,
1691
- port: config.port,
1692
- instanceId: this.instanceId,
1693
- cwd: process.cwd(),
1694
- timestamp: Date.now(),
1695
- };
1696
-
1697
- try {
1698
- const handle = await open(path, "wx");
1699
- await handle.writeFile(JSON.stringify(metadata), "utf-8");
1700
- await handle.close();
1701
- await this.releaseClientLock();
1702
- this.claimedClientLock = { clientId, requestedClientId, path };
1703
- return true;
1704
- } catch (error: any) {
1705
- if (error?.code !== "EEXIST") throw error;
1706
- }
1707
-
1708
- const lock = await readClientLock(path);
1709
- if (!lock || !isProcessAlive(lock.pid)) {
1710
- await rm(path, { force: true }).catch(() => {});
1711
- return this.tryClaimClientLock(config, clientId, requestedClientId);
1712
- }
1713
- if (lock.ownerToken === this.clientLockOwner) {
1714
- await this.releaseClientLock();
1715
- this.claimedClientLock = { clientId, requestedClientId, path };
1716
- return true;
1717
- }
1718
- return false;
1719
- }
1720
-
1721
- private async releaseClientLock(): Promise<void> {
1722
- const lock = this.claimedClientLock;
1723
- this.claimedClientLock = null;
1724
- if (!lock) return;
1725
- await rm(lock.path, { force: true }).catch(() => {});
1726
- }
1727
-
1728
- private updateSnapshot(snapshot: IbkrSnapshot): void {
1729
- this.snapshot = snapshot;
1730
- for (const listener of this.listeners) listener();
1731
- }
1732
-
1733
- private teardownQuoteStreams(): void {
1734
- for (const stream of this.quoteStreams.values()) {
1735
- stream.stop();
1736
- }
1737
- this.quoteStreams.clear();
1738
- this.quoteStreamStarts.clear();
332
+ const remote = getBrokerRemoteClient();
333
+ return remote
334
+ ? remote.invoke(this.instanceId, "getDetailedPriceHistory", [ticker, exchange, startDate, endDate, barSize, instrument])
335
+ : (await getNativeService(this.instanceId)).getDetailedPriceHistory(ticker, config, exchange, startDate, endDate, barSize, instrument);
1739
336
  }
1740
337
 
1741
- private async ensureQuoteStream(
1742
- key: string,
1743
- target: QuoteSubscriptionTarget,
1744
- config: IbkrGatewayConfig,
1745
- ): Promise<void> {
1746
- await this.connect(config);
1747
- const contract = await withTimeout(
1748
- this.resolveContract(target.symbol, target.exchange ?? "", target.context?.instrument ?? null),
1749
- IBKR_DATA_TIMEOUT,
1750
- "resolveContract",
1751
- );
1752
- const details = await withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails");
1753
- const priceDivisor = getIbkrPriceDivisor(contract, details);
1754
- const stream = this.quoteStreams.get(key);
1755
- if (!stream || stream.listeners.size === 0) {
1756
- this.quoteStreams.delete(key);
1757
- return;
1758
- }
1759
-
1760
- const seededQuote = await this.loadSeedQuote(contract, details, config).catch(() => null);
1761
- if (seededQuote) {
1762
- stream.lastQuote = seededQuote;
1763
- for (const [listener, listenerTarget] of stream.listeners.entries()) {
1764
- listener(listenerTarget, seededQuote);
1765
- }
1766
- }
1767
-
1768
- const subscription = this.api!.getMarketData(contract, IBKR_QUOTE_STREAM_TICKS, false, false).subscribe({
1769
- next: (update) => this.emitQuoteStreamUpdate(key, contract, details, update),
1770
- error: (error) => {
1771
- gatewayLog.warn("Quote stream error", {
1772
- instanceId: this.instanceId,
1773
- symbol: target.symbol,
1774
- exchange: target.exchange ?? "",
1775
- error: error instanceof Error ? error.message : String(error ?? ""),
1776
- });
1777
- },
1778
- });
1779
- const tradeSubscription = this.api!.getTickByTickAllLastDataUpdates(contract, 0, false).subscribe({
1780
- next: (tick) => this.emitTickByTickAllLastUpdate(key, contract, details, tick, priceDivisor),
1781
- error: (error) => {
1782
- gatewayLog.warn("Tick-by-tick trade stream error", {
1783
- instanceId: this.instanceId,
1784
- symbol: target.symbol,
1785
- exchange: target.exchange ?? "",
1786
- error: error instanceof Error ? error.message : String(error ?? ""),
1787
- });
1788
- },
1789
- });
1790
- const bidAskStop = this.startTickByTickBidAskStream(
1791
- contract,
1792
- (update) => this.emitTickByTickBidAskUpdate(key, priceDivisor, update),
1793
- (error) => {
1794
- gatewayLog.warn("Tick-by-tick bid/ask stream error", {
1795
- instanceId: this.instanceId,
1796
- symbol: target.symbol,
1797
- exchange: target.exchange ?? "",
1798
- error,
1799
- });
1800
- },
1801
- );
1802
-
1803
- const liveStream = this.quoteStreams.get(key);
1804
- if (!liveStream || liveStream.listeners.size === 0) {
1805
- subscription.unsubscribe();
1806
- tradeSubscription.unsubscribe();
1807
- bidAskStop();
1808
- this.quoteStreams.delete(key);
1809
- return;
1810
- }
1811
- liveStream.stop = () => {
1812
- subscription.unsubscribe();
1813
- tradeSubscription.unsubscribe();
1814
- bidAskStop();
1815
- };
1816
- }
1817
-
1818
- private emitQuoteStreamUpdate(
1819
- key: string,
1820
- contract: Contract,
1821
- details: ContractDetails,
1822
- update: MarketDataUpdate,
1823
- ): void {
1824
- const stream = this.quoteStreams.get(key);
1825
- if (!stream || stream.listeners.size === 0) return;
1826
-
1827
- try {
1828
- const quote = this.marketDataToQuote(contract, details, update.all);
1829
- quote.providerId = "ibkr";
1830
- quote.dataSource = hasDelayedMarketData(update.all) || this.activeMarketDataType !== MarketDataType.REALTIME
1831
- ? "delayed"
1832
- : "live";
1833
- stream.lastQuote = quote;
1834
- for (const [listener, listenerTarget] of stream.listeners.entries()) {
1835
- listener(listenerTarget, quote);
1836
- }
1837
- } catch {
1838
- // Ignore partial tick snapshots until IBKR has sent a usable quote.
1839
- }
1840
- }
1841
-
1842
- private emitTickByTickAllLastUpdate(
1843
- key: string,
1844
- contract: Contract,
1845
- details: ContractDetails,
1846
- tick: TickByTickAllLast,
1847
- priceDivisor: number,
1848
- ): void {
1849
- const stream = this.quoteStreams.get(key);
1850
- if (!stream || stream.listeners.size === 0) return;
1851
-
1852
- const nextQuote = applyTickByTickAllLastToQuote(
1853
- stream.lastQuote,
1854
- contract,
1855
- details,
1856
- tick,
1857
- priceDivisor,
1858
- stream.lastQuote?.dataSource ?? (this.activeMarketDataType === MarketDataType.REALTIME ? "live" : "delayed"),
1859
- );
1860
- if (!nextQuote) return;
1861
- stream.lastQuote = nextQuote;
1862
- for (const [listener, listenerTarget] of stream.listeners.entries()) {
1863
- listener(listenerTarget, nextQuote);
1864
- }
1865
- }
1866
-
1867
- private emitTickByTickBidAskUpdate(
1868
- key: string,
1869
- priceDivisor: number,
1870
- update: TickByTickBidAskUpdate,
1871
- ): void {
1872
- const stream = this.quoteStreams.get(key);
1873
- if (!stream || stream.listeners.size === 0) return;
1874
- const nextQuote = applyTickByTickBidAskToQuote(stream.lastQuote, update, priceDivisor);
1875
- if (!nextQuote) return;
1876
- stream.lastQuote = nextQuote;
1877
- for (const [listener, listenerTarget] of stream.listeners.entries()) {
1878
- listener(listenerTarget, nextQuote);
1879
- }
1880
- }
1881
-
1882
- private async loadSeedQuote(
1883
- contract: Contract,
1884
- details: ContractDetails,
338
+ subscribeQuotes(
1885
339
  config: IbkrGatewayConfig,
1886
- ): Promise<Quote | null> {
1887
- try {
1888
- const marketData = await this.withMarketDataFallback(
1889
- config,
1890
- () => withTimeout(this.api!.getMarketDataSnapshot(contract, "", false), IBKR_DATA_TIMEOUT, "getMarketDataSnapshot"),
1891
- );
1892
- const quote = this.marketDataToQuote(contract, details, marketData);
1893
- quote.providerId = "ibkr";
1894
- quote.dataSource = hasDelayedMarketData(marketData) || this.activeMarketDataType !== MarketDataType.REALTIME
1895
- ? "delayed"
1896
- : "live";
1897
- return quote;
1898
- } catch {
1899
- return null;
1900
- }
1901
- }
1902
-
1903
- private startTickByTickBidAskStream(
1904
- contract: Contract,
1905
- onUpdate: (update: TickByTickBidAskUpdate) => void,
1906
- onError: (message: string) => void,
340
+ targets: QuoteSubscriptionTarget[],
341
+ onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
1907
342
  ): () => void {
1908
- const rawApi = this.getRawApi();
1909
- const reqId = (this.api as any)?.nextReqId;
1910
- if (typeof reqId !== "number" || !Number.isFinite(reqId)) {
1911
- onError("No request id available for tick-by-tick bid/ask stream");
1912
- return () => {};
1913
- }
1914
-
1915
- const handleBidAsk = (
1916
- incomingReqId: number,
1917
- time: number,
1918
- bidPrice: number,
1919
- askPrice: number,
1920
- bidSize: number,
1921
- askSize: number,
1922
- ) => {
1923
- if (incomingReqId !== reqId) return;
1924
- onUpdate({ time, bidPrice, askPrice, bidSize, askSize });
1925
- };
1926
- const handleError = (error: Error, code: number, incomingReqId: number) => {
1927
- if (incomingReqId !== reqId) return;
1928
- onError(error?.message || `IBKR error ${code}`);
1929
- };
1930
-
1931
- rawApi.on(EventName.tickByTickBidAsk, handleBidAsk);
1932
- rawApi.on(EventName.error, handleError);
1933
- rawApi.reqTickByTickData(reqId, contract, "BidAsk", 0, false);
1934
-
343
+ const remote = getBrokerRemoteClient();
344
+ if (remote) return remote.subscribeQuotes(this.instanceId, targets, onQuote);
345
+
346
+ let nativeDispose: (() => void) | null = null;
347
+ let disposed = false;
348
+ void getNativeService(this.instanceId).then((service) => {
349
+ if (disposed) return;
350
+ nativeDispose = service.subscribeQuotes(config, targets, onQuote);
351
+ }).catch(() => {});
1935
352
  return () => {
1936
- rawApi.off(EventName.tickByTickBidAsk, handleBidAsk);
1937
- rawApi.off(EventName.error, handleError);
1938
- try {
1939
- rawApi.cancelTickByTickData(reqId);
1940
- } catch {
1941
- // ignore shutdown races
1942
- }
353
+ disposed = true;
354
+ nativeDispose?.();
1943
355
  };
1944
356
  }
1945
357
 
1946
- private async withMarketDataFallback<T>(
1947
- config: IbkrGatewayConfig,
1948
- operation: () => Promise<T>,
1949
- ): Promise<T> {
1950
- try {
1951
- return await operation();
1952
- } catch (error: any) {
1953
- const message = error?.message || String(error || "");
1954
- const isPermission = isMarketDataPermissionError(undefined, message);
1955
- const isTimeout = message.includes("timed out");
1956
- const isNoData = message.includes("No valid market data");
1957
- if (
1958
- !this.autoMarketData
1959
- || this.activeMarketDataType === MarketDataType.DELAYED
1960
- || !(isPermission || isTimeout || isNoData)
1961
- ) {
1962
- throw error;
1963
- }
1964
-
1965
- this.api?.setMarketDataType(MarketDataType.DELAYED);
1966
- this.activeMarketDataType = MarketDataType.DELAYED;
1967
- this.updateSnapshot({
1968
- ...this.snapshot,
1969
- status: {
1970
- state: "connected",
1971
- updatedAt: Date.now(),
1972
- mode: "gateway",
1973
- message: "Using delayed IBKR market data because live API market data is not enabled for this account.",
1974
- },
1975
- lastError: message,
1976
- });
1977
- return operation();
1978
- }
1979
- }
1980
-
1981
- private async loadAccounts(): Promise<BrokerAccount[]> {
1982
- let managedAccounts: string[];
1983
- try {
1984
- managedAccounts = await withTimeout(this.api!.getManagedAccounts(), IBKR_DATA_TIMEOUT, "getManagedAccounts");
1985
- } catch {
1986
- // getManagedAccounts() can hang after reconnects or when another client with the same
1987
- // clientId is connected. Fall back to requesting via the raw event API, or use cached IDs.
1988
- if (this.cachedAccountIds.length > 0) {
1989
- managedAccounts = this.cachedAccountIds;
1990
- } else {
1991
- managedAccounts = await withTimeout(
1992
- new Promise<string[]>((resolve) => {
1993
- const rawApi = this.getRawApi();
1994
- const handler = (accountsList: string) => {
1995
- rawApi.off(EventName.managedAccounts, handler);
1996
- resolve(accountsList.split(",").map((s: string) => s.trim()).filter(Boolean));
1997
- };
1998
- rawApi.on(EventName.managedAccounts, handler);
1999
- rawApi.reqManagedAccts();
2000
- }),
2001
- IBKR_DATA_TIMEOUT,
2002
- "getManagedAccounts-fallback",
2003
- );
2004
- this.cachedAccountIds = managedAccounts;
2005
- }
2006
- }
2007
- if (!managedAccounts.length) return [];
2008
- let summary: ReadonlyMap<string, ReadonlyMap<string, ReadonlyMap<string, { value: string }>>> | undefined;
2009
- try {
2010
- const result = await firstValueFrom(
2011
- this.api!.getAccountSummary(
2012
- "All",
2013
- "NetLiquidation,TotalCashValue,SettledCash,AvailableFunds,BuyingPower,ExcessLiquidity,InitMarginReq,MaintMarginReq,$LEDGER:ALL",
2014
- )
2015
- .pipe(take(1), timeout(10_000)),
2016
- );
2017
- summary = result.all;
2018
- } catch {
2019
- // Account summary may fail; return accounts with basic info only.
2020
- }
2021
-
2022
- const updatedAt = Date.now();
2023
- const aggregateTags = summary?.get("All");
2024
- const allowAggregateCashBalances = managedAccounts.length === 1;
2025
- return managedAccounts.map((accountId) => summarizeBrokerAccount(
2026
- accountId,
2027
- summary?.get(accountId),
2028
- updatedAt,
2029
- aggregateTags,
2030
- allowAggregateCashBalances,
2031
- ));
2032
- }
2033
-
2034
- private async loadAccountsAndUpdateSnapshot(): Promise<BrokerAccount[]> {
2035
- const accounts = await this.loadAccounts();
2036
- this.updateSnapshot({ ...this.snapshot, accounts });
2037
- return accounts;
2038
- }
2039
-
2040
- private async loadPortfolioSnapshotsForAccounts(
2041
- accountIds: string[],
2042
- ): Promise<Map<string, AccountPortfolioSnapshotPosition>> {
2043
- const snapshots = new Map<string, AccountPortfolioSnapshotPosition>();
2044
- for (const accountId of accountIds) {
2045
- const positions = await this.loadPortfolioSnapshotForAccount(accountId);
2046
- for (const position of positions) {
2047
- snapshots.set(buildPortfolioPositionKey(accountId, position.contract), position);
2048
- }
2049
- }
2050
- return snapshots;
2051
- }
2052
-
2053
- private async loadPortfolioSnapshotForAccount(accountId: string): Promise<AccountPortfolioSnapshotPosition[]> {
2054
- if (!this.api || !accountId) return [];
2055
-
2056
- try {
2057
- const update = await firstValueFrom(
2058
- this.api.getAccountUpdates(accountId).pipe(
2059
- filter((value) => value.changed == null && value.added == null && value.removed == null),
2060
- take(1),
2061
- timeout(IBKR_DATA_TIMEOUT),
2062
- ),
2063
- );
2064
- return (update.all.portfolio?.get(accountId) ?? []) as AccountPortfolioSnapshotPosition[];
2065
- } catch (error: any) {
2066
- gatewayLog.warn("Failed to load IBKR portfolio snapshot", {
2067
- instanceId: this.instanceId,
2068
- accountId,
2069
- error: error?.message || String(error || ""),
2070
- });
2071
- return [];
2072
- }
2073
- }
2074
-
2075
- private async loadOpenOrders(): Promise<BrokerOrder[]> {
2076
- const orders = await withTimeout(this.api!.getAllOpenOrders(), IBKR_DATA_TIMEOUT, "getAllOpenOrders");
2077
- const mapped = orders.map((order) => this.openOrderToBrokerOrder(order));
2078
- this.updateSnapshot({ ...this.snapshot, openOrders: mapped });
2079
- return mapped;
2080
- }
2081
-
2082
- private async loadExecutions(): Promise<BrokerExecution[]> {
2083
- const executions = await withTimeout(this.api!.getExecutionDetails({}), IBKR_DATA_TIMEOUT, "getExecutionDetails");
2084
- const mapped = executions.map((detail) => ({
2085
- execId: detail.execution.execId || `${detail.execution.orderId ?? "exec"}-${detail.execution.time ?? Date.now()}`,
2086
- brokerInstanceId: this.instanceId,
2087
- orderId: detail.execution.orderId,
2088
- accountId: detail.execution.acctNumber,
2089
- side: detail.execution.side || "",
2090
- shares: detail.execution.shares ?? 0,
2091
- price: detail.execution.price ?? 0,
2092
- time: detail.execution.time ? Date.parse(detail.execution.time) : Date.now(),
2093
- exchange: detail.execution.exchange,
2094
- contract: this.contractToRef(detail.contract),
2095
- }));
2096
- this.updateSnapshot({ ...this.snapshot, executions: mapped });
2097
- return mapped;
2098
- }
2099
-
2100
- private contractDescriptionToSearchResult(description: ContractDescription): InstrumentSearchResult | null {
2101
- const contract = description.contract;
2102
- if (!contract?.symbol) return null;
2103
- return {
2104
- providerId: "ibkr",
2105
- brokerInstanceId: this.instanceId,
2106
- symbol: contract.localSymbol || contract.symbol,
2107
- name: contract.description || contract.symbol,
2108
- exchange: contract.primaryExch || contract.exchange || "",
2109
- type: contract.secType || "",
2110
- currency: contract.currency,
2111
- primaryExchange: contract.primaryExch,
2112
- brokerContract: this.contractToRef(contract),
2113
- };
2114
- }
2115
-
2116
- private contractToRef(contract: Contract): BrokerContractRef {
2117
- return {
2118
- brokerId: "ibkr",
2119
- brokerInstanceId: this.instanceId,
2120
- conId: contract.conId,
2121
- symbol: contract.symbol || "",
2122
- localSymbol: contract.localSymbol,
2123
- secType: contract.secType,
2124
- exchange: contract.exchange,
2125
- primaryExchange: contract.primaryExch,
2126
- currency: contract.currency,
2127
- lastTradeDateOrContractMonth: contract.lastTradeDateOrContractMonth,
2128
- right: contract.right === OptionType.Call ? "C" : contract.right === OptionType.Put ? "P" : undefined,
2129
- strike: contract.strike,
2130
- multiplier: contract.multiplier != null ? String(contract.multiplier) : undefined,
2131
- tradingClass: contract.tradingClass,
2132
- };
2133
- }
2134
-
2135
- private refToContract(ref: BrokerContractRef): Contract {
2136
- return {
2137
- conId: ref.conId,
2138
- symbol: ref.symbol,
2139
- localSymbol: ref.localSymbol,
2140
- secType: (ref.secType as SecType | undefined) ?? SecType.STK,
2141
- exchange: "SMART",
2142
- primaryExch: ref.primaryExchange || ref.exchange || undefined,
2143
- currency: ref.currency || "USD",
2144
- lastTradeDateOrContractMonth: ref.lastTradeDateOrContractMonth,
2145
- right: ref.right === "C" ? OptionType.Call : ref.right === "P" ? OptionType.Put : undefined,
2146
- strike: ref.strike,
2147
- multiplier: ref.multiplier ? parseFloat(ref.multiplier) : undefined,
2148
- tradingClass: ref.tradingClass,
2149
- };
2150
- }
2151
-
2152
- private async resolveContract(ticker: string, exchange: string, instrument: BrokerContractRef | null): Promise<Contract> {
2153
- if (instrument) {
2154
- if (instrument.conId) {
2155
- return this.refToContract(instrument);
2156
- }
2157
- return this.getPrimaryContractDetails(this.refToContract(instrument)).then((detail) => detail.contract);
2158
- }
2159
-
2160
- const directMatches = await withTimeout(this.getDirectContractMatches(ticker), IBKR_DATA_TIMEOUT, "getDirectContractMatches");
2161
- const direct = directMatches.find((result) => {
2162
- const symbol = result.symbol.toUpperCase();
2163
- const upperTicker = ticker.toUpperCase();
2164
- return symbol === upperTicker || symbol === ticker;
2165
- });
2166
- if (direct?.brokerContract) {
2167
- return this.refToContract(direct.brokerContract);
2168
- }
2169
-
2170
- const symbolResults = (await withTimeout(this.api!.getMatchingSymbols(ticker), IBKR_DATA_TIMEOUT, "getMatchingSymbols"))
2171
- .map((description) => this.contractDescriptionToSearchResult(description))
2172
- .filter((value): value is InstrumentSearchResult => value != null);
2173
- const matched = symbolResults.find((result) => {
2174
- const symbol = result.symbol.toUpperCase();
2175
- const upperTicker = ticker.toUpperCase();
2176
- return symbol === upperTicker || symbol === ticker;
2177
- });
2178
- if (matched?.brokerContract) {
2179
- return this.refToContract(matched.brokerContract);
2180
- }
2181
-
2182
- const fallbackContract: Contract = {
2183
- symbol: ticker,
2184
- exchange: exchange || "SMART",
2185
- currency: "USD",
2186
- secType: SecType.STK,
2187
- };
2188
- const details = await this.getPrimaryContractDetails(fallbackContract);
2189
- return details.contract;
2190
- }
2191
-
2192
- private async getPrimaryContractDetails(contract: Contract): Promise<ContractDetails> {
2193
- const details = await withTimeout(this.api!.getContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails");
2194
- if (!details.length) {
2195
- throw new Error(`Unable to resolve contract ${contract.symbol || contract.localSymbol || contract.conId}`);
2196
- }
2197
- return details[0]!;
2198
- }
2199
-
2200
- private async getDirectContractMatches(query: string): Promise<InstrumentSearchResult[]> {
2201
- const trimmed = query.trim();
2202
- if (!trimmed) return [];
2203
-
2204
- const candidates: Contract[] = [];
2205
- const optionLike = /^\S+\s+\d{6}[CP]\d{8}$/i.test(trimmed);
2206
-
2207
- if (optionLike) {
2208
- candidates.push({
2209
- localSymbol: trimmed,
2210
- secType: SecType.OPT,
2211
- exchange: "SMART",
2212
- });
2213
- }
2214
-
2215
- candidates.push({
2216
- localSymbol: trimmed,
2217
- exchange: "SMART",
2218
- });
2219
-
2220
- const seen = new Set<string>();
2221
- const results: InstrumentSearchResult[] = [];
2222
-
2223
- for (const candidate of candidates) {
2224
- try {
2225
- const details = await withTimeout(this.api!.getContractDetails(candidate), IBKR_DATA_TIMEOUT, "getDirectContractDetails");
2226
- for (const detail of details) {
2227
- const contract = detail.contract;
2228
- const result: InstrumentSearchResult = {
2229
- providerId: "ibkr",
2230
- brokerInstanceId: this.instanceId,
2231
- symbol: contract.localSymbol || contract.symbol || trimmed,
2232
- name: detail.longName || detail.marketName || contract.description || contract.symbol || trimmed,
2233
- exchange: contract.primaryExch || contract.exchange || "",
2234
- type: contract.secType || "",
2235
- currency: contract.currency,
2236
- primaryExchange: contract.primaryExch,
2237
- brokerContract: this.contractToRef(contract),
2238
- };
2239
- const key = `${result.symbol}|${result.exchange}|${result.type}`;
2240
- if (seen.has(key)) continue;
2241
- seen.add(key);
2242
- results.push(result);
2243
- }
2244
- } catch {
2245
- // Ignore exact-match lookup failures and fall back to symbol search.
2246
- }
2247
- }
2248
-
2249
- return results;
2250
- }
2251
-
2252
- private marketDataToQuote(contract: Contract, details: ContractDetails, marketData: ReadonlyMap<number, { value?: number; ingressTm: number }>): Quote {
2253
- const priceDivisor = getIbkrPriceDivisor(contract, details);
2254
- const mark = normalizeIbkrPriceValue(marketData.get(IBApiTickType.MARK_PRICE)?.value, priceDivisor);
2255
- const last = normalizeIbkrPriceValue(
2256
- marketData.get(IBApiTickType.LAST)?.value
2257
- ?? marketData.get(IBApiTickType.DELAYED_LAST)?.value
2258
- ?? marketData.get(IBApiTickType.MARK_PRICE)?.value
2259
- ?? marketData.get(IBApiTickType.CLOSE)?.value,
2260
- priceDivisor,
2261
- );
2262
- if (last == null || last <= 0) {
2263
- throw new Error(`No valid market data for ${contract.symbol || contract.localSymbol || contract.conId}`);
2264
- }
2265
- const close = normalizeIbkrPriceValue(
2266
- marketData.get(IBApiTickType.CLOSE)?.value
2267
- ?? marketData.get(IBApiTickType.DELAYED_CLOSE)?.value
2268
- ?? last,
2269
- priceDivisor,
2270
- ) ?? last;
2271
- const change = last - close;
2272
- const ingressTm = marketData.get(IBApiTickType.LAST)?.ingressTm
2273
- ?? marketData.get(IBApiTickType.DELAYED_LAST)?.ingressTm
2274
- ?? marketData.get(IBApiTickType.MARK_PRICE)?.ingressTm
2275
- ?? Date.now();
2276
-
2277
- return {
2278
- symbol: contract.localSymbol || contract.symbol || "",
2279
- price: last,
2280
- currency: contract.currency || "USD",
2281
- change,
2282
- changePercent: close ? (change / close) * 100 : 0,
2283
- previousClose: close,
2284
- high52w: normalizeIbkrPriceValue(marketData.get(IBApiTickType.HIGH_52_WEEK)?.value, priceDivisor),
2285
- low52w: normalizeIbkrPriceValue(marketData.get(IBApiTickType.LOW_52_WEEK)?.value, priceDivisor),
2286
- volume: marketData.get(IBApiTickType.VOLUME)?.value ?? marketData.get(IBApiTickType.DELAYED_VOLUME)?.value,
2287
- name: details.longName || details.marketName || contract.symbol,
2288
- lastUpdated: ingressTm,
2289
- ...ibkrVenueFields(undefined, contract, details),
2290
- bid: normalizeIbkrPriceValue(
2291
- marketData.get(IBApiTickType.BID)?.value ?? marketData.get(IBApiTickType.DELAYED_BID)?.value,
2292
- priceDivisor,
2293
- ),
2294
- ask: normalizeIbkrPriceValue(
2295
- marketData.get(IBApiTickType.ASK)?.value ?? marketData.get(IBApiTickType.DELAYED_ASK)?.value,
2296
- priceDivisor,
2297
- ),
2298
- bidSize: marketData.get(IBApiTickType.BID_SIZE)?.value ?? marketData.get(IBApiTickType.DELAYED_BID_SIZE)?.value,
2299
- askSize: marketData.get(IBApiTickType.ASK_SIZE)?.value ?? marketData.get(IBApiTickType.DELAYED_ASK_SIZE)?.value,
2300
- open: normalizeIbkrPriceValue(
2301
- marketData.get(IBApiTickType.OPEN)?.value ?? marketData.get(IBApiTickType.DELAYED_OPEN)?.value,
2302
- priceDivisor,
2303
- ),
2304
- high: normalizeIbkrPriceValue(
2305
- marketData.get(IBApiTickType.HIGH)?.value ?? marketData.get(IBApiTickType.DELAYED_HIGH)?.value,
2306
- priceDivisor,
2307
- ),
2308
- low: normalizeIbkrPriceValue(
2309
- marketData.get(IBApiTickType.LOW)?.value ?? marketData.get(IBApiTickType.DELAYED_LOW)?.value,
2310
- priceDivisor,
2311
- ),
2312
- mark,
2313
- };
358
+ async previewOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrderPreview> {
359
+ const remote = getBrokerRemoteClient();
360
+ return remote
361
+ ? remote.invoke(this.instanceId, "previewOrder", [request])
362
+ : (await getNativeService(this.instanceId)).previewOrder(config, request);
2314
363
  }
2315
364
 
2316
- private openOrderToBrokerOrder(openOrder: {
2317
- orderId: number;
2318
- contract: Contract;
2319
- order: Order;
2320
- orderState: OrderState;
2321
- orderStatus?: {
2322
- status: string;
2323
- filled: number;
2324
- remaining: number;
2325
- avgFillPrice: number;
2326
- };
2327
- }): BrokerOrder {
2328
- return {
2329
- orderId: openOrder.orderId,
2330
- brokerInstanceId: this.instanceId,
2331
- accountId: openOrder.order.account,
2332
- status: openOrder.orderStatus?.status || openOrder.orderState.status || "Unknown",
2333
- action: (openOrder.order.action || "BUY") as BrokerOrder["action"],
2334
- orderType: openOrder.order.orderType || "",
2335
- quantity: openOrder.order.totalQuantity ?? 0,
2336
- filled: openOrder.orderStatus?.filled ?? 0,
2337
- remaining: openOrder.orderStatus?.remaining ?? openOrder.order.totalQuantity ?? 0,
2338
- avgFillPrice: openOrder.orderStatus?.avgFillPrice,
2339
- limitPrice: openOrder.order.lmtPrice,
2340
- stopPrice: openOrder.order.auxPrice,
2341
- tif: openOrder.order.tif,
2342
- warningText: openOrder.orderState.warningText,
2343
- updatedAt: Date.now(),
2344
- contract: this.contractToRef(openOrder.contract),
2345
- };
365
+ async placeOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrder> {
366
+ const remote = getBrokerRemoteClient();
367
+ return remote
368
+ ? remote.invoke(this.instanceId, "placeOrder", [request])
369
+ : (await getNativeService(this.instanceId)).placeOrder(config, request);
2346
370
  }
2347
371
 
2348
- private buildOrder(request: BrokerOrderRequest, whatIf: boolean): Order {
2349
- return {
2350
- account: request.accountId,
2351
- action: request.action === "BUY" ? OrderAction.BUY : OrderAction.SELL,
2352
- totalQuantity: request.quantity,
2353
- orderType: request.orderType as OrderType,
2354
- lmtPrice: request.limitPrice,
2355
- auxPrice: request.stopPrice,
2356
- tif: (request.tif ?? TimeInForce.DAY) as typeof TimeInForce[keyof typeof TimeInForce],
2357
- outsideRth: request.outsideRth ?? false,
2358
- whatIf,
2359
- transmit: true,
2360
- };
372
+ async modifyOrder(config: IbkrGatewayConfig, orderId: number, request: BrokerOrderRequest): Promise<BrokerOrder> {
373
+ const remote = getBrokerRemoteClient();
374
+ return remote
375
+ ? remote.invoke(this.instanceId, "modifyOrder", [orderId, request])
376
+ : (await getNativeService(this.instanceId)).modifyOrder(config, orderId, request);
2361
377
  }
2362
378
 
2363
- private getRawApi(): any {
2364
- return (this.api as any)?.api ?? this.api;
379
+ async cancelOrder(config: IbkrGatewayConfig, orderId: number): Promise<void> {
380
+ const remote = getBrokerRemoteClient();
381
+ return remote
382
+ ? remote.invoke(this.instanceId, "cancelOrder", [orderId])
383
+ : (await getNativeService(this.instanceId)).cancelOrder(config, orderId);
2365
384
  }
2366
385
  }
2367
386
 
2368
387
  export class IbkrGatewayServiceManager {
2369
- private services = new Map<string, IbkrGatewayService>();
388
+ private services = new Map<string, IbkrGatewayServiceFacade>();
2370
389
 
2371
- getService(instanceId: string): IbkrGatewayService {
390
+ getService(instanceId: string): IbkrGatewayServiceFacade {
2372
391
  let service = this.services.get(instanceId);
2373
392
  if (!service) {
2374
- service = new IbkrGatewayService(instanceId);
393
+ service = new IbkrGatewayServiceFacade(instanceId);
2375
394
  this.services.set(instanceId, service);
2376
395
  }
2377
396
  return service;
@@ -2382,22 +401,32 @@ export class IbkrGatewayServiceManager {
2382
401
  return this.getService(instanceId).getSnapshot();
2383
402
  }
2384
403
 
2385
- subscribe(instanceId: string | undefined, listener: () => void): () => void {
404
+ subscribe(instanceId: string | undefined, listener: Listener): () => void {
2386
405
  if (!instanceId) return () => {};
2387
406
  return this.getService(instanceId).subscribe(listener);
2388
407
  }
2389
408
 
2390
409
  async removeInstance(instanceId: string): Promise<void> {
2391
- const service = this.services.get(instanceId);
2392
- if (!service) return;
2393
- await service.disconnect();
410
+ const remote = getBrokerRemoteClient();
411
+ if (remote) {
412
+ await remote.removeInstance(instanceId);
413
+ } else if (nativeGatewayManager) {
414
+ await nativeGatewayManager.removeInstance(instanceId);
415
+ }
2394
416
  this.services.delete(instanceId);
417
+ snapshots.delete(instanceId);
418
+ notify(instanceId);
2395
419
  }
2396
420
 
2397
421
  async destroyAll(): Promise<void> {
2398
- const services = [...this.services.values()];
422
+ const remote = getBrokerRemoteClient();
423
+ if (remote) {
424
+ await remote.destroyAll();
425
+ } else if (nativeGatewayManager) {
426
+ await nativeGatewayManager.destroyAll();
427
+ }
2399
428
  this.services.clear();
2400
- await Promise.allSettled(services.map((service) => service.disconnect()));
429
+ snapshots.clear();
2401
430
  }
2402
431
  }
2403
432