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,100 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createDefaultConfig, type BrokerInstanceConfig } from "../types/config";
3
+ import type { BrokerAdapter } from "../types/broker";
4
+ import type { TickerRecord } from "../types/ticker";
5
+ import { syncBrokerInstance } from "./sync-broker-instance";
6
+
7
+ function createTickerRepository(initial: TickerRecord[] = []) {
8
+ const tickers = new Map(initial.map((ticker) => [ticker.metadata.ticker, ticker] as const));
9
+
10
+ return {
11
+ async loadAllTickers() {
12
+ return [...tickers.values()];
13
+ },
14
+ async loadTicker(symbol: string) {
15
+ return tickers.get(symbol) ?? null;
16
+ },
17
+ async saveTicker(ticker: TickerRecord) {
18
+ tickers.set(ticker.metadata.ticker, ticker);
19
+ },
20
+ async createTicker(metadata: TickerRecord["metadata"]) {
21
+ const ticker = { metadata };
22
+ tickers.set(metadata.ticker, ticker);
23
+ return ticker;
24
+ },
25
+ async deleteTicker(symbol: string) {
26
+ tickers.delete(symbol);
27
+ },
28
+ };
29
+ }
30
+
31
+ function createBrokerInstance(): BrokerInstanceConfig {
32
+ return {
33
+ id: "demo-broker",
34
+ brokerType: "demo",
35
+ label: "Demo Broker",
36
+ config: { apiKey: "demo-key" },
37
+ enabled: true,
38
+ };
39
+ }
40
+
41
+ function createDemoBroker(): BrokerAdapter {
42
+ return {
43
+ id: "demo",
44
+ name: "Demo Broker",
45
+ configSchema: [{ key: "apiKey", label: "API Key", type: "text", required: true }],
46
+ validate: async () => true,
47
+ listAccounts: async () => [{ accountId: "ACC-1", name: "Primary", currency: "USD" }],
48
+ importPositions: async () => [{
49
+ ticker: "AAPL",
50
+ exchange: "NASDAQ",
51
+ shares: 12,
52
+ avgCost: 180,
53
+ currency: "USD",
54
+ accountId: "ACC-1",
55
+ name: "Apple Inc.",
56
+ assetCategory: "STK",
57
+ }],
58
+ };
59
+ }
60
+
61
+ describe("syncBrokerInstance", () => {
62
+ test("creates broker portfolios and imports positions into local tickers", async () => {
63
+ const config = {
64
+ ...createDefaultConfig("/tmp/gloomberb-sync-broker-instance"),
65
+ portfolios: [],
66
+ brokerInstances: [createBrokerInstance()],
67
+ };
68
+ const tickerRepository = createTickerRepository();
69
+
70
+ const result = await syncBrokerInstance({
71
+ config,
72
+ instanceId: "demo-broker",
73
+ brokers: new Map([["demo", createDemoBroker()]]),
74
+ tickerRepository: tickerRepository as any,
75
+ });
76
+
77
+ expect(result.portfolioIds).toEqual(["broker:demo-broker:ACC-1"]);
78
+ expect(result.config.portfolios).toEqual([
79
+ {
80
+ id: "broker:demo-broker:ACC-1",
81
+ name: "Primary",
82
+ currency: "USD",
83
+ brokerId: "demo",
84
+ brokerInstanceId: "demo-broker",
85
+ brokerAccountId: "ACC-1",
86
+ },
87
+ ]);
88
+ expect(result.positions).toHaveLength(1);
89
+ expect(result.addedTickers).toHaveLength(1);
90
+ expect(result.tickers.get("AAPL")?.metadata.positions).toEqual([
91
+ expect.objectContaining({
92
+ portfolio: "broker:demo-broker:ACC-1",
93
+ broker: "demo",
94
+ shares: 12,
95
+ brokerInstanceId: "demo-broker",
96
+ brokerAccountId: "ACC-1",
97
+ }),
98
+ ]);
99
+ });
100
+ });
@@ -0,0 +1,341 @@
1
+ import type { ResourceStore } from "../data/resource-store";
2
+ import type { TickerRepository } from "../data/ticker-repository";
3
+ import { persistIbkrAccounts } from "../plugins/ibkr/account-cache";
4
+ import { buildPersistedIbkrGatewayConfig } from "../plugins/ibkr/config";
5
+ import { ibkrGatewayManager } from "../plugins/ibkr/gateway-service";
6
+ import type { BrokerAdapter, BrokerPosition } from "../types/broker";
7
+ import type { AppConfig, BrokerInstanceConfig } from "../types/config";
8
+ import type { BrokerContractRef } from "../types/instrument";
9
+ import type { BrokerAccount } from "../types/trading";
10
+ import type { TickerMetadata, TickerPosition, TickerRecord } from "../types/ticker";
11
+ import { buildBrokerPortfolioId, getBrokerInstance } from "../utils/broker-instances";
12
+
13
+ export interface SyncBrokerInstanceArgs {
14
+ config: AppConfig;
15
+ instanceId: string;
16
+ brokers: ReadonlyMap<string, BrokerAdapter>;
17
+ tickerRepository: TickerRepository;
18
+ existingTickers?: Map<string, TickerRecord>;
19
+ resources?: ResourceStore;
20
+ persistResolvedIbkrConnection?: boolean;
21
+ }
22
+
23
+ export interface SyncBrokerInstanceResult {
24
+ config: AppConfig;
25
+ tickers: Map<string, TickerRecord>;
26
+ brokerAccounts: BrokerAccount[];
27
+ positions: BrokerPosition[];
28
+ portfolioIds: string[];
29
+ addedTickers: TickerRecord[];
30
+ updatedTickers: TickerRecord[];
31
+ }
32
+
33
+ function mergeBrokerContracts(existing: BrokerContractRef[], next: BrokerContractRef[]): BrokerContractRef[] {
34
+ const merged = new Map<string, BrokerContractRef>();
35
+ for (const contract of [...existing, ...next]) {
36
+ const key = `${contract.brokerId}:${contract.brokerInstanceId ?? ""}:${contract.conId ?? contract.localSymbol ?? contract.symbol}:${contract.secType ?? ""}`;
37
+ merged.set(key, contract);
38
+ }
39
+ return [...merged.values()];
40
+ }
41
+
42
+ function ensureBrokerPortfolio(
43
+ config: AppConfig,
44
+ instance: BrokerInstanceConfig,
45
+ portfolioId: string,
46
+ name: string,
47
+ currency: string,
48
+ brokerAccountId?: string,
49
+ ): AppConfig {
50
+ if (config.portfolios.some((portfolio) => portfolio.id === portfolioId)) {
51
+ return config;
52
+ }
53
+
54
+ return {
55
+ ...config,
56
+ portfolios: [
57
+ ...config.portfolios,
58
+ {
59
+ id: portfolioId,
60
+ name,
61
+ currency,
62
+ brokerId: instance.brokerType,
63
+ brokerInstanceId: instance.id,
64
+ brokerAccountId,
65
+ },
66
+ ],
67
+ };
68
+ }
69
+
70
+ function maybePersistResolvedIbkrConnection(
71
+ config: AppConfig,
72
+ instance: BrokerInstanceConfig,
73
+ persistResolvedIbkrConnection: boolean,
74
+ ): AppConfig {
75
+ if (!persistResolvedIbkrConnection || instance.brokerType !== "ibkr") {
76
+ return config;
77
+ }
78
+
79
+ const resolved = ibkrGatewayManager.getService(instance.id).getResolvedConnection();
80
+ if (!resolved) {
81
+ return config;
82
+ }
83
+
84
+ const nextConfig = buildPersistedIbkrGatewayConfig(instance.config, resolved);
85
+ if (!nextConfig) {
86
+ return config;
87
+ }
88
+
89
+ return {
90
+ ...config,
91
+ brokerInstances: config.brokerInstances.map((entry) =>
92
+ entry.id === instance.id
93
+ ? {
94
+ ...entry,
95
+ connectionMode: typeof nextConfig.connectionMode === "string" ? nextConfig.connectionMode : entry.connectionMode,
96
+ config: {
97
+ ...entry.config,
98
+ ...nextConfig,
99
+ },
100
+ }
101
+ : entry,
102
+ ),
103
+ };
104
+ }
105
+
106
+ async function loadTickerMap(
107
+ tickerRepository: TickerRepository,
108
+ existingTickers?: Map<string, TickerRecord>,
109
+ ): Promise<Map<string, TickerRecord>> {
110
+ if (existingTickers) {
111
+ return new Map(existingTickers);
112
+ }
113
+
114
+ const tickers = await tickerRepository.loadAllTickers();
115
+ return new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker] as const));
116
+ }
117
+
118
+ function buildPositionEntry(
119
+ instance: BrokerInstanceConfig,
120
+ portfolioId: string,
121
+ position: BrokerPosition,
122
+ brokerContract?: BrokerContractRef,
123
+ ): TickerPosition {
124
+ return {
125
+ portfolio: portfolioId,
126
+ shares: position.shares,
127
+ avgCost: position.avgCost ?? 0,
128
+ currency: position.currency,
129
+ broker: instance.brokerType,
130
+ side: position.side,
131
+ marketValue: position.marketValue,
132
+ unrealizedPnl: position.unrealizedPnl,
133
+ multiplier: position.multiplier,
134
+ markPrice: position.markPrice,
135
+ brokerInstanceId: instance.id,
136
+ brokerAccountId: position.accountId,
137
+ brokerContractId: brokerContract?.conId,
138
+ };
139
+ }
140
+
141
+ function createTickerMetadata(
142
+ position: BrokerPosition,
143
+ portfolioId: string,
144
+ positionEntry: TickerPosition,
145
+ brokerContract?: BrokerContractRef,
146
+ ): TickerMetadata {
147
+ return {
148
+ ticker: position.ticker,
149
+ exchange: position.exchange,
150
+ currency: position.currency,
151
+ name: position.name || position.ticker,
152
+ assetCategory: position.assetCategory,
153
+ isin: position.isin,
154
+ portfolios: [portfolioId],
155
+ watchlists: [],
156
+ positions: [positionEntry],
157
+ broker_contracts: brokerContract ? [brokerContract] : [],
158
+ custom: {},
159
+ tags: [],
160
+ };
161
+ }
162
+
163
+ function updateExistingTicker(
164
+ ticker: TickerRecord,
165
+ instance: BrokerInstanceConfig,
166
+ portfolioId: string,
167
+ position: BrokerPosition,
168
+ positionEntry: TickerPosition,
169
+ brokerContract?: BrokerContractRef,
170
+ ): TickerRecord {
171
+ if (position.name && ticker.metadata.name === ticker.metadata.ticker) {
172
+ ticker.metadata.name = position.name;
173
+ }
174
+ if (position.assetCategory && !ticker.metadata.assetCategory) {
175
+ ticker.metadata.assetCategory = position.assetCategory;
176
+ }
177
+ if (position.isin && !ticker.metadata.isin) {
178
+ ticker.metadata.isin = position.isin;
179
+ }
180
+
181
+ const otherPositions = ticker.metadata.positions.filter(
182
+ (entry) => !(entry.brokerInstanceId === instance.id && entry.portfolio === portfolioId),
183
+ );
184
+ ticker.metadata.positions = [...otherPositions, positionEntry];
185
+ ticker.metadata.broker_contracts = mergeBrokerContracts(
186
+ ticker.metadata.broker_contracts ?? [],
187
+ brokerContract ? [brokerContract] : [],
188
+ );
189
+ if (!ticker.metadata.portfolios.includes(portfolioId)) {
190
+ ticker.metadata.portfolios.push(portfolioId);
191
+ }
192
+
193
+ return ticker;
194
+ }
195
+
196
+ export async function syncBrokerInstance({
197
+ config,
198
+ instanceId,
199
+ brokers,
200
+ tickerRepository,
201
+ existingTickers,
202
+ resources,
203
+ persistResolvedIbkrConnection = false,
204
+ }: SyncBrokerInstanceArgs): Promise<SyncBrokerInstanceResult> {
205
+ const instance = getBrokerInstance(config.brokerInstances, instanceId);
206
+ if (!instance) {
207
+ throw new Error(`Broker instance "${instanceId}" was not found.`);
208
+ }
209
+ if (instance.enabled === false) {
210
+ throw new Error(`Broker instance "${instance.label}" is disabled.`);
211
+ }
212
+
213
+ const broker = brokers.get(instance.brokerType);
214
+ if (!broker) {
215
+ throw new Error(`Broker "${instance.brokerType}" is not available.`);
216
+ }
217
+
218
+ const valid = await broker.validate(instance).catch(() => false);
219
+ if (!valid) {
220
+ throw new Error(`${broker.name} setup is incomplete.`);
221
+ }
222
+
223
+ const tickers = await loadTickerMap(tickerRepository, existingTickers);
224
+
225
+ let brokerAccounts: BrokerAccount[] = [];
226
+ if (broker.listAccounts) {
227
+ try {
228
+ brokerAccounts = await broker.listAccounts(instance);
229
+ if (instance.brokerType === "ibkr" && resources) {
230
+ try {
231
+ persistIbkrAccounts(resources, instance, brokerAccounts);
232
+ } catch {}
233
+ }
234
+ } catch {
235
+ brokerAccounts = [];
236
+ }
237
+ }
238
+
239
+ const accountMetadata = new Map(
240
+ brokerAccounts.map((account) => [
241
+ account.accountId,
242
+ {
243
+ name: account.name || account.accountId,
244
+ currency: account.currency || "USD",
245
+ },
246
+ ]),
247
+ );
248
+
249
+ const positions = await broker.importPositions(instance);
250
+
251
+ let nextConfig = config;
252
+ const accountIds = new Set<string>();
253
+ for (const account of brokerAccounts) {
254
+ if (account.accountId) {
255
+ accountIds.add(account.accountId);
256
+ }
257
+ }
258
+ for (const position of positions) {
259
+ if (position.accountId) {
260
+ accountIds.add(position.accountId);
261
+ }
262
+ }
263
+
264
+ const portfolioIds: string[] = [];
265
+ if (accountIds.size > 0) {
266
+ for (const accountId of accountIds) {
267
+ const portfolioId = buildBrokerPortfolioId(instance.id, accountId);
268
+ const account = accountMetadata.get(accountId);
269
+ nextConfig = ensureBrokerPortfolio(
270
+ nextConfig,
271
+ instance,
272
+ portfolioId,
273
+ account?.name || accountId,
274
+ account?.currency || "USD",
275
+ accountId,
276
+ );
277
+ portfolioIds.push(portfolioId);
278
+ }
279
+ } else {
280
+ const defaultAccount = brokerAccounts[0];
281
+ const portfolioId = buildBrokerPortfolioId(instance.id, defaultAccount?.accountId);
282
+ const fallbackName = defaultAccount?.name || defaultAccount?.accountId || instance.label || broker.name;
283
+ nextConfig = ensureBrokerPortfolio(
284
+ nextConfig,
285
+ instance,
286
+ portfolioId,
287
+ fallbackName,
288
+ defaultAccount?.currency || "USD",
289
+ defaultAccount?.accountId,
290
+ );
291
+ portfolioIds.push(portfolioId);
292
+ }
293
+
294
+ nextConfig = maybePersistResolvedIbkrConnection(nextConfig, instance, persistResolvedIbkrConnection);
295
+
296
+ const addedTickers = new Map<string, TickerRecord>();
297
+ const updatedTickers = new Map<string, TickerRecord>();
298
+
299
+ for (const position of positions) {
300
+ const portfolioId = buildBrokerPortfolioId(instance.id, position.accountId);
301
+ const brokerContract = position.brokerContract
302
+ ? {
303
+ ...position.brokerContract,
304
+ brokerId: instance.brokerType,
305
+ brokerInstanceId: instance.id,
306
+ }
307
+ : undefined;
308
+ const positionEntry = buildPositionEntry(instance, portfolioId, position, brokerContract);
309
+
310
+ let ticker = tickers.get(position.ticker);
311
+ if (!ticker) {
312
+ ticker = await tickerRepository.createTicker(
313
+ createTickerMetadata(position, portfolioId, positionEntry, brokerContract),
314
+ );
315
+ addedTickers.set(position.ticker, ticker);
316
+ } else {
317
+ ticker = updateExistingTicker(ticker, instance, portfolioId, position, positionEntry, brokerContract);
318
+ await tickerRepository.saveTicker(ticker);
319
+ if (!addedTickers.has(position.ticker)) {
320
+ updatedTickers.set(position.ticker, ticker);
321
+ }
322
+ }
323
+
324
+ tickers.set(position.ticker, ticker);
325
+ if (addedTickers.has(position.ticker)) {
326
+ addedTickers.set(position.ticker, ticker);
327
+ } else {
328
+ updatedTickers.set(position.ticker, ticker);
329
+ }
330
+ }
331
+
332
+ return {
333
+ config: nextConfig,
334
+ tickers,
335
+ brokerAccounts,
336
+ positions,
337
+ portfolioIds,
338
+ addedTickers: [...addedTickers.values()],
339
+ updatedTickers: [...updatedTickers.values()],
340
+ };
341
+ }
@@ -0,0 +1,173 @@
1
+ import { join } from "path";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "fs";
3
+ import { execSync } from "child_process";
4
+ import { getPluginsDir } from "../../plugins/loader";
5
+ import {
6
+ cliStyles,
7
+ renderSection,
8
+ renderStat,
9
+ renderTable,
10
+ } from "../../utils/cli-output";
11
+ import { fail } from "../errors";
12
+
13
+ const PLUGINS_DIR = getPluginsDir();
14
+
15
+ function ensurePluginsDir() {
16
+ if (!existsSync(PLUGINS_DIR)) {
17
+ mkdirSync(PLUGINS_DIR, { recursive: true });
18
+ }
19
+ }
20
+
21
+ function parseGitHubRef(ref: string): { url: string; name: string } {
22
+ if (ref.startsWith("https://github.com/")) {
23
+ const clean = ref.replace(/\.git$/, "");
24
+ const name = clean.split("/").pop()!;
25
+ return { url: clean.endsWith(".git") ? ref : `${clean}.git`, name };
26
+ }
27
+ if (ref.startsWith("github:")) {
28
+ ref = ref.slice(7);
29
+ }
30
+ if (ref.includes("/") && !ref.includes("://")) {
31
+ const name = ref.split("/").pop()!;
32
+ return { url: `https://github.com/${ref}.git`, name };
33
+ }
34
+ throw new Error(`Invalid plugin reference: ${ref}. Use user/repo or a GitHub URL.`);
35
+ }
36
+
37
+ export async function installPlugin(ref: string) {
38
+ ensurePluginsDir();
39
+ const { url, name } = parseGitHubRef(ref);
40
+ const targetDir = join(PLUGINS_DIR, name);
41
+
42
+ if (existsSync(targetDir)) {
43
+ fail(`Plugin "${name}" already exists.`, `Use "gloomberb update ${name}" to refresh it.`);
44
+ }
45
+
46
+ console.log(cliStyles.accent(`Installing ${name}`));
47
+ console.log(cliStyles.muted(url));
48
+
49
+ try {
50
+ execSync(`git clone --depth 1 ${url} ${targetDir}`, { stdio: "inherit" });
51
+ } catch {
52
+ fail(`Failed to clone ${url}.`);
53
+ }
54
+
55
+ const pkgPath = join(targetDir, "package.json");
56
+ if (existsSync(pkgPath)) {
57
+ console.log(cliStyles.muted("Installing plugin dependencies..."));
58
+ try {
59
+ execSync("bun install", { cwd: targetDir, stdio: "inherit" });
60
+ } catch {
61
+ console.error(cliStyles.warning("Warning: failed to install plugin dependencies."));
62
+ }
63
+ }
64
+
65
+ try {
66
+ let entryFile: string | null = null;
67
+ if (existsSync(pkgPath)) {
68
+ const pkg = JSON.parse(await Bun.file(pkgPath).text());
69
+ if (pkg.main) entryFile = join(targetDir, pkg.main);
70
+ }
71
+ if (!entryFile) {
72
+ for (const candidate of ["index.ts", "index.tsx", "index.js"]) {
73
+ const path = join(targetDir, candidate);
74
+ if (existsSync(path)) {
75
+ entryFile = path;
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ if (entryFile) {
81
+ const mod = await import(entryFile);
82
+ const plugin = mod.default ?? mod.plugin;
83
+ if (plugin?.id && plugin?.name) {
84
+ console.log(cliStyles.success(`Installed ${plugin.name} v${plugin.version || "0.0.0"}`));
85
+ return;
86
+ }
87
+ }
88
+ console.log(cliStyles.warning("Installed files, but no valid GloomPlugin export was found."));
89
+ } catch (err) {
90
+ console.log(cliStyles.warning(`Plugin validation failed: ${err}`));
91
+ }
92
+ }
93
+
94
+ export async function removePlugin(name: string) {
95
+ const targetDir = join(PLUGINS_DIR, name);
96
+ if (!existsSync(targetDir)) {
97
+ fail(`Plugin "${name}" was not found.`, PLUGINS_DIR);
98
+ }
99
+ rmSync(targetDir, { recursive: true, force: true });
100
+ console.log(cliStyles.success(`Removed plugin "${name}".`));
101
+ }
102
+
103
+ export async function updatePlugins(name?: string) {
104
+ ensurePluginsDir();
105
+ const dirs = name
106
+ ? [name]
107
+ : readdirSync(PLUGINS_DIR, { withFileTypes: true })
108
+ .filter((entry) => entry.isDirectory())
109
+ .map((entry) => entry.name);
110
+
111
+ if (dirs.length === 0) {
112
+ console.log(cliStyles.muted("No plugins installed."));
113
+ return;
114
+ }
115
+
116
+ for (const dir of dirs) {
117
+ const targetDir = join(PLUGINS_DIR, dir);
118
+ if (!existsSync(join(targetDir, ".git"))) {
119
+ console.log(cliStyles.warning(`Skipping ${dir} (not a git repo)`));
120
+ continue;
121
+ }
122
+ console.log(cliStyles.accent(`Updating ${dir}...`));
123
+ try {
124
+ execSync("git pull", { cwd: targetDir, stdio: "inherit" });
125
+ const pkgPath = join(targetDir, "package.json");
126
+ if (existsSync(pkgPath)) {
127
+ execSync("bun install", { cwd: targetDir, stdio: "inherit" });
128
+ }
129
+ } catch {
130
+ console.error(cliStyles.danger(`Failed to update ${dir}.`));
131
+ }
132
+ }
133
+ }
134
+
135
+ export function listPlugins() {
136
+ ensurePluginsDir();
137
+ const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory());
138
+
139
+ if (entries.length === 0) {
140
+ console.log(cliStyles.muted("No plugins installed."));
141
+ console.log(cliStyles.muted("Install one with: gloomberb install <github-user/repo>"));
142
+ return;
143
+ }
144
+
145
+ const rows = entries.map((entry) => {
146
+ const dir = join(PLUGINS_DIR, entry.name);
147
+ let version = "—";
148
+ let description = "—";
149
+ const pkgPath = join(dir, "package.json");
150
+ if (existsSync(pkgPath)) {
151
+ try {
152
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
153
+ version = pkg.version || "—";
154
+ description = pkg.description || "—";
155
+ } catch {
156
+ description = "Unreadable package.json";
157
+ }
158
+ }
159
+ return [entry.name, version, description];
160
+ });
161
+
162
+ console.log(renderSection("Installed Plugins"));
163
+ console.log(renderTable(
164
+ [
165
+ { header: "Plugin" },
166
+ { header: "Version" },
167
+ { header: "Description" },
168
+ ],
169
+ rows,
170
+ ));
171
+ console.log("");
172
+ console.log(renderStat("Directory", PLUGINS_DIR));
173
+ }