gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -279,6 +279,7 @@ function PortfolioHarness({
279
279
  stateMutator,
280
280
  runtime = createTestPluginRuntime(),
281
281
  paneHeight = 24,
282
+ paneFocused = true,
282
283
  }: {
283
284
  config: AppConfig;
284
285
  collectionId: string;
@@ -290,6 +291,7 @@ function PortfolioHarness({
290
291
  stateMutator?: (state: ReturnType<typeof createInitialState>) => void;
291
292
  runtime?: PluginRuntimeAccess;
292
293
  paneHeight?: number;
294
+ paneFocused?: boolean;
293
295
  }) {
294
296
  const initialState = createPortfolioState(config, collectionId, expanded, {
295
297
  ticker,
@@ -309,7 +311,7 @@ function PortfolioHarness({
309
311
  <PortfolioPane
310
312
  paneId={TEST_PANE_ID}
311
313
  paneType="portfolio-list"
312
- focused
314
+ focused={paneFocused}
313
315
  width={100}
314
316
  height={paneHeight}
315
317
  />
@@ -323,11 +325,162 @@ async function flushFrame() {
323
325
  await act(async () => {
324
326
  await Promise.resolve();
325
327
  await testSetup!.renderOnce();
328
+ await new Promise((resolve) => setTimeout(resolve, 0));
326
329
  await Promise.resolve();
327
330
  await testSetup!.renderOnce();
328
331
  });
329
332
  }
330
333
 
334
+ function makeSortWarmupQuote(symbol: string): Quote {
335
+ return makeQuote({
336
+ symbol,
337
+ price: symbol === "SIVE" ? 46.7 : 100,
338
+ change: symbol === "SIVE" ? -9.6 : 0,
339
+ changePercent: symbol === "SIVE" ? -17.05 : 0,
340
+ previousClose: symbol === "SIVE" ? 56.3 : 100,
341
+ listingExchangeName: "NASDAQ",
342
+ });
343
+ }
344
+
345
+ function makeSortWarmupBrokerTicker(portfolioId: string, symbol: string, index: number): TickerRecord {
346
+ return makeTicker({
347
+ ticker: symbol,
348
+ name: symbol,
349
+ portfolios: [portfolioId],
350
+ positions: [{
351
+ portfolio: portfolioId,
352
+ shares: 10,
353
+ avgCost: 100,
354
+ currency: "USD",
355
+ broker: "ibkr",
356
+ brokerInstanceId: "ibkr-live",
357
+ brokerAccountId: "DU12345",
358
+ brokerContractId: 10_000 + index,
359
+ markPrice: 100,
360
+ }],
361
+ broker_contracts: [{
362
+ brokerId: "ibkr",
363
+ brokerInstanceId: "ibkr-live",
364
+ symbol,
365
+ localSymbol: symbol,
366
+ exchange: "SMART",
367
+ primaryExchange: "NASDAQ",
368
+ conId: 10_000 + index,
369
+ }],
370
+ });
371
+ }
372
+
373
+ async function renderHiddenChangePctSortWarmup(options: { staleCachedSiveSnapshot?: boolean } = {}) {
374
+ const portfolioId = "broker:ibkr-live:DU12345";
375
+ const config = createPortfolioConfigWithColumns(
376
+ portfolioId,
377
+ ["ticker", "price", "change_pct", "latency"],
378
+ [createBrokerInstance("gateway", "ibkr-live")],
379
+ );
380
+ const requestedSnapshots: string[] = [];
381
+ const provider: DataProvider = {
382
+ id: "test-provider",
383
+ name: "Test Provider",
384
+ async getTickerFinancials(symbol) {
385
+ requestedSnapshots.push(symbol);
386
+ return {
387
+ annualStatements: [],
388
+ quarterlyStatements: [],
389
+ priceHistory: [],
390
+ quote: makeSortWarmupQuote(symbol),
391
+ };
392
+ },
393
+ async getQuote(symbol) {
394
+ return makeSortWarmupQuote(symbol);
395
+ },
396
+ async getExchangeRate() {
397
+ return 1;
398
+ },
399
+ async search() {
400
+ return [];
401
+ },
402
+ async getArticleSummary() {
403
+ return null;
404
+ },
405
+ async getPriceHistory() {
406
+ return [];
407
+ },
408
+ subscribeQuotes() {
409
+ return () => {};
410
+ },
411
+ };
412
+ sharedCoordinator = new MarketDataCoordinator(provider);
413
+ setSharedMarketDataCoordinator(sharedCoordinator);
414
+
415
+ const tickers = Array.from({ length: 29 }, (_, index) => makeSortWarmupBrokerTicker(portfolioId, `T${String(index).padStart(2, "0")}`, index));
416
+ const sive = makeSortWarmupBrokerTicker(portfolioId, "SIVE", 29);
417
+ if (options.staleCachedSiveSnapshot) {
418
+ const siveInstrument = instrumentFromTicker(sive, "SIVE", { portfolioId });
419
+ if (!siveInstrument) throw new Error("expected SIVE instrument");
420
+ sharedCoordinator.primeCachedFinancials([{
421
+ instrument: siveInstrument,
422
+ financials: {
423
+ annualStatements: [],
424
+ quarterlyStatements: [],
425
+ priceHistory: [],
426
+ quote: makeQuote({
427
+ symbol: "SIVE",
428
+ price: 56.3,
429
+ change: 56.3,
430
+ changePercent: 100,
431
+ previousClose: 0,
432
+ listingExchangeName: "NASDAQ",
433
+ lastUpdated: Date.now() - 24 * 60 * 60_000,
434
+ }),
435
+ },
436
+ }]);
437
+ }
438
+
439
+ testSetup = await testRender(
440
+ <PortfolioHarness
441
+ config={config}
442
+ collectionId={portfolioId}
443
+ stateMutator={(state) => {
444
+ state.tickers = new Map([...tickers, sive].map((entry) => [entry.metadata.ticker, entry]));
445
+ state.financials = new Map(tickers.map((entry, index) => [
446
+ entry.metadata.ticker,
447
+ {
448
+ annualStatements: [],
449
+ quarterlyStatements: [],
450
+ priceHistory: [],
451
+ quote: makeQuote({
452
+ symbol: entry.metadata.ticker,
453
+ price: 100 + index,
454
+ change: index,
455
+ changePercent: index,
456
+ listingExchangeName: "NASDAQ",
457
+ }),
458
+ },
459
+ ]));
460
+ state.paneState[TEST_PANE_ID] = {
461
+ collectionId: portfolioId,
462
+ cursorSymbol: "T00",
463
+ cashDrawerExpanded: false,
464
+ collectionSorts: {
465
+ [portfolioId]: { columnId: "change_pct", direction: "asc" },
466
+ },
467
+ };
468
+ }}
469
+ paneHeight={12}
470
+ />,
471
+ { width: 100, height: 12 },
472
+ );
473
+
474
+ await flushFrame();
475
+ const beforeFrame = testSetup.captureCharFrame();
476
+ await act(async () => {
477
+ await new Promise((resolve) => setTimeout(resolve, 400));
478
+ });
479
+ await flushFrame();
480
+
481
+ return { beforeFrame, frame: testSetup.captureCharFrame(), requestedSnapshots };
482
+ }
483
+
331
484
  afterEach(async () => {
332
485
  if (testSetup) {
333
486
  testSetup.renderer.destroy();
@@ -991,18 +1144,20 @@ describe("PortfolioListPane cash and margin UI", () => {
991
1144
  expect(frame).toContain("22.0");
992
1145
  });
993
1146
 
994
- test("force-refreshes stale quote data for visible rows", async () => {
1147
+ test("force-refreshes old visible quote data even when the pane is not focused", async () => {
995
1148
  const config = createPortfolioConfigWithColumns(
996
1149
  "broker:ibkr-flex:DU12345",
997
1150
  ["ticker", "price", "day_pnl"],
998
1151
  [createBrokerInstance("flex")],
999
1152
  );
1000
- const staleQuote = makeQuote({
1153
+ const oldQuote = makeQuote({
1001
1154
  price: 120,
1002
- change: 0,
1003
- changePercent: 0,
1004
- marketState: "PRE",
1005
- lastUpdated: Date.now() - 24 * 60 * 60 * 1000,
1155
+ change: -5,
1156
+ changePercent: -4,
1157
+ currency: "EUR",
1158
+ listingExchangeName: "FWB2",
1159
+ marketState: "REGULAR",
1160
+ lastUpdated: Date.now() - 5 * 60 * 1000,
1006
1161
  });
1007
1162
  const batchOptions: Array<{ forceRefresh?: boolean } | undefined> = [];
1008
1163
  const provider: DataProvider = {
@@ -1013,7 +1168,7 @@ describe("PortfolioListPane cash and margin UI", () => {
1013
1168
  annualStatements: [],
1014
1169
  quarterlyStatements: [],
1015
1170
  priceHistory: [],
1016
- quote: staleQuote,
1171
+ quote: oldQuote,
1017
1172
  };
1018
1173
  },
1019
1174
  async getQuotesBatch(targets, options) {
@@ -1023,12 +1178,11 @@ describe("PortfolioListPane cash and margin UI", () => {
1023
1178
  quote: makeQuote({
1024
1179
  symbol: target.symbol,
1025
1180
  price: 126,
1026
- change: 6,
1027
- changePercent: 5,
1028
- marketState: "PRE",
1029
- preMarketPrice: 126,
1030
- preMarketChange: 6,
1031
- preMarketChangePercent: 5,
1181
+ change: 1,
1182
+ changePercent: 0.8,
1183
+ currency: "EUR",
1184
+ listingExchangeName: "FWB2",
1185
+ marketState: "REGULAR",
1032
1186
  }),
1033
1187
  }));
1034
1188
  },
@@ -1058,14 +1212,25 @@ describe("PortfolioListPane cash and margin UI", () => {
1058
1212
  <PortfolioHarness
1059
1213
  config={config}
1060
1214
  collectionId="broker:ibkr-flex:DU12345"
1061
- quote={staleQuote}
1215
+ ticker={makeTicker({ exchange: "FWB2", currency: "EUR" })}
1216
+ quote={oldQuote}
1217
+ paneFocused={false}
1218
+ stateMutator={(state) => {
1219
+ state.focusedPaneId = "portfolio-list:other";
1220
+ state.paneState[TEST_PANE_ID] = {
1221
+ ...(state.paneState[TEST_PANE_ID] ?? {}),
1222
+ collectionSorts: {
1223
+ "broker:ibkr-flex:DU12345": { columnId: "ticker", direction: "asc" },
1224
+ },
1225
+ };
1226
+ }}
1062
1227
  />,
1063
1228
  { width: 100, height: 12 },
1064
1229
  );
1065
1230
 
1066
1231
  await flushFrame();
1067
1232
  await act(async () => {
1068
- await new Promise((resolve) => setTimeout(resolve, 380));
1233
+ await Promise.resolve();
1069
1234
  });
1070
1235
  await flushFrame();
1071
1236
 
@@ -1413,4 +1578,137 @@ describe("PortfolioListPane cash and margin UI", () => {
1413
1578
  expect(frame).toContain("+5.41%");
1414
1579
  });
1415
1580
 
1581
+ test("streams portfolio rows with the active collection broker contract", async () => {
1582
+ const config = createPortfolioConfigWithColumns(
1583
+ "broker:ibkr-live:DU12345",
1584
+ ["ticker", "price", "change_pct", "latency"],
1585
+ [createBrokerInstance("gateway", "ibkr-flex"), createBrokerInstance("gateway", "ibkr-live")],
1586
+ );
1587
+ let subscribedTargets: Array<{ symbol: string; context?: { brokerInstanceId?: string; instrument?: unknown } }> = [];
1588
+ const provider: DataProvider = {
1589
+ id: "test-provider",
1590
+ name: "Test Provider",
1591
+ async getTickerFinancials(symbol) {
1592
+ return {
1593
+ annualStatements: [],
1594
+ quarterlyStatements: [],
1595
+ priceHistory: [],
1596
+ quote: makeQuote({ symbol }),
1597
+ };
1598
+ },
1599
+ async getQuote(symbol) {
1600
+ return makeQuote({ symbol });
1601
+ },
1602
+ async getExchangeRate() {
1603
+ return 1;
1604
+ },
1605
+ async search() {
1606
+ return [];
1607
+ },
1608
+ async getArticleSummary() {
1609
+ return null;
1610
+ },
1611
+ async getPriceHistory() {
1612
+ return [];
1613
+ },
1614
+ subscribeQuotes(targets) {
1615
+ subscribedTargets = targets;
1616
+ return () => {};
1617
+ },
1618
+ };
1619
+ sharedCoordinator = new MarketDataCoordinator(provider);
1620
+ setSharedMarketDataCoordinator(sharedCoordinator);
1621
+
1622
+ const flexContract = {
1623
+ brokerId: "ibkr",
1624
+ brokerInstanceId: "ibkr-flex",
1625
+ symbol: "VICR",
1626
+ localSymbol: "VICR",
1627
+ exchange: "NASDAQ",
1628
+ conId: 275759,
1629
+ };
1630
+ const liveContract = {
1631
+ brokerId: "ibkr",
1632
+ brokerInstanceId: "ibkr-live",
1633
+ symbol: "VICR",
1634
+ localSymbol: "VICR",
1635
+ exchange: "NASDAQ",
1636
+ primaryExchange: "NASDAQ",
1637
+ conId: 275759,
1638
+ };
1639
+
1640
+ testSetup = await testRender(
1641
+ <PortfolioHarness
1642
+ config={config}
1643
+ collectionId="broker:ibkr-live:DU12345"
1644
+ stateMutator={(state) => {
1645
+ state.tickers = new Map([["VICR", makeTicker({
1646
+ ticker: "VICR",
1647
+ name: "Vicor",
1648
+ portfolios: ["broker:ibkr-flex:DU12345", "broker:ibkr-live:DU12345"],
1649
+ positions: [
1650
+ {
1651
+ portfolio: "broker:ibkr-flex:DU12345",
1652
+ shares: 170,
1653
+ avgCost: 198,
1654
+ currency: "USD",
1655
+ broker: "ibkr",
1656
+ brokerInstanceId: "ibkr-flex",
1657
+ brokerAccountId: "DU12345",
1658
+ brokerContractId: 275759,
1659
+ },
1660
+ {
1661
+ portfolio: "broker:ibkr-live:DU12345",
1662
+ shares: 350,
1663
+ avgCost: 290,
1664
+ currency: "USD",
1665
+ broker: "ibkr",
1666
+ brokerInstanceId: "ibkr-live",
1667
+ brokerAccountId: "DU12345",
1668
+ brokerContractId: 275759,
1669
+ },
1670
+ ],
1671
+ broker_contracts: [flexContract, liveContract],
1672
+ })]]);
1673
+ state.financials = new Map();
1674
+ state.paneState[TEST_PANE_ID] = {
1675
+ collectionId: "broker:ibkr-live:DU12345",
1676
+ cursorSymbol: "VICR",
1677
+ cashDrawerExpanded: false,
1678
+ };
1679
+ }}
1680
+ />,
1681
+ { width: 100, height: 12 },
1682
+ );
1683
+
1684
+ await flushFrame();
1685
+ await act(async () => {
1686
+ await Promise.resolve();
1687
+ });
1688
+
1689
+ const vicrTarget = subscribedTargets.find((target) => target.symbol === "VICR");
1690
+ expect(vicrTarget?.context?.brokerInstanceId).toBe("ibkr-live");
1691
+ expect(vicrTarget?.context?.instrument).toEqual(liveContract);
1692
+ });
1693
+
1694
+ test("warms hidden quote-missing rows when sorting by change percent", async () => {
1695
+ const { beforeFrame, frame, requestedSnapshots } = await renderHiddenChangePctSortWarmup();
1696
+ expect(beforeFrame).not.toContain("SIVE");
1697
+ expect(requestedSnapshots).toContain("SIVE");
1698
+ expect(frame).toContain("SIVE");
1699
+ expect(frame).toContain("-17.05%");
1700
+ expect(frame).toContain("46.7");
1701
+ });
1702
+
1703
+ test("force-refreshes hidden stale cached snapshots when sorting by change percent", async () => {
1704
+ const { beforeFrame, frame, requestedSnapshots } = await renderHiddenChangePctSortWarmup({
1705
+ staleCachedSiveSnapshot: true,
1706
+ });
1707
+ expect(beforeFrame).not.toContain("SIVE");
1708
+ expect(requestedSnapshots).toContain("SIVE");
1709
+ expect(frame).toContain("SIVE");
1710
+ expect(frame).toContain("-17.05%");
1711
+ expect(frame).toContain("46.7");
1712
+ });
1713
+
1416
1714
  });
@@ -107,7 +107,10 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
107
107
  () => getCollectionTickersFromConfig(config, tickersBySymbol, activeCollectionId),
108
108
  [activeCollectionId, config, tickersBySymbol],
109
109
  );
110
- const marketFinancialsMap = useTickerFinancialsMap(tickers);
110
+ const financialsInstrumentOptions = useMemo(() => ({
111
+ portfolioId: isPortfolioTab ? activeCollectionId : undefined,
112
+ }), [activeCollectionId, isPortfolioTab]);
113
+ const marketFinancialsMap = useTickerFinancialsMap(tickers, financialsInstrumentOptions);
111
114
  const financialsMap = useMemo(() => {
112
115
  const merged = new Map(cachedFinancials);
113
116
  for (const [symbol, financials] of marketFinancialsMap) {
@@ -303,16 +306,20 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
303
306
  const effectiveStreamWindow = useMemo(() => (
304
307
  viewMode === "grid"
305
308
  ? { start: 0, end: Math.min(sortedTickers.length, 96) }
306
- : streamWindow
307
- ), [sortedTickers.length, streamWindow, viewMode]);
309
+ : {
310
+ start: streamWindow.start,
311
+ end: Math.min(sortedTickers.length, Math.max(streamWindow.end, height + 4)),
312
+ }
313
+ ), [height, sortedTickers.length, streamWindow, viewMode]);
308
314
 
309
315
  usePortfolioPaneStreaming({
310
316
  appActive,
311
- focused,
317
+ activeCollectionId: isPortfolioTab ? activeCollectionId : undefined,
312
318
  sortedTickers,
313
319
  cursorSymbol,
314
320
  streamWindow: effectiveStreamWindow,
315
321
  isPortfolioTab,
322
+ activeSort,
316
323
  financialsMap,
317
324
  visibleWarmupRequirements,
318
325
  });
@@ -4,36 +4,42 @@ import { instrumentFromTicker, quoteSubscriptionTargetFromTicker } from "../../.
4
4
  import { useQuoteStreaming } from "../../../../state/hooks/quote-streaming";
5
5
  import type { TickerFinancials } from "../../../../types/financials";
6
6
  import type { TickerRecord } from "../../../../types/ticker";
7
+ import type { CollectionSortPreference } from "../../../../state/app/context";
7
8
  import {
8
9
  VISIBLE_FINANCIAL_WARMUP_DELAY_MS,
9
10
  VISIBLE_QUOTE_REFRESH_COOLDOWN_MS,
10
11
  VISIBLE_QUOTE_STREAM_WATCHDOG_MS,
11
12
  VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT,
12
13
  VISIBLE_SNAPSHOT_REFRESH_COOLDOWN_MS,
14
+ SORT_QUOTE_WARMUP_BATCH_LIMIT,
13
15
  needsVisibleQuoteWarmup,
14
16
  needsVisibleQuoteWatchdogRefresh,
15
17
  needsVisibleSnapshotWarmup,
18
+ selectQuoteWarmupTickers,
16
19
  selectStreamTickers,
20
+ sortPreferenceUsesQuote,
17
21
  visibleWarmupKey,
18
22
  type VisibleWarmupRequirements,
19
23
  } from "./data";
20
24
 
21
25
  export function usePortfolioPaneStreaming({
22
26
  appActive,
23
- focused,
27
+ activeCollectionId,
24
28
  sortedTickers,
25
29
  cursorSymbol,
26
30
  streamWindow,
27
31
  isPortfolioTab,
32
+ activeSort,
28
33
  financialsMap,
29
34
  visibleWarmupRequirements,
30
35
  }: {
31
36
  appActive: boolean;
32
- focused: boolean;
37
+ activeCollectionId?: string;
33
38
  sortedTickers: TickerRecord[];
34
39
  cursorSymbol: string | null;
35
40
  streamWindow: { start: number; end: number };
36
41
  isPortfolioTab: boolean;
42
+ activeSort: CollectionSortPreference;
37
43
  financialsMap: Map<string, TickerFinancials>;
38
44
  visibleWarmupRequirements: VisibleWarmupRequirements;
39
45
  }) {
@@ -57,10 +63,13 @@ export function usePortfolioPaneStreaming({
57
63
  [streamTickers],
58
64
  );
59
65
  const streamSurface: "portfolio" | "watchlist" = isPortfolioTab ? "portfolio" : "watchlist";
66
+ const instrumentOptions = useMemo(() => ({
67
+ portfolioId: isPortfolioTab ? activeCollectionId : undefined,
68
+ }), [activeCollectionId, isPortfolioTab]);
60
69
  const streamTargets = useMemo(() => (
61
70
  sortedTickers
62
71
  .map((ticker) => {
63
- const target = quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker, "provider");
72
+ const target = quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker, "provider", instrumentOptions);
64
73
  if (!target) return null;
65
74
  const selected = ticker.metadata.ticker === cursorSymbol;
66
75
  const visible = priorityStreamSymbols.has(ticker.metadata.ticker);
@@ -73,32 +82,64 @@ export function usePortfolioPaneStreaming({
73
82
  };
74
83
  })
75
84
  .filter((target): target is NonNullable<typeof target> => target != null)
76
- ), [cursorSymbol, priorityStreamSymbols, sortedTickers, streamSurface]);
85
+ ), [cursorSymbol, instrumentOptions, priorityStreamSymbols, sortedTickers, streamSurface]);
77
86
  const visibleFinancialTickers = useMemo(
78
87
  () => sortedTickers.slice(streamWindow.start, streamWindow.end),
79
88
  [sortedTickers, streamWindow.end, streamWindow.start],
80
89
  );
90
+ const watchdogInputsRef = useRef<{
91
+ financialsMap: Map<string, TickerFinancials>;
92
+ instrumentOptions: typeof instrumentOptions;
93
+ visibleFinancialTickers: TickerRecord[];
94
+ }>({ financialsMap, instrumentOptions, visibleFinancialTickers });
95
+
96
+ useEffect(() => {
97
+ watchdogInputsRef.current = {
98
+ financialsMap,
99
+ instrumentOptions,
100
+ visibleFinancialTickers,
101
+ };
102
+ }, [financialsMap, instrumentOptions, visibleFinancialTickers]);
81
103
 
82
104
  useEffect(() => {
83
105
  if (!appActive) return;
84
- if (!focused) return;
85
106
  if (!sharedCoordinator) return;
86
107
 
87
108
  const nowTimestamp = Date.now();
88
109
  const quoteQueue: TickerRecord[] = [];
110
+ const quoteSnapshotQueue: TickerRecord[] = [];
89
111
  const snapshotQueue: TickerRecord[] = [];
90
- for (const ticker of visibleFinancialTickers) {
112
+ const snapshotQueueSymbols = new Set<string>();
113
+ const useSnapshotForQuoteWarmup = sortPreferenceUsesQuote(activeSort);
114
+ const quoteWarmupTickers = selectQuoteWarmupTickers(
115
+ sortedTickers,
116
+ streamWindow,
117
+ financialsMap,
118
+ activeSort,
119
+ nowTimestamp,
120
+ );
121
+ for (const ticker of quoteWarmupTickers) {
91
122
  const financials = financialsMap.get(ticker.metadata.ticker);
92
123
  const quoteKey = visibleWarmupKey("quote", ticker);
124
+ const warmupWithSnapshot = useSnapshotForQuoteWarmup && ticker.metadata.assetCategory !== "OPT";
125
+ const warmupKey = warmupWithSnapshot ? visibleWarmupKey("snapshot", ticker) : quoteKey;
93
126
  if (
94
127
  needsVisibleQuoteWarmup(financials, nowTimestamp)
95
- && !warmupInFlightRef.current.has(quoteKey)
96
- && nowTimestamp - (warmupAttemptRef.current.get(quoteKey) ?? 0) >= VISIBLE_QUOTE_REFRESH_COOLDOWN_MS
128
+ && !warmupInFlightRef.current.has(warmupKey)
129
+ && nowTimestamp - (warmupAttemptRef.current.get(warmupKey) ?? 0) >= VISIBLE_QUOTE_REFRESH_COOLDOWN_MS
97
130
  ) {
98
- quoteQueue.push(ticker);
99
- continue;
131
+ if (warmupWithSnapshot) {
132
+ quoteSnapshotQueue.push(ticker);
133
+ snapshotQueueSymbols.add(ticker.metadata.ticker);
134
+ } else {
135
+ quoteQueue.push(ticker);
136
+ }
100
137
  }
138
+ }
101
139
 
140
+ for (const ticker of visibleFinancialTickers) {
141
+ const financials = financialsMap.get(ticker.metadata.ticker);
142
+ if (snapshotQueueSymbols.has(ticker.metadata.ticker)) continue;
102
143
  const snapshotKey = visibleWarmupKey("snapshot", ticker);
103
144
  if (
104
145
  needsVisibleSnapshotWarmup(ticker, financials, visibleWarmupRequirements)
@@ -106,43 +147,56 @@ export function usePortfolioPaneStreaming({
106
147
  && nowTimestamp - (warmupAttemptRef.current.get(snapshotKey) ?? 0) >= VISIBLE_SNAPSHOT_REFRESH_COOLDOWN_MS
107
148
  ) {
108
149
  snapshotQueue.push(ticker);
150
+ snapshotQueueSymbols.add(ticker.metadata.ticker);
109
151
  }
110
152
  }
153
+ const limitedQuoteSnapshotQueue = quoteSnapshotQueue.slice(0, SORT_QUOTE_WARMUP_BATCH_LIMIT);
111
154
  const limitedSnapshotQueue = snapshotQueue.slice(0, VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT);
112
- if (quoteQueue.length === 0 && limitedSnapshotQueue.length === 0) return;
155
+ if (quoteQueue.length === 0 && limitedQuoteSnapshotQueue.length === 0 && limitedSnapshotQueue.length === 0) return;
113
156
 
114
157
  let cancelled = false;
115
158
  const runBatch = async (): Promise<void> => {
116
159
  const quoteEntries = quoteQueue.flatMap((ticker) => {
117
- const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
160
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, instrumentOptions);
118
161
  if (!instrument) return [];
119
162
  const key = visibleWarmupKey("quote", ticker);
120
163
  warmupInFlightRef.current.add(key);
121
164
  warmupAttemptRef.current.set(key, nowTimestamp);
122
165
  return [{ key, instrument }];
123
166
  });
124
- const snapshotEntries = limitedSnapshotQueue.flatMap((ticker) => {
125
- const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
167
+ const forcedSnapshotEntries = limitedQuoteSnapshotQueue.flatMap((ticker) => {
168
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, instrumentOptions);
126
169
  if (!instrument) return [];
127
170
  const key = visibleWarmupKey("snapshot", ticker);
128
171
  warmupInFlightRef.current.add(key);
129
172
  warmupAttemptRef.current.set(key, nowTimestamp);
130
173
  return [{ key, instrument }];
131
174
  });
132
- if (quoteEntries.length === 0 && snapshotEntries.length === 0) return;
175
+ const normalSnapshotEntries = limitedSnapshotQueue.flatMap((ticker) => {
176
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, instrumentOptions);
177
+ if (!instrument) return [];
178
+ const key = visibleWarmupKey("snapshot", ticker);
179
+ warmupInFlightRef.current.add(key);
180
+ warmupAttemptRef.current.set(key, nowTimestamp);
181
+ return [{ key, instrument }];
182
+ });
183
+ if (quoteEntries.length === 0 && forcedSnapshotEntries.length === 0 && normalSnapshotEntries.length === 0) return;
133
184
  try {
134
185
  await Promise.allSettled([
135
186
  quoteEntries.length > 0
136
187
  ? sharedCoordinator.loadQuotesBatch(quoteEntries.map((entry) => entry.instrument), { forceRefresh: true })
137
188
  : Promise.resolve(),
138
- snapshotEntries.length > 0
139
- ? sharedCoordinator.loadSnapshotsBatch(snapshotEntries.map((entry) => entry.instrument))
189
+ forcedSnapshotEntries.length > 0
190
+ ? sharedCoordinator.loadSnapshotsBatch(forcedSnapshotEntries.map((entry) => entry.instrument), { forceRefresh: true })
191
+ : Promise.resolve(),
192
+ normalSnapshotEntries.length > 0
193
+ ? sharedCoordinator.loadSnapshotsBatch(normalSnapshotEntries.map((entry) => entry.instrument))
140
194
  : Promise.resolve(),
141
195
  ]);
142
196
  } catch {
143
197
  // Best-effort warmup for visible rows only.
144
198
  } finally {
145
- for (const entry of [...quoteEntries, ...snapshotEntries]) {
199
+ for (const entry of [...quoteEntries, ...forcedSnapshotEntries, ...normalSnapshotEntries]) {
146
200
  warmupInFlightRef.current.delete(entry.key);
147
201
  }
148
202
  }
@@ -156,18 +210,22 @@ export function usePortfolioPaneStreaming({
156
210
  cancelled = true;
157
211
  clearTimeout(timeoutId);
158
212
  };
159
- }, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers, visibleWarmupRequirements]);
213
+ }, [activeSort, appActive, financialsMap, instrumentOptions, sharedCoordinator, sortedTickers, streamWindow, visibleFinancialTickers, visibleWarmupRequirements]);
160
214
 
161
215
  useEffect(() => {
162
216
  if (!appActive) return;
163
- if (!focused) return;
164
217
  if (!sharedCoordinator) return;
165
218
 
166
219
  let cancelled = false;
167
220
  const runWatchdog = async (): Promise<void> => {
168
221
  const nowTimestamp = Date.now();
169
- const quoteEntries = visibleFinancialTickers.flatMap((ticker) => {
170
- const financials = financialsMap.get(ticker.metadata.ticker);
222
+ const {
223
+ financialsMap: latestFinancialsMap,
224
+ instrumentOptions: latestInstrumentOptions,
225
+ visibleFinancialTickers: latestVisibleFinancialTickers,
226
+ } = watchdogInputsRef.current;
227
+ const quoteEntries = latestVisibleFinancialTickers.flatMap((ticker) => {
228
+ const financials = latestFinancialsMap.get(ticker.metadata.ticker);
171
229
  const key = visibleWarmupKey("quote", ticker);
172
230
  if (
173
231
  !needsVisibleQuoteWatchdogRefresh(financials, nowTimestamp)
@@ -176,7 +234,7 @@ export function usePortfolioPaneStreaming({
176
234
  ) {
177
235
  return [];
178
236
  }
179
- const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
237
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, latestInstrumentOptions);
180
238
  if (!instrument) return [];
181
239
  warmupInFlightRef.current.add(key);
182
240
  warmupAttemptRef.current.set(key, nowTimestamp);
@@ -195,6 +253,7 @@ export function usePortfolioPaneStreaming({
195
253
  }
196
254
  };
197
255
 
256
+ void runWatchdog();
198
257
  const intervalId = setInterval(() => {
199
258
  if (!cancelled && mountedRef.current) void runWatchdog();
200
259
  }, VISIBLE_QUOTE_STREAM_WATCHDOG_MS);
@@ -203,7 +262,7 @@ export function usePortfolioPaneStreaming({
203
262
  cancelled = true;
204
263
  clearInterval(intervalId);
205
264
  };
206
- }, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers]);
265
+ }, [appActive, sharedCoordinator]);
207
266
 
208
267
  useQuoteStreaming(streamTargets);
209
268
  }