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,199 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { GloomberbCloudProvider } from "./gloomberb-cloud";
3
+ import { apiClient, type AuthUser } from "../utils/api-client";
4
+
5
+ const verifiedUser: AuthUser = {
6
+ id: "user-1",
7
+ name: "Test User",
8
+ email: "test@example.com",
9
+ username: "test",
10
+ emailVerified: true,
11
+ image: null,
12
+ createdAt: "2026-03-30T00:00:00.000Z",
13
+ updatedAt: "2026-03-30T00:00:00.000Z",
14
+ };
15
+
16
+ const originalEnsureVerifiedSession = apiClient.ensureVerifiedSession.bind(apiClient);
17
+ const originalGetCloudHistory = apiClient.getCloudHistory.bind(apiClient);
18
+ const originalGetCloudQuote = apiClient.getCloudQuote.bind(apiClient);
19
+ const originalSubscribeQuotes = apiClient.subscribeQuotes.bind(apiClient);
20
+
21
+ afterEach(() => {
22
+ apiClient.ensureVerifiedSession = originalEnsureVerifiedSession;
23
+ apiClient.getCloudHistory = originalGetCloudHistory;
24
+ apiClient.getCloudQuote = originalGetCloudQuote;
25
+ apiClient.subscribeQuotes = originalSubscribeQuotes;
26
+ });
27
+
28
+ describe("GloomberbCloudProvider", () => {
29
+ test("fetches detailed intraday chart history with Twelve Data intervals", async () => {
30
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
31
+
32
+ let requestArgs: { symbol: string; exchange: string; params: Record<string, string | number | undefined> } | null = null;
33
+ apiClient.getCloudHistory = async (symbol, exchange, params = {}) => {
34
+ requestArgs = { symbol, exchange, params };
35
+ return {
36
+ status: "success",
37
+ data: [{
38
+ date: "2026-03-27 10:15:00",
39
+ close: 250.12,
40
+ }],
41
+ };
42
+ };
43
+
44
+ const provider = new GloomberbCloudProvider();
45
+ const history = await provider.getDetailedPriceHistory(
46
+ "AAPL",
47
+ "NASDAQ",
48
+ new Date(2026, 2, 27, 10, 0, 0),
49
+ new Date(2026, 2, 27, 12, 0, 0),
50
+ "15m",
51
+ );
52
+
53
+ expect(requestArgs).toEqual({
54
+ symbol: "AAPL",
55
+ exchange: "NASDAQ",
56
+ params: {
57
+ interval: "15min",
58
+ startDate: "2026-03-27 10:00:00",
59
+ endDate: "2026-03-27 12:00:00",
60
+ },
61
+ });
62
+ expect(history).toHaveLength(1);
63
+ expect(history[0]?.close).toBe(250.12);
64
+ });
65
+
66
+ test("normalizes daily detailed history requests to 1day", async () => {
67
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
68
+
69
+ let requestArgs: Record<string, string | number | undefined> | null = null;
70
+ apiClient.getCloudHistory = async (_symbol, _exchange, params = {}) => {
71
+ requestArgs = params;
72
+ return {
73
+ status: "success",
74
+ data: [],
75
+ };
76
+ };
77
+
78
+ const provider = new GloomberbCloudProvider();
79
+ await provider.getDetailedPriceHistory(
80
+ "AAPL",
81
+ "NASDAQ",
82
+ new Date(2026, 0, 1, 0, 0, 0),
83
+ new Date(2026, 2, 27, 0, 0, 0),
84
+ "1d",
85
+ );
86
+
87
+ expect(requestArgs).toEqual({
88
+ interval: "1day",
89
+ startDate: "2026-01-01",
90
+ endDate: "2026-03-27",
91
+ });
92
+ });
93
+
94
+ test("normalizes sub-unit cloud quotes to their main currency", async () => {
95
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
96
+ apiClient.getCloudQuote = async () => ({
97
+ status: "success",
98
+ data: {
99
+ symbol: "IQE",
100
+ providerId: "gloomberb-cloud",
101
+ price: 23.1,
102
+ currency: "GBp",
103
+ change: -1.4,
104
+ changePercent: -5.71,
105
+ previousClose: 24.5,
106
+ lastUpdated: Date.now(),
107
+ dataSource: "delayed",
108
+ },
109
+ });
110
+
111
+ const provider = new GloomberbCloudProvider();
112
+ const quote = await provider.getQuote("IQE", "LSE");
113
+
114
+ expect(quote.currency).toBe("GBP");
115
+ expect(quote.price).toBeCloseTo(0.231, 8);
116
+ expect(quote.change).toBeCloseTo(-0.014, 8);
117
+ expect(quote.previousClose).toBeCloseTo(0.245, 8);
118
+ });
119
+
120
+ test("normalizes sub-unit cloud history using the response currency metadata", async () => {
121
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
122
+ apiClient.getCloudHistory = async () => ({
123
+ status: "success",
124
+ providerMeta: {
125
+ currency: "GBp",
126
+ },
127
+ data: [{
128
+ date: "2026-03-27 10:15:00",
129
+ open: 22.55,
130
+ high: 23.4,
131
+ low: 22.1,
132
+ close: 23.1,
133
+ }],
134
+ });
135
+
136
+ const provider = new GloomberbCloudProvider();
137
+ const history = await provider.getPriceHistory("IQE", "LSE", "1Y");
138
+
139
+ expect(history[0]?.open).toBeCloseTo(0.2255, 8);
140
+ expect(history[0]?.high).toBeCloseTo(0.234, 8);
141
+ expect(history[0]?.low).toBeCloseTo(0.221, 8);
142
+ expect(history[0]?.close).toBeCloseTo(0.231, 8);
143
+ });
144
+
145
+ test("preserves original target context when streaming quotes", () => {
146
+ let unsubscribeCalled = false;
147
+ const seenQuotes: Array<{ price: number; currency: string }> = [];
148
+ apiClient.subscribeQuotes = (_targets, onQuote) => {
149
+ onQuote(
150
+ { symbol: "AAPL", exchange: "NASDAQ" },
151
+ {
152
+ symbol: "AAPL",
153
+ providerId: "gloomberb-cloud",
154
+ price: 23.1,
155
+ currency: "GBp",
156
+ change: -1.4,
157
+ changePercent: 0.5,
158
+ lastUpdated: Date.now(),
159
+ dataSource: "live",
160
+ },
161
+ );
162
+ return () => {
163
+ unsubscribeCalled = true;
164
+ };
165
+ };
166
+
167
+ const provider = new GloomberbCloudProvider();
168
+ const seenTargets: Array<{ brokerId?: string; brokerInstanceId?: string }> = [];
169
+ const unsubscribe = provider.subscribeQuotes([{
170
+ symbol: "AAPL",
171
+ exchange: "NASDAQ",
172
+ context: {
173
+ brokerId: "ibkr",
174
+ brokerInstanceId: "ibkr-live",
175
+ },
176
+ }], (target, quote) => {
177
+ seenTargets.push({
178
+ brokerId: target.context?.brokerId,
179
+ brokerInstanceId: target.context?.brokerInstanceId,
180
+ });
181
+ seenQuotes.push({
182
+ price: quote.price,
183
+ currency: quote.currency,
184
+ });
185
+ });
186
+
187
+ expect(seenTargets).toEqual([{
188
+ brokerId: "ibkr",
189
+ brokerInstanceId: "ibkr-live",
190
+ }]);
191
+ expect(seenQuotes).toEqual([{
192
+ price: 0.231,
193
+ currency: "GBP",
194
+ }]);
195
+
196
+ unsubscribe();
197
+ expect(unsubscribeCalled).toBe(true);
198
+ });
199
+ });
@@ -0,0 +1,326 @@
1
+ import type { TimeRange } from "../components/chart/chart-types";
2
+ import type { DataProvider, MarketDataRequestContext, NewsItem, QuoteSubscriptionTarget, SearchRequestContext, SecFilingItem } from "../types/data-provider";
3
+ import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
4
+ import type { InstrumentSearchResult } from "../types/instrument";
5
+ import {
6
+ apiClient,
7
+ type CloudCompanyProfile,
8
+ type CloudFundamentals,
9
+ type CloudMarketResponse,
10
+ type CloudPricePointPayload,
11
+ type CloudQuotePayload,
12
+ } from "../utils/api-client";
13
+ import { normalizePriceValueByDivisor, resolveCurrencyUnit } from "../utils/currency-units";
14
+ import { createProviderMiss } from "./provider-errors";
15
+
16
+ const providerId = "gloomberb-cloud" as const;
17
+ const CLOUD_PROVIDER_MISS_PATTERNS = [
18
+ /data not found/i,
19
+ /symbol.*missing or invalid/i,
20
+ /figi.*missing or invalid/i,
21
+ /error in the query/i,
22
+ ];
23
+
24
+ function isCloudProviderMiss(error: unknown): boolean {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ return CLOUD_PROVIDER_MISS_PATTERNS.some((pattern) => pattern.test(message));
27
+ }
28
+
29
+ async function withCloudFallback<T>(load: () => Promise<T>, message: string): Promise<T> {
30
+ try {
31
+ return await load();
32
+ } catch (error) {
33
+ if (isCloudProviderMiss(error)) {
34
+ throw createProviderMiss(message);
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ function mapQuote(quote: CloudQuotePayload): Quote {
41
+ const { currency, divisor } = resolveCurrencyUnit(quote.currency);
42
+ return {
43
+ ...quote,
44
+ currency: currency || quote.currency,
45
+ price: normalizePriceValueByDivisor(quote.price, divisor) ?? quote.price,
46
+ change: normalizePriceValueByDivisor(quote.change, divisor) ?? quote.change,
47
+ previousClose: normalizePriceValueByDivisor(quote.previousClose, divisor),
48
+ high52w: normalizePriceValueByDivisor(quote.high52w, divisor),
49
+ low52w: normalizePriceValueByDivisor(quote.low52w, divisor),
50
+ bid: normalizePriceValueByDivisor(quote.bid, divisor),
51
+ ask: normalizePriceValueByDivisor(quote.ask, divisor),
52
+ open: normalizePriceValueByDivisor(quote.open, divisor),
53
+ high: normalizePriceValueByDivisor(quote.high, divisor),
54
+ low: normalizePriceValueByDivisor(quote.low, divisor),
55
+ mark: normalizePriceValueByDivisor(quote.mark, divisor),
56
+ preMarketPrice: normalizePriceValueByDivisor(quote.preMarketPrice, divisor),
57
+ preMarketChange: normalizePriceValueByDivisor(quote.preMarketChange, divisor),
58
+ postMarketPrice: normalizePriceValueByDivisor(quote.postMarketPrice, divisor),
59
+ postMarketChange: normalizePriceValueByDivisor(quote.postMarketChange, divisor),
60
+ providerId,
61
+ };
62
+ }
63
+
64
+ function mapPricePoint(point: CloudPricePointPayload, divisor = 1): PricePoint {
65
+ return {
66
+ date: new Date(point.date),
67
+ open: normalizePriceValueByDivisor(point.open, divisor),
68
+ high: normalizePriceValueByDivisor(point.high, divisor),
69
+ low: normalizePriceValueByDivisor(point.low, divisor),
70
+ close: normalizePriceValueByDivisor(point.close, divisor) ?? point.close,
71
+ volume: point.volume,
72
+ };
73
+ }
74
+
75
+ function quoteTargetKey(symbol: string, exchange?: string): string {
76
+ const normalizedSymbol = symbol.trim().toUpperCase();
77
+ const normalizedExchange = (exchange ?? "").trim().toUpperCase();
78
+ return normalizedExchange ? `${normalizedSymbol}:${normalizedExchange}` : normalizedSymbol;
79
+ }
80
+
81
+ function toCloudInterval(interval: string): string {
82
+ switch (interval) {
83
+ case "1m":
84
+ return "1min";
85
+ case "5m":
86
+ return "5min";
87
+ case "15m":
88
+ return "15min";
89
+ case "30m":
90
+ return "30min";
91
+ case "45m":
92
+ return "45min";
93
+ case "1d":
94
+ return "1day";
95
+ case "1w":
96
+ return "1week";
97
+ case "1mo":
98
+ return "1month";
99
+ default:
100
+ return interval;
101
+ }
102
+ }
103
+
104
+ function padTimePart(value: number): string {
105
+ return String(value).padStart(2, "0");
106
+ }
107
+
108
+ function formatCloudDateTime(date: Date, includeTime: boolean): string {
109
+ const year = date.getFullYear();
110
+ const month = padTimePart(date.getMonth() + 1);
111
+ const day = padTimePart(date.getDate());
112
+ if (!includeTime) {
113
+ return `${year}-${month}-${day}`;
114
+ }
115
+ const hours = padTimePart(date.getHours());
116
+ const minutes = padTimePart(date.getMinutes());
117
+ const seconds = padTimePart(date.getSeconds());
118
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
119
+ }
120
+
121
+ function toHistoryRequest(range: TimeRange): {
122
+ interval: string;
123
+ outputsize: number;
124
+ rangeKey: TimeRange;
125
+ } {
126
+ switch (range) {
127
+ case "1W":
128
+ return { interval: "1h", outputsize: 7 * 24, rangeKey: range };
129
+ case "1M":
130
+ return { interval: "1day", outputsize: 31, rangeKey: range };
131
+ case "3M":
132
+ return { interval: "1day", outputsize: 93, rangeKey: range };
133
+ case "6M":
134
+ return { interval: "1day", outputsize: 186, rangeKey: range };
135
+ case "1Y":
136
+ return { interval: "1day", outputsize: 366, rangeKey: range };
137
+ case "5Y":
138
+ return { interval: "1week", outputsize: 261, rangeKey: range };
139
+ case "ALL":
140
+ return { interval: "1month", outputsize: 240, rangeKey: range };
141
+ default:
142
+ return { interval: "1day", outputsize: 366, rangeKey: range };
143
+ }
144
+ }
145
+
146
+ async function requireVerifiedSession(): Promise<void> {
147
+ const user = await apiClient.ensureVerifiedSession();
148
+ if (!user) {
149
+ throw createProviderMiss("Gloomberb Cloud requires signup and email verification");
150
+ }
151
+ }
152
+
153
+ function isEmptyCloudStatus(status: CloudMarketResponse<unknown>["status"]): boolean {
154
+ return status === "empty" || status === "unsupported";
155
+ }
156
+
157
+ function unwrapRequiredCloudResponse<T>(response: CloudMarketResponse<T>, message: string): T {
158
+ if ((response.status === "success" || response.status === "partial") && response.data != null) {
159
+ return response.data;
160
+ }
161
+ if (isEmptyCloudStatus(response.status)) {
162
+ throw createProviderMiss(response.reasonCode ?? message);
163
+ }
164
+ throw new Error(response.reasonCode ?? message);
165
+ }
166
+
167
+ function unwrapOptionalCloudResponse<T>(response: CloudMarketResponse<T>): T | null {
168
+ if ((response.status === "success" || response.status === "partial") && response.data != null) {
169
+ return response.data;
170
+ }
171
+ if (isEmptyCloudStatus(response.status)) {
172
+ return null;
173
+ }
174
+ throw new Error(response.reasonCode ?? "Cloud data request failed");
175
+ }
176
+
177
+ export class GloomberbCloudProvider implements DataProvider {
178
+ readonly id = providerId;
179
+ readonly name = "Gloomberb Cloud";
180
+ readonly priority = 100;
181
+
182
+ async canProvide(): Promise<boolean> {
183
+ return !!(await apiClient.ensureVerifiedSession());
184
+ }
185
+
186
+ async getTickerFinancials(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<TickerFinancials> {
187
+ await requireVerifiedSession();
188
+ return withCloudFallback(async () => {
189
+ const [quoteResponse, profileResponse, fundamentalsResponse, statementsResponse] = await Promise.all([
190
+ apiClient.getCloudQuote(ticker, exchange),
191
+ apiClient.getCloudProfile(ticker, exchange),
192
+ apiClient.getCloudFundamentals(ticker, exchange),
193
+ apiClient.getCloudStatements(ticker, exchange, "both"),
194
+ ]);
195
+ const quote = mapQuote(unwrapRequiredCloudResponse(quoteResponse, `Cloud quote is unavailable for ${ticker}`));
196
+ const profile = unwrapOptionalCloudResponse(profileResponse) as CloudCompanyProfile | null;
197
+ const fundamentals = unwrapOptionalCloudResponse(fundamentalsResponse) as CloudFundamentals | null;
198
+ const statements = unwrapOptionalCloudResponse(statementsResponse);
199
+ return {
200
+ quote,
201
+ profile: profile ?? undefined,
202
+ fundamentals: fundamentals ?? undefined,
203
+ annualStatements: statements?.annualStatements ?? [],
204
+ quarterlyStatements: statements?.quarterlyStatements ?? [],
205
+ priceHistory: [],
206
+ };
207
+ }, `Cloud financials are unavailable for ${ticker}`);
208
+ }
209
+
210
+ async getQuote(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<Quote> {
211
+ await requireVerifiedSession();
212
+ return withCloudFallback(
213
+ async () => mapQuote(unwrapRequiredCloudResponse(
214
+ await apiClient.getCloudQuote(ticker, exchange),
215
+ `Cloud quotes are unavailable for ${ticker}`,
216
+ )),
217
+ `Cloud quotes are unavailable for ${ticker}`,
218
+ );
219
+ }
220
+
221
+ async getExchangeRate(fromCurrency: string): Promise<number> {
222
+ await requireVerifiedSession();
223
+ const response = await apiClient.getCloudExchangeRate(fromCurrency);
224
+ return unwrapRequiredCloudResponse(response, `Cloud exchange rate is unavailable for ${fromCurrency}`).rate;
225
+ }
226
+
227
+ async search(query: string, _context?: SearchRequestContext): Promise<InstrumentSearchResult[]> {
228
+ await requireVerifiedSession();
229
+ return withCloudFallback(
230
+ () => apiClient.searchInstruments(query, 10),
231
+ "Cloud search is unavailable",
232
+ );
233
+ }
234
+
235
+ async getNews(_ticker: string, _count?: number, _exchange?: string, _context?: MarketDataRequestContext): Promise<NewsItem[]> {
236
+ throw createProviderMiss("Cloud news is not available");
237
+ }
238
+
239
+ async getSecFilings(_ticker: string, _count?: number, _exchange?: string, _context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
240
+ throw createProviderMiss("Cloud SEC filings are not available");
241
+ }
242
+
243
+ async getSecFilingContent(_filing: SecFilingItem): Promise<string | null> {
244
+ throw createProviderMiss("Cloud filing content is not available");
245
+ }
246
+
247
+ async getArticleSummary(_url: string): Promise<string | null> {
248
+ throw createProviderMiss("Cloud article summaries are not available");
249
+ }
250
+
251
+ async getPriceHistory(ticker: string, exchange: string, range: TimeRange, _context?: MarketDataRequestContext): Promise<PricePoint[]> {
252
+ await requireVerifiedSession();
253
+ const request = toHistoryRequest(range);
254
+ const response = await withCloudFallback(
255
+ () => apiClient.getCloudHistory(ticker, exchange, request),
256
+ `Cloud chart data is unavailable for ${ticker}`,
257
+ );
258
+ const { divisor } = resolveCurrencyUnit(response.providerMeta?.currency);
259
+ return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
260
+ .map((point) => mapPricePoint(point, divisor));
261
+ }
262
+
263
+ async getDetailedPriceHistory(
264
+ ticker: string,
265
+ exchange: string,
266
+ startDate: Date,
267
+ endDate: Date,
268
+ barSize: string,
269
+ _context?: MarketDataRequestContext,
270
+ ): Promise<PricePoint[]> {
271
+ await requireVerifiedSession();
272
+ const interval = toCloudInterval(barSize);
273
+ const includeTime = /(min|h)$/i.test(interval);
274
+ const response = await withCloudFallback(
275
+ () => apiClient.getCloudHistory(ticker, exchange, {
276
+ interval,
277
+ startDate: formatCloudDateTime(startDate, includeTime),
278
+ endDate: formatCloudDateTime(endDate, includeTime),
279
+ }),
280
+ `Cloud detailed chart history is unavailable for ${ticker}`,
281
+ );
282
+ const { divisor } = resolveCurrencyUnit(response.providerMeta?.currency);
283
+ return unwrapRequiredCloudResponse(response, `Cloud detailed chart history is unavailable for ${ticker}`)
284
+ .map((point) => mapPricePoint(point, divisor));
285
+ }
286
+
287
+ async getOptionsChain(_ticker: string, _exchange?: string, _expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
288
+ throw createProviderMiss("Cloud options chains are not available");
289
+ }
290
+
291
+ subscribeQuotes(
292
+ targets: QuoteSubscriptionTarget[],
293
+ onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
294
+ ): () => void {
295
+ void apiClient.ensureVerifiedSession().catch(() => {});
296
+ const targetMap = new Map<string, QuoteSubscriptionTarget[]>();
297
+ for (const target of targets) {
298
+ const key = quoteTargetKey(target.symbol, target.exchange);
299
+ const matches = targetMap.get(key) ?? [];
300
+ matches.push(target);
301
+ targetMap.set(key, matches);
302
+ }
303
+
304
+ return apiClient.subscribeQuotes(
305
+ targets.map((target) => ({
306
+ symbol: target.symbol,
307
+ exchange: target.exchange,
308
+ })),
309
+ (target, quote) => {
310
+ const key = quoteTargetKey(target.symbol, target.exchange);
311
+ const matches = targetMap.get(key) ?? [{
312
+ symbol: target.symbol,
313
+ exchange: target.exchange,
314
+ }];
315
+ const mappedQuote = mapQuote(quote);
316
+ for (const match of matches) {
317
+ onQuote(match, mappedQuote);
318
+ }
319
+ },
320
+ );
321
+ }
322
+ }
323
+
324
+ export function createGloomberbCloudProvider(): DataProvider {
325
+ return new GloomberbCloudProvider();
326
+ }
@@ -0,0 +1,19 @@
1
+ const PROVIDER_MISS_BRAND = Symbol("provider-miss");
2
+
3
+ export class ProviderMissError extends Error {
4
+ readonly [PROVIDER_MISS_BRAND] = true;
5
+
6
+ constructor(message = "Provider could not satisfy request") {
7
+ super(message);
8
+ this.name = "ProviderMissError";
9
+ }
10
+ }
11
+
12
+ export function createProviderMiss(message?: string): ProviderMissError {
13
+ return new ProviderMissError(message);
14
+ }
15
+
16
+ export function isProviderMiss(error: unknown): error is ProviderMissError {
17
+ return error instanceof ProviderMissError
18
+ || (!!error && typeof error === "object" && PROVIDER_MISS_BRAND in error);
19
+ }