gloomberb 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { createGloomberbCloudCapabilities, GloomberbCloudProvider } from "./index";
3
+ import { getRangeStartDate, toHistoryRequest } from "./normalizers";
3
4
  import type { NewsCapability } from "../../capabilities";
4
5
  import { apiClient, type AuthUser, type CloudNewsPayload } from "../../api-client";
5
6
 
@@ -25,6 +26,17 @@ const originalGetCloudNews = apiClient.getCloudNews.bind(apiClient);
25
26
  const originalGetCloudNewsStory = apiClient.getCloudNewsStory.bind(apiClient);
26
27
  const originalSubscribeQuotes = apiClient.subscribeQuotes.bind(apiClient);
27
28
 
29
+ test("requests fifty years of cloud history for the ALL range", () => {
30
+ const endDate = new Date("2026-07-30T20:00:00.000Z");
31
+
32
+ expect(getRangeStartDate("ALL", endDate).toISOString()).toBe("1976-07-30T20:00:00.000Z");
33
+ expect(toHistoryRequest("ALL")).toEqual({
34
+ interval: "1month",
35
+ outputsize: 600,
36
+ rangeKey: "ALL",
37
+ });
38
+ });
39
+
28
40
  function makeCloudNewsPayload(overrides: Partial<CloudNewsPayload> = {}): CloudNewsPayload {
29
41
  return {
30
42
  id: "story-1",
@@ -124,6 +136,154 @@ describe("GloomberbCloudProvider", () => {
124
136
  expect(history[0]?.date.toISOString()).toBe("2026-03-27T14:15:00.000Z");
125
137
  });
126
138
 
139
+ test("rejects isolated cloud OHLC spikes so another history source can be used", async () => {
140
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
141
+ apiClient.getCloudHistory = async () => ({
142
+ status: "success",
143
+ data: [
144
+ {
145
+ date: "2026-07-29T20:46:00Z",
146
+ open: 728.4,
147
+ high: 728.7,
148
+ low: 728.3,
149
+ close: 728.5,
150
+ volume: 0,
151
+ },
152
+ {
153
+ date: "2026-07-29T20:47:00Z",
154
+ open: 686.98,
155
+ high: 728.67,
156
+ low: 686.98,
157
+ close: 728.6,
158
+ volume: 0,
159
+ },
160
+ {
161
+ date: "2026-07-29T20:48:00Z",
162
+ open: 728.6,
163
+ high: 728.7,
164
+ low: 728.1,
165
+ close: 728.3,
166
+ volume: 0,
167
+ },
168
+ ],
169
+ });
170
+
171
+ const provider = new GloomberbCloudProvider();
172
+
173
+ await expect(
174
+ provider.getPriceHistoryForResolution("SPY", "NYSEARCA", "1W", "1m"),
175
+ ).rejects.toThrow("failed OHLC validation");
176
+ });
177
+
178
+ test("validates an outlier after normalizing descending cloud history", async () => {
179
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
180
+ apiClient.getCloudHistory = async () => ({
181
+ status: "success",
182
+ data: [
183
+ {
184
+ date: "2026-07-29T20:48:00Z",
185
+ open: 728.6,
186
+ high: 728.7,
187
+ low: 728.1,
188
+ close: 728.3,
189
+ volume: 0,
190
+ },
191
+ {
192
+ date: "2026-07-29T20:47:00Z",
193
+ open: 686.98,
194
+ high: 728.67,
195
+ low: 686.98,
196
+ close: 728.6,
197
+ volume: 0,
198
+ },
199
+ {
200
+ date: "2026-07-29T20:46:00Z",
201
+ open: 728.4,
202
+ high: 728.7,
203
+ low: 728.3,
204
+ close: 728.5,
205
+ volume: 0,
206
+ },
207
+ ],
208
+ });
209
+
210
+ const provider = new GloomberbCloudProvider();
211
+
212
+ await expect(
213
+ provider.getPriceHistoryForResolution("SPY", "NYSEARCA", "1W", "1m"),
214
+ ).rejects.toThrow("failed OHLC validation");
215
+ });
216
+
217
+ test("preserves a wide cloud bar during a continuing price move", async () => {
218
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
219
+ apiClient.getCloudHistory = async () => ({
220
+ status: "success",
221
+ data: [
222
+ {
223
+ date: "2026-07-29T20:10:00Z",
224
+ open: 561.1,
225
+ high: 561.3,
226
+ low: 560.7,
227
+ close: 560.8994,
228
+ },
229
+ {
230
+ date: "2026-07-29T20:15:00Z",
231
+ open: 560.11,
232
+ high: 585.61,
233
+ low: 553.41,
234
+ close: 555.4,
235
+ },
236
+ {
237
+ date: "2026-07-29T20:20:00Z",
238
+ open: 555.2,
239
+ high: 556,
240
+ low: 548.9,
241
+ close: 549.01,
242
+ },
243
+ ],
244
+ });
245
+
246
+ const provider = new GloomberbCloudProvider();
247
+ const history = await provider.getPriceHistoryForResolution(
248
+ "META",
249
+ "NASDAQ",
250
+ "1W",
251
+ "5m",
252
+ );
253
+
254
+ expect(history).toHaveLength(3);
255
+ });
256
+
257
+ test("does not reject an explicitly Yahoo-backed cloud history response", async () => {
258
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
259
+ apiClient.getCloudHistory = async () => ({
260
+ status: "success",
261
+ providerMeta: { provider: "yahoo" },
262
+ data: [
263
+ { date: "2026-07-29T20:46:00Z", close: 728.5 },
264
+ {
265
+ date: "2026-07-29T20:47:00Z",
266
+ open: 686.98,
267
+ high: 728.67,
268
+ low: 686.98,
269
+ close: 728.6,
270
+ volume: 0,
271
+ },
272
+ { date: "2026-07-29T20:48:00Z", close: 728.3 },
273
+ ],
274
+ });
275
+
276
+ const provider = new GloomberbCloudProvider();
277
+ const history = await provider.getPriceHistoryForResolution(
278
+ "SPY",
279
+ "NYSEARCA",
280
+ "1W",
281
+ "1m",
282
+ );
283
+
284
+ expect(history[1]?.low).toBe(686.98);
285
+ });
286
+
127
287
  test("normalizes daily detailed history requests to 1day", async () => {
128
288
  apiClient.ensureVerifiedSession = async () => verifiedUser;
129
289
 
@@ -362,6 +522,11 @@ describe("GloomberbCloudProvider", () => {
362
522
  providerId: "gloomberb-cloud",
363
523
  underlyingSymbol: "AMD",
364
524
  expirationDates: [1_800_000_000],
525
+ dataSource: "live",
526
+ feed: "opra",
527
+ delayMinutes: 0,
528
+ realtimeEligible: true,
529
+ asOf: "2027-01-15T14:30:00.000Z",
365
530
  calls: [{
366
531
  contractSymbol: "AMD270917C00230000",
367
532
  strike: 230,
@@ -393,6 +558,14 @@ describe("GloomberbCloudProvider", () => {
393
558
  });
394
559
  expect(chain.underlyingSymbol).toBe("AMD");
395
560
  expect(chain.calls[0]?.contractSymbol).toBe("AMD270917C00230000");
561
+ expect(chain).toMatchObject({
562
+ providerId: "gloomberb-cloud",
563
+ dataSource: "live",
564
+ feed: "opra",
565
+ delayMinutes: 0,
566
+ realtimeEligible: true,
567
+ asOf: "2027-01-15T14:30:00.000Z",
568
+ });
396
569
  });
397
570
 
398
571
  test("preserves original target context when streaming quotes", () => {
@@ -1,9 +1,9 @@
1
- import type { TimeRange } from "../../components/chart/core/types";
1
+ import type { TimeRange } from "../../time-series/range";
2
2
  import {
3
3
  normalizeChartResolutionSupport,
4
4
  type ChartResolutionSupport,
5
5
  type ManualChartResolution,
6
- } from "../../components/chart/core/resolution";
6
+ } from "../../time-series/resolution";
7
7
  import { assetDataProvider, newsProvider, type PluginCapability } from "../../capabilities";
8
8
  import type {
9
9
  AssetDataProvider,
@@ -22,11 +22,14 @@ import {
22
22
  type CloudCorporateActionsPayload,
23
23
  type CloudHoldersPayload,
24
24
  type CloudMarketResponse,
25
+ type CloudPricePointPayload,
25
26
  } from "../../api-client";
26
27
  import type { NewsArticle, NewsQuery } from "../../types/news-source";
27
28
  import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
28
29
  import { canonicalTickerKey } from "../../utils/exchanges";
30
+ import { normalizePriceHistory } from "../../utils/price-history";
29
31
  import { createProviderMiss } from "../provider-errors";
32
+ import { hasMalformedCloudIntradayHistory } from "./history-quality";
30
33
  import {
31
34
  cloudNewsParams,
32
35
  mapCloudNewsArticle,
@@ -83,6 +86,40 @@ function isStaleCloudResponse(response: CloudMarketResponse<unknown>): boolean {
83
86
  return response.stale === true || response.providerMeta?.stale === true;
84
87
  }
85
88
 
89
+ function mapCloudPriceHistory(
90
+ response: CloudMarketResponse<CloudPricePointPayload[]>,
91
+ ticker: string,
92
+ exchange: string,
93
+ interval: string,
94
+ ): PricePoint[] {
95
+ if (isStaleCloudResponse(response)) {
96
+ throw createProviderMiss(`Cloud chart data is stale for ${ticker}`);
97
+ }
98
+ const { divisor } = resolvePriceHistoryCurrencyUnit(
99
+ response.currency ?? response.providerMeta?.currency,
100
+ exchange,
101
+ );
102
+ const points = normalizePriceHistory(
103
+ unwrapRequiredCloudResponse(
104
+ response,
105
+ `Cloud chart data is unavailable for ${ticker}`,
106
+ ).map((point) => mapPricePoint(point, divisor, exchange)),
107
+ );
108
+ const upstream = (
109
+ response.providerMeta?.provider
110
+ ?? response.providerMeta?.upstream
111
+ ?? ""
112
+ ).trim().toLowerCase();
113
+ if (
114
+ /(min|h)$/i.test(interval)
115
+ && upstream !== "yahoo"
116
+ && hasMalformedCloudIntradayHistory(points)
117
+ ) {
118
+ throw createProviderMiss(`Cloud chart data failed OHLC validation for ${ticker}`);
119
+ }
120
+ return points;
121
+ }
122
+
86
123
  function quoteTargetKey(symbol: string, exchange?: string): string {
87
124
  return canonicalTickerKey(symbol, exchange);
88
125
  }
@@ -269,12 +306,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
269
306
  () => apiClient.getCloudHistory(ticker, exchange, request),
270
307
  `Cloud chart data is unavailable for ${ticker}`,
271
308
  );
272
- if (isStaleCloudResponse(response)) {
273
- throw createProviderMiss(`Cloud chart data is stale for ${ticker}`);
274
- }
275
- const { divisor } = resolvePriceHistoryCurrencyUnit(response.currency ?? response.providerMeta?.currency, exchange);
276
- return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
277
- .map((point) => mapPricePoint(point, divisor, exchange));
309
+ return mapCloudPriceHistory(response, ticker, exchange, request.interval);
278
310
  }
279
311
 
280
312
  async getPriceHistoryForResolution(
@@ -297,12 +329,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
297
329
  }),
298
330
  `Cloud chart data is unavailable for ${ticker}`,
299
331
  );
300
- if (isStaleCloudResponse(response)) {
301
- throw createProviderMiss(`Cloud chart data is stale for ${ticker}`);
302
- }
303
- const { divisor } = resolvePriceHistoryCurrencyUnit(response.currency ?? response.providerMeta?.currency, exchange);
304
- return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
305
- .map((point) => mapPricePoint(point, divisor, exchange));
332
+ return mapCloudPriceHistory(response, ticker, exchange, interval);
306
333
  }
307
334
 
308
335
  async getDetailedPriceHistory(
@@ -324,12 +351,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
324
351
  }),
325
352
  `Cloud detailed chart history is unavailable for ${ticker}`,
326
353
  );
327
- if (isStaleCloudResponse(response)) {
328
- throw createProviderMiss(`Cloud detailed chart history is stale for ${ticker}`);
329
- }
330
- const { divisor } = resolvePriceHistoryCurrencyUnit(response.currency ?? response.providerMeta?.currency, exchange);
331
- return unwrapRequiredCloudResponse(response, `Cloud detailed chart history is unavailable for ${ticker}`)
332
- .map((point) => mapPricePoint(point, divisor, exchange));
354
+ return mapCloudPriceHistory(response, ticker, exchange, interval);
333
355
  }
334
356
 
335
357
  async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../../components/chart/core/types";
1
+ import type { TimeRange } from "../../time-series/range";
2
2
  import type {
3
3
  OptionsChain,
4
4
  PricePoint,
@@ -217,6 +217,12 @@ export function mapOptionsChain(
217
217
  expirationDates: chain.expirationDates ?? [],
218
218
  calls: chain.calls ?? [],
219
219
  puts: chain.puts ?? [],
220
+ providerId: chain.providerId,
221
+ dataSource: chain.dataSource,
222
+ feed: chain.feed,
223
+ delayMinutes: chain.delayMinutes,
224
+ realtimeEligible: chain.realtimeEligible,
225
+ asOf: chain.asOf,
220
226
  };
221
227
  }
222
228
 
@@ -320,7 +326,7 @@ export function getRangeStartDate(
320
326
  startDate.setFullYear(startDate.getFullYear() - 5);
321
327
  break;
322
328
  case "ALL":
323
- startDate.setFullYear(startDate.getFullYear() - 20);
329
+ startDate.setFullYear(startDate.getFullYear() - 50);
324
330
  break;
325
331
  }
326
332
  return startDate;
@@ -347,7 +353,7 @@ export function toHistoryRequest(range: TimeRange): {
347
353
  case "5Y":
348
354
  return { interval: "1week", outputsize: 261, rangeKey: range };
349
355
  case "ALL":
350
- return { interval: "1month", outputsize: 240, rangeKey: range };
356
+ return { interval: "1month", outputsize: 600, rangeKey: range };
351
357
  default:
352
358
  return { interval: "1day", outputsize: 366, rangeKey: range };
353
359
  }
@@ -1,5 +1,5 @@
1
1
  import type { CachedResourceRecord, ResourceStore } from "../../data/resource-store";
2
- import type { TimeRange } from "../../components/chart/core/types";
2
+ import type { TimeRange } from "../../time-series/range";
3
3
  import type { BrokerContractRef } from "../../types/instrument";
4
4
  import type { PricePoint } from "../../types/financials";
5
5
  import type { CachePolicy, CachePolicyMap } from "../../types/persistence";
@@ -13,7 +13,7 @@ import { shouldLogProviderError } from "../provider-errors";
13
13
  import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
14
14
  import {
15
15
  firstProviderResult,
16
- makeRouterRevalidationKey,
16
+ makeRouterRequestIdentity,
17
17
  resolveProviderBySourceKey,
18
18
  scheduleRouterRevalidation,
19
19
  } from "./routing";
@@ -24,15 +24,20 @@ export class ProviderRouterDocumentRoutes {
24
24
  constructor(private readonly deps: ProviderRouterCoreDeps) {}
25
25
 
26
26
  async getSecFilings(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
27
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
27
+ const identity = makeRouterRequestIdentity(this.deps, {
28
+ kind: "sec-filings",
29
+ ticker,
30
+ context,
31
+ variantParts: [["exchange", canonicalExchange(exchange)], ["count", count]],
32
+ });
28
33
  const variantKeys = [
29
- buildVariantKey([["exchange", canonicalExchange(exchange)], ["count", count]]),
34
+ identity.variantKey,
30
35
  buildVariantKey([["count", count]]),
31
36
  "",
32
37
  ];
33
- const cached = selectCachedResource<SecFilingItem[]>(this.deps.resources, "sec-filings", entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
38
+ const cached = selectCachedResource<SecFilingItem[]>(this.deps.resources, identity.kind, identity.entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
34
39
  if (cached) {
35
- scheduleRouterRevalidation(this.revalidationInFlight, makeRouterRevalidationKey(this.deps, "sec-filings", ticker, context, count), async () => {
40
+ scheduleRouterRevalidation(this.revalidationInFlight, identity.revalidationKey, async () => {
36
41
  await this.revalidateSecFilings(ticker, count, exchange, context);
37
42
  });
38
43
  return cached.value;
@@ -22,6 +22,20 @@ describe("provider-router financial quote usability", () => {
22
22
  }), "FWB2")).toBe(false);
23
23
  });
24
24
 
25
+ test("accepts a streamed 15-minute delayed quote during the active session", () => {
26
+ expect(
27
+ isProviderQuoteUsableForCurrentSession(
28
+ makeQuote({
29
+ dataSource: "delayed",
30
+ listingExchangeName: "NASDAQ",
31
+ marketState: "REGULAR",
32
+ lastUpdated: Date.now() - 15 * 60_000,
33
+ }),
34
+ "NASDAQ",
35
+ ),
36
+ ).toBe(true);
37
+ });
38
+
25
39
  test("rejects empty zero provider quotes", () => {
26
40
  expect(isProviderQuoteUsableForCurrentSession(makeQuote({
27
41
  price: 0,
@@ -82,12 +82,17 @@ function finitePositiveNumber(value: unknown): value is number {
82
82
  }
83
83
 
84
84
  const ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS = 10 * 60_000;
85
+ const ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS = 20 * 60_000;
85
86
  const ACTIVE_QUOTE_MARKET_STATES = new Set(["PRE", "REGULAR", "POST"]);
86
87
 
87
88
  function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
88
89
  if (!ACTIVE_QUOTE_MARKET_STATES.has(quote.marketState ?? "")) return false;
89
90
  if (!Number.isFinite(quote.lastUpdated)) return false;
90
- return now - quote.lastUpdated > ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
91
+ const maxAge =
92
+ quote.dataSource === "delayed"
93
+ ? ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS
94
+ : ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
95
+ return now - quote.lastUpdated > maxAge;
91
96
  }
92
97
 
93
98
  export function isProviderQuoteUsableForCurrentSession(quote: Quote | null | undefined, exchange?: string): quote is Quote {
@@ -135,6 +135,59 @@ describe("AssetDataRouter chart history", () => {
135
135
  persistence.close();
136
136
  });
137
137
 
138
+ test("ignores the previous chart history cache generation", async () => {
139
+ const dbPath = createTempDbPath("previous-chart-cache-generation");
140
+ const persistence = new AppPersistence(dbPath);
141
+ const latestDate = new Date(Date.now() - 60_000);
142
+ const previousDate = new Date(latestDate.getTime() - 5 * 60_000);
143
+
144
+ persistence.resources.set(
145
+ {
146
+ namespace: "market",
147
+ kind: "price-history",
148
+ entityKey: "META",
149
+ variantKey: "exchange=NASDAQ;range=1M;resolution=5m;version=2",
150
+ sourceKey: "provider:gloomberb-cloud",
151
+ },
152
+ [
153
+ { date: previousDate, close: 620 },
154
+ { date: latestDate, close: 680 },
155
+ ],
156
+ {
157
+ cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
158
+ },
159
+ );
160
+
161
+ let providerCalls = 0;
162
+ const router = new AssetDataRouter({
163
+ ...fallbackProvider,
164
+ id: "gloomberb-cloud",
165
+ name: "Gloomberb Cloud",
166
+ async getPriceHistoryForResolution() {
167
+ providerCalls += 1;
168
+ return [
169
+ { date: previousDate, close: 620 },
170
+ { date: latestDate, close: 560 },
171
+ ];
172
+ },
173
+ }, [], persistence.resources);
174
+
175
+ const history = await router.getPriceHistoryForResolution("META", "NASDAQ", "1M", "5m");
176
+
177
+ expect(providerCalls).toBe(1);
178
+ expect(history.map((point) => point.close)).toEqual([620, 560]);
179
+
180
+ const cachedRows = persistence.database.connection
181
+ .query("SELECT variant_key FROM resource_cache WHERE namespace = ? AND kind = ? AND entity_key = ? ORDER BY variant_key")
182
+ .all("market", "price-history", "META") as Array<{ variant_key: string }>;
183
+ expect(cachedRows.map((row) => row.variant_key)).toEqual([
184
+ "exchange=NASDAQ;range=1M;resolution=5m;version=2",
185
+ "exchange=NASDAQ;range=1M;resolution=5m;version=3",
186
+ ]);
187
+
188
+ persistence.close();
189
+ });
190
+
138
191
  test("ignores legacy unversioned sub-unit chart history caches", async () => {
139
192
  const dbPath = createTempDbPath("subunit-chart-cache");
140
193
  const persistence = new AppPersistence(dbPath);
@@ -180,7 +233,7 @@ describe("AssetDataRouter chart history", () => {
180
233
  .all("market", "price-history", "FTC") as Array<{ variant_key: string }>;
181
234
  expect(cachedRows.map((row) => row.variant_key)).toEqual([
182
235
  "exchange=LSE;range=ALL;resolution=1wk",
183
- "exchange=LSE;range=ALL;resolution=1wk;unit=GBP",
236
+ "exchange=LSE;range=ALL;resolution=1wk;version=3;unit=GBP",
184
237
  ]);
185
238
 
186
239
  persistence.close();
@@ -349,6 +402,115 @@ describe("AssetDataRouter chart history", () => {
349
402
  expect(await router.getChartResolutionCapabilities("AAPL", "NASDAQ")).toEqual(["1d", "1wk"]);
350
403
  });
351
404
 
405
+ test("skips unavailable providers when resolving chart resolution support", async () => {
406
+ let cloudSupportCalls = 0;
407
+ const cloudProvider: DataProvider = {
408
+ ...fallbackProvider,
409
+ id: "cloud",
410
+ name: "Cloud",
411
+ priority: 100,
412
+ async canProvide() {
413
+ return false;
414
+ },
415
+ getChartResolutionSupport() {
416
+ cloudSupportCalls += 1;
417
+ return [{ resolution: "1m", maxRange: "1W" }];
418
+ },
419
+ };
420
+ const yahooProvider: DataProvider = {
421
+ ...fallbackProvider,
422
+ id: "yahoo",
423
+ name: "Yahoo",
424
+ priority: 1000,
425
+ getChartResolutionSupport() {
426
+ return [{ resolution: "1d", maxRange: "5Y" }];
427
+ },
428
+ };
429
+
430
+ const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
431
+
432
+ expect(await router.getChartResolutionSupport("AAPL", "NASDAQ")).toEqual([
433
+ { resolution: "1d", maxRange: "5Y" },
434
+ ]);
435
+ expect(cloudSupportCalls).toBe(0);
436
+ });
437
+
438
+ test("refreshes each history request type before falling back to its cached value", async () => {
439
+ const dbPath = createTempDbPath("forced-history-refresh");
440
+ const persistence = new AppPersistence(dbPath);
441
+ const startDate = new Date("2026-03-28T09:30:00Z");
442
+ const endDate = new Date("2026-03-28T16:00:00Z");
443
+ const seedProvider: DataProvider = {
444
+ ...fallbackProvider,
445
+ async getPriceHistory() {
446
+ return [{ date: endDate, close: 101 }];
447
+ },
448
+ async getPriceHistoryForResolution() {
449
+ return [{ date: endDate, close: 102 }];
450
+ },
451
+ async getDetailedPriceHistory() {
452
+ return [{ date: endDate, close: 103 }];
453
+ },
454
+ };
455
+ const seedRouter = new AssetDataRouter(seedProvider, [], persistence.resources);
456
+ await seedRouter.getPriceHistory("AAPL", "NASDAQ", "1Y");
457
+ await seedRouter.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1d");
458
+ await seedRouter.getDetailedPriceHistory("AAPL", "NASDAQ", startDate, endDate, "15m");
459
+
460
+ const calls = { range: 0, resolution: 0, detailed: 0 };
461
+ const refreshProvider: DataProvider = {
462
+ ...fallbackProvider,
463
+ async getPriceHistory() {
464
+ calls.range += 1;
465
+ return [];
466
+ },
467
+ async getPriceHistoryForResolution() {
468
+ calls.resolution += 1;
469
+ return [];
470
+ },
471
+ async getDetailedPriceHistory() {
472
+ calls.detailed += 1;
473
+ return [];
474
+ },
475
+ };
476
+ const refreshRouter = new AssetDataRouter(refreshProvider, [], persistence.resources);
477
+
478
+ const range = await refreshRouter.getPriceHistory("AAPL", "NASDAQ", "1Y", { cacheMode: "refresh" });
479
+ const resolution = await refreshRouter.getPriceHistoryForResolution(
480
+ "AAPL",
481
+ "NASDAQ",
482
+ "1Y",
483
+ "1d",
484
+ { cacheMode: "refresh" },
485
+ );
486
+ const detailed = await refreshRouter.getDetailedPriceHistory(
487
+ "AAPL",
488
+ "NASDAQ",
489
+ startDate,
490
+ endDate,
491
+ "15m",
492
+ { cacheMode: "refresh" },
493
+ );
494
+
495
+ expect(calls).toEqual({ range: 1, resolution: 1, detailed: 1 });
496
+ expect([range[0]?.close, resolution[0]?.close, detailed[0]?.close]).toEqual([101, 102, 103]);
497
+
498
+ persistence.close();
499
+ });
500
+
501
+ test("preserves each history request type's missing-provider result", async () => {
502
+ const router = new AssetDataRouter(null);
503
+ const startDate = new Date("2026-03-28T09:30:00Z");
504
+ const endDate = new Date("2026-03-28T16:00:00Z");
505
+
506
+ await expect(router.getPriceHistory("AAPL", "NASDAQ", "1Y"))
507
+ .rejects.toThrow("No history provider available for AAPL");
508
+ await expect(router.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1d"))
509
+ .rejects.toThrow("No resolution-aware history provider available for AAPL");
510
+ await expect(router.getDetailedPriceHistory("AAPL", "NASDAQ", startDate, endDate, "15m"))
511
+ .resolves.toEqual([]);
512
+ });
513
+
352
514
  test("falls back to later providers when detailed chart history is empty", async () => {
353
515
  const cloudProvider: DataProvider = {
354
516
  ...fallbackProvider,