gloomberb 0.7.4 → 0.8.0

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 (1293) 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 +110 -1461
  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 +1 -46
  606. package/src/plugins/builtin/holders/index.tsx +3 -955
  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 +110 -4
  817. package/src/plugins/pane-manager.ts +57 -1035
  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 +191 -947
  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 +9 -2
  888. package/src/renderers/electrobun/view/app-services.ts +4 -375
  889. package/src/renderers/electrobun/view/backend-rpc.ts +5 -0
  890. package/src/renderers/electrobun/view/build-assets.ts +117 -0
  891. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +398 -0
  892. package/src/renderers/electrobun/view/config-host.ts +1 -1
  893. package/src/renderers/electrobun/view/context-menu.test.ts +10 -4
  894. package/src/renderers/electrobun/view/context-menu.ts +1 -1
  895. package/src/renderers/electrobun/view/data-table/dom.ts +211 -0
  896. package/src/renderers/electrobun/view/data-table/index.tsx +358 -0
  897. package/src/renderers/electrobun/view/data-table/row.tsx +335 -0
  898. package/src/renderers/electrobun/view/desktop/control-styles.ts +77 -0
  899. package/src/renderers/electrobun/view/desktop/controls.tsx +421 -0
  900. package/src/renderers/electrobun/view/desktop/list-view.tsx +200 -0
  901. package/src/renderers/electrobun/view/desktop/window/bridge.ts +70 -0
  902. package/src/renderers/electrobun/view/desktop/window/snapshot.test.ts +57 -0
  903. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +41 -0
  904. package/src/renderers/electrobun/view/fatal-screen.tsx +118 -0
  905. package/src/renderers/electrobun/view/host/box.tsx +160 -0
  906. package/src/renderers/electrobun/view/host/chart-surface.tsx +142 -0
  907. package/src/renderers/electrobun/view/host/input.tsx +294 -0
  908. package/src/renderers/electrobun/view/host/mouse.ts +139 -0
  909. package/src/renderers/electrobun/view/host/native.ts +49 -0
  910. package/src/renderers/electrobun/view/host/scroll-box.tsx +276 -0
  911. package/src/renderers/electrobun/view/host/style.ts +124 -0
  912. package/src/renderers/electrobun/view/host/tabs.tsx +255 -0
  913. package/src/renderers/electrobun/view/host/text.tsx +121 -0
  914. package/src/renderers/electrobun/view/http-fetch.ts +1 -1
  915. package/src/renderers/electrobun/view/input-host.tsx +12 -1
  916. package/src/renderers/electrobun/view/main.tsx +45 -26
  917. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +59 -0
  918. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +9 -0
  919. package/src/renderers/electrobun/view/remote/asset-data-client.ts +201 -0
  920. package/src/renderers/electrobun/view/remote/persistence.ts +158 -0
  921. package/src/renderers/electrobun/view/remote/ticker-repository.ts +26 -0
  922. package/src/renderers/electrobun/view/styles.css +91 -4
  923. package/src/renderers/electrobun/view/ui-host.tsx +22 -1469
  924. package/src/renderers/opentui/chart-surface.tsx +4 -5
  925. package/src/renderers/opentui/host.tsx +30 -4
  926. package/src/renderers/opentui/image/loader.test.ts +25 -0
  927. package/src/renderers/opentui/image/loader.ts +105 -0
  928. package/src/renderers/opentui/image/surface.tsx +266 -0
  929. package/src/renderers/opentui/input-host.tsx +12 -1
  930. package/src/renderers/opentui/start.tsx +2 -2
  931. package/src/renderers/opentui/test-utils.tsx +2 -2
  932. package/src/renderers/opentui/ui-host.tsx +1 -1
  933. package/src/sources/gloomberb-cloud/index.test.ts +660 -0
  934. package/src/sources/gloomberb-cloud/index.ts +444 -0
  935. package/src/sources/gloomberb-cloud/news.ts +127 -0
  936. package/src/sources/gloomberb-cloud/normalizers.ts +354 -0
  937. package/src/sources/provider-errors.ts +8 -2
  938. package/src/sources/provider-router/batches.ts +170 -0
  939. package/src/sources/provider-router/brokers.ts +114 -0
  940. package/src/sources/provider-router/cache.ts +171 -0
  941. package/src/sources/provider-router/cached-financials.test.ts +498 -0
  942. package/src/sources/provider-router/document-routes.ts +153 -0
  943. package/src/sources/provider-router/financial-routes.ts +225 -0
  944. package/src/sources/provider-router/financials.ts +259 -0
  945. package/src/sources/provider-router/history.test.ts +383 -0
  946. package/src/sources/provider-router/history.ts +448 -0
  947. package/src/sources/provider-router/index.test.ts +1438 -0
  948. package/src/sources/provider-router/index.ts +389 -0
  949. package/src/sources/provider-router/news.ts +36 -0
  950. package/src/sources/provider-router/primary.ts +164 -0
  951. package/src/sources/provider-router/route-types.ts +34 -0
  952. package/src/sources/provider-router/routing.ts +58 -0
  953. package/src/sources/provider-router/search.ts +183 -0
  954. package/src/sources/provider-router/sources.ts +55 -0
  955. package/src/sources/provider-router/streaming.ts +92 -0
  956. package/src/sources/provider-router/supplemental.ts +373 -0
  957. package/src/sources/provider-router/test-support.ts +136 -0
  958. package/src/sources/sec-edgar/content.ts +211 -0
  959. package/src/sources/sec-edgar.ts +9 -217
  960. package/src/sources/yahoo-finance/financials.ts +339 -0
  961. package/src/sources/yahoo-finance/history.ts +106 -0
  962. package/src/sources/yahoo-finance/http.ts +100 -0
  963. package/src/sources/yahoo-finance/mappers.ts +409 -0
  964. package/src/sources/yahoo-finance/options.ts +134 -0
  965. package/src/sources/yahoo-finance/quote-summary.ts +210 -0
  966. package/src/sources/yahoo-finance/requests.ts +192 -0
  967. package/src/sources/yahoo-finance/snapshots.ts +236 -0
  968. package/src/sources/yahoo-finance/symbols.ts +102 -0
  969. package/src/sources/yahoo-finance/types.ts +167 -0
  970. package/src/sources/yahoo-finance.test.ts +116 -12
  971. package/src/sources/yahoo-finance.ts +115 -1408
  972. package/src/state/app/activity.test.ts +41 -0
  973. package/src/state/app/activity.ts +106 -0
  974. package/src/state/app/bootstrap.test.ts +498 -0
  975. package/src/state/app/bootstrap.ts +412 -0
  976. package/src/state/app/context/index.test.ts +564 -0
  977. package/src/state/app/context/index.test.tsx +193 -0
  978. package/src/state/app/context/index.tsx +476 -0
  979. package/src/state/app/context/selector.test.tsx +291 -0
  980. package/src/state/config-save-scheduler.ts +2 -6
  981. package/src/state/hooks/inline-tickers.test.tsx +123 -0
  982. package/src/state/hooks/inline-tickers.ts +212 -0
  983. package/src/state/hooks/quote-streaming.test.ts +31 -0
  984. package/src/state/hooks/quote-streaming.test.tsx +70 -0
  985. package/src/state/hooks/quote-streaming.ts +92 -0
  986. package/src/state/hooks/session-snapshot.ts +99 -0
  987. package/src/state/pane-scroll-registry.tsx +1 -1
  988. package/src/state/selectors-ui.ts +1 -1
  989. package/src/state/selectors.ts +1 -1
  990. package/src/test-support/plugin-persistence.ts +112 -0
  991. package/src/test-support/plugin-runtime.ts +2 -1
  992. package/src/theme/colors.ts +23 -9
  993. package/src/theme/themes.test.ts +15 -0
  994. package/src/tickers/list.test.ts +16 -0
  995. package/src/tickers/open-target.ts +77 -0
  996. package/src/tickers/search/index.test.ts +252 -0
  997. package/src/tickers/search/index.ts +367 -0
  998. package/src/tickers/search/ranking.ts +387 -0
  999. package/src/tickers/search/result.ts +27 -0
  1000. package/src/tickers/search/types.ts +41 -0
  1001. package/src/tickers/search/upsert.ts +91 -0
  1002. package/src/tickers/tokenizer.test.ts +31 -0
  1003. package/src/tickers/tokenizer.ts +37 -0
  1004. package/src/types/broker.ts +12 -4
  1005. package/src/types/config.ts +42 -23
  1006. package/src/types/context-menu.ts +1 -1
  1007. package/src/types/data-provider.ts +2 -2
  1008. package/src/types/desktop-window.ts +1 -1
  1009. package/src/types/financials.ts +109 -4
  1010. package/src/types/news-source.ts +0 -1
  1011. package/src/types/persistence.ts +1 -1
  1012. package/src/types/plugin.ts +37 -21
  1013. package/src/types/ticker.ts +1 -0
  1014. package/src/types/trading.ts +23 -1
  1015. package/src/ui/context-menu.tsx +6 -5
  1016. package/src/ui/dialog.tsx +6 -3
  1017. package/src/ui/host.tsx +10 -10
  1018. package/src/ui/index.tsx +14 -33
  1019. package/src/updater/github-releases.test.ts +41 -0
  1020. package/src/updater/github-releases.ts +73 -0
  1021. package/src/updater.test.ts +21 -0
  1022. package/src/updater.ts +10 -3
  1023. package/src/utils/back-navigation.ts +2 -2
  1024. package/src/utils/cli-output.ts +4 -4
  1025. package/src/utils/currency-units.test.ts +23 -1
  1026. package/src/utils/currency-units.ts +56 -0
  1027. package/src/utils/datetime-format.ts +40 -0
  1028. package/src/utils/dom-screenshot.ts +2 -2
  1029. package/src/utils/double-escape-close.ts +1 -1
  1030. package/src/utils/exchanges.ts +2 -2
  1031. package/src/utils/format.ts +0 -7
  1032. package/src/utils/fuzzy-search.ts +1 -1
  1033. package/src/utils/inline-content-tokenizer.test.ts +11 -2
  1034. package/src/utils/inline-content-tokenizer.ts +33 -12
  1035. package/src/utils/keyboard.ts +1 -1
  1036. package/src/utils/link-tokenizer.ts +2 -2
  1037. package/src/utils/options.ts +1 -1
  1038. package/src/utils/price-history.test.ts +20 -0
  1039. package/src/utils/price-history.ts +24 -2
  1040. package/src/utils/relative-time.ts +8 -0
  1041. package/src/utils/selection-clipboard.ts +1 -1
  1042. package/src/utils/shortcut-labels.ts +2 -2
  1043. package/src/utils/sort-values.ts +17 -0
  1044. package/src/utils/terminal-input-reset.ts +1 -1
  1045. package/src/utils/text-wrap.ts +20 -0
  1046. package/src/version.ts +1 -1
  1047. package/src/components/chart/chart-controller.test.ts +0 -110
  1048. package/src/components/chart/chart-controller.ts +0 -650
  1049. package/src/components/chart/chart-data.ts +0 -350
  1050. package/src/components/chart/chart-indicator-selector.tsx +0 -75
  1051. package/src/components/chart/chart-pane-settings.ts +0 -37
  1052. package/src/components/chart/chart-renderer.ts +0 -1562
  1053. package/src/components/chart/chart-resolution.test.ts +0 -87
  1054. package/src/components/chart/chart-resolution.ts +0 -405
  1055. package/src/components/chart/chart-scroll.test.ts +0 -70
  1056. package/src/components/chart/chart-scroll.ts +0 -104
  1057. package/src/components/chart/chart-types.ts +0 -119
  1058. package/src/components/chart/chart-viewport.test.ts +0 -29
  1059. package/src/components/chart/chart-viewport.ts +0 -50
  1060. package/src/components/chart/chart.test.ts +0 -922
  1061. package/src/components/chart/comparison-chart-data.test.ts +0 -105
  1062. package/src/components/chart/comparison-chart-data.ts +0 -292
  1063. package/src/components/chart/comparison-chart-renderer.test.ts +0 -187
  1064. package/src/components/chart/comparison-chart-renderer.ts +0 -437
  1065. package/src/components/chart/native/kitty-adapter.ts +0 -9
  1066. package/src/components/chart/native/kitty-manager.ts +0 -142
  1067. package/src/components/chart/native/kitty-support.ts +0 -71
  1068. package/src/components/chart/native/kitty.test.ts +0 -728
  1069. package/src/components/chart/native/surface-manager.ts +0 -225
  1070. package/src/components/chart/native/surface-sync.ts +0 -34
  1071. package/src/components/chart/native/surface-visibility.ts +0 -48
  1072. package/src/components/chart/static-chart-surface.test.tsx +0 -51
  1073. package/src/components/chart/static-chart-surface.tsx +0 -217
  1074. package/src/components/chart/stock-chart-renderer-switch.test.tsx +0 -1323
  1075. package/src/components/chart/stock-chart.test.ts +0 -330
  1076. package/src/components/chart/stock-chart.tsx +0 -4148
  1077. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -109
  1078. package/src/components/command-bar/command-bar.test.tsx +0 -3244
  1079. package/src/components/command-bar/command-bar.tsx +0 -5868
  1080. package/src/components/command-bar/command-registry.ts +0 -267
  1081. package/src/components/command-bar/confirm-body.tsx +0 -51
  1082. package/src/components/command-bar/list-model.ts +0 -80
  1083. package/src/components/command-bar/pane-template-display.ts +0 -8
  1084. package/src/components/command-bar/root-shortcuts.ts +0 -188
  1085. package/src/components/command-bar/workflow-ops.test.ts +0 -144
  1086. package/src/components/command-bar/workflow-ops.ts +0 -459
  1087. package/src/components/command-bar/workflow-types.ts +0 -127
  1088. package/src/components/data-table-stack-view.test.tsx +0 -241
  1089. package/src/components/data-table-stack-view.tsx +0 -59
  1090. package/src/components/data-table-view.test.tsx +0 -211
  1091. package/src/components/data-table-view.tsx +0 -244
  1092. package/src/components/feed-data-table-stack-view.test.tsx +0 -225
  1093. package/src/components/feed-data-table-stack-view.tsx +0 -359
  1094. package/src/components/layout/pane-content.test.tsx +0 -64
  1095. package/src/components/layout/pane-content.tsx +0 -44
  1096. package/src/components/layout/pane-cycle-order.test.ts +0 -30
  1097. package/src/components/layout/pane-cycle-order.ts +0 -32
  1098. package/src/components/layout/pane-footer.test.tsx +0 -195
  1099. package/src/components/layout/pane-footer.tsx +0 -432
  1100. package/src/components/layout/pane-header.test.tsx +0 -39
  1101. package/src/components/layout/pane-header.tsx +0 -248
  1102. package/src/components/layout/pane-keyboard-scroll.test.tsx +0 -226
  1103. package/src/components/layout/pane-sizing.ts +0 -34
  1104. package/src/components/layout/pane-title.ts +0 -38
  1105. package/src/components/layout/pane.tsx +0 -129
  1106. package/src/components/layout/shell.test.tsx +0 -1427
  1107. package/src/components/layout/shell.tsx +0 -1912
  1108. package/src/components/price-sparkline-view.tsx +0 -463
  1109. package/src/components/price-sparkline.ts +0 -46
  1110. package/src/components/speedometer-gauge.test.tsx +0 -45
  1111. package/src/components/speedometer-gauge.tsx +0 -562
  1112. package/src/components/ticker-badge-list.tsx +0 -53
  1113. package/src/components/ticker-badge-text.test.tsx +0 -253
  1114. package/src/components/ticker-badge-text.tsx +0 -113
  1115. package/src/components/ticker-badge.tsx +0 -109
  1116. package/src/components/ticker-list-table-view.tsx +0 -233
  1117. package/src/components/ticker-list-table.test.tsx +0 -210
  1118. package/src/components/ticker-list-table.tsx +0 -458
  1119. package/src/components/ui/data-table.tsx +0 -522
  1120. package/src/components/ui/multi-select-dialog.tsx +0 -336
  1121. package/src/core/state/app-state.ts +0 -1009
  1122. package/src/data/config-layout.ts +0 -225
  1123. package/src/data/config-store-node.ts +0 -478
  1124. package/src/data/config-store.test.ts +0 -481
  1125. package/src/data/config-store.ts +0 -64
  1126. package/src/data/sqlite-cache.test.ts +0 -151
  1127. package/src/data/sqlite-database.ts +0 -68
  1128. package/src/data/sqlite-json.ts +0 -16
  1129. package/src/data/sqlite-retry.test.ts +0 -63
  1130. package/src/market-data/coordinator-subscriptions.test.ts +0 -209
  1131. package/src/market-data/coordinator.test.ts +0 -830
  1132. package/src/market-data/coordinator.ts +0 -1155
  1133. package/src/plugins/builtin/ai/screener-contract.test.ts +0 -117
  1134. package/src/plugins/builtin/ai/screener-contract.ts +0 -174
  1135. package/src/plugins/builtin/ai/screener-pane.test.tsx +0 -348
  1136. package/src/plugins/builtin/ai/screener-pane.tsx +0 -1118
  1137. package/src/plugins/builtin/ai/utils.ts +0 -1
  1138. package/src/plugins/builtin/ask-ai.test.tsx +0 -348
  1139. package/src/plugins/builtin/chat-controller.test.ts +0 -1376
  1140. package/src/plugins/builtin/chat-controller.ts +0 -1248
  1141. package/src/plugins/builtin/chat.test.tsx +0 -2245
  1142. package/src/plugins/builtin/chat.tsx +0 -2267
  1143. package/src/plugins/builtin/cloud-auth-actions.tsx +0 -72
  1144. package/src/plugins/builtin/cloud-tweets.tsx +0 -896
  1145. package/src/plugins/builtin/comparison-chart.test.tsx +0 -532
  1146. package/src/plugins/builtin/comparison-chart.tsx +0 -194
  1147. package/src/plugins/builtin/debug.tsx +0 -365
  1148. package/src/plugins/builtin/earnings/earnings-cache.test.ts +0 -181
  1149. package/src/plugins/builtin/earnings/earnings-cache.ts +0 -145
  1150. package/src/plugins/builtin/earnings/earnings-data.test.ts +0 -90
  1151. package/src/plugins/builtin/earnings/earnings-data.ts +0 -115
  1152. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +0 -72
  1153. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +0 -366
  1154. package/src/plugins/builtin/help.test.tsx +0 -166
  1155. package/src/plugins/builtin/help.tsx +0 -450
  1156. package/src/plugins/builtin/layout-manager.tsx +0 -247
  1157. package/src/plugins/builtin/manual-entry.tsx +0 -25
  1158. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +0 -98
  1159. package/src/plugins/builtin/news-wire/breaking-notifications.ts +0 -110
  1160. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +0 -156
  1161. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -68
  1162. package/src/plugins/builtin/news-wire/categories.ts +0 -114
  1163. package/src/plugins/builtin/news-wire/default-feeds.ts +0 -3
  1164. package/src/plugins/builtin/news-wire/feed-config.test.ts +0 -90
  1165. package/src/plugins/builtin/news-wire/feed-config.ts +0 -216
  1166. package/src/plugins/builtin/news-wire/index.tsx +0 -129
  1167. package/src/plugins/builtin/news-wire/industry-pane.tsx +0 -126
  1168. package/src/plugins/builtin/news-wire/news-detail-view.tsx +0 -299
  1169. package/src/plugins/builtin/news-wire/news-footer.ts +0 -28
  1170. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +0 -98
  1171. package/src/plugins/builtin/news-wire/news-query-presets.ts +0 -40
  1172. package/src/plugins/builtin/news-wire/news-table.test.tsx +0 -206
  1173. package/src/plugins/builtin/news-wire/news-table.tsx +0 -296
  1174. package/src/plugins/builtin/news-wire/persisted-articles.ts +0 -98
  1175. package/src/plugins/builtin/news-wire/read-state.ts +0 -76
  1176. package/src/plugins/builtin/news-wire/rss-parser.test.ts +0 -184
  1177. package/src/plugins/builtin/news-wire/rss-parser.ts +0 -213
  1178. package/src/plugins/builtin/news-wire/rss-source.test.ts +0 -159
  1179. package/src/plugins/builtin/news-wire/rss-source.ts +0 -181
  1180. package/src/plugins/builtin/news.tsx +0 -194
  1181. package/src/plugins/builtin/notes.tsx +0 -561
  1182. package/src/plugins/builtin/options.test.tsx +0 -241
  1183. package/src/plugins/builtin/options.tsx +0 -542
  1184. package/src/plugins/builtin/portfolio-list/cli/commands.ts +0 -657
  1185. package/src/plugins/builtin/portfolio-list/pane.tsx +0 -724
  1186. package/src/plugins/builtin/portfolio-list/quick-add.tsx +0 -422
  1187. package/src/plugins/builtin/portfolio-list/summary.tsx +0 -275
  1188. package/src/plugins/builtin/portfolio-list.test.ts +0 -133
  1189. package/src/plugins/builtin/portfolio-list.test.tsx +0 -1535
  1190. package/src/plugins/builtin/portfolio-metrics.test.ts +0 -236
  1191. package/src/plugins/builtin/quote-monitor.test.tsx +0 -222
  1192. package/src/plugins/builtin/sec-filing-display.tsx +0 -84
  1193. package/src/plugins/builtin/sec.tsx +0 -342
  1194. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +0 -461
  1195. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +0 -479
  1196. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +0 -269
  1197. package/src/plugins/builtin/ticker-detail.test.tsx +0 -888
  1198. package/src/plugins/builtin/ticker-surface.ts +0 -68
  1199. package/src/plugins/builtin/yahoo.ts +0 -72
  1200. package/src/plugins/catalog-ui.test.ts +0 -24
  1201. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +0 -35
  1202. package/src/plugins/ibkr/flex.test.ts +0 -162
  1203. package/src/plugins/ibkr/flex.ts +0 -371
  1204. package/src/plugins/ibkr/gateway-helpers.ts +0 -33
  1205. package/src/plugins/ibkr/gateway-selection.ts +0 -51
  1206. package/src/plugins/ibkr/gateway-service-native.ts +0 -2377
  1207. package/src/plugins/ibkr/gateway-service.test.ts +0 -584
  1208. package/src/plugins/ibkr/gateway-service.ts +0 -433
  1209. package/src/plugins/ibkr/gateway-types.ts +0 -27
  1210. package/src/plugins/ibkr/price-normalization.test.ts +0 -33
  1211. package/src/plugins/ibkr/price-normalization.ts +0 -60
  1212. package/src/plugins/ibkr/trade-badge.tsx +0 -28
  1213. package/src/plugins/ibkr/trade-tab.test.tsx +0 -276
  1214. package/src/plugins/ibkr/trade-tab.tsx +0 -1117
  1215. package/src/plugins/ibkr/trade-utils.test.ts +0 -175
  1216. package/src/plugins/ibkr/trade-utils.ts +0 -150
  1217. package/src/plugins/ibkr/trading-pane.tsx +0 -474
  1218. package/src/plugins/ibkr/trading-state.test.ts +0 -109
  1219. package/src/plugins/ibkr/trading-state.ts +0 -339
  1220. package/src/plugins/plugin-runtime.test.tsx +0 -323
  1221. package/src/plugins/plugin-runtime.tsx +0 -433
  1222. package/src/plugins/prediction-markets/controller-data.ts +0 -785
  1223. package/src/plugins/prediction-markets/controller.ts +0 -659
  1224. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +0 -572
  1225. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +0 -696
  1226. package/src/plugins/prediction-markets/services/polymarket-ws.ts +0 -128
  1227. package/src/plugins/registry.test.ts +0 -362
  1228. package/src/plugins/registry.ts +0 -854
  1229. package/src/renderers/electrobun/bun/application-menu-click.test.ts +0 -50
  1230. package/src/renderers/electrobun/bun/application-menu-click.ts +0 -58
  1231. package/src/renderers/electrobun/bun/application-menu.ts +0 -123
  1232. package/src/renderers/electrobun/bun/context-menu-click.test.ts +0 -67
  1233. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +0 -52
  1234. package/src/renderers/electrobun/bun/context-menu-normalize.ts +0 -67
  1235. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +0 -63
  1236. package/src/renderers/electrobun/bun/desktop-workspace.ts +0 -180
  1237. package/src/renderers/electrobun/bun/window-focus.ts +0 -37
  1238. package/src/renderers/electrobun/bun/window-frame.test.ts +0 -40
  1239. package/src/renderers/electrobun/view/data-table.tsx +0 -810
  1240. package/src/renderers/electrobun/view/desktop-controls.tsx +0 -673
  1241. package/src/renderers/electrobun/view/desktop-window-bridge.ts +0 -70
  1242. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +0 -57
  1243. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +0 -41
  1244. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +0 -9
  1245. package/src/renderers/opentui/image-loader.test.ts +0 -25
  1246. package/src/renderers/opentui/image-loader.ts +0 -105
  1247. package/src/renderers/opentui/image-surface.tsx +0 -266
  1248. package/src/sources/gloomberb-cloud.test.ts +0 -637
  1249. package/src/sources/gloomberb-cloud.ts +0 -828
  1250. package/src/sources/provider-router.test.ts +0 -2600
  1251. package/src/sources/provider-router.ts +0 -2231
  1252. package/src/state/app-activity.test.ts +0 -41
  1253. package/src/state/app-activity.ts +0 -106
  1254. package/src/state/app-bootstrap.test.ts +0 -498
  1255. package/src/state/app-bootstrap.ts +0 -411
  1256. package/src/state/app-context-selector.test.tsx +0 -369
  1257. package/src/state/app-context.test.ts +0 -670
  1258. package/src/state/app-context.test.tsx +0 -193
  1259. package/src/state/app-context.tsx +0 -485
  1260. package/src/state/use-inline-tickers.test.tsx +0 -73
  1261. package/src/state/use-inline-tickers.ts +0 -188
  1262. package/src/state/use-quote-streaming.test.ts +0 -31
  1263. package/src/state/use-quote-streaming.test.tsx +0 -70
  1264. package/src/state/use-quote-streaming.ts +0 -92
  1265. package/src/state/use-session-snapshot.ts +0 -99
  1266. package/src/utils/api-client.test.ts +0 -603
  1267. package/src/utils/api-client.ts +0 -1379
  1268. package/src/utils/market-format.test.ts +0 -109
  1269. package/src/utils/market-format.ts +0 -299
  1270. package/src/utils/market-status.ts +0 -141
  1271. package/src/utils/quote-freshness.test.ts +0 -150
  1272. package/src/utils/quote-freshness.ts +0 -145
  1273. package/src/utils/quote-resolution.test.ts +0 -277
  1274. package/src/utils/quote-resolution.ts +0 -642
  1275. package/src/utils/quote-time.test.ts +0 -34
  1276. package/src/utils/quote-time.ts +0 -59
  1277. package/src/utils/ticker-list.test.ts +0 -16
  1278. package/src/utils/ticker-search.test.ts +0 -227
  1279. package/src/utils/ticker-search.ts +0 -855
  1280. package/src/utils/ticker-tokenizer.test.ts +0 -31
  1281. package/src/utils/ticker-tokenizer.ts +0 -37
  1282. /package/src/components/chart/native/{kitty-protocol.ts → kitty/protocol.ts} +0 -0
  1283. /package/src/components/ui/{multi-select.ts → multi-select/index.ts} +0 -0
  1284. /package/src/data/{sqlite-retry.ts → sqlite/retry.ts} +0 -0
  1285. /package/src/plugins/builtin/{news-wire → news/wire}/hash.ts +0 -0
  1286. /package/src/plugins/builtin/{news-wire → news/wire}/read-state.test.ts +0 -0
  1287. /package/src/plugins/builtin/{notes-files.ts → notes/files.ts} +0 -0
  1288. /package/src/renderers/electrobun/bun/{context-menu-click.ts → context-menu/click.ts} +0 -0
  1289. /package/src/renderers/electrobun/bun/{window-frame.ts → window/frame.ts} +0 -0
  1290. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-manager.ts → chart/surface-manager.ts} +0 -0
  1291. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-sync.ts → chart/surface-sync.ts} +0 -0
  1292. /package/src/{utils/ticker-list.ts → tickers/list.ts} +0 -0
  1293. /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();