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,578 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useReducer, type ReactElement } from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import type { ScrollBoxRenderable } from "@opentui/core";
5
+ import {
6
+ AppContext,
7
+ appReducer,
8
+ createInitialState,
9
+ PaneInstanceProvider,
10
+ type AppAction,
11
+ } from "../../state/app-context";
12
+ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
13
+ import { createTestDataProvider } from "../../test-support/data-provider";
14
+ import {
15
+ cloneLayout,
16
+ createDefaultConfig,
17
+ type AppConfig,
18
+ type BrokerInstanceConfig,
19
+ } from "../../types/config";
20
+ import type { DataProvider } from "../../types/data-provider";
21
+ import type { TickerFinancials } from "../../types/financials";
22
+ import type { DetailTabDef } from "../../types/plugin";
23
+ import type { TickerRecord } from "../../types/ticker";
24
+ import type { PluginRegistry } from "../registry";
25
+ import { setSharedRegistryForTests } from "../registry";
26
+ import { resetOptionsAvailabilityCache } from "./options-availability";
27
+ import { FinancialsTab, tickerDetailPlugin } from "./ticker-detail";
28
+ import { isUsEquityTicker } from "../../utils/sec";
29
+
30
+ const TEST_PANE_ID = "ticker-detail:test";
31
+
32
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
33
+ let harnessDispatch: React.Dispatch<AppAction> | null = null;
34
+ let sharedCoordinator: MarketDataCoordinator | null = null;
35
+
36
+ const DetailPane = tickerDetailPlugin.panes![0]!.component as (props: {
37
+ paneId: string;
38
+ paneType: string;
39
+ focused: boolean;
40
+ width: number;
41
+ height: number;
42
+ }) => ReactElement;
43
+
44
+ function makeTicker(
45
+ symbol: string,
46
+ name = symbol,
47
+ overrides: Partial<TickerRecord["metadata"]> = {},
48
+ ): TickerRecord {
49
+ return {
50
+ metadata: {
51
+ ticker: symbol,
52
+ exchange: "NASDAQ",
53
+ currency: "USD",
54
+ name,
55
+ portfolios: [],
56
+ watchlists: [],
57
+ positions: [],
58
+ custom: {},
59
+ tags: [],
60
+ ...overrides,
61
+ },
62
+ };
63
+ }
64
+
65
+ function makeFinancials(overrides: Partial<TickerFinancials> = {}): TickerFinancials {
66
+ return {
67
+ annualStatements: [],
68
+ quarterlyStatements: [],
69
+ priceHistory: [],
70
+ ...overrides,
71
+ };
72
+ }
73
+
74
+ function createFinancialsTabHarness() {
75
+ const config = createDefaultConfig("/tmp/gloomberb-test");
76
+ config.layout.instances = config.layout.instances.map((instance) => (
77
+ instance.instanceId === "ticker-detail:main"
78
+ ? { ...instance, binding: { kind: "fixed" as const, symbol: "2337" } }
79
+ : instance
80
+ ));
81
+
82
+ const state = createInitialState(config);
83
+ const ticker = makeTicker("2337", "Mock Co");
84
+ const financials: TickerFinancials = {
85
+ annualStatements: [
86
+ { date: "2021-12-31" },
87
+ { date: "2022-12-31", totalRevenue: 43.49e9, operatingIncome: 9.37e9, eps: 4.68 },
88
+ { date: "2023-12-31", totalRevenue: 27.62e9, operatingIncome: -2.4e9, eps: -0.92 },
89
+ { date: "2024-12-31", totalRevenue: 25.88e9, operatingIncome: -3.92e9, eps: -1.73 },
90
+ { date: "2025-12-31", totalRevenue: 28.88e9, operatingIncome: -3.7e9, eps: -1.77 },
91
+ ],
92
+ quarterlyStatements: [
93
+ { date: "2025-03-31", totalRevenue: 6e9, operatingIncome: -1e9, eps: -0.4 },
94
+ { date: "2025-06-30", totalRevenue: 6.5e9, operatingIncome: -1.1e9, eps: -0.42 },
95
+ { date: "2025-09-30", totalRevenue: 7e9, operatingIncome: -1.2e9, eps: -0.45 },
96
+ { date: "2025-12-31", totalRevenue: 7.08e9, operatingIncome: -1.01e9, eps: -0.5 },
97
+ ],
98
+ priceHistory: [],
99
+ };
100
+
101
+ state.tickers = new Map([["2337", ticker]]);
102
+ state.financials = new Map([["2337", financials]]);
103
+
104
+ return (
105
+ <AppContext value={{ state, dispatch: () => {} }}>
106
+ <PaneInstanceProvider paneId="ticker-detail:main">
107
+ <FinancialsTab
108
+ focused
109
+ headerScrollId="financials-header-scroll"
110
+ bodyScrollId="financials-body-scroll"
111
+ />
112
+ </PaneInstanceProvider>
113
+ </AppContext>
114
+ );
115
+ }
116
+
117
+ function createProvider(hasOptions: boolean): DataProvider {
118
+ return createTestDataProvider({
119
+ getExchangeRate: async (currency) => (currency === "EUR" ? 1.1 : 1),
120
+ getOptionsChain: async (ticker) => ({
121
+ underlyingSymbol: ticker,
122
+ expirationDates: hasOptions ? [1_717_113_600] : [],
123
+ calls: [],
124
+ puts: [],
125
+ }),
126
+ });
127
+ }
128
+
129
+ function setOptionsProvider(provider: DataProvider | undefined): void {
130
+ sharedCoordinator = provider ? new MarketDataCoordinator(provider) : null;
131
+ setSharedMarketDataCoordinator(sharedCoordinator);
132
+ }
133
+
134
+ function makeRegistry(): PluginRegistry {
135
+ const stubTab = (_props: { width: number; height: number; focused: boolean; onCapture: (capturing: boolean) => void }) => (
136
+ <text>stub</text>
137
+ );
138
+ const detailTabs = new Map<string, DetailTabDef>([
139
+ ["ibkr-trade", { id: "ibkr-trade", name: "Trade", order: 25, component: stubTab }],
140
+ ["options", { id: "options", name: "Options", order: 35, component: stubTab }],
141
+ ["sec", { id: "sec", name: "SEC", order: 45, component: stubTab, isVisible: ({ ticker }) => isUsEquityTicker(ticker) }],
142
+ ["ai-chat", { id: "ai-chat", name: "Ask AI", order: 60, component: stubTab }],
143
+ ]);
144
+ return { detailTabs } as unknown as PluginRegistry;
145
+ }
146
+
147
+ function createGatewayInstance(id = "ibkr-paper"): BrokerInstanceConfig {
148
+ return {
149
+ id,
150
+ brokerType: "ibkr",
151
+ label: "Paper",
152
+ connectionMode: "gateway",
153
+ config: { connectionMode: "gateway", gateway: { host: "127.0.0.1", port: 4002, clientId: 1 } },
154
+ enabled: true,
155
+ };
156
+ }
157
+
158
+ function createDetailConfig(symbol: string, brokerInstances: BrokerInstanceConfig[] = []): AppConfig {
159
+ const config = createDefaultConfig("/tmp/gloomberb-test");
160
+ const layout = {
161
+ dockRoot: { kind: "pane" as const, instanceId: TEST_PANE_ID },
162
+ instances: [{
163
+ instanceId: TEST_PANE_ID,
164
+ paneId: "ticker-detail",
165
+ binding: { kind: "fixed" as const, symbol },
166
+ }],
167
+ floating: [],
168
+ };
169
+
170
+ return {
171
+ ...config,
172
+ brokerInstances,
173
+ layout,
174
+ layouts: [{ name: "Default", layout: cloneLayout(layout) }],
175
+ };
176
+ }
177
+
178
+ function createDetailState(
179
+ config: AppConfig,
180
+ ticker: TickerRecord,
181
+ financials: TickerFinancials | null,
182
+ activeTabId = "overview",
183
+ exchangeRates?: Map<string, number>,
184
+ ) {
185
+ const state = createInitialState(config);
186
+ state.focusedPaneId = TEST_PANE_ID;
187
+ state.tickers = new Map([[ticker.metadata.ticker, ticker]]);
188
+ state.financials = financials ? new Map([[ticker.metadata.ticker, financials]]) : new Map();
189
+ state.paneState[TEST_PANE_ID] = { activeTabId };
190
+ if (exchangeRates) {
191
+ state.exchangeRates = exchangeRates;
192
+ }
193
+ return state;
194
+ }
195
+
196
+ function DetailHarness({
197
+ config,
198
+ ticker,
199
+ financials,
200
+ activeTabId = "overview",
201
+ exchangeRates,
202
+ width = 90,
203
+ height = 24,
204
+ }: {
205
+ config: AppConfig;
206
+ ticker: TickerRecord;
207
+ financials: TickerFinancials | null;
208
+ activeTabId?: string;
209
+ exchangeRates?: Map<string, number>;
210
+ width?: number;
211
+ height?: number;
212
+ }) {
213
+ const initialState = createDetailState(config, ticker, financials, activeTabId, exchangeRates);
214
+ const [state, dispatch] = useReducer(appReducer, initialState);
215
+ harnessDispatch = dispatch;
216
+
217
+ return (
218
+ <AppContext value={{ state, dispatch }}>
219
+ <PaneInstanceProvider paneId={TEST_PANE_ID}>
220
+ <text>{`active:${state.paneState[TEST_PANE_ID]?.activeTabId ?? ""}`}</text>
221
+ <DetailPane
222
+ paneId={TEST_PANE_ID}
223
+ paneType="ticker-detail"
224
+ focused
225
+ width={width}
226
+ height={height}
227
+ />
228
+ </PaneInstanceProvider>
229
+ </AppContext>
230
+ );
231
+ }
232
+
233
+ async function flushFrame() {
234
+ await act(async () => {
235
+ await testSetup!.renderOnce();
236
+ });
237
+ }
238
+
239
+ function getFinancialsScroll(id: string): ScrollBoxRenderable {
240
+ const renderable = testSetup!.renderer.root.findDescendantById(id) as ScrollBoxRenderable | undefined;
241
+ expect(renderable).toBeDefined();
242
+ return renderable!;
243
+ }
244
+
245
+ afterEach(() => {
246
+ if (testSetup) {
247
+ testSetup.renderer.destroy();
248
+ testSetup = undefined;
249
+ }
250
+ harnessDispatch = null;
251
+ resetOptionsAvailabilityCache();
252
+ setSharedRegistryForTests(undefined);
253
+ setOptionsProvider(undefined);
254
+ });
255
+
256
+ describe("FinancialsTab", () => {
257
+ test("keeps negative-value rows aligned with the annual columns", async () => {
258
+ testSetup = await testRender(createFinancialsTabHarness(), {
259
+ width: 140,
260
+ height: 20,
261
+ });
262
+
263
+ await testSetup.renderOnce();
264
+
265
+ const frame = testSetup.captureCharFrame();
266
+ const revenueLine = frame.split("\n").find((line) => line.includes("Revenue (B)"));
267
+ const operatingIncomeLine = frame.split("\n").find((line) => line.includes("Operating Inc (B)"));
268
+
269
+ expect(revenueLine).toBeDefined();
270
+ expect(operatingIncomeLine).toBeDefined();
271
+
272
+ expect(operatingIncomeLine!.indexOf("-4.31")).toBe(revenueLine!.indexOf("26.58"));
273
+ expect(operatingIncomeLine!.indexOf("-3.70")).toBe(revenueLine!.indexOf("28.88"));
274
+ expect(operatingIncomeLine!.indexOf("-3.92")).toBe(revenueLine!.indexOf("25.88"));
275
+ expect(operatingIncomeLine!.indexOf("-2.40")).toBe(revenueLine!.indexOf("27.62"));
276
+ expect(operatingIncomeLine!.indexOf("—")).toBe(revenueLine!.lastIndexOf("—"));
277
+ });
278
+
279
+ test("centers the annual headers over the value columns", async () => {
280
+ testSetup = await testRender(createFinancialsTabHarness(), {
281
+ width: 140,
282
+ height: 20,
283
+ });
284
+
285
+ await flushFrame();
286
+
287
+ const frame = testSetup.captureCharFrame();
288
+ const headerLine = frame.split("\n").find((line) => line.includes("2025-12"));
289
+ const revenueLine = frame.split("\n").find((line) => line.includes("Revenue (B)"));
290
+
291
+ expect(headerLine).toBeDefined();
292
+ expect(revenueLine).toBeDefined();
293
+
294
+ expect(headerLine!.indexOf("2025-12")).toBe(revenueLine!.indexOf("28.88") - 1);
295
+ expect(headerLine!.indexOf("2024-12")).toBe(revenueLine!.indexOf("25.88") - 1);
296
+ expect(headerLine!.indexOf("2023-12")).toBe(revenueLine!.indexOf("27.62") - 1);
297
+ });
298
+
299
+ test("allows the financial statements table to scroll horizontally", async () => {
300
+ testSetup = await testRender(createFinancialsTabHarness(), {
301
+ width: 56,
302
+ height: 18,
303
+ });
304
+
305
+ await flushFrame();
306
+
307
+ let frame = testSetup.captureCharFrame();
308
+ expect(frame).toContain("TTM");
309
+ expect(frame).not.toContain("2021-12");
310
+ expect(frame).not.toContain("43.49");
311
+
312
+ const headerScroll = getFinancialsScroll("financials-header-scroll");
313
+ const bodyScroll = getFinancialsScroll("financials-body-scroll");
314
+ expect(bodyScroll.scrollWidth).toBeGreaterThan(bodyScroll.viewport.width);
315
+
316
+ await act(async () => {
317
+ bodyScroll.scrollTo({ x: bodyScroll.scrollWidth, y: 0 });
318
+ headerScroll.scrollTo({ x: bodyScroll.scrollLeft, y: 0 });
319
+ await Promise.resolve();
320
+ });
321
+
322
+ await flushFrame();
323
+ await flushFrame();
324
+
325
+ frame = testSetup.captureCharFrame();
326
+ expect(frame).toContain("2021-12");
327
+ expect(frame).toContain("43.49");
328
+ });
329
+
330
+ test("hides the vertical scrollbar when the statements fit in view", async () => {
331
+ testSetup = await testRender(createFinancialsTabHarness(), {
332
+ width: 140,
333
+ height: 24,
334
+ });
335
+
336
+ await flushFrame();
337
+ await flushFrame();
338
+
339
+ const bodyScroll = getFinancialsScroll("financials-body-scroll");
340
+ expect(bodyScroll.scrollHeight).toBeLessThanOrEqual(bodyScroll.viewport.height);
341
+ expect(bodyScroll.verticalScrollBar.visible).toBe(false);
342
+ });
343
+ });
344
+
345
+ describe("TickerDetailPane", () => {
346
+ test("defaults new detail panes to floating", () => {
347
+ const paneDef = tickerDetailPlugin.panes?.find((entry) => entry.id === "ticker-detail");
348
+
349
+ expect(paneDef?.defaultMode).toBe("floating");
350
+ });
351
+
352
+ test("shows only applicable tabs when no gateway, statements, or options are available", async () => {
353
+ setSharedRegistryForTests(makeRegistry());
354
+ setOptionsProvider(createProvider(false));
355
+
356
+ testSetup = await testRender(
357
+ <DetailHarness
358
+ config={createDetailConfig("AAPL")}
359
+ ticker={makeTicker("AAPL")}
360
+ financials={null}
361
+ />,
362
+ { width: 90, height: 24 },
363
+ );
364
+
365
+ await flushFrame();
366
+ await flushFrame();
367
+
368
+ const frame = testSetup.captureCharFrame();
369
+ expect(frame).toContain("Overview");
370
+ expect(frame).toContain("Chart");
371
+ expect(frame).toContain("Ask AI");
372
+ expect(frame).not.toContain("Financials");
373
+ expect(frame).not.toContain("Trade");
374
+ expect(frame).not.toContain("Options");
375
+ });
376
+
377
+ test("shows Financials when statement data exists", async () => {
378
+ setSharedRegistryForTests(makeRegistry());
379
+ setOptionsProvider(createProvider(false));
380
+
381
+ testSetup = await testRender(
382
+ <DetailHarness
383
+ config={createDetailConfig("AAPL")}
384
+ ticker={makeTicker("AAPL")}
385
+ financials={makeFinancials({
386
+ annualStatements: [{ date: "2024-12-31", totalRevenue: 1_000 }],
387
+ })}
388
+ />,
389
+ { width: 90, height: 24 },
390
+ );
391
+
392
+ await flushFrame();
393
+ const frame = testSetup.captureCharFrame();
394
+ expect(frame).toContain("Financials");
395
+ });
396
+
397
+ test("shows Trade when an IBKR gateway profile exists", async () => {
398
+ setSharedRegistryForTests(makeRegistry());
399
+ setOptionsProvider(createProvider(false));
400
+
401
+ testSetup = await testRender(
402
+ <DetailHarness
403
+ config={createDetailConfig("AAPL", [createGatewayInstance()])}
404
+ ticker={makeTicker("AAPL")}
405
+ financials={null}
406
+ />,
407
+ { width: 90, height: 24 },
408
+ );
409
+
410
+ await flushFrame();
411
+ const frame = testSetup.captureCharFrame();
412
+ expect(frame).toContain("Trade");
413
+ });
414
+
415
+ test("shows Options after the preflight confirms a chain exists", async () => {
416
+ setSharedRegistryForTests(makeRegistry());
417
+ setOptionsProvider(createProvider(true));
418
+
419
+ testSetup = await testRender(
420
+ <DetailHarness
421
+ config={createDetailConfig("AAPL")}
422
+ ticker={makeTicker("AAPL")}
423
+ financials={null}
424
+ />,
425
+ { width: 90, height: 24 },
426
+ );
427
+
428
+ await flushFrame();
429
+ await flushFrame();
430
+
431
+ const frame = testSetup.captureCharFrame();
432
+ expect(frame).toContain("Options");
433
+ });
434
+
435
+ test("shows SEC for US equities", async () => {
436
+ setSharedRegistryForTests(makeRegistry());
437
+ setOptionsProvider(createProvider(false));
438
+
439
+ testSetup = await testRender(
440
+ <DetailHarness
441
+ config={createDetailConfig("AAPL")}
442
+ ticker={makeTicker("AAPL")}
443
+ financials={null}
444
+ />,
445
+ { width: 90, height: 24 },
446
+ );
447
+
448
+ await flushFrame();
449
+ const frame = testSetup.captureCharFrame();
450
+ expect(frame).toContain("SEC");
451
+ });
452
+
453
+ test("hides SEC for non-US equities", async () => {
454
+ setSharedRegistryForTests(makeRegistry());
455
+ setOptionsProvider(createProvider(false));
456
+
457
+ testSetup = await testRender(
458
+ <DetailHarness
459
+ config={createDetailConfig("0700")}
460
+ ticker={makeTicker("0700", "Tencent", {
461
+ exchange: "HKEX",
462
+ currency: "HKD",
463
+ assetCategory: "STK",
464
+ })}
465
+ financials={null}
466
+ />,
467
+ { width: 90, height: 24 },
468
+ );
469
+
470
+ await flushFrame();
471
+ const frame = testSetup.captureCharFrame();
472
+ expect(frame).not.toContain("SEC");
473
+ });
474
+
475
+ test("renders the company description in Overview when profile data is available", async () => {
476
+ setSharedRegistryForTests(makeRegistry());
477
+ setOptionsProvider(createProvider(false));
478
+
479
+ testSetup = await testRender(
480
+ <DetailHarness
481
+ config={createDetailConfig("AAPL")}
482
+ ticker={makeTicker("AAPL")}
483
+ financials={makeFinancials({
484
+ profile: {
485
+ description: "Builds widgets for industrial customers.",
486
+ },
487
+ })}
488
+ />,
489
+ { width: 90, height: 24 },
490
+ );
491
+
492
+ await flushFrame();
493
+ const frame = testSetup.captureCharFrame();
494
+ expect(frame).toContain("Description");
495
+ expect(frame).toContain("Builds widgets for industrial customers.");
496
+ });
497
+
498
+ test("keeps quote prices native while converting market cap and position totals to base currency", async () => {
499
+ setSharedRegistryForTests(makeRegistry());
500
+ setOptionsProvider(createProvider(false));
501
+
502
+ testSetup = await testRender(
503
+ <DetailHarness
504
+ config={createDetailConfig("SAP")}
505
+ ticker={makeTicker("SAP", "SAP SE", {
506
+ exchange: "XETRA",
507
+ currency: "EUR",
508
+ positions: [{
509
+ portfolio: "main",
510
+ shares: 10,
511
+ avgCost: 100,
512
+ currency: "EUR",
513
+ broker: "manual",
514
+ markPrice: 125,
515
+ marketValue: 1250,
516
+ unrealizedPnl: 250,
517
+ }],
518
+ })}
519
+ financials={makeFinancials({
520
+ quote: {
521
+ symbol: "SAP",
522
+ price: 125,
523
+ currency: "EUR",
524
+ change: 5,
525
+ changePercent: 4.17,
526
+ marketCap: 2_000_000_000,
527
+ previousClose: 120,
528
+ name: "SAP SE",
529
+ lastUpdated: Date.now(),
530
+ marketState: "REGULAR",
531
+ },
532
+ })}
533
+ exchangeRates={new Map([["USD", 1], ["EUR", 1.1]])}
534
+ height={32}
535
+ />,
536
+ { width: 90, height: 32 },
537
+ );
538
+
539
+ await flushFrame();
540
+
541
+ const frame = testSetup.captureCharFrame();
542
+ expect(frame).toContain("€125.00");
543
+ expect(frame).toContain("2.2B USD");
544
+ expect(frame).toContain("@ €100.00");
545
+ expect(frame).toContain("= $1,100.00");
546
+ expect(frame).toContain("P&L: +$275.00");
547
+ expect(frame).toContain("Mark: €125.00");
548
+ expect(frame).toContain("Mkt Value: $1,375.00");
549
+ });
550
+
551
+ test("falls back to Overview when a hidden active tab becomes unavailable", async () => {
552
+ const gatewayConfig = createDetailConfig("AAPL", [createGatewayInstance()]);
553
+ const noGatewayConfig = createDetailConfig("AAPL");
554
+ setSharedRegistryForTests(makeRegistry());
555
+ setOptionsProvider(createProvider(false));
556
+
557
+ testSetup = await testRender(
558
+ <DetailHarness
559
+ config={gatewayConfig}
560
+ ticker={makeTicker("AAPL")}
561
+ financials={null}
562
+ activeTabId="ibkr-trade"
563
+ />,
564
+ { width: 90, height: 24 },
565
+ );
566
+
567
+ await flushFrame();
568
+ await act(async () => {
569
+ harnessDispatch!({ type: "SET_CONFIG", config: noGatewayConfig });
570
+ await Promise.resolve();
571
+ });
572
+ await flushFrame();
573
+
574
+ const frame = testSetup.captureCharFrame();
575
+ expect(frame).toContain("active:overview");
576
+ expect(frame).not.toContain("Trade");
577
+ });
578
+ });
@@ -0,0 +1,18 @@
1
+ import type { GloomPlugin } from "../../types/plugin";
2
+ import { YahooFinanceClient } from "../../sources/yahoo-finance";
3
+
4
+ class YahooPluginProvider extends YahooFinanceClient {
5
+ readonly priority = 1000;
6
+ }
7
+
8
+ export function createYahooProvider() {
9
+ return new YahooPluginProvider();
10
+ }
11
+
12
+ export const yahooPlugin: GloomPlugin = {
13
+ id: "yahoo",
14
+ name: "Yahoo Fallback",
15
+ version: "1.0.0",
16
+ description: "Built-in delayed fallback for quotes, fundamentals, charts, and unsupported cloud data.",
17
+ dataProvider: createYahooProvider(),
18
+ };
@@ -0,0 +1,37 @@
1
+ import type { TickerRecord } from "../types/ticker";
2
+ import type { TickerFinancials } from "../types/financials";
3
+ import type { AppConfig } from "../types/config";
4
+ import { debugLog } from "../utils/debug-log";
5
+
6
+ const busLog = debugLog.createLogger("event-bus");
7
+
8
+ /** All events plugins can subscribe to or emit */
9
+ export interface PluginEvents {
10
+ "ticker:selected": { symbol: string | null; previous: string | null };
11
+ "ticker:refreshed": { symbol: string; financials: TickerFinancials };
12
+ "ticker:added": { symbol: string; ticker: TickerRecord };
13
+ "ticker:removed": { symbol: string };
14
+ "config:changed": { config: AppConfig };
15
+ "plugin:registered": { pluginId: string };
16
+ "plugin:unregistered": { pluginId: string };
17
+ }
18
+
19
+ type EventHandler<T> = (payload: T) => void;
20
+
21
+ export class EventBus {
22
+ private listeners = new Map<string, Set<EventHandler<any>>>();
23
+
24
+ on<K extends keyof PluginEvents>(event: K, handler: EventHandler<PluginEvents[K]>): () => void {
25
+ if (!this.listeners.has(event)) this.listeners.set(event, new Set());
26
+ this.listeners.get(event)!.add(handler);
27
+ return () => { this.listeners.get(event)?.delete(handler); };
28
+ }
29
+
30
+ emit<K extends keyof PluginEvents>(event: K, payload: PluginEvents[K]): void {
31
+ for (const handler of this.listeners.get(event) ?? []) {
32
+ try { handler(payload); } catch (err) {
33
+ busLog.error(`Handler error on ${String(event)}: ${err}`);
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,35 @@
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
+
3
+ exports[`renders the compact trade tab layout 1`] = `
4
+ " Trade AMD · Advanced Micro Devices, Inc.
5
+ $199.80 -3.97 · Bid 199.70 · Ask 199.90 · Spd 0.20
6
+
7
+ Broker Account Net Liq
8
+ Paper Gateway DU123456 —
9
+
10
+
11
+ Next Submit order Ticket Standby Refresh r
12
+
13
+ Refreshed Paper.
14
+
15
+ ╭────────────────────────────────────────────────────────────────────────────────────╮
16
+ │ Ticket Click field / Enter │
17
+ │ Click a field or press Enter to edit. Shortcuts are shown inline. │
18
+ │ │
19
+ │ Profile Paper Ticker AMD STK Account DU123456 │
20
+ │ │
21
+ │ Side [b/v] BUY Type [t] MKT Qty [q] 1 TIF DAY │
22
+ │ │
23
+ │ Advanced Micro Devices, Inc. │
24
+ ╰────────────────────────────────────────────────────────────────────────────────────╯
25
+
26
+ ╭────────────────────────────────────────────────────────────────────────────────────╮
27
+ │ Preview Preview ready │
28
+ │ What-if: init 12.4k → 13.2k · commission $1.25 │
29
+ │ Fee $1.25 Init 12.4k → 13.2k │
30
+ │ Maint 10.1k → 10.7k Equity 58.4k → 57.1k │
31
+ │ Preview p Submit Order Enter │
32
+ ╰────────────────────────────────────────────────────────────────────────────────────╯
33
+
34
+ "
35
+ `;