gloomberb 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) 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 +1330 -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 +1524 -0
  45. package/src/components/command-bar/command-bar.tsx +4823 -0
  46. package/src/components/command-bar/command-registry.ts +261 -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 +129 -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 +625 -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/onboarding/onboarding-wizard.test.tsx +184 -0
  70. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  71. package/src/components/pane-settings-dialog.tsx +468 -0
  72. package/src/components/pane-template-wizard.test.tsx +52 -0
  73. package/src/components/pane-template-wizard.tsx +199 -0
  74. package/src/components/price-selector-dialog.tsx +195 -0
  75. package/src/components/spinner.tsx +2 -0
  76. package/src/components/tab-bar.tsx +17 -0
  77. package/src/components/ticker-badge-text.test.tsx +160 -0
  78. package/src/components/ticker-badge-text.tsx +58 -0
  79. package/src/components/ticker-badge.tsx +88 -0
  80. package/src/components/ticker-list-table.tsx +188 -0
  81. package/src/components/toggle-list.tsx +74 -0
  82. package/src/components/ui/button.tsx +77 -0
  83. package/src/components/ui/data-table.tsx +210 -0
  84. package/src/components/ui/external-link.tsx +22 -0
  85. package/src/components/ui/fields.tsx +187 -0
  86. package/src/components/ui/frame.tsx +73 -0
  87. package/src/components/ui/index.ts +53 -0
  88. package/src/components/ui/list-view.tsx +166 -0
  89. package/src/components/ui/loading.tsx +70 -0
  90. package/src/components/ui/status.tsx +83 -0
  91. package/src/components/ui/tabs.tsx +47 -0
  92. package/src/components/ui/toggle.tsx +164 -0
  93. package/src/components/ui/ui.test.tsx +134 -0
  94. package/src/data/app-persistence.ts +25 -0
  95. package/src/data/config-store.test.ts +310 -0
  96. package/src/data/config-store.ts +469 -0
  97. package/src/data/plugin-state-store.ts +102 -0
  98. package/src/data/resource-store.ts +323 -0
  99. package/src/data/session-store.ts +52 -0
  100. package/src/data/sqlite-cache.test.ts +103 -0
  101. package/src/data/sqlite-database.ts +62 -0
  102. package/src/data/sqlite-json.ts +19 -0
  103. package/src/data/ticker-repository.ts +45 -0
  104. package/src/data/ticker-store.ts +46 -0
  105. package/src/index.tsx +67 -0
  106. package/src/market-data/coordinator.test.ts +369 -0
  107. package/src/market-data/coordinator.ts +580 -0
  108. package/src/market-data/hooks.tsx +234 -0
  109. package/src/market-data/query-store.ts +28 -0
  110. package/src/market-data/request-types.test.ts +64 -0
  111. package/src/market-data/request-types.ts +70 -0
  112. package/src/market-data/result-types.ts +56 -0
  113. package/src/market-data/selectors.ts +166 -0
  114. package/src/market-data/trace.ts +7 -0
  115. package/src/pane-settings.test.ts +71 -0
  116. package/src/pane-settings.ts +73 -0
  117. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
  118. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  119. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  120. package/src/plugins/builtin/ai/index.tsx +86 -0
  121. package/src/plugins/builtin/ai/providers.ts +99 -0
  122. package/src/plugins/builtin/ai/runner.ts +82 -0
  123. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  124. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  125. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  126. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  127. package/src/plugins/builtin/ai/settings.ts +79 -0
  128. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  129. package/src/plugins/builtin/ai/utils.ts +71 -0
  130. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  131. package/src/plugins/builtin/ask-ai.tsx +9 -0
  132. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  133. package/src/plugins/builtin/chat-controller.ts +340 -0
  134. package/src/plugins/builtin/chat.test.tsx +451 -0
  135. package/src/plugins/builtin/chat.tsx +620 -0
  136. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  137. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  138. package/src/plugins/builtin/data-providers.ts +16 -0
  139. package/src/plugins/builtin/debug.tsx +351 -0
  140. package/src/plugins/builtin/help.test.tsx +110 -0
  141. package/src/plugins/builtin/help.tsx +281 -0
  142. package/src/plugins/builtin/layout-manager.tsx +264 -0
  143. package/src/plugins/builtin/manual-entry.tsx +25 -0
  144. package/src/plugins/builtin/news.tsx +143 -0
  145. package/src/plugins/builtin/notes-files.ts +58 -0
  146. package/src/plugins/builtin/notes.tsx +396 -0
  147. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  148. package/src/plugins/builtin/options-availability.ts +158 -0
  149. package/src/plugins/builtin/options.tsx +364 -0
  150. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  151. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  152. package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
  153. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  154. package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
  155. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  156. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  157. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  158. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  159. package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
  160. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  161. package/src/plugins/builtin/sec.tsx +250 -0
  162. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  163. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  164. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  165. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  166. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  167. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  168. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  169. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  170. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  171. package/src/plugins/builtin/yahoo.ts +18 -0
  172. package/src/plugins/event-bus.ts +37 -0
  173. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  174. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  175. package/src/plugins/ibkr/account-cache.ts +137 -0
  176. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  177. package/src/plugins/ibkr/config.test.ts +68 -0
  178. package/src/plugins/ibkr/config.ts +262 -0
  179. package/src/plugins/ibkr/dialogs.tsx +83 -0
  180. package/src/plugins/ibkr/flex.test.ts +107 -0
  181. package/src/plugins/ibkr/flex.ts +267 -0
  182. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  183. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  184. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  185. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  186. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  187. package/src/plugins/ibkr/index.test.tsx +183 -0
  188. package/src/plugins/ibkr/index.tsx +153 -0
  189. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  190. package/src/plugins/ibkr/instance-selection.ts +36 -0
  191. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  192. package/src/plugins/ibkr/price-normalization.ts +59 -0
  193. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  194. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  195. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  196. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  197. package/src/plugins/ibkr/trade-utils.ts +149 -0
  198. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  199. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  200. package/src/plugins/ibkr/trading-state.ts +339 -0
  201. package/src/plugins/loader.ts +71 -0
  202. package/src/plugins/pane-manager.test.ts +103 -0
  203. package/src/plugins/pane-manager.ts +974 -0
  204. package/src/plugins/plugin-persistence.ts +85 -0
  205. package/src/plugins/plugin-runtime.test.tsx +185 -0
  206. package/src/plugins/plugin-runtime.tsx +175 -0
  207. package/src/plugins/prediction-markets/cache.ts +114 -0
  208. package/src/plugins/prediction-markets/categories.ts +223 -0
  209. package/src/plugins/prediction-markets/chart.tsx +512 -0
  210. package/src/plugins/prediction-markets/columns.ts +133 -0
  211. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  212. package/src/plugins/prediction-markets/controller.ts +725 -0
  213. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  214. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  215. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  216. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  217. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  218. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  219. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  220. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  221. package/src/plugins/prediction-markets/index.tsx +102 -0
  222. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  223. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  224. package/src/plugins/prediction-markets/launch.ts +240 -0
  225. package/src/plugins/prediction-markets/metrics.ts +311 -0
  226. package/src/plugins/prediction-markets/navigation.ts +90 -0
  227. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  228. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  229. package/src/plugins/prediction-markets/pane.tsx +294 -0
  230. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  231. package/src/plugins/prediction-markets/rows.ts +229 -0
  232. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  233. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  234. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  235. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  236. package/src/plugins/prediction-markets/settings.ts +164 -0
  237. package/src/plugins/prediction-markets/status.ts +24 -0
  238. package/src/plugins/prediction-markets/table.tsx +84 -0
  239. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  240. package/src/plugins/prediction-markets/types.ts +186 -0
  241. package/src/plugins/registry.ts +633 -0
  242. package/src/sources/exchange-rates.ts +18 -0
  243. package/src/sources/gloomberb-cloud.test.ts +199 -0
  244. package/src/sources/gloomberb-cloud.ts +326 -0
  245. package/src/sources/provider-errors.ts +19 -0
  246. package/src/sources/provider-router.test.ts +1476 -0
  247. package/src/sources/provider-router.ts +1405 -0
  248. package/src/sources/sec-edgar.test.ts +286 -0
  249. package/src/sources/sec-edgar.ts +526 -0
  250. package/src/sources/yahoo-finance.test.ts +14 -0
  251. package/src/sources/yahoo-finance.ts +1017 -0
  252. package/src/state/app-activity.test.ts +41 -0
  253. package/src/state/app-activity.ts +106 -0
  254. package/src/state/app-bootstrap.test.ts +471 -0
  255. package/src/state/app-bootstrap.ts +308 -0
  256. package/src/state/app-context.test.ts +406 -0
  257. package/src/state/app-context.test.tsx +132 -0
  258. package/src/state/app-context.tsx +969 -0
  259. package/src/state/selectors.ts +67 -0
  260. package/src/state/session-persistence.test.ts +128 -0
  261. package/src/state/session-persistence.ts +161 -0
  262. package/src/state/ticker-refresh-queue.test.ts +46 -0
  263. package/src/state/ticker-refresh-queue.ts +40 -0
  264. package/src/state/use-inline-tickers.ts +174 -0
  265. package/src/state/use-quote-streaming.test.ts +37 -0
  266. package/src/state/use-quote-streaming.test.tsx +70 -0
  267. package/src/state/use-quote-streaming.ts +80 -0
  268. package/src/state/use-session-snapshot.ts +84 -0
  269. package/src/test-support/data-provider.ts +22 -0
  270. package/src/theme/color-utils.ts +67 -0
  271. package/src/theme/colors.ts +128 -0
  272. package/src/theme/themes.test.ts +45 -0
  273. package/src/theme/themes.ts +491 -0
  274. package/src/types/broker.ts +105 -0
  275. package/src/types/config.ts +434 -0
  276. package/src/types/data-provider.ts +81 -0
  277. package/src/types/financials.ts +142 -0
  278. package/src/types/instrument.ts +29 -0
  279. package/src/types/persistence.ts +21 -0
  280. package/src/types/plugin.ts +354 -0
  281. package/src/types/ticker.ts +58 -0
  282. package/src/types/trading.ts +85 -0
  283. package/src/updater.test.ts +162 -0
  284. package/src/updater.ts +247 -0
  285. package/src/utils/api-client.ts +666 -0
  286. package/src/utils/ascii-chart.ts +172 -0
  287. package/src/utils/broker-instances.ts +47 -0
  288. package/src/utils/cli-output.ts +83 -0
  289. package/src/utils/currency-units.test.ts +49 -0
  290. package/src/utils/currency-units.ts +82 -0
  291. package/src/utils/debug-log.ts +133 -0
  292. package/src/utils/exchange-rate-map.ts +8 -0
  293. package/src/utils/format.ts +158 -0
  294. package/src/utils/fuzzy-search.ts +34 -0
  295. package/src/utils/market-status.ts +95 -0
  296. package/src/utils/options.ts +86 -0
  297. package/src/utils/price-history.ts +39 -0
  298. package/src/utils/quote-time.test.ts +22 -0
  299. package/src/utils/quote-time.ts +50 -0
  300. package/src/utils/sec.test.ts +55 -0
  301. package/src/utils/sec.ts +45 -0
  302. package/src/utils/ticker-list.test.ts +22 -0
  303. package/src/utils/ticker-list.ts +35 -0
  304. package/src/utils/ticker-metadata.ts +51 -0
  305. package/src/utils/ticker-search.test.ts +227 -0
  306. package/src/utils/ticker-search.ts +764 -0
  307. package/src/utils/ticker-tokenizer.test.ts +48 -0
  308. package/src/utils/ticker-tokenizer.ts +72 -0
  309. package/src/version.ts +1 -0
  310. package/install.mjs +0 -68
@@ -0,0 +1,267 @@
1
+ import type { BrokerPosition } from "../../types/broker";
2
+ import type { BrokerContractRef } from "../../types/instrument";
3
+ import type { BrokerAccount, BrokerCashBalance } from "../../types/trading";
4
+ import type { FlexQueryConfig } from "./config";
5
+ import { IBKR_STATEMENT_URL } from "./config";
6
+
7
+ const IBKR_STATEMENT_GET_URL = "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.GetStatement";
8
+ const FLEX_STATEMENT_CACHE_MS = 15_000;
9
+ const flexStatementCache = new Map<string, { createdAt: number; promise: Promise<string> }>();
10
+
11
+ function parseAttributes(raw: string): Record<string, string> {
12
+ const attributes: Record<string, string> = {};
13
+ for (const [, key, value] of raw.matchAll(/([a-zA-Z0-9]+)="([^"]*)"/g)) {
14
+ if (!key) continue;
15
+ attributes[key] = value ?? "";
16
+ }
17
+ return attributes;
18
+ }
19
+
20
+ function parseNumber(value: string | undefined): number | undefined {
21
+ if (!value) return undefined;
22
+ const parsed = Number(value);
23
+ return Number.isFinite(parsed) ? parsed : undefined;
24
+ }
25
+
26
+ function parseFlexTimestamp(raw: string | undefined, fallbackDate: string | undefined): number | undefined {
27
+ if (raw) {
28
+ const match = raw.match(/^(\d{4})(\d{2})(\d{2});(\d{2})(\d{2})(\d{2})$/);
29
+ if (match) {
30
+ const [, year, month, day, hour, minute, second] = match;
31
+ return new Date(
32
+ Number(year),
33
+ Number(month) - 1,
34
+ Number(day),
35
+ Number(hour),
36
+ Number(minute),
37
+ Number(second),
38
+ ).getTime();
39
+ }
40
+ }
41
+
42
+ if (!fallbackDate) return undefined;
43
+ const match = fallbackDate.match(/^(\d{4})(\d{2})(\d{2})$/);
44
+ if (!match) return undefined;
45
+ const [, year, month, day] = match;
46
+ return new Date(Number(year), Number(month) - 1, Number(day)).getTime();
47
+ }
48
+
49
+ export async function requestFlexStatement(config: FlexQueryConfig): Promise<string> {
50
+ const endpoint = config.endpoint || IBKR_STATEMENT_URL;
51
+ const url = `${endpoint}?t=${config.token}&q=${config.queryId}&v=3`;
52
+ const resp = await fetch(url, { signal: AbortSignal.timeout(30_000) });
53
+ const text = await resp.text();
54
+
55
+ const codeMatch = text.match(/<ReferenceCode>(\d+)<\/ReferenceCode>/);
56
+ if (!codeMatch) {
57
+ const errorMatch = text.match(/<ErrorMessage>([^<]+)<\/ErrorMessage>/);
58
+ throw new Error(errorMatch?.[1] || "Failed to request Flex statement");
59
+ }
60
+
61
+ return codeMatch[1]!;
62
+ }
63
+
64
+ export async function getFlexStatement(token: string, referenceCode: string): Promise<string> {
65
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
66
+
67
+ const url = `${IBKR_STATEMENT_GET_URL}?t=${token}&q=${referenceCode}&v=3`;
68
+ for (let attempt = 0; attempt < 5; attempt += 1) {
69
+ const resp = await fetch(url, { signal: AbortSignal.timeout(30_000) });
70
+ const text = await resp.text();
71
+
72
+ if (text.includes("<FlexQueryResponse") || text.includes("<FlexStatements")) {
73
+ return text;
74
+ }
75
+
76
+ if (text.includes("Statement generation in progress")) {
77
+ await new Promise((resolve) => setTimeout(resolve, 2_000 * (attempt + 1)));
78
+ continue;
79
+ }
80
+
81
+ const errorMatch = text.match(/<ErrorMessage>([^<]+)<\/ErrorMessage>/);
82
+ if (errorMatch) {
83
+ throw new Error(errorMatch[1]);
84
+ }
85
+ }
86
+
87
+ throw new Error("Flex statement generation timed out");
88
+ }
89
+
90
+ export async function loadFlexStatement(config: FlexQueryConfig): Promise<string> {
91
+ const cacheKey = `${config.endpoint || IBKR_STATEMENT_URL}|${config.token}|${config.queryId}`;
92
+ const existing = flexStatementCache.get(cacheKey);
93
+ if (existing && Date.now() - existing.createdAt < FLEX_STATEMENT_CACHE_MS) {
94
+ return existing.promise;
95
+ }
96
+
97
+ const promise = (async () => {
98
+ const referenceCode = await requestFlexStatement(config);
99
+ return getFlexStatement(config.token, referenceCode);
100
+ })();
101
+ flexStatementCache.set(cacheKey, { createdAt: Date.now(), promise });
102
+ setTimeout(() => {
103
+ const cached = flexStatementCache.get(cacheKey);
104
+ if (cached?.promise === promise) {
105
+ flexStatementCache.delete(cacheKey);
106
+ }
107
+ }, FLEX_STATEMENT_CACHE_MS);
108
+ return promise;
109
+ }
110
+
111
+ function buildContractRef(attributes: Record<string, string>, symbol: string, assetCategory: string): BrokerContractRef | undefined {
112
+ const conIdRaw = attributes.conid || attributes.conId;
113
+ const strikeRaw = attributes.strike;
114
+ const multiplierRaw = attributes.multiplier;
115
+ const lastTrade = attributes.expiry || attributes.lastTradeDate;
116
+ const right = attributes.putCall?.toUpperCase() === "CALL"
117
+ ? "C"
118
+ : attributes.putCall?.toUpperCase() === "PUT"
119
+ ? "P"
120
+ : undefined;
121
+
122
+ const conId = conIdRaw ? Number(conIdRaw) : undefined;
123
+ const strike = strikeRaw ? Number(strikeRaw) : undefined;
124
+ const multiplier = multiplierRaw || undefined;
125
+
126
+ if (!conId && !assetCategory && !lastTrade && !right && !strike) return undefined;
127
+
128
+ return {
129
+ brokerId: "ibkr",
130
+ conId: Number.isFinite(conId) ? conId : undefined,
131
+ symbol,
132
+ localSymbol: attributes.localSymbol || symbol,
133
+ secType: assetCategory || undefined,
134
+ exchange: attributes.exchange || attributes.listingExchange || undefined,
135
+ primaryExchange: attributes.listingExchange || undefined,
136
+ currency: attributes.currency || undefined,
137
+ lastTradeDateOrContractMonth: lastTrade || undefined,
138
+ right,
139
+ strike: Number.isFinite(strike) ? strike : undefined,
140
+ multiplier,
141
+ tradingClass: attributes.tradingClass || undefined,
142
+ };
143
+ }
144
+
145
+ export function parseFlexPositions(xml: string): BrokerPosition[] {
146
+ const positions: BrokerPosition[] = [];
147
+ const posRegex = /<OpenPosition[^>]*>/g;
148
+ let match: RegExpExecArray | null;
149
+
150
+ while ((match = posRegex.exec(xml)) !== null) {
151
+ const attributes = parseAttributes(match[0]);
152
+
153
+ const attr = (name: string) => attributes[name] || "";
154
+ const numAttr = (name: string) => {
155
+ return parseNumber(attr(name));
156
+ };
157
+
158
+ const symbol = attr("symbol");
159
+ const quantity = Number(attr("position") || attr("quantity") || "0");
160
+ const costBasis = Number(attr("costBasisPrice") || attr("costPrice") || "0");
161
+ const currency = attr("currency") || "USD";
162
+ const exchange = attr("listingExchange") || attr("exchange") || "";
163
+ const accountId = attr("accountId");
164
+ const description = attr("description");
165
+ const assetCategory = attr("assetCategory");
166
+ const isin = attr("isin") || attr("securityID");
167
+ const side = attr("side")?.toLowerCase();
168
+ const contract = buildContractRef(attributes, symbol, assetCategory);
169
+
170
+ if (!symbol || quantity === 0) continue;
171
+
172
+ positions.push({
173
+ ticker: symbol,
174
+ exchange,
175
+ shares: Math.abs(quantity),
176
+ avgCost: costBasis,
177
+ currency,
178
+ accountId: accountId || undefined,
179
+ name: description || undefined,
180
+ assetCategory: assetCategory || undefined,
181
+ isin: isin || undefined,
182
+ side: side === "long" || side === "short" ? side : undefined,
183
+ markPrice: numAttr("markPrice"),
184
+ marketValue: numAttr("positionValue"),
185
+ unrealizedPnl: numAttr("fifoPnlUnrealized") ?? numAttr("unrealizedCapitalGainsPnl"),
186
+ fxRateToBase: numAttr("fxRateToBase"),
187
+ multiplier: numAttr("multiplier"),
188
+ percentOfNav: numAttr("percentOfNAV"),
189
+ brokerContract: contract,
190
+ });
191
+ }
192
+
193
+ return positions;
194
+ }
195
+
196
+ export function parseFlexAccounts(xml: string): BrokerAccount[] {
197
+ const accounts: BrokerAccount[] = [];
198
+ const statementRegex = /<FlexStatement\b([^>]*)>([\s\S]*?)<\/FlexStatement>/g;
199
+
200
+ for (const match of xml.matchAll(statementRegex)) {
201
+ const statementAttrs = parseAttributes(match[1] ?? "");
202
+ const body = match[2] ?? "";
203
+ const statementAccountId = statementAttrs.accountId || "";
204
+
205
+ const changeInNavAttrs = [...body.matchAll(/<ChangeInNAV\b([^>]*)\/>/g)]
206
+ .map((entry) => parseAttributes(entry[1] ?? ""))
207
+ .find((attributes) => !statementAccountId || !attributes.accountId || attributes.accountId === statementAccountId);
208
+ const baseCashAttrs = [...body.matchAll(/<CashReportCurrency\b([^>]*)\/>/g)]
209
+ .map((entry) => parseAttributes(entry[1] ?? ""))
210
+ .find((attributes) =>
211
+ attributes.currency === "BASE_SUMMARY"
212
+ && (!statementAccountId || !attributes.accountId || attributes.accountId === statementAccountId),
213
+ );
214
+ const fallbackCurrencyAttrs = [...body.matchAll(/<CashReportCurrency\b([^>]*)\/>/g)]
215
+ .map((entry) => parseAttributes(entry[1] ?? ""))
216
+ .find((attributes) =>
217
+ attributes.currency !== "BASE_SUMMARY"
218
+ && (!statementAccountId || !attributes.accountId || attributes.accountId === statementAccountId),
219
+ );
220
+
221
+ const balancesByCurrency = new Map<string, BrokerCashBalance>();
222
+ for (const entry of body.matchAll(/<FxPosition\b([^>]*)\/>/g)) {
223
+ const attributes = parseAttributes(entry[1] ?? "");
224
+ const accountId = attributes.accountId || statementAccountId;
225
+ if (!accountId || (statementAccountId && accountId !== statementAccountId)) continue;
226
+ if (attributes.assetCategory !== "CASH") continue;
227
+
228
+ const currency = attributes.fxCurrency || "";
229
+ if (!currency) continue;
230
+
231
+ const existing = balancesByCurrency.get(currency);
232
+ const quantity = parseNumber(attributes.quantity) ?? 0;
233
+ const baseValue = parseNumber(attributes.value);
234
+ balancesByCurrency.set(currency, {
235
+ currency,
236
+ quantity: (existing?.quantity ?? 0) + quantity,
237
+ baseValue: existing?.baseValue != null || baseValue != null
238
+ ? (existing?.baseValue ?? 0) + (baseValue ?? 0)
239
+ : undefined,
240
+ baseCurrency: attributes.functionalCurrency || existing?.baseCurrency,
241
+ });
242
+ }
243
+
244
+ const accountId = statementAccountId || changeInNavAttrs?.accountId;
245
+ if (!accountId) continue;
246
+
247
+ const cashBalances = [...balancesByCurrency.values()];
248
+ const accountCurrency = changeInNavAttrs?.currency
249
+ || cashBalances[0]?.baseCurrency
250
+ || fallbackCurrencyAttrs?.currency
251
+ || undefined;
252
+
253
+ accounts.push({
254
+ accountId,
255
+ name: changeInNavAttrs?.acctAlias || statementAttrs.acctAlias || accountId,
256
+ currency: accountCurrency,
257
+ source: "flex",
258
+ updatedAt: parseFlexTimestamp(statementAttrs.whenGenerated, statementAttrs.toDate || statementAttrs.fromDate),
259
+ netLiquidation: parseNumber(changeInNavAttrs?.endingValue),
260
+ totalCashValue: parseNumber(baseCashAttrs?.endingCash),
261
+ settledCash: parseNumber(baseCashAttrs?.endingSettledCash),
262
+ cashBalances: cashBalances.length > 0 ? cashBalances : undefined,
263
+ });
264
+ }
265
+
266
+ return accounts;
267
+ }
@@ -0,0 +1,149 @@
1
+ import type { Fundamentals, FinancialStatement } from "../../types/financials";
2
+
3
+ /**
4
+ * Parse IBKR ReportSnapshot XML (from getFundamentalData "ReportSnapshot")
5
+ * into a Fundamentals object. Uses <Ratio FieldName="...">value</Ratio> tags.
6
+ */
7
+ export function parseReportSnapshot(xml: string): Fundamentals {
8
+ const ratios = new Map<string, number>();
9
+ const ratioRegex = /<Ratio\s+FieldName="([^"]+)"[^>]*>([^<]+)<\/Ratio>/gi;
10
+ let match: RegExpExecArray | null;
11
+ while ((match = ratioRegex.exec(xml)) !== null) {
12
+ const value = parseFloat(match[2]!);
13
+ if (Number.isFinite(value)) {
14
+ ratios.set(match[1]!.toUpperCase(), value);
15
+ }
16
+ }
17
+
18
+ const get = (key: string): number | undefined => ratios.get(key.toUpperCase());
19
+
20
+ const fundamentals: Fundamentals = {};
21
+
22
+ const trailingPE = get("PEEXCLXOR");
23
+ if (trailingPE != null) fundamentals.trailingPE = trailingPE;
24
+
25
+ const forwardPE = get("APENORM");
26
+ if (forwardPE != null) fundamentals.forwardPE = forwardPE;
27
+
28
+ const pegRatio = get("PEGR");
29
+ if (pegRatio != null) fundamentals.pegRatio = pegRatio;
30
+
31
+ const eps = get("TTMEPSXCLX");
32
+ if (eps != null) fundamentals.eps = eps;
33
+
34
+ const dividendYield = get("YIELD");
35
+ if (dividendYield != null) fundamentals.dividendYield = dividendYield / 100;
36
+
37
+ const revenue = get("TTMREV");
38
+ if (revenue != null) fundamentals.revenue = revenue * 1_000_000;
39
+
40
+ const netIncome = get("TTMNIAC");
41
+ if (netIncome != null) fundamentals.netIncome = netIncome * 1_000_000;
42
+
43
+ const freeCashFlow = get("TTMFCF");
44
+ if (freeCashFlow != null) fundamentals.freeCashFlow = freeCashFlow * 1_000_000;
45
+
46
+ const operatingMargin = get("TTMOPMGN");
47
+ if (operatingMargin != null) fundamentals.operatingMargin = operatingMargin / 100;
48
+
49
+ const profitMargin = get("TTMNPMGN");
50
+ if (profitMargin != null) fundamentals.profitMargin = profitMargin / 100;
51
+
52
+ const enterpriseValue = get("ENTRVAL");
53
+ if (enterpriseValue != null) fundamentals.enterpriseValue = enterpriseValue * 1_000_000;
54
+
55
+ const revenueGrowth = get("REVCHNGYR");
56
+ if (revenueGrowth != null) fundamentals.revenueGrowth = revenueGrowth / 100;
57
+
58
+ const sharesOutstanding = get("SHAESSION");
59
+ if (sharesOutstanding != null) fundamentals.sharesOutstanding = sharesOutstanding;
60
+
61
+ return fundamentals;
62
+ }
63
+
64
+ /** COA code → FinancialStatement field mapping */
65
+ const COA_MAP: Record<string, keyof FinancialStatement> = {
66
+ SREV: "totalRevenue",
67
+ SCOR: "costOfRevenue",
68
+ SGRP: "grossProfit",
69
+ SSGA: "sellingGeneralAndAdministration",
70
+ ERAD: "researchAndDevelopment",
71
+ ETOE: "operatingExpense",
72
+ SOPI: "operatingIncome",
73
+ STIE: "interestExpense",
74
+ TTAX: "taxProvision",
75
+ NINC: "netIncome",
76
+ SDAJ: "eps",
77
+ SDBF: "basicEps",
78
+ SDWS: "dilutedShares",
79
+ ATOT: "totalAssets",
80
+ ATCA: "currentAssets",
81
+ ACAE: "cashAndCashEquivalents",
82
+ LTLL: "totalLiabilities",
83
+ LTCL: "currentLiabilities",
84
+ LTTD: "longTermDebt",
85
+ STLD: "totalDebt",
86
+ QTLE: "totalEquity",
87
+ QRED: "retainedEarnings",
88
+ OTLO: "operatingCashFlow",
89
+ SCEX: "capitalExpenditure",
90
+ ITLI: "investingCashFlow",
91
+ FTLF: "financingCashFlow",
92
+ FPRD: "issuanceOfDebt",
93
+ FPSS: "repurchaseOfCapitalStock",
94
+ FCDP: "cashDividendsPaid",
95
+ };
96
+
97
+ function parsePeriods(xml: string, sectionTag: string): FinancialStatement[] {
98
+ const sectionRegex = new RegExp(`<${sectionTag}[^>]*>([\\s\\S]*?)<\\/${sectionTag}>`, "i");
99
+ const sectionMatch = sectionRegex.exec(xml);
100
+ if (!sectionMatch) return [];
101
+
102
+ const section = sectionMatch[1]!;
103
+ const statements: FinancialStatement[] = [];
104
+
105
+ // Match each FiscalPeriod
106
+ const periodRegex = /<FiscalPeriod[^>]*EndDate="([^"]+)"[^>]*>([\s\S]*?)<\/FiscalPeriod>/gi;
107
+ let periodMatch: RegExpExecArray | null;
108
+
109
+ while ((periodMatch = periodRegex.exec(section)) !== null) {
110
+ const endDate = periodMatch[1]!;
111
+ const periodContent = periodMatch[2]!;
112
+ const stmt: FinancialStatement = { date: endDate };
113
+
114
+ // Match all lineItems across all Statement types
115
+ const lineItemRegex = /<lineItem\s+[^>]*coaCode="([^"]+)"[^>]*>([^<]+)<\/lineItem>/gi;
116
+ let lineMatch: RegExpExecArray | null;
117
+
118
+ while ((lineMatch = lineItemRegex.exec(periodContent)) !== null) {
119
+ const coaCode = lineMatch[1]!.toUpperCase();
120
+ const field = COA_MAP[coaCode];
121
+ if (field && field !== "date") {
122
+ const value = parseFloat(lineMatch[2]!);
123
+ if (Number.isFinite(value)) {
124
+ (stmt as any)[field] = value;
125
+ }
126
+ }
127
+ }
128
+
129
+ // Compute freeCashFlow if not directly available
130
+ if (stmt.freeCashFlow == null && stmt.operatingCashFlow != null && stmt.capitalExpenditure != null) {
131
+ stmt.freeCashFlow = stmt.operatingCashFlow - Math.abs(stmt.capitalExpenditure);
132
+ }
133
+
134
+ statements.push(stmt);
135
+ }
136
+
137
+ return statements;
138
+ }
139
+
140
+ /**
141
+ * Parse IBKR ReportsFinStatements XML (from getFundamentalData "ReportsFinStatements")
142
+ * into annual and quarterly FinancialStatement arrays.
143
+ */
144
+ export function parseFinStatements(xml: string): { annual: FinancialStatement[]; quarterly: FinancialStatement[] } {
145
+ return {
146
+ annual: parsePeriods(xml, "AnnualPeriods"),
147
+ quarterly: parsePeriods(xml, "InterimPeriods"),
148
+ };
149
+ }
@@ -0,0 +1,28 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import type { BrokerInstanceConfig } from "../../types/config";
3
+ import { getGatewayConfig } from "./config";
4
+ import { ibkrGatewayManager } from "./gateway-service";
5
+
6
+ export function useGatewaySnapshot(instanceId?: string) {
7
+ return useSyncExternalStore(
8
+ (listener) => ibkrGatewayManager.subscribe(instanceId, listener),
9
+ () => ibkrGatewayManager.getSnapshot(instanceId),
10
+ );
11
+ }
12
+
13
+ export function getGatewayRequiredMessage(instanceCount: number) {
14
+ return instanceCount > 0
15
+ ? "Choose a Gateway / TWS IBKR profile first."
16
+ : "Connect a Gateway / TWS IBKR profile first.";
17
+ }
18
+
19
+ export async function refreshGatewayData(instance: BrokerInstanceConfig): Promise<void> {
20
+ const gateway = getGatewayConfig(instance.config);
21
+ const service = ibkrGatewayManager.getService(instance.id);
22
+ await service.connect(gateway);
23
+ await Promise.allSettled([
24
+ service.getAccounts(gateway),
25
+ service.listOpenOrders(gateway),
26
+ service.listExecutions(gateway),
27
+ ]);
28
+ }
@@ -0,0 +1,51 @@
1
+ import type { AppConfig } from "../../types/config";
2
+ import type { BrokerAccount } from "../../types/trading";
3
+ import { getBrokerInstance } from "../../utils/broker-instances";
4
+ import { normalizeIbkrConfig } from "./config";
5
+ import { ibkrGatewayManager } from "./gateway-service";
6
+ import {
7
+ getConfiguredIbkrGatewayInstances,
8
+ getLockedIbkrTradingInstanceId,
9
+ resolveIbkrTradingInstanceId,
10
+ } from "./instance-selection";
11
+ import {
12
+ getGatewayRequiredMessage,
13
+ useGatewaySnapshot,
14
+ } from "./gateway-helpers";
15
+ import { getKnownIbkrAccounts } from "./trade-utils";
16
+
17
+ export function useIbkrGatewaySelection(
18
+ config: AppConfig,
19
+ brokerAccounts: Record<string, BrokerAccount[]>,
20
+ collectionId: string | null | undefined,
21
+ preferredInstanceId?: string,
22
+ ) {
23
+ const activePortfolio = config.portfolios.find((portfolio) => portfolio.id === collectionId);
24
+ const gatewayInstances = getConfiguredIbkrGatewayInstances(config);
25
+ const lockedBrokerInstanceId = getLockedIbkrTradingInstanceId(config, collectionId);
26
+ const selectedBrokerInstanceId = resolveIbkrTradingInstanceId(config, collectionId, preferredInstanceId);
27
+ const selectedInstance = getBrokerInstance(config.brokerInstances, selectedBrokerInstanceId);
28
+ const gatewaySnapshot = useGatewaySnapshot(selectedBrokerInstanceId);
29
+ const gatewayService = selectedBrokerInstanceId ? ibkrGatewayManager.getService(selectedBrokerInstanceId) : null;
30
+ const normalizedConfig = selectedInstance ? normalizeIbkrConfig(selectedInstance.config) : null;
31
+ const isGatewayMode = selectedInstance != null && normalizedConfig?.connectionMode === "gateway";
32
+ const availableAccounts = getKnownIbkrAccounts(
33
+ brokerAccounts,
34
+ selectedBrokerInstanceId,
35
+ gatewaySnapshot.accounts,
36
+ );
37
+
38
+ return {
39
+ activePortfolio,
40
+ gatewayInstances,
41
+ lockedBrokerInstanceId,
42
+ selectedBrokerInstanceId,
43
+ selectedInstance,
44
+ gatewaySnapshot,
45
+ gatewayService,
46
+ normalizedConfig,
47
+ isGatewayMode,
48
+ availableAccounts,
49
+ gatewayRequiredMessage: getGatewayRequiredMessage(gatewayInstances.length),
50
+ };
51
+ }