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