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,234 @@
1
+ import { useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import type { NewsItem, SecFilingItem } from "../types/data-provider";
3
+ import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
4
+ import type { TickerRecord } from "../types/ticker";
5
+ import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "./request-types";
6
+ import { instrumentFromTicker } from "./request-types";
7
+ import {
8
+ getSharedMarketDataCoordinator,
9
+ resolveEntryValue,
10
+ } from "./coordinator";
11
+ import type { QueryEntry } from "./result-types";
12
+
13
+ function useCoordinatorVersion(): number {
14
+ const coordinator = getSharedMarketDataCoordinator();
15
+ return useSyncExternalStore(
16
+ coordinator?.subscribe.bind(coordinator) ?? (() => () => {}),
17
+ () => coordinator?.getVersion() ?? 0,
18
+ () => 0,
19
+ );
20
+ }
21
+
22
+ function useCoordinatorSelector<T>(selector: (coordinator: NonNullable<ReturnType<typeof getSharedMarketDataCoordinator>>) => T, fallback: T): T {
23
+ const coordinator = getSharedMarketDataCoordinator();
24
+ useCoordinatorVersion();
25
+ return coordinator ? selector(coordinator) : fallback;
26
+ }
27
+
28
+ function stableCurrencyList(currencies: Array<string | null | undefined>): string[] {
29
+ return [...new Set(
30
+ currencies
31
+ .map((currency) => currency?.trim().toUpperCase() ?? "")
32
+ .filter(Boolean),
33
+ )].sort((left, right) => left.localeCompare(right));
34
+ }
35
+
36
+ export function useTickerInstrument(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): InstrumentRef | null {
37
+ return useMemo(() => instrumentFromTicker(ticker, symbol ?? null), [symbol, ticker]);
38
+ }
39
+
40
+ export function useTickerFinancials(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): TickerFinancials | null {
41
+ const instrument = useTickerInstrument(symbol, ticker);
42
+ const financials = useCoordinatorSelector(
43
+ (coordinator) => (instrument ? coordinator.getTickerFinancialsSync(instrument) : null),
44
+ null,
45
+ );
46
+
47
+ useEffect(() => {
48
+ const coordinator = getSharedMarketDataCoordinator();
49
+ if (!coordinator || !instrument) return;
50
+ void coordinator.loadSnapshot(instrument);
51
+ }, [instrument?.brokerId, instrument?.brokerInstanceId, instrument?.exchange, instrument?.instrument?.conId, instrument?.symbol]);
52
+
53
+ return financials;
54
+ }
55
+
56
+ export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, TickerFinancials> {
57
+ return useCoordinatorSelector((coordinator) => {
58
+ const result = new Map<string, TickerFinancials>();
59
+ for (const ticker of tickers) {
60
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
61
+ if (!instrument) continue;
62
+ const financials = coordinator.getTickerFinancialsSync(instrument);
63
+ if (financials) {
64
+ result.set(ticker.metadata.ticker, financials);
65
+ }
66
+ }
67
+ return result;
68
+ }, new Map<string, TickerFinancials>());
69
+ }
70
+
71
+ export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
72
+ const instrument = useTickerInstrument(symbol, ticker);
73
+ const entry = useCoordinatorSelector(
74
+ (coordinator) => (instrument ? coordinator.getQuoteEntry(instrument) : null),
75
+ null,
76
+ );
77
+
78
+ useEffect(() => {
79
+ const coordinator = getSharedMarketDataCoordinator();
80
+ if (!coordinator || !instrument) return;
81
+ void coordinator.loadQuote(instrument);
82
+ }, [instrument?.brokerId, instrument?.brokerInstanceId, instrument?.exchange, instrument?.instrument?.conId, instrument?.symbol]);
83
+
84
+ return entry;
85
+ }
86
+
87
+ export function useChartQuery(request: ChartRequest | null | undefined): QueryEntry<PricePoint[]> | null {
88
+ const key = request ? [
89
+ request.instrument.symbol,
90
+ request.instrument.exchange ?? "",
91
+ request.range,
92
+ request.granularity ?? "daily",
93
+ request.startDate?.toISOString() ?? "",
94
+ request.endDate?.toISOString() ?? "",
95
+ request.barSize ?? "",
96
+ ].join("|") : null;
97
+ const entry = useCoordinatorSelector(
98
+ (coordinator) => (request ? coordinator.getChartEntry(request) : null),
99
+ null,
100
+ );
101
+
102
+ useEffect(() => {
103
+ const coordinator = getSharedMarketDataCoordinator();
104
+ if (!coordinator || !request) return;
105
+ void coordinator.loadChart(request);
106
+ }, [key]);
107
+
108
+ return entry;
109
+ }
110
+
111
+ export function useNewsQuery(request: NewsRequest | null | undefined): QueryEntry<NewsItem[]> | null {
112
+ const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.count ?? 50}` : null;
113
+ const entry = useCoordinatorSelector(
114
+ (coordinator) => (request ? coordinator.getNewsEntry(request) : null),
115
+ null,
116
+ );
117
+
118
+ useEffect(() => {
119
+ const coordinator = getSharedMarketDataCoordinator();
120
+ if (!coordinator || !request) return;
121
+ void coordinator.loadNews(request);
122
+ }, [key]);
123
+
124
+ return entry;
125
+ }
126
+
127
+ export function useOptionsQuery(request: OptionsRequest | null | undefined): QueryEntry<OptionsChain> | null {
128
+ const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.expirationDate ?? "default"}` : null;
129
+ const entry = useCoordinatorSelector(
130
+ (coordinator) => (request ? coordinator.getOptionsEntry(request) : null),
131
+ null,
132
+ );
133
+
134
+ useEffect(() => {
135
+ const coordinator = getSharedMarketDataCoordinator();
136
+ if (!coordinator || !request) return;
137
+ void coordinator.loadOptions(request);
138
+ }, [key]);
139
+
140
+ return entry;
141
+ }
142
+
143
+ export function useSecFilingsQuery(request: SecFilingsRequest | null | undefined): QueryEntry<SecFilingItem[]> | null {
144
+ const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.count ?? 50}` : null;
145
+ const entry = useCoordinatorSelector(
146
+ (coordinator) => (request ? coordinator.getSecFilingsEntry(request) : null),
147
+ null,
148
+ );
149
+
150
+ useEffect(() => {
151
+ const coordinator = getSharedMarketDataCoordinator();
152
+ if (!coordinator || !request) return;
153
+ void coordinator.loadSecFilings(request);
154
+ }, [key]);
155
+
156
+ return entry;
157
+ }
158
+
159
+ export function useSecFilingContent(filing: SecFilingItem | null | undefined): QueryEntry<string | null> | null {
160
+ const key = filing?.accessionNumber ?? null;
161
+ const entry = useCoordinatorSelector(
162
+ (coordinator) => (filing ? coordinator.getSecContentEntry(filing.accessionNumber) : null),
163
+ null,
164
+ );
165
+
166
+ useEffect(() => {
167
+ const coordinator = getSharedMarketDataCoordinator();
168
+ if (!coordinator || !filing) return;
169
+ void coordinator.loadSecFilingContent(filing);
170
+ }, [key]);
171
+
172
+ return entry;
173
+ }
174
+
175
+ export function useArticleSummary(url: string | null | undefined): QueryEntry<string | null> | null {
176
+ const entry = useCoordinatorSelector(
177
+ (coordinator) => (url ? coordinator.getArticleSummaryEntry(url) : null),
178
+ null,
179
+ );
180
+
181
+ useEffect(() => {
182
+ const coordinator = getSharedMarketDataCoordinator();
183
+ if (!coordinator || !url) return;
184
+ void coordinator.loadArticleSummary(url);
185
+ }, [url]);
186
+
187
+ return entry;
188
+ }
189
+
190
+ export function useFxRate(currency: string | null | undefined): QueryEntry<number> | null {
191
+ const entry = useCoordinatorSelector(
192
+ (coordinator) => (currency ? coordinator.getFxEntry(currency) : null),
193
+ null,
194
+ );
195
+
196
+ useEffect(() => {
197
+ const coordinator = getSharedMarketDataCoordinator();
198
+ if (!coordinator || !currency) return;
199
+ void coordinator.loadFxRate(currency);
200
+ }, [currency]);
201
+
202
+ return entry;
203
+ }
204
+
205
+ export function useFxRatesMap(currencies: Array<string | null | undefined>): Map<string, number> {
206
+ const normalizedCurrencies = useMemo(() => stableCurrencyList(currencies), [currencies]);
207
+ const entries = useCoordinatorSelector((coordinator) => (
208
+ normalizedCurrencies.map((currency) => [currency, coordinator.getFxEntry(currency)] as const)
209
+ ), [] as Array<readonly [string, QueryEntry<number>]>);
210
+
211
+ useEffect(() => {
212
+ const coordinator = getSharedMarketDataCoordinator();
213
+ if (!coordinator) return;
214
+ for (const currency of normalizedCurrencies) {
215
+ void coordinator.loadFxRate(currency);
216
+ }
217
+ }, [normalizedCurrencies.join("|")]);
218
+
219
+ return useMemo(() => {
220
+ const rates = new Map<string, number>();
221
+ rates.set("USD", 1);
222
+ for (const [currency, entry] of entries) {
223
+ const rate = resolveEntryValue(entry);
224
+ if (rate != null) {
225
+ rates.set(currency, rate);
226
+ }
227
+ }
228
+ return rates;
229
+ }, [entries]);
230
+ }
231
+
232
+ export function useResolvedEntryValue<T>(entry: QueryEntry<T> | null | undefined): T | null {
233
+ return useMemo(() => (entry ? resolveEntryValue(entry) : null), [entry]);
234
+ }
@@ -0,0 +1,28 @@
1
+ import type { QueryEntry } from "./result-types";
2
+ import { createIdleEntry } from "./result-types";
3
+
4
+ export class QueryStore<T> {
5
+ private readonly entries = new Map<string, QueryEntry<T>>();
6
+
7
+ constructor(private readonly onChange: () => void) {}
8
+
9
+ get(key: string): QueryEntry<T> {
10
+ return this.entries.get(key) ?? createIdleEntry<T>();
11
+ }
12
+
13
+ set(key: string, entry: QueryEntry<T>): void {
14
+ this.entries.set(key, entry);
15
+ this.onChange();
16
+ }
17
+
18
+ update(key: string, updater: (current: QueryEntry<T>) => QueryEntry<T>): QueryEntry<T> {
19
+ const next = updater(this.get(key));
20
+ this.entries.set(key, next);
21
+ this.onChange();
22
+ return next;
23
+ }
24
+
25
+ values(): IterableIterator<QueryEntry<T>> {
26
+ return this.entries.values();
27
+ }
28
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { TickerRecord } from "../types/ticker";
3
+ import { quoteSubscriptionTargetFromTicker } from "./request-types";
4
+
5
+ function makeTicker(overrides: Partial<TickerRecord["metadata"]> = {}): TickerRecord {
6
+ return {
7
+ metadata: {
8
+ ticker: "AAPL",
9
+ exchange: "NASDAQ",
10
+ currency: "USD",
11
+ name: "Apple",
12
+ broker_contracts: [],
13
+ portfolios: [],
14
+ watchlists: [],
15
+ positions: [],
16
+ custom: {},
17
+ tags: [],
18
+ ...overrides,
19
+ },
20
+ };
21
+ }
22
+
23
+ describe("quoteSubscriptionTargetFromTicker", () => {
24
+ test("preserves broker contract context for streaming targets", () => {
25
+ const ticker = makeTicker({
26
+ broker_contracts: [{
27
+ brokerId: "ibkr",
28
+ brokerInstanceId: "ibkr-live",
29
+ conId: 265598,
30
+ symbol: "AAPL",
31
+ localSymbol: "AAPL",
32
+ exchange: "NASDAQ",
33
+ currency: "USD",
34
+ secType: "STK",
35
+ }],
36
+ });
37
+
38
+ expect(quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker)).toEqual({
39
+ symbol: "AAPL",
40
+ exchange: "NASDAQ",
41
+ route: "auto",
42
+ context: {
43
+ brokerId: "ibkr",
44
+ brokerInstanceId: "ibkr-live",
45
+ instrument: ticker.metadata.broker_contracts?.[0],
46
+ },
47
+ });
48
+ });
49
+
50
+ test("still returns a valid unscoped target when no broker contract exists", () => {
51
+ const ticker = makeTicker();
52
+
53
+ expect(quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker)).toEqual({
54
+ symbol: "AAPL",
55
+ exchange: "NASDAQ",
56
+ route: "auto",
57
+ context: {
58
+ brokerId: undefined,
59
+ brokerInstanceId: undefined,
60
+ instrument: null,
61
+ },
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,70 @@
1
+ import type { TimeRange } from "../components/chart/chart-types";
2
+ import type { QuoteSubscriptionTarget } from "../types/data-provider";
3
+ import type { BrokerContractRef } from "../types/instrument";
4
+ import type { TickerRecord } from "../types/ticker";
5
+
6
+ export interface InstrumentRef {
7
+ symbol: string;
8
+ exchange?: string;
9
+ brokerId?: string;
10
+ brokerInstanceId?: string;
11
+ instrument?: BrokerContractRef | null;
12
+ }
13
+
14
+ export type ChartGranularity = "daily" | "detail";
15
+
16
+ export interface ChartRequest {
17
+ instrument: InstrumentRef;
18
+ range: TimeRange;
19
+ granularity?: ChartGranularity;
20
+ startDate?: Date;
21
+ endDate?: Date;
22
+ barSize?: string;
23
+ }
24
+
25
+ export interface NewsRequest {
26
+ instrument: InstrumentRef;
27
+ count?: number;
28
+ }
29
+
30
+ export interface OptionsRequest {
31
+ instrument: InstrumentRef;
32
+ expirationDate?: number;
33
+ }
34
+
35
+ export interface SecFilingsRequest {
36
+ instrument: InstrumentRef;
37
+ count?: number;
38
+ }
39
+
40
+ export function instrumentFromTicker(ticker: TickerRecord | null | undefined, fallbackSymbol?: string | null): InstrumentRef | null {
41
+ const symbol = ticker?.metadata.ticker ?? fallbackSymbol ?? null;
42
+ if (!symbol) return null;
43
+ const instrument = ticker?.metadata.broker_contracts?.[0] ?? null;
44
+ return {
45
+ symbol,
46
+ exchange: ticker?.metadata.exchange ?? "",
47
+ brokerId: instrument?.brokerId,
48
+ brokerInstanceId: instrument?.brokerInstanceId,
49
+ instrument,
50
+ };
51
+ }
52
+
53
+ export function quoteSubscriptionTargetFromTicker(
54
+ ticker: TickerRecord | null | undefined,
55
+ fallbackSymbol?: string | null,
56
+ route: QuoteSubscriptionTarget["route"] = "auto",
57
+ ): QuoteSubscriptionTarget | null {
58
+ const instrument = instrumentFromTicker(ticker, fallbackSymbol);
59
+ if (!instrument) return null;
60
+ return {
61
+ symbol: instrument.symbol,
62
+ exchange: instrument.exchange,
63
+ route,
64
+ context: {
65
+ brokerId: instrument.brokerId,
66
+ brokerInstanceId: instrument.brokerInstanceId,
67
+ instrument: instrument.instrument ?? null,
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,56 @@
1
+ export type ProviderStatus = "success" | "partial" | "empty" | "unsupported" | "retryable_error" | "fatal_error";
2
+
3
+ export type ProviderReasonCode =
4
+ | "NO_DATA"
5
+ | "NOT_FOUND"
6
+ | "TIMEOUT"
7
+ | "UNSUPPORTED_RANGE"
8
+ | "BAD_MAPPING"
9
+ | "UPSTREAM_ERROR";
10
+
11
+ export interface ProviderAttempt {
12
+ providerId: string;
13
+ status: ProviderStatus;
14
+ startedAt: number;
15
+ finishedAt: number;
16
+ latencyMs: number;
17
+ reasonCode?: ProviderReasonCode;
18
+ message?: string;
19
+ }
20
+
21
+ export interface ProviderResult<T> {
22
+ status: ProviderStatus;
23
+ data: T | null;
24
+ providerId: string;
25
+ latencyMs: number;
26
+ completeness: "full" | "partial";
27
+ reasonCode?: ProviderReasonCode;
28
+ asOf?: number;
29
+ staleAt?: number;
30
+ }
31
+
32
+ export type QueryPhase = "idle" | "loading" | "ready" | "refreshing" | "error";
33
+
34
+ export interface QueryEntry<T> {
35
+ phase: QueryPhase;
36
+ data: T | null;
37
+ lastGoodData: T | null;
38
+ source: string | null;
39
+ fetchedAt: number | null;
40
+ staleAt: number | null;
41
+ error: { reasonCode: string; message: string } | null;
42
+ attempts: ProviderAttempt[];
43
+ }
44
+
45
+ export function createIdleEntry<T>(): QueryEntry<T> {
46
+ return {
47
+ phase: "idle",
48
+ data: null,
49
+ lastGoodData: null,
50
+ source: null,
51
+ fetchedAt: null,
52
+ staleAt: null,
53
+ error: null,
54
+ attempts: [],
55
+ };
56
+ }
@@ -0,0 +1,166 @@
1
+ import type { MarketDataRequestContext } from "../types/data-provider";
2
+ import type { Quote, TickerFinancials } from "../types/financials";
3
+ import type { InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest, ChartRequest } from "./request-types";
4
+ import type { QueryEntry } from "./result-types";
5
+ import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
6
+ import { normalizePriceHistory } from "../utils/price-history";
7
+
8
+ export function buildInstrumentKey(instrument: InstrumentRef): string {
9
+ const contractKey = instrument.instrument?.conId
10
+ ?? instrument.instrument?.localSymbol
11
+ ?? instrument.instrument?.symbol
12
+ ?? "";
13
+ return [
14
+ instrument.symbol.trim().toUpperCase(),
15
+ (instrument.exchange ?? "").trim().toUpperCase(),
16
+ instrument.brokerId ?? "",
17
+ instrument.brokerInstanceId ?? "",
18
+ contractKey,
19
+ ].join("|");
20
+ }
21
+
22
+ export function toMarketDataContext(instrument: InstrumentRef): MarketDataRequestContext {
23
+ return {
24
+ brokerId: instrument.brokerId,
25
+ brokerInstanceId: instrument.brokerInstanceId,
26
+ instrument: instrument.instrument ?? null,
27
+ };
28
+ }
29
+
30
+ export function buildQuoteKey(instrument: InstrumentRef): string {
31
+ return `quote:${buildInstrumentKey(instrument)}`;
32
+ }
33
+
34
+ export function buildSnapshotKey(instrument: InstrumentRef): string {
35
+ return `snapshot:${buildInstrumentKey(instrument)}`;
36
+ }
37
+
38
+ export function buildProfileKey(instrument: InstrumentRef): string {
39
+ return `profile:${buildInstrumentKey(instrument)}`;
40
+ }
41
+
42
+ export function buildFundamentalsKey(instrument: InstrumentRef): string {
43
+ return `fundamentals:${buildInstrumentKey(instrument)}`;
44
+ }
45
+
46
+ export function buildStatementsKey(instrument: InstrumentRef): string {
47
+ return `statements:${buildInstrumentKey(instrument)}`;
48
+ }
49
+
50
+ export function buildChartKey(request: ChartRequest): string {
51
+ return [
52
+ "chart",
53
+ buildInstrumentKey(request.instrument),
54
+ request.range,
55
+ request.granularity ?? "daily",
56
+ request.startDate ? request.startDate.toISOString() : "",
57
+ request.endDate ? request.endDate.toISOString() : "",
58
+ request.barSize ?? "",
59
+ ].join(":");
60
+ }
61
+
62
+ export function buildNewsKey(request: NewsRequest): string {
63
+ return `news:${buildInstrumentKey(request.instrument)}:${request.count ?? 50}`;
64
+ }
65
+
66
+ export function buildOptionsKey(request: OptionsRequest): string {
67
+ return `options:${buildInstrumentKey(request.instrument)}:${request.expirationDate ?? "default"}`;
68
+ }
69
+
70
+ export function buildSecFilingsKey(request: SecFilingsRequest): string {
71
+ return `sec:${buildInstrumentKey(request.instrument)}:${request.count ?? 50}`;
72
+ }
73
+
74
+ export function buildSecContentKey(accessionNumber: string): string {
75
+ return `sec-content:${accessionNumber}`;
76
+ }
77
+
78
+ export function buildArticleSummaryKey(url: string): string {
79
+ return `article-summary:${url.trim()}`;
80
+ }
81
+
82
+ export function buildFxKey(currency: string): string {
83
+ return `fx:${currency.trim().toUpperCase()}`;
84
+ }
85
+
86
+ export function resolveEntryData<T>(entry: QueryEntry<T> | null | undefined): T | null {
87
+ if (!entry) return null;
88
+ return entry.data ?? entry.lastGoodData ?? null;
89
+ }
90
+
91
+ export function hasLikelyPriceUnitMismatch(
92
+ left: Quote | null | undefined,
93
+ right: Quote | null | undefined,
94
+ ): boolean {
95
+ return hasLikelyQuoteUnitMismatch(left, right);
96
+ }
97
+
98
+ function mergeDefinedQuoteFields(
99
+ preferred: Quote | null | undefined,
100
+ fallback: Quote | null | undefined,
101
+ ): Quote | undefined {
102
+ const mergedEntries: Array<[string, unknown]> = [];
103
+ for (const source of [fallback, preferred]) {
104
+ if (!source) continue;
105
+ for (const [key, value] of Object.entries(source)) {
106
+ if (value !== undefined) {
107
+ mergedEntries.push([key, value]);
108
+ }
109
+ }
110
+ }
111
+ if (mergedEntries.length === 0) return undefined;
112
+ return Object.fromEntries(mergedEntries) as Quote;
113
+ }
114
+
115
+ function projectExtendedHoursQuote(
116
+ snapshotQuote: Quote | null | undefined,
117
+ liveQuote: Quote | null | undefined,
118
+ ): Quote | undefined {
119
+ const merged = mergeDefinedQuoteFields(liveQuote, snapshotQuote);
120
+ if (!merged) return undefined;
121
+ if (!snapshotQuote || !liveQuote) return merged;
122
+
123
+ const snapshotState = snapshotQuote.marketState;
124
+ if (snapshotState !== "PRE" && snapshotState !== "POST") return merged;
125
+ if (liveQuote.marketState === "PRE" || liveQuote.marketState === "POST") return merged;
126
+
127
+ if (snapshotState === "PRE") {
128
+ return {
129
+ ...merged,
130
+ marketState: "PRE",
131
+ preMarketPrice: liveQuote.price,
132
+ preMarketChange: liveQuote.change,
133
+ preMarketChangePercent: liveQuote.changePercent,
134
+ };
135
+ }
136
+
137
+ return {
138
+ ...merged,
139
+ marketState: "POST",
140
+ postMarketPrice: liveQuote.price,
141
+ postMarketChange: liveQuote.change,
142
+ postMarketChangePercent: liveQuote.changePercent,
143
+ };
144
+ }
145
+
146
+ export function buildTickerFinancialsSnapshot(
147
+ snapshotEntry: QueryEntry<TickerFinancials>,
148
+ quoteEntry?: QueryEntry<TickerFinancials["quote"]>,
149
+ chartEntry?: QueryEntry<TickerFinancials["priceHistory"]>,
150
+ ): TickerFinancials | null {
151
+ const snapshot = resolveEntryData(snapshotEntry);
152
+ const quote = resolveEntryData(quoteEntry);
153
+ const priceHistory = resolveEntryData(chartEntry);
154
+ if (!snapshot && !quote && !priceHistory) return null;
155
+ const resolvedQuote = snapshot?.quote && quote && hasLikelyPriceUnitMismatch(snapshot.quote, quote)
156
+ ? snapshot.quote
157
+ : (projectExtendedHoursQuote(snapshot?.quote, quote) ?? snapshot?.quote);
158
+ return {
159
+ quote: resolvedQuote,
160
+ fundamentals: snapshot?.fundamentals,
161
+ profile: snapshot?.profile,
162
+ annualStatements: snapshot?.annualStatements ?? [],
163
+ quarterlyStatements: snapshot?.quarterlyStatements ?? [],
164
+ priceHistory: normalizePriceHistory(priceHistory ?? snapshot?.priceHistory ?? []),
165
+ };
166
+ }
@@ -0,0 +1,7 @@
1
+ import { debugLog } from "../utils/debug-log";
2
+
3
+ const marketDataLog = debugLog.createLogger("market-data");
4
+
5
+ export function traceMarketData(event: string, payload: Record<string, unknown>): void {
6
+ marketDataLog.info(event, payload);
7
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { cloneLayout, createDefaultConfig, createPaneInstance, type LayoutConfig } from "./types/config";
3
+ import { deletePaneSetting, getPaneSettingValue, setPaneSetting } from "./pane-settings";
4
+
5
+ describe("pane settings helpers", () => {
6
+ test("seeds built-in default panes with persisted settings", () => {
7
+ const config = createDefaultConfig("/tmp/gloomberb-pane-settings");
8
+ const portfolioPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
9
+ const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
10
+
11
+ expect(portfolioPane?.settings).toMatchObject({
12
+ collectionScope: "all",
13
+ hideTabs: false,
14
+ lockedCollectionId: "main",
15
+ });
16
+ expect(portfolioPane?.settings?.columnIds).toEqual([
17
+ "ticker",
18
+ "price",
19
+ "change_pct",
20
+ "market_cap",
21
+ "pe",
22
+ "forward_pe",
23
+ "latency",
24
+ ]);
25
+ expect(detailPane?.settings).toEqual({
26
+ hideTabs: false,
27
+ lockedTabId: "overview",
28
+ });
29
+ });
30
+
31
+ test("cloneLayout deep-clones pane settings", () => {
32
+ const layout = cloneLayout(createDefaultConfig("/tmp/gloomberb-pane-settings").layout);
33
+ const cloned = cloneLayout(layout);
34
+ const clonedPortfolioSettings = cloned.instances.find((instance) => instance.instanceId === "portfolio-list:main")?.settings;
35
+ const originalPortfolioSettings = layout.instances.find((instance) => instance.instanceId === "portfolio-list:main")?.settings;
36
+
37
+ expect(clonedPortfolioSettings).toBeDefined();
38
+ expect(clonedPortfolioSettings).not.toBe(originalPortfolioSettings);
39
+
40
+ (clonedPortfolioSettings?.columnIds as string[]).push("change");
41
+ expect(originalPortfolioSettings?.columnIds).toEqual([
42
+ "ticker",
43
+ "price",
44
+ "change_pct",
45
+ "market_cap",
46
+ "pe",
47
+ "forward_pe",
48
+ "latency",
49
+ ]);
50
+ });
51
+
52
+ test("setPaneSetting and deletePaneSetting update pane-scoped settings immutably", () => {
53
+ const config = createDefaultConfig("/tmp/gloomberb-pane-settings");
54
+ const extraPane = createPaneInstance("quote-monitor", {
55
+ instanceId: "quote-monitor:test",
56
+ binding: { kind: "fixed", symbol: "AAPL" },
57
+ settings: { symbol: "AAPL" },
58
+ });
59
+ const layout: LayoutConfig = {
60
+ ...config.layout,
61
+ instances: [...config.layout.instances, extraPane],
62
+ };
63
+
64
+ const updated = setPaneSetting(layout, "quote-monitor:test", "symbol", "MSFT");
65
+ expect(getPaneSettingValue(updated.instances.find((instance) => instance.instanceId === "quote-monitor:test"), "symbol", "")).toBe("MSFT");
66
+ expect(getPaneSettingValue(layout.instances.find((instance) => instance.instanceId === "quote-monitor:test"), "symbol", "")).toBe("AAPL");
67
+
68
+ const cleared = deletePaneSetting(updated, "quote-monitor:test", "symbol");
69
+ expect(getPaneSettingValue(cleared.instances.find((instance) => instance.instanceId === "quote-monitor:test"), "symbol", null)).toBeNull();
70
+ });
71
+ });