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
@@ -1,4 +1,5 @@
1
1
  import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
2
+ import { useNativeRenderer } from "../../ui";
2
3
  import {
3
4
  InputHostProvider,
4
5
  shouldDeliverShortcut,
@@ -19,6 +20,7 @@ interface ShortcutEntry {
19
20
  let nextShortcutOrder = 1;
20
21
 
21
22
  export function OpenTuiInputHostProvider({ children }: { children: ReactNode }) {
23
+ const renderer = useNativeRenderer();
22
24
  const shortcutsRef = useRef<ShortcutEntry[]>([]);
23
25
  const dispatchShortcut = (event: KeyEventLike) => {
24
26
  const shortcuts = shortcutsRef.current;
@@ -34,7 +36,9 @@ export function OpenTuiInputHostProvider({ children }: { children: ReactNode })
34
36
  };
35
37
 
36
38
  useKeyboard((event) => {
37
- dispatchShortcut(toKeyEventLike(event));
39
+ const shortcutEvent = toKeyEventLike(event);
40
+ shortcutEvent.targetEditable = renderer.currentFocusedEditor != null;
41
+ dispatchShortcut(shortcutEvent);
38
42
  });
39
43
 
40
44
  const host = useMemo<InputHost>(() => ({
@@ -450,78 +450,6 @@ describe("GloomberbCloudProvider", () => {
450
450
  expect(unsubscribeCalled).toBe(true);
451
451
  });
452
452
 
453
- test("maps backend news stories without requiring verified market-data access", async () => {
454
- type CloudNewsRequest = NonNullable<Parameters<typeof apiClient.getCloudNews>[0]>;
455
- let verifyCalled = false;
456
- const requestArgs: { current: CloudNewsRequest | null } = { current: null };
457
- apiClient.ensureVerifiedSession = async () => {
458
- verifyCalled = true;
459
- return verifiedUser;
460
- };
461
- apiClient.getCloudNews = async (params = {}) => {
462
- requestArgs.current = params;
463
- return {
464
- items: [{
465
- id: "story-1",
466
- headline: "Apple raises guidance",
467
- summary: "Apple lifted its outlook after stronger iPhone demand.",
468
- topic: "guidance",
469
- topics: ["guidance"],
470
- category: "guidance",
471
- sentiment: "positive",
472
- sectors: ["information_technology"],
473
- firstPublishedAt: "2026-04-01T10:00:00.000Z",
474
- lastPublishedAt: "2026-04-01T10:05:00.000Z",
475
- firstSeenAt: "2026-04-01T10:00:10.000Z",
476
- lastSeenAt: "2026-04-01T10:05:10.000Z",
477
- primaryUrl: "https://example.com/aapl-guidance",
478
- primarySource: "example-wire",
479
- scores: {
480
- importance: 91,
481
- urgency: 74,
482
- marketImpact: 88,
483
- novelty: 86,
484
- confidence: 95,
485
- },
486
- flags: {
487
- breaking: true,
488
- developing: false,
489
- stale: false,
490
- },
491
- variantCount: 1,
492
- sourceCount: 1,
493
- sources: ["example-wire"],
494
- entities: [],
495
- tickerLinks: [{
496
- symbol: "AAPL",
497
- exchange: "XNAS",
498
- canonicalTicker: "AAPL:XNAS",
499
- relationType: "direct",
500
- displayTier: "primary",
501
- confidence: 0.98,
502
- relevanceScore: 95,
503
- impactScore: 93,
504
- sentiment: "positive",
505
- }],
506
- }],
507
- nextCursor: null,
508
- };
509
- };
510
-
511
- const provider = new GloomberbCloudProvider();
512
- const news = await provider.getNews("AAPL", 10, "NASDAQ");
513
-
514
- expect(verifyCalled).toBe(false);
515
- expect(requestArgs.current).toEqual({ feed: "ticker", ticker: "AAPL", exchange: "XNAS", tickerTier: "primary", limit: 10 });
516
- expect(news).toEqual([{
517
- title: "Apple raises guidance",
518
- url: "https://example.com/aapl-guidance",
519
- source: "example-wire",
520
- publishedAt: new Date("2026-04-01T10:05:00.000Z"),
521
- summary: "Apple lifted its outlook after stronger iPhone demand.",
522
- }]);
523
- });
524
-
525
453
  test("maps news ticker labels from validated story links only", async () => {
526
454
  apiClient.getCloudNews = async () => ({
527
455
  items: [{
@@ -9,11 +9,9 @@ import type {
9
9
  AssetDataProvider,
10
10
  CachedFinancialsTarget,
11
11
  MarketDataRequestContext,
12
- NewsItem,
13
12
  QuoteBatchResult,
14
13
  QuoteSubscriptionTarget,
15
14
  SearchRequestContext,
16
- SecFilingItem,
17
15
  TickerFinancialsBatchResult,
18
16
  } from "../../types/data-provider";
19
17
  import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
@@ -27,12 +25,11 @@ import {
27
25
  } from "../../api-client";
28
26
  import type { NewsArticle, NewsQuery } from "../../types/news-source";
29
27
  import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
30
- import { canonicalTickerKey, publicExchange } from "../../utils/exchanges";
28
+ import { canonicalTickerKey } from "../../utils/exchanges";
31
29
  import { createProviderMiss } from "../provider-errors";
32
30
  import {
33
31
  cloudNewsParams,
34
32
  mapCloudNewsArticle,
35
- mapCloudNewsItem,
36
33
  } from "./news";
37
34
  import {
38
35
  GLOOMBERB_CLOUD_PROVIDER_ID,
@@ -237,20 +234,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
237
234
  );
238
235
  }
239
236
 
240
- async getNews(ticker: string, count?: number, exchange?: string, _context?: MarketDataRequestContext): Promise<NewsItem[]> {
241
- const response = await withCloudFallback(
242
- () => apiClient.getCloudNews({
243
- feed: "ticker",
244
- ticker,
245
- exchange: exchange ? publicExchange(exchange) : undefined,
246
- tickerTier: "primary",
247
- limit: count,
248
- }),
249
- `Cloud news is unavailable for ${ticker}`,
250
- );
251
- return response.items.map(mapCloudNewsItem);
252
- }
253
-
254
237
  async getHolders(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<HolderData> {
255
238
  await requireVerifiedSession();
256
239
  return withCloudFallback(async () => {
@@ -275,18 +258,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
275
258
  }, `Cloud corporate actions are unavailable for ${ticker}`);
276
259
  }
277
260
 
278
- async getSecFilings(_ticker: string, _count?: number, _exchange?: string, _context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
279
- throw createProviderMiss("Cloud SEC filings are not available");
280
- }
281
-
282
- async getSecFilingDocuments(_filing: SecFilingItem) {
283
- throw createProviderMiss("Cloud filing documents are not available");
284
- }
285
-
286
- async getSecFilingContent(_filing: SecFilingItem): Promise<string | null> {
287
- throw createProviderMiss("Cloud filing content is not available");
288
- }
289
-
290
261
  async getArticleSummary(_url: string): Promise<string | null> {
291
262
  throw createProviderMiss("Cloud article summaries are not available");
292
263
  }
@@ -1,5 +1,4 @@
1
1
  import { collectNewsDisplayTickers } from "../../news/ticker-symbols";
2
- import type { NewsItem } from "../../types/data-provider";
3
2
  import type { NewsArticle, NewsQuery, NewsStoryItem } from "../../types/news-source";
4
3
  import type {
5
4
  CloudNewsPayload,
@@ -90,17 +89,6 @@ export function mapCloudNewsArticle(
90
89
  };
91
90
  }
92
91
 
93
- export function mapCloudNewsItem(item: CloudNewsPayload): NewsItem {
94
- const article = mapCloudNewsArticle(item);
95
- return {
96
- title: article.title,
97
- url: article.url,
98
- source: article.source,
99
- publishedAt: article.publishedAt,
100
- summary: article.summary,
101
- };
102
- }
103
-
104
92
  export function cloudNewsParams(query: NewsQuery): CloudNewsParams {
105
93
  const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
106
94
  return {
@@ -12,8 +12,10 @@ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshnes
12
12
  import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
13
13
  import { selectCachedResource } from "./cache";
14
14
  import {
15
+ dropUnusableProviderQuote,
15
16
  hasDeepStatementHistory,
16
17
  hasDetailedStatementRows,
18
+ isProviderQuoteUsableForCurrentSession,
17
19
  quoteWithFreshnessExchange,
18
20
  type CachedFinancialsSelection,
19
21
  } from "./financials";
@@ -52,8 +54,9 @@ export class ProviderRouterBatchRoutes {
52
54
  const context = target.context;
53
55
  const entityKey = this.deps.getEntityKey(target.symbol, context?.instrument);
54
56
  const variantKeys = this.deps.getTickerVariantCandidates(target.exchange);
57
+ const brokerSourceKeys = this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate));
55
58
  const sourceKeys = [
56
- ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
59
+ ...brokerSourceKeys,
57
60
  ...this.deps.getProviderSourceKeys(),
58
61
  ];
59
62
  const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
@@ -61,6 +64,10 @@ export class ProviderRouterBatchRoutes {
61
64
  ? rawCached
62
65
  : null;
63
66
  if (cached && !forceRefresh && !cached.stale) {
67
+ if (brokerSourceKeys.includes(cached.sourceKey)) {
68
+ misses.push({ index, target });
69
+ return;
70
+ }
64
71
  results[index] = { target, quote: cached.value };
65
72
  return;
66
73
  }
@@ -80,7 +87,7 @@ export class ProviderRouterBatchRoutes {
80
87
  const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
81
88
  const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
82
89
  for (const item of batchResults) {
83
- if (!item.quote || isQuoteStaleForCurrentSession(item.quote)) continue;
90
+ if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange)) continue;
84
91
  const key = this.quoteBatchKey(item.target);
85
92
  const sourceKey = this.deps.providerSourceKey(batchProvider);
86
93
  for (const entry of providerIndexes.get(key) ?? []) {
@@ -142,7 +149,8 @@ export class ProviderRouterBatchRoutes {
142
149
  for (const item of batchResults) {
143
150
  if (!item.financials) continue;
144
151
  const key = this.cachedFinancialsBatchKey(item.target);
145
- const value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
152
+ let value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
153
+ if (value) value = dropUnusableProviderQuote(value, item.target.exchange);
146
154
  if (!value) continue;
147
155
  const sourceKey = this.deps.providerSourceKey(batchProvider);
148
156
  for (const entry of providerIndexes.get(key) ?? []) {
@@ -405,6 +405,129 @@ describe("AssetDataRouter cached financials", () => {
405
405
  persistence.close();
406
406
  });
407
407
 
408
+ test("uses symbol provider quote reference for broker-linked snapshot reads", async () => {
409
+ const dbPath = createTempDbPath("cached-symbol-reference-for-contract-snapshot");
410
+ const persistence = new AppPersistence(dbPath);
411
+ const now = Date.now();
412
+
413
+ persistence.resources.set(
414
+ {
415
+ namespace: "market",
416
+ kind: "financials",
417
+ entityKey: "contract:275759",
418
+ variantKey: "exchange=NASDAQ",
419
+ sourceKey: "provider:gloomberb-cloud",
420
+ },
421
+ makeFinancials({
422
+ quote: makeQuote({
423
+ symbol: "VICR",
424
+ providerId: "gloomberb-cloud",
425
+ price: 292.83,
426
+ currency: "USD",
427
+ previousClose: 380.07,
428
+ change: -87.24,
429
+ changePercent: -22.95,
430
+ lastUpdated: now,
431
+ marketState: "REGULAR",
432
+ exchangeName: "NASDAQ",
433
+ listingExchangeName: "NASDAQ",
434
+ dataSource: "live",
435
+ }),
436
+ profile: {
437
+ description: "Cloud profile",
438
+ },
439
+ fundamentals: {
440
+ trailingPE: 94.3,
441
+ },
442
+ }),
443
+ {
444
+ cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
445
+ fetchedAt: now,
446
+ },
447
+ );
448
+ persistence.resources.set(
449
+ {
450
+ namespace: "market",
451
+ kind: "financials",
452
+ entityKey: "contract:275759",
453
+ variantKey: "exchange=NASDAQ",
454
+ sourceKey: "provider:yahoo",
455
+ },
456
+ makeFinancials({
457
+ annualStatements: [{ date: "2025-12-31", totalRevenue: 100 }],
458
+ }),
459
+ {
460
+ cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
461
+ fetchedAt: now + 1_000,
462
+ },
463
+ );
464
+ persistence.resources.set(
465
+ {
466
+ namespace: "market",
467
+ kind: "financials",
468
+ entityKey: "VICR",
469
+ variantKey: "exchange=NASDAQ",
470
+ sourceKey: "provider:yahoo",
471
+ },
472
+ makeFinancials({
473
+ quote: makeQuote({
474
+ symbol: "VICR",
475
+ providerId: "yahoo",
476
+ price: 294.39,
477
+ currency: "USD",
478
+ previousClose: 282.95,
479
+ change: 11.44,
480
+ changePercent: 4.04,
481
+ lastUpdated: now - 1_000,
482
+ marketState: "REGULAR",
483
+ exchangeName: "NASDAQ",
484
+ listingExchangeName: "NASDAQ",
485
+ dataSource: "delayed",
486
+ }),
487
+ }),
488
+ {
489
+ cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
490
+ fetchedAt: now - 1_000,
491
+ },
492
+ );
493
+
494
+ const router = new AssetDataRouter({
495
+ ...fallbackProvider,
496
+ id: "yahoo",
497
+ name: "Yahoo",
498
+ async getTickerFinancials() {
499
+ throw new Error("should not fetch financials");
500
+ },
501
+ }, [{
502
+ ...fallbackProvider,
503
+ id: "gloomberb-cloud",
504
+ name: "Cloud",
505
+ priority: 100,
506
+ async getTickerFinancials() {
507
+ throw new Error("should not fetch financials");
508
+ },
509
+ }], persistence.resources);
510
+
511
+ const financials = await router.getTickerFinancials("VICR", "NASDAQ", {
512
+ brokerId: "ibkr",
513
+ brokerInstanceId: "ibkr-work",
514
+ instrument: {
515
+ brokerId: "ibkr",
516
+ brokerInstanceId: "ibkr-work",
517
+ conId: 275759,
518
+ symbol: "VICR",
519
+ },
520
+ });
521
+
522
+ expect(financials.quote?.providerId).toBe("gloomberb-cloud");
523
+ expect(financials.quote?.price).toBe(292.83);
524
+ expect(financials.quote?.previousClose).toBe(282.95);
525
+ expect(financials.quote?.changePercent).toBeCloseTo(((292.83 - 282.95) / 282.95) * 100, 10);
526
+ expect(financials.quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
527
+
528
+ persistence.close();
529
+ });
530
+
408
531
  test("overlays standalone quote cache on stale financial snapshots during startup priming", () => {
409
532
  const dbPath = createTempDbPath("cached-quote-over-financial-snapshot");
410
533
  const persistence = new AppPersistence(dbPath);
@@ -43,6 +43,19 @@ export interface ProviderRouterFinancialRouteDeps extends Pick<
43
43
  primaryRoutes: ProviderRouterPrimaryRoutes;
44
44
  }
45
45
 
46
+ function withoutSessionState(quote: Quote): Quote {
47
+ const fallback = { ...quote };
48
+ delete fallback.marketState;
49
+ delete fallback.sessionConfidence;
50
+ delete fallback.preMarketPrice;
51
+ delete fallback.preMarketChange;
52
+ delete fallback.preMarketChangePercent;
53
+ delete fallback.postMarketPrice;
54
+ delete fallback.postMarketChange;
55
+ delete fallback.postMarketChangePercent;
56
+ return fallback;
57
+ }
58
+
46
59
  export class ProviderRouterFinancialRoutes {
47
60
  constructor(private readonly deps: ProviderRouterFinancialRouteDeps) {}
48
61
 
@@ -81,7 +94,9 @@ export class ProviderRouterFinancialRoutes {
81
94
  quarterlyStatements: base?.quarterlyStatements ?? [],
82
95
  priceHistory: base?.priceHistory ?? [],
83
96
  });
84
- const cached = this.readCachedMergedFinancialsSelection(ticker, exchange, context, false);
97
+ const cached = this.readCachedMergedFinancialsSelection(ticker, exchange, context, false, {
98
+ includeSymbolProviderFallback: true,
99
+ });
85
100
  const forceRefresh = context?.cacheMode === "refresh";
86
101
  if (cached.value && !forceRefresh) {
87
102
  if (isOptionTicker && !cached.value.quote) {
@@ -128,8 +143,9 @@ export class ProviderRouterFinancialRoutes {
128
143
  async getQuote(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<Quote> {
129
144
  const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
130
145
  const variantKeys = this.deps.getTickerVariantCandidates(exchange);
146
+ const brokerSourceKeys = this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate));
131
147
  const sourceKeys = [
132
- ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
148
+ ...brokerSourceKeys,
133
149
  ...this.deps.getProviderSourceKeys(),
134
150
  ];
135
151
  const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
@@ -138,12 +154,19 @@ export class ProviderRouterFinancialRoutes {
138
154
  : null;
139
155
  const forceRefresh = context?.cacheMode === "refresh";
140
156
  if (cached && !forceRefresh && !cached.stale) {
141
- return cached.value;
157
+ if (!brokerSourceKeys.includes(cached.sourceKey)) return cached.value;
158
+ return this.mergeBrokerQuoteWithProviderReference(
159
+ cached.value,
160
+ await this.getProviderReferenceQuote(ticker, exchange, context),
161
+ );
142
162
  }
143
163
 
144
164
  const brokerQuote = await withBrokerTimeout(this.deps.primaryRoutes.fetchBrokerQuote(ticker, exchange, context));
145
165
  if (brokerQuote && !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(brokerQuote.value, exchange))) {
146
- return brokerQuote.value;
166
+ return this.mergeBrokerQuoteWithProviderReference(
167
+ brokerQuote.value,
168
+ await this.getProviderReferenceQuote(ticker, exchange, context),
169
+ );
147
170
  }
148
171
 
149
172
  const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context);
@@ -154,6 +177,54 @@ export class ProviderRouterFinancialRoutes {
154
177
  throw new Error(`No quote provider available for ${ticker}`);
155
178
  }
156
179
 
180
+ private async getProviderReferenceQuote(
181
+ ticker: string,
182
+ exchange?: string,
183
+ context?: MarketDataRequestContext,
184
+ ): Promise<Quote | null> {
185
+ const cached = context?.cacheMode === "refresh"
186
+ ? null
187
+ : this.readCachedProviderQuote(ticker, exchange, context);
188
+ if (cached) return cached;
189
+ const staleFallback = this.readCachedProviderQuote(ticker, exchange, context, true);
190
+ const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context);
191
+ if (providerQuote) return providerQuote.value;
192
+ return staleFallback ? withoutSessionState(staleFallback) : null;
193
+ }
194
+
195
+ private readCachedProviderQuote(
196
+ ticker: string,
197
+ exchange?: string,
198
+ context?: MarketDataRequestContext,
199
+ includeStale = false,
200
+ ): Quote | null {
201
+ const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
202
+ const entityKeys = [...new Set([entityKey, normalizeTicker(ticker)])];
203
+ const variantKeys = this.deps.getTickerVariantCandidates(exchange);
204
+ const sourceKeys = this.deps.getProviderSourceKeys();
205
+ for (const candidateEntityKey of entityKeys) {
206
+ const record = selectCachedResource<Quote>(this.deps.resources, "quote", candidateEntityKey, variantKeys, sourceKeys, false);
207
+ if (
208
+ record
209
+ && (includeStale || !record.stale)
210
+ && !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(record.value, exchange))
211
+ ) {
212
+ return record.value;
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+
218
+ private mergeBrokerQuoteWithProviderReference(brokerQuote: Quote, providerQuote: Quote | null): Quote {
219
+ if (!providerQuote) return brokerQuote;
220
+ return resolveTickerFinancialsQuoteState({
221
+ quote: providerQuote,
222
+ annualStatements: [],
223
+ quarterlyStatements: [],
224
+ priceHistory: [],
225
+ }, brokerQuote)?.quote ?? brokerQuote;
226
+ }
227
+
157
228
  readCachedMergedFinancialsSelection(
158
229
  ticker: string,
159
230
  exchange?: string,
@@ -171,7 +242,8 @@ export class ProviderRouterFinancialRoutes {
171
242
  ? { ...brokerRecord, value: sanitizeCachedFinancials(brokerRecord.value, options) }
172
243
  : null;
173
244
  const providerSourceKeys = this.deps.getProviderSourceKeys();
174
- const providerEntityKeys = options.includeSymbolProviderFallback
245
+ const includeSymbolProviderFallback = options.includeSymbolProviderFallback !== false;
246
+ const providerEntityKeys = includeSymbolProviderFallback
175
247
  ? [...new Set([entityKey, normalizeTicker(ticker)])]
176
248
  : [entityKey];
177
249
  const providerRecords = sortCachedRecords(
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ dropUnusableProviderQuote,
4
+ isProviderQuoteUsableForCurrentSession,
5
+ } from "./financials";
6
+ import { makeFinancials, makeQuote } from "./test-support";
7
+
8
+ describe("provider-router financial quote usability", () => {
9
+ test("rejects active-session labels without active-session prices", () => {
10
+ expect(isProviderQuoteUsableForCurrentSession(makeQuote({
11
+ listingExchangeName: "NASDAQ",
12
+ marketState: "PRE",
13
+ lastUpdated: Date.now(),
14
+ }), "NASDAQ")).toBe(false);
15
+ });
16
+
17
+ test("rejects old active-session provider quotes", () => {
18
+ expect(isProviderQuoteUsableForCurrentSession(makeQuote({
19
+ listingExchangeName: "FWB2",
20
+ marketState: "REGULAR",
21
+ lastUpdated: Date.now() - 20 * 60_000,
22
+ }), "FWB2")).toBe(false);
23
+ });
24
+
25
+ test("rejects empty zero provider quotes", () => {
26
+ expect(isProviderQuoteUsableForCurrentSession(makeQuote({
27
+ price: 0,
28
+ change: 0,
29
+ changePercent: 0,
30
+ listingExchangeName: "SFB",
31
+ lastUpdated: Date.now(),
32
+ }), "SFB")).toBe(false);
33
+ });
34
+
35
+ test("strips unusable quotes while preserving non-quote financials", () => {
36
+ const value = dropUnusableProviderQuote(makeFinancials({
37
+ profile: { sector: "Industrials" },
38
+ quote: makeQuote({
39
+ price: 0,
40
+ change: 0,
41
+ changePercent: 0,
42
+ listingExchangeName: "SFB",
43
+ }),
44
+ }), "SFB");
45
+
46
+ expect(value.profile?.sector).toBe("Industrials");
47
+ expect(value.quote).toBeUndefined();
48
+ });
49
+ });
@@ -77,6 +77,46 @@ export function quoteWithFreshnessExchange(quote: Quote, exchange?: string): Quo
77
77
  };
78
78
  }
79
79
 
80
+ function finitePositiveNumber(value: unknown): value is number {
81
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
82
+ }
83
+
84
+ const ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS = 10 * 60_000;
85
+ const ACTIVE_QUOTE_MARKET_STATES = new Set(["PRE", "REGULAR", "POST"]);
86
+
87
+ function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
88
+ if (!ACTIVE_QUOTE_MARKET_STATES.has(quote.marketState ?? "")) return false;
89
+ if (!Number.isFinite(quote.lastUpdated)) return false;
90
+ return now - quote.lastUpdated > ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
91
+ }
92
+
93
+ export function isProviderQuoteUsableForCurrentSession(quote: Quote | null | undefined, exchange?: string): quote is Quote {
94
+ if (!quote) return false;
95
+ const normalized = quoteWithFreshnessExchange(quote, exchange);
96
+ if (isQuoteStaleForCurrentSession(normalized)) return false;
97
+ if (isActiveProviderQuoteTooOld(normalized)) return false;
98
+ return [
99
+ normalized.price,
100
+ normalized.preMarketPrice,
101
+ normalized.postMarketPrice,
102
+ normalized.bid,
103
+ normalized.ask,
104
+ normalized.mark,
105
+ ].some(finitePositiveNumber);
106
+ }
107
+
108
+ export function dropUnusableProviderQuote(value: TickerFinancials, exchange?: string): TickerFinancials {
109
+ if (!value.quote || isProviderQuoteUsableForCurrentSession(value.quote, exchange)) {
110
+ return value;
111
+ }
112
+
113
+ return {
114
+ ...value,
115
+ quote: undefined,
116
+ quoteContributions: undefined,
117
+ };
118
+ }
119
+
80
120
  function sanitizeCachedQuote(
81
121
  quote: Quote,
82
122
  exchange: string | undefined,
@@ -216,13 +256,10 @@ export function mergeCachedFinancialRecords(
216
256
  value: TickerFinancials | null;
217
257
  stale: boolean;
218
258
  } {
219
- const seenSources = new Set<string>();
220
259
  let merged: TickerFinancials | null = null;
221
260
  let stale = false;
222
261
 
223
262
  for (const record of records) {
224
- if (seenSources.has(record.sourceKey)) continue;
225
- seenSources.add(record.sourceKey);
226
263
  merged = mergeFinancials(merged, sanitizeCachedFinancials(record.value, options));
227
264
  stale = stale || record.stale === true;
228
265
  }