gloomberb 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +20 -19
  2. package/bin/gloomberb +3 -0
  3. package/package.json +5 -4
  4. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  5. package/src/app.tsx +1365 -0
  6. package/src/brokers/sync-broker-instance.test.ts +100 -0
  7. package/src/brokers/sync-broker-instance.ts +341 -0
  8. package/src/cli/commands/plugins.ts +173 -0
  9. package/src/cli/commands/portfolio.ts +210 -0
  10. package/src/cli/commands/search.ts +120 -0
  11. package/src/cli/commands/ticker.ts +375 -0
  12. package/src/cli/commands/watchlist.ts +210 -0
  13. package/src/cli/context.ts +62 -0
  14. package/src/cli/errors.ts +13 -0
  15. package/src/cli/helpers.ts +85 -0
  16. package/src/cli/index.ts +143 -0
  17. package/src/cli/ticker-resolution.ts +39 -0
  18. package/src/cli/types.ts +15 -0
  19. package/src/cli.test.ts +361 -0
  20. package/src/components/chart/chart-data.ts +239 -0
  21. package/src/components/chart/chart-renderer.ts +1199 -0
  22. package/src/components/chart/chart-types.ts +82 -0
  23. package/src/components/chart/chart-viewport.ts +21 -0
  24. package/src/components/chart/chart.test.ts +439 -0
  25. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  26. package/src/components/chart/comparison-chart-data.ts +316 -0
  27. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  28. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  29. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  30. package/src/components/chart/cursor-motion.ts +48 -0
  31. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  32. package/src/components/chart/native/kitty-adapter.ts +12 -0
  33. package/src/components/chart/native/kitty-manager.ts +142 -0
  34. package/src/components/chart/native/kitty-protocol.ts +110 -0
  35. package/src/components/chart/native/kitty-support.ts +71 -0
  36. package/src/components/chart/native/kitty.test.ts +620 -0
  37. package/src/components/chart/native/renderer-selection.ts +24 -0
  38. package/src/components/chart/native/surface-manager.ts +225 -0
  39. package/src/components/chart/native/surface-sync.ts +34 -0
  40. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  41. package/src/components/chart/stock-chart.test.ts +107 -0
  42. package/src/components/chart/stock-chart.tsx +1556 -0
  43. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  44. package/src/components/command-bar/command-bar.test.tsx +1546 -0
  45. package/src/components/command-bar/command-bar.tsx +4841 -0
  46. package/src/components/command-bar/command-registry.ts +269 -0
  47. package/src/components/command-bar/helpers.test.ts +224 -0
  48. package/src/components/command-bar/helpers.ts +371 -0
  49. package/src/components/command-bar/root-shortcuts.ts +158 -0
  50. package/src/components/command-bar/view-model.test.ts +152 -0
  51. package/src/components/command-bar/view-model.ts +185 -0
  52. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  53. package/src/components/command-bar/workflow-ops.ts +433 -0
  54. package/src/components/command-bar/workflow-types.ts +127 -0
  55. package/src/components/detail-feed-view.test.tsx +105 -0
  56. package/src/components/detail-feed-view.tsx +295 -0
  57. package/src/components/index.ts +32 -0
  58. package/src/components/layout/floating-pane.tsx +49 -0
  59. package/src/components/layout/header.tsx +151 -0
  60. package/src/components/layout/pane-header.tsx +44 -0
  61. package/src/components/layout/pane.tsx +49 -0
  62. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  63. package/src/components/layout/pixel-mouse.test.ts +40 -0
  64. package/src/components/layout/shell.test.tsx +651 -0
  65. package/src/components/layout/shell.tsx +1200 -0
  66. package/src/components/layout/status-bar.test.tsx +173 -0
  67. package/src/components/layout/status-bar.tsx +138 -0
  68. package/src/components/markdown-editor.tsx +134 -0
  69. package/src/components/markdown-text.tsx +218 -0
  70. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  71. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  72. package/src/components/pane-settings-dialog.tsx +468 -0
  73. package/src/components/pane-template-wizard.test.tsx +52 -0
  74. package/src/components/pane-template-wizard.tsx +199 -0
  75. package/src/components/price-selector-dialog.tsx +195 -0
  76. package/src/components/spinner.tsx +2 -0
  77. package/src/components/tab-bar.tsx +17 -0
  78. package/src/components/ticker-badge-text.test.tsx +160 -0
  79. package/src/components/ticker-badge-text.tsx +58 -0
  80. package/src/components/ticker-badge.tsx +88 -0
  81. package/src/components/ticker-list-table.tsx +188 -0
  82. package/src/components/toggle-list.tsx +74 -0
  83. package/src/components/ui/button.tsx +77 -0
  84. package/src/components/ui/data-table.tsx +210 -0
  85. package/src/components/ui/external-link.tsx +22 -0
  86. package/src/components/ui/fields.tsx +187 -0
  87. package/src/components/ui/frame.tsx +73 -0
  88. package/src/components/ui/index.ts +53 -0
  89. package/src/components/ui/list-view.tsx +166 -0
  90. package/src/components/ui/loading.tsx +70 -0
  91. package/src/components/ui/status.tsx +83 -0
  92. package/src/components/ui/tabs.tsx +47 -0
  93. package/src/components/ui/toggle.tsx +164 -0
  94. package/src/components/ui/ui.test.tsx +134 -0
  95. package/src/data/app-persistence.ts +25 -0
  96. package/src/data/config-store.test.ts +347 -0
  97. package/src/data/config-store.ts +507 -0
  98. package/src/data/plugin-state-store.ts +102 -0
  99. package/src/data/resource-store.ts +323 -0
  100. package/src/data/session-store.ts +52 -0
  101. package/src/data/sqlite-cache.test.ts +103 -0
  102. package/src/data/sqlite-database.ts +62 -0
  103. package/src/data/sqlite-json.ts +19 -0
  104. package/src/data/ticker-repository.ts +45 -0
  105. package/src/data/ticker-store.ts +46 -0
  106. package/src/index.tsx +67 -0
  107. package/src/market-data/coordinator.test.ts +369 -0
  108. package/src/market-data/coordinator.ts +580 -0
  109. package/src/market-data/hooks.tsx +234 -0
  110. package/src/market-data/query-store.ts +28 -0
  111. package/src/market-data/request-types.test.ts +64 -0
  112. package/src/market-data/request-types.ts +70 -0
  113. package/src/market-data/result-types.ts +56 -0
  114. package/src/market-data/selectors.ts +166 -0
  115. package/src/market-data/trace.ts +7 -0
  116. package/src/pane-settings.test.ts +55 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +350 -0
  119. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  120. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  121. package/src/plugins/builtin/ai/index.tsx +86 -0
  122. package/src/plugins/builtin/ai/providers.ts +99 -0
  123. package/src/plugins/builtin/ai/runner.ts +82 -0
  124. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  125. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  126. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  127. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  128. package/src/plugins/builtin/ai/settings.ts +79 -0
  129. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  130. package/src/plugins/builtin/ai/utils.ts +71 -0
  131. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  132. package/src/plugins/builtin/ask-ai.tsx +9 -0
  133. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  134. package/src/plugins/builtin/chat-controller.ts +340 -0
  135. package/src/plugins/builtin/chat.test.tsx +451 -0
  136. package/src/plugins/builtin/chat.tsx +620 -0
  137. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  138. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  139. package/src/plugins/builtin/data-providers.ts +16 -0
  140. package/src/plugins/builtin/debug.tsx +351 -0
  141. package/src/plugins/builtin/help.test.tsx +110 -0
  142. package/src/plugins/builtin/help.tsx +281 -0
  143. package/src/plugins/builtin/layout-manager.tsx +264 -0
  144. package/src/plugins/builtin/manual-entry.tsx +25 -0
  145. package/src/plugins/builtin/news.tsx +143 -0
  146. package/src/plugins/builtin/notes-files.ts +58 -0
  147. package/src/plugins/builtin/notes.tsx +396 -0
  148. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  149. package/src/plugins/builtin/options-availability.ts +158 -0
  150. package/src/plugins/builtin/options.tsx +364 -0
  151. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  152. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  153. package/src/plugins/builtin/portfolio-list/metrics.ts +460 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +270 -0
  156. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  157. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  158. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  159. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  160. package/src/plugins/builtin/portfolio-metrics.test.ts +224 -0
  161. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  162. package/src/plugins/builtin/sec.tsx +250 -0
  163. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  164. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  165. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  166. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  167. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  168. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  169. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  170. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  171. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  172. package/src/plugins/builtin/yahoo.ts +18 -0
  173. package/src/plugins/event-bus.ts +37 -0
  174. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  175. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  176. package/src/plugins/ibkr/account-cache.ts +137 -0
  177. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  178. package/src/plugins/ibkr/config.test.ts +68 -0
  179. package/src/plugins/ibkr/config.ts +262 -0
  180. package/src/plugins/ibkr/dialogs.tsx +83 -0
  181. package/src/plugins/ibkr/flex.test.ts +107 -0
  182. package/src/plugins/ibkr/flex.ts +267 -0
  183. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  184. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  185. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  186. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  187. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  188. package/src/plugins/ibkr/index.test.tsx +183 -0
  189. package/src/plugins/ibkr/index.tsx +153 -0
  190. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  191. package/src/plugins/ibkr/instance-selection.ts +36 -0
  192. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  193. package/src/plugins/ibkr/price-normalization.ts +59 -0
  194. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  195. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  196. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  197. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  198. package/src/plugins/ibkr/trade-utils.ts +149 -0
  199. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  200. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  201. package/src/plugins/ibkr/trading-state.ts +339 -0
  202. package/src/plugins/loader.ts +71 -0
  203. package/src/plugins/pane-manager.test.ts +103 -0
  204. package/src/plugins/pane-manager.ts +974 -0
  205. package/src/plugins/plugin-persistence.ts +85 -0
  206. package/src/plugins/plugin-runtime.test.tsx +185 -0
  207. package/src/plugins/plugin-runtime.tsx +175 -0
  208. package/src/plugins/prediction-markets/cache.ts +114 -0
  209. package/src/plugins/prediction-markets/categories.ts +223 -0
  210. package/src/plugins/prediction-markets/chart.tsx +512 -0
  211. package/src/plugins/prediction-markets/columns.ts +133 -0
  212. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  213. package/src/plugins/prediction-markets/controller.ts +725 -0
  214. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  215. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  216. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  217. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  218. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  219. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  220. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  221. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  222. package/src/plugins/prediction-markets/index.tsx +102 -0
  223. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  224. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  225. package/src/plugins/prediction-markets/launch.ts +240 -0
  226. package/src/plugins/prediction-markets/metrics.ts +311 -0
  227. package/src/plugins/prediction-markets/navigation.ts +90 -0
  228. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  229. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  230. package/src/plugins/prediction-markets/pane.tsx +294 -0
  231. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  232. package/src/plugins/prediction-markets/rows.ts +229 -0
  233. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  234. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  235. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  236. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  237. package/src/plugins/prediction-markets/settings.ts +164 -0
  238. package/src/plugins/prediction-markets/status.ts +24 -0
  239. package/src/plugins/prediction-markets/table.tsx +84 -0
  240. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  241. package/src/plugins/prediction-markets/types.ts +186 -0
  242. package/src/plugins/registry.ts +633 -0
  243. package/src/sources/exchange-rates.ts +18 -0
  244. package/src/sources/gloomberb-cloud.test.ts +199 -0
  245. package/src/sources/gloomberb-cloud.ts +326 -0
  246. package/src/sources/provider-errors.ts +19 -0
  247. package/src/sources/provider-router.test.ts +1476 -0
  248. package/src/sources/provider-router.ts +1405 -0
  249. package/src/sources/sec-edgar.test.ts +286 -0
  250. package/src/sources/sec-edgar.ts +526 -0
  251. package/src/sources/yahoo-finance.test.ts +14 -0
  252. package/src/sources/yahoo-finance.ts +1017 -0
  253. package/src/state/app-activity.test.ts +41 -0
  254. package/src/state/app-activity.ts +106 -0
  255. package/src/state/app-bootstrap.test.ts +471 -0
  256. package/src/state/app-bootstrap.ts +308 -0
  257. package/src/state/app-context.test.ts +406 -0
  258. package/src/state/app-context.test.tsx +162 -0
  259. package/src/state/app-context.tsx +981 -0
  260. package/src/state/selectors.ts +67 -0
  261. package/src/state/session-persistence.test.ts +128 -0
  262. package/src/state/session-persistence.ts +161 -0
  263. package/src/state/ticker-refresh-queue.test.ts +46 -0
  264. package/src/state/ticker-refresh-queue.ts +40 -0
  265. package/src/state/use-inline-tickers.ts +174 -0
  266. package/src/state/use-quote-streaming.test.ts +37 -0
  267. package/src/state/use-quote-streaming.test.tsx +70 -0
  268. package/src/state/use-quote-streaming.ts +80 -0
  269. package/src/state/use-session-snapshot.ts +84 -0
  270. package/src/test-support/data-provider.ts +22 -0
  271. package/src/theme/color-utils.ts +67 -0
  272. package/src/theme/colors.ts +128 -0
  273. package/src/theme/themes.test.ts +45 -0
  274. package/src/theme/themes.ts +491 -0
  275. package/src/types/broker.ts +105 -0
  276. package/src/types/config.ts +444 -0
  277. package/src/types/data-provider.ts +81 -0
  278. package/src/types/financials.ts +142 -0
  279. package/src/types/instrument.ts +29 -0
  280. package/src/types/persistence.ts +21 -0
  281. package/src/types/plugin.ts +354 -0
  282. package/src/types/ticker.ts +58 -0
  283. package/src/types/trading.ts +85 -0
  284. package/src/updater.test.ts +282 -0
  285. package/src/updater.ts +303 -0
  286. package/src/utils/api-client.ts +666 -0
  287. package/src/utils/ascii-chart.ts +172 -0
  288. package/src/utils/broker-instances.ts +47 -0
  289. package/src/utils/cli-output.ts +83 -0
  290. package/src/utils/currency-units.test.ts +49 -0
  291. package/src/utils/currency-units.ts +82 -0
  292. package/src/utils/debug-log.ts +133 -0
  293. package/src/utils/exchange-rate-map.ts +8 -0
  294. package/src/utils/format.ts +158 -0
  295. package/src/utils/fuzzy-search.ts +34 -0
  296. package/src/utils/market-status.ts +95 -0
  297. package/src/utils/options.ts +86 -0
  298. package/src/utils/price-history.ts +39 -0
  299. package/src/utils/quote-time.test.ts +22 -0
  300. package/src/utils/quote-time.ts +50 -0
  301. package/src/utils/sec.test.ts +55 -0
  302. package/src/utils/sec.ts +45 -0
  303. package/src/utils/ticker-list.test.ts +22 -0
  304. package/src/utils/ticker-list.ts +35 -0
  305. package/src/utils/ticker-metadata.ts +51 -0
  306. package/src/utils/ticker-search.test.ts +227 -0
  307. package/src/utils/ticker-search.ts +764 -0
  308. package/src/utils/ticker-tokenizer.test.ts +48 -0
  309. package/src/utils/ticker-tokenizer.ts +72 -0
  310. package/src/version.ts +1 -0
  311. package/install.mjs +0 -68
@@ -0,0 +1,101 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { AppPersistence } from "../../data/app-persistence";
6
+ import type { BrokerInstanceConfig } from "../../types/config";
7
+ import type { BrokerAccount } from "../../types/trading";
8
+ import {
9
+ clearPersistedIbkrAccounts,
10
+ loadPersistedIbkrAccountMap,
11
+ loadPersistedIbkrAccounts,
12
+ persistIbkrAccounts,
13
+ } from "./account-cache";
14
+
15
+ const tempPaths: string[] = [];
16
+
17
+ function createTempDbPath(name: string): string {
18
+ const path = join(tmpdir(), `gloomberb-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
19
+ tempPaths.push(path);
20
+ return path;
21
+ }
22
+
23
+ function createIbkrInstance(connectionMode: "flex" | "gateway", overrides: Partial<BrokerInstanceConfig> = {}): BrokerInstanceConfig {
24
+ return {
25
+ id: overrides.id ?? `ibkr-${connectionMode}`,
26
+ brokerType: "ibkr",
27
+ label: overrides.label ?? `IBKR ${connectionMode}`,
28
+ connectionMode,
29
+ config: connectionMode === "gateway"
30
+ ? { connectionMode, gateway: { host: "127.0.0.1", port: 4002, clientId: 1 } }
31
+ : { connectionMode, flex: { token: "token", queryId: "query" } },
32
+ enabled: true,
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ const TEST_ACCOUNTS: BrokerAccount[] = [{
38
+ accountId: "DU12345",
39
+ name: "DU12345",
40
+ currency: "USD",
41
+ source: "gateway",
42
+ updatedAt: 1_717_000_000_000,
43
+ totalCashValue: 125000,
44
+ cashBalances: [{ currency: "USD", quantity: 125000, baseValue: 125000, baseCurrency: "USD" }],
45
+ }];
46
+
47
+ afterEach(() => {
48
+ for (const path of tempPaths.splice(0)) {
49
+ if (existsSync(path)) rmSync(path, { force: true });
50
+ }
51
+ });
52
+
53
+ describe("IBKR account cache", () => {
54
+ test("persists and reloads account snapshots by broker instance", () => {
55
+ const persistence = new AppPersistence(createTempDbPath("ibkr-account-cache"));
56
+ const instance = createIbkrInstance("flex", { id: "ibkr-personal" });
57
+
58
+ persistIbkrAccounts(persistence.resources, instance, TEST_ACCOUNTS);
59
+
60
+ expect(loadPersistedIbkrAccounts(persistence.resources, instance)).toEqual(TEST_ACCOUNTS);
61
+ expect(loadPersistedIbkrAccountMap(persistence.resources, [instance])).toEqual({
62
+ "ibkr-personal": TEST_ACCOUNTS,
63
+ });
64
+
65
+ persistence.close();
66
+ });
67
+
68
+ test("drops snapshots whose config fingerprint no longer matches", () => {
69
+ const persistence = new AppPersistence(createTempDbPath("ibkr-account-cache-mismatch"));
70
+ const original = createIbkrInstance("gateway", { id: "ibkr-live" });
71
+ const updated = createIbkrInstance("gateway", {
72
+ id: "ibkr-live",
73
+ config: { connectionMode: "gateway", gateway: { host: "127.0.0.1", port: 4003, clientId: 1 } },
74
+ });
75
+
76
+ persistIbkrAccounts(persistence.resources, original, TEST_ACCOUNTS);
77
+
78
+ expect(loadPersistedIbkrAccounts(persistence.resources, updated)).toBeNull();
79
+ expect(persistence.resources.list({
80
+ namespace: "plugin:ibkr",
81
+ kind: "account-snapshot",
82
+ entityKey: "ibkr-live",
83
+ }, {
84
+ allowExpired: true,
85
+ schemaVersion: 1,
86
+ })).toHaveLength(0);
87
+
88
+ persistence.close();
89
+ });
90
+
91
+ test("clears all persisted snapshots for an IBKR instance", () => {
92
+ const persistence = new AppPersistence(createTempDbPath("ibkr-account-cache-clear"));
93
+ const instance = createIbkrInstance("gateway", { id: "ibkr-live" });
94
+
95
+ persistIbkrAccounts(persistence.resources, instance, TEST_ACCOUNTS);
96
+ clearPersistedIbkrAccounts(persistence.resources, instance.id);
97
+
98
+ expect(loadPersistedIbkrAccounts(persistence.resources, instance)).toBeNull();
99
+ persistence.close();
100
+ });
101
+ });
@@ -0,0 +1,137 @@
1
+ import { createHash } from "crypto";
2
+ import type { ResourceStore } from "../../data/resource-store";
3
+ import type { BrokerInstanceConfig } from "../../types/config";
4
+ import type { BrokerAccount } from "../../types/trading";
5
+ import { normalizeIbkrConfig, type IbkrConnectionMode } from "./config";
6
+
7
+ const IBKR_PLUGIN_NAMESPACE = "plugin:ibkr";
8
+ const IBKR_ACCOUNT_SNAPSHOT_KIND = "account-snapshot";
9
+ const IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION = 1;
10
+
11
+ const FLEX_ACCOUNT_CACHE_POLICY = {
12
+ staleMs: 6 * 60 * 60 * 1000,
13
+ expireMs: 30 * 24 * 60 * 60 * 1000,
14
+ } as const;
15
+
16
+ const GATEWAY_ACCOUNT_CACHE_POLICY = {
17
+ staleMs: 30 * 1000,
18
+ expireMs: 7 * 24 * 60 * 60 * 1000,
19
+ } as const;
20
+
21
+ interface PersistedIbkrAccountSnapshot {
22
+ connectionMode: IbkrConnectionMode;
23
+ accounts: BrokerAccount[];
24
+ }
25
+
26
+ function getIbkrAccountSnapshotSourceKey(instance: BrokerInstanceConfig): string {
27
+ const normalized = normalizeIbkrConfig(instance.config);
28
+ return createHash("sha256")
29
+ .update(JSON.stringify(normalized))
30
+ .digest("hex");
31
+ }
32
+
33
+ function getAccountCachePolicy(instance: BrokerInstanceConfig) {
34
+ const normalized = normalizeIbkrConfig(instance.config);
35
+ return normalized.connectionMode === "gateway"
36
+ ? GATEWAY_ACCOUNT_CACHE_POLICY
37
+ : FLEX_ACCOUNT_CACHE_POLICY;
38
+ }
39
+
40
+ function pruneMismatchedSnapshots(resources: ResourceStore, instance: BrokerInstanceConfig, sourceKey: string): void {
41
+ const records = resources.list<PersistedIbkrAccountSnapshot>({
42
+ namespace: IBKR_PLUGIN_NAMESPACE,
43
+ kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
44
+ entityKey: instance.id,
45
+ }, {
46
+ schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
47
+ allowExpired: true,
48
+ });
49
+
50
+ for (const record of records) {
51
+ if (record.sourceKey === sourceKey) continue;
52
+ resources.delete({
53
+ namespace: IBKR_PLUGIN_NAMESPACE,
54
+ kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
55
+ entityKey: instance.id,
56
+ sourceKey: record.sourceKey,
57
+ });
58
+ }
59
+ }
60
+
61
+ export function loadPersistedIbkrAccounts(
62
+ resources: ResourceStore,
63
+ instance: BrokerInstanceConfig,
64
+ ): BrokerAccount[] | null {
65
+ const sourceKey = getIbkrAccountSnapshotSourceKey(instance);
66
+ pruneMismatchedSnapshots(resources, instance, sourceKey);
67
+
68
+ return resources.get<PersistedIbkrAccountSnapshot>({
69
+ namespace: IBKR_PLUGIN_NAMESPACE,
70
+ kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
71
+ entityKey: instance.id,
72
+ sourceKey,
73
+ }, {
74
+ schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
75
+ })?.value.accounts ?? null;
76
+ }
77
+
78
+ export function persistIbkrAccounts(
79
+ resources: ResourceStore,
80
+ instance: BrokerInstanceConfig,
81
+ accounts: BrokerAccount[],
82
+ ): void {
83
+ const sourceKey = getIbkrAccountSnapshotSourceKey(instance);
84
+ pruneMismatchedSnapshots(resources, instance, sourceKey);
85
+
86
+ resources.set<PersistedIbkrAccountSnapshot>({
87
+ namespace: IBKR_PLUGIN_NAMESPACE,
88
+ kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
89
+ entityKey: instance.id,
90
+ sourceKey,
91
+ }, {
92
+ connectionMode: normalizeIbkrConfig(instance.config).connectionMode,
93
+ accounts,
94
+ }, {
95
+ schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
96
+ cachePolicy: getAccountCachePolicy(instance),
97
+ });
98
+ }
99
+
100
+ export function loadPersistedIbkrAccountMap(
101
+ resources: ResourceStore,
102
+ brokerInstances: BrokerInstanceConfig[],
103
+ ): Record<string, BrokerAccount[]> {
104
+ const accountMap: Record<string, BrokerAccount[]> = {};
105
+
106
+ for (const instance of brokerInstances) {
107
+ if (instance.brokerType !== "ibkr") continue;
108
+ const accounts = loadPersistedIbkrAccounts(resources, instance);
109
+ if (!accounts || accounts.length === 0) continue;
110
+ accountMap[instance.id] = accounts;
111
+ }
112
+
113
+ return accountMap;
114
+ }
115
+
116
+ export function clearPersistedIbkrAccounts(
117
+ resources: ResourceStore,
118
+ instanceId: string,
119
+ ): void {
120
+ const records = resources.list<PersistedIbkrAccountSnapshot>({
121
+ namespace: IBKR_PLUGIN_NAMESPACE,
122
+ kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
123
+ entityKey: instanceId,
124
+ }, {
125
+ schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
126
+ allowExpired: true,
127
+ });
128
+
129
+ for (const record of records) {
130
+ resources.delete({
131
+ namespace: IBKR_PLUGIN_NAMESPACE,
132
+ kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
133
+ entityKey: instanceId,
134
+ sourceKey: record.sourceKey,
135
+ });
136
+ }
137
+ }
@@ -0,0 +1,168 @@
1
+ import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
2
+ import type { BrokerInstanceConfig } from "../../types/config";
3
+ import {
4
+ getGatewayConfig,
5
+ IBKR_CONFIG_FIELDS,
6
+ isFlexConfigured,
7
+ isGatewayConfigured,
8
+ normalizeIbkrConfig,
9
+ type FlexQueryConfig,
10
+ } from "./config";
11
+ import { loadFlexStatement, parseFlexAccounts, parseFlexPositions } from "./flex";
12
+ import { ibkrGatewayManager } from "./gateway-service";
13
+ import { refreshGatewayData } from "./gateway-helpers";
14
+
15
+ async function importFlexPositions(config: FlexQueryConfig): Promise<BrokerPosition[]> {
16
+ const xml = await loadFlexStatement(config);
17
+ return parseFlexPositions(xml);
18
+ }
19
+
20
+ export const ibkrBroker: BrokerAdapter = {
21
+ id: "ibkr",
22
+ name: "Interactive Brokers",
23
+ configSchema: IBKR_CONFIG_FIELDS,
24
+
25
+ async validate(instance) {
26
+ const normalized = normalizeIbkrConfig(instance.config);
27
+ return normalized.connectionMode === "gateway"
28
+ ? isGatewayConfigured(instance.config)
29
+ : isFlexConfigured(instance.config);
30
+ },
31
+
32
+ async importPositions(instance) {
33
+ const normalized = normalizeIbkrConfig(instance.config);
34
+ if (normalized.connectionMode === "gateway") {
35
+ await refreshGatewayData(instance);
36
+ return ibkrGatewayManager.getService(instance.id).getPositions(normalized.gateway);
37
+ }
38
+ return importFlexPositions(normalized.flex);
39
+ },
40
+
41
+ async connect(instance) {
42
+ const normalized = normalizeIbkrConfig(instance.config);
43
+ if (normalized.connectionMode !== "gateway") return;
44
+ await ibkrGatewayManager.getService(instance.id).connect(normalized.gateway);
45
+ },
46
+
47
+ async disconnect(instance) {
48
+ await ibkrGatewayManager.removeInstance(instance.id);
49
+ },
50
+
51
+ getStatus(instance) {
52
+ return { ...ibkrGatewayManager.getSnapshot(instance.id).status, mode: "gateway" };
53
+ },
54
+
55
+ async listAccounts(instance) {
56
+ const normalized = normalizeIbkrConfig(instance.config);
57
+ if (normalized.connectionMode === "gateway") {
58
+ return ibkrGatewayManager.getService(instance.id).getAccounts(normalized.gateway);
59
+ }
60
+ const xml = await loadFlexStatement(normalized.flex);
61
+ return parseFlexAccounts(xml);
62
+ },
63
+
64
+ async searchInstruments(query, instance) {
65
+ const normalized = normalizeIbkrConfig(instance.config);
66
+ if (normalized.connectionMode !== "gateway") return [];
67
+ return (await ibkrGatewayManager.getService(instance.id).searchInstruments(query, normalized.gateway)).map((result) => ({
68
+ ...result,
69
+ brokerInstanceId: result.brokerInstanceId ?? instance.id,
70
+ brokerLabel: result.brokerLabel ?? instance.label,
71
+ brokerContract: result.brokerContract
72
+ ? { ...result.brokerContract, brokerInstanceId: result.brokerContract.brokerInstanceId ?? instance.id }
73
+ : undefined,
74
+ }));
75
+ },
76
+
77
+ async getTickerFinancials(ticker, instance, exchange, instrument) {
78
+ const normalized = normalizeIbkrConfig(instance.config);
79
+ if (normalized.connectionMode !== "gateway") {
80
+ throw new Error("Gateway mode is required for broker market data");
81
+ }
82
+ return ibkrGatewayManager.getService(instance.id).getTickerFinancials(ticker, normalized.gateway, exchange, instrument);
83
+ },
84
+
85
+ async getQuote(ticker, instance, exchange, instrument) {
86
+ const normalized = normalizeIbkrConfig(instance.config);
87
+ if (normalized.connectionMode !== "gateway") {
88
+ throw new Error("Gateway mode is required for broker quotes");
89
+ }
90
+ return ibkrGatewayManager.getService(instance.id).getQuote(ticker, normalized.gateway, exchange, instrument);
91
+ },
92
+
93
+ async getPriceHistory(ticker, instance, exchange, range, instrument) {
94
+ const normalized = normalizeIbkrConfig(instance.config);
95
+ if (normalized.connectionMode !== "gateway") {
96
+ throw new Error("Gateway mode is required for broker history");
97
+ }
98
+ return ibkrGatewayManager.getService(instance.id).getPriceHistory(ticker, normalized.gateway, exchange, range, instrument);
99
+ },
100
+
101
+ async getDetailedPriceHistory(ticker, instance, exchange, startDate, endDate, barSize, instrument) {
102
+ const normalized = normalizeIbkrConfig(instance.config);
103
+ if (normalized.connectionMode !== "gateway") {
104
+ throw new Error("Gateway mode is required for broker history");
105
+ }
106
+ return ibkrGatewayManager.getService(instance.id).getDetailedPriceHistory(
107
+ ticker,
108
+ normalized.gateway,
109
+ exchange,
110
+ startDate,
111
+ endDate,
112
+ barSize,
113
+ instrument,
114
+ );
115
+ },
116
+
117
+ subscribeQuotes(instance, targets, onQuote) {
118
+ const normalized = normalizeIbkrConfig(instance.config);
119
+ if (normalized.connectionMode !== "gateway") {
120
+ return () => {};
121
+ }
122
+ return ibkrGatewayManager.getService(instance.id).subscribeQuotes(normalized.gateway, targets, onQuote);
123
+ },
124
+
125
+ async listOpenOrders(instance) {
126
+ const normalized = normalizeIbkrConfig(instance.config);
127
+ if (normalized.connectionMode !== "gateway") return [];
128
+ return ibkrGatewayManager.getService(instance.id).listOpenOrders(normalized.gateway);
129
+ },
130
+
131
+ async listExecutions(instance) {
132
+ const normalized = normalizeIbkrConfig(instance.config);
133
+ if (normalized.connectionMode !== "gateway") return [];
134
+ return ibkrGatewayManager.getService(instance.id).listExecutions(normalized.gateway);
135
+ },
136
+
137
+ async previewOrder(instance, request) {
138
+ const normalized = normalizeIbkrConfig(instance.config);
139
+ if (normalized.connectionMode !== "gateway") {
140
+ throw new Error("Gateway mode is required for order preview");
141
+ }
142
+ return ibkrGatewayManager.getService(instance.id).previewOrder(normalized.gateway, request);
143
+ },
144
+
145
+ async placeOrder(instance, request) {
146
+ const normalized = normalizeIbkrConfig(instance.config);
147
+ if (normalized.connectionMode !== "gateway") {
148
+ throw new Error("Gateway mode is required for trading");
149
+ }
150
+ return ibkrGatewayManager.getService(instance.id).placeOrder(normalized.gateway, request);
151
+ },
152
+
153
+ async modifyOrder(instance, orderId, request) {
154
+ const normalized = normalizeIbkrConfig(instance.config);
155
+ if (normalized.connectionMode !== "gateway") {
156
+ throw new Error("Gateway mode is required for trading");
157
+ }
158
+ return ibkrGatewayManager.getService(instance.id).modifyOrder(normalized.gateway, orderId, request);
159
+ },
160
+
161
+ async cancelOrder(instance, orderId) {
162
+ const normalized = normalizeIbkrConfig(instance.config);
163
+ if (normalized.connectionMode !== "gateway") {
164
+ throw new Error("Gateway mode is required for trading");
165
+ }
166
+ return ibkrGatewayManager.getService(instance.id).cancelOrder(normalized.gateway, orderId);
167
+ },
168
+ };
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildIbkrConfigFromValues, normalizeIbkrConfig } from "./config";
3
+
4
+ describe("ibkr config helpers", () => {
5
+ test("builds nested manual gateway config from flat wizard values", () => {
6
+ const config = buildIbkrConfigFromValues({
7
+ connectionMode: "gateway",
8
+ gatewaySetupMode: "manual",
9
+ host: "127.0.0.1",
10
+ port: "4002",
11
+ });
12
+
13
+ expect(config.connectionMode).toBe("gateway");
14
+ expect(config.gatewaySetupMode).toBe("manual");
15
+ expect(config.gateway).toEqual({
16
+ host: "127.0.0.1",
17
+ port: 4002,
18
+ clientId: undefined,
19
+ lastSuccessfulPort: undefined,
20
+ lastSuccessfulClientId: undefined,
21
+ marketDataType: "auto",
22
+ });
23
+ });
24
+
25
+ test("defaults gateway profiles to automatic connection detection", () => {
26
+ const config = buildIbkrConfigFromValues({
27
+ connectionMode: "gateway",
28
+ gatewaySetupMode: "auto",
29
+ });
30
+
31
+ expect(config.connectionMode).toBe("gateway");
32
+ expect(config.gatewaySetupMode).toBe("auto");
33
+ expect(config.gateway).toEqual({
34
+ host: "127.0.0.1",
35
+ port: undefined,
36
+ clientId: undefined,
37
+ lastSuccessfulPort: undefined,
38
+ lastSuccessfulClientId: undefined,
39
+ marketDataType: "auto",
40
+ });
41
+ });
42
+
43
+ test("treats explicit port/client configs as manual at runtime", () => {
44
+ const config = normalizeIbkrConfig({
45
+ connectionMode: "gateway",
46
+ host: "127.0.0.1",
47
+ port: 4002,
48
+ clientId: 1,
49
+ });
50
+
51
+ expect(config.gatewaySetupMode).toBe("manual");
52
+ expect(config.gateway.port).toBe(4002);
53
+ expect(config.gateway.clientId).toBe(1);
54
+ });
55
+
56
+ test("normalizes flat flex wizard values", () => {
57
+ const config = normalizeIbkrConfig({
58
+ token: "abc",
59
+ queryId: "123",
60
+ });
61
+
62
+ expect(config.connectionMode).toBe("flex");
63
+ expect(config.gatewaySetupMode).toBe("auto");
64
+ expect(config.flex.token).toBe("abc");
65
+ expect(config.flex.queryId).toBe("123");
66
+ expect(config.flex.endpoint).toContain("FlexStatementService.SendRequest");
67
+ });
68
+ });