gloomberb 0.10.1 → 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 (351) 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/remote/asset-data-client.ts +6 -44
  274. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  275. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  276. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  277. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  278. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  279. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  280. package/src/renderers/electrobun/view/update-host.ts +1 -1
  281. package/src/renderers/opentui/chart-surface.tsx +2 -1
  282. package/src/renderers/opentui/dialog-host.tsx +20 -4
  283. package/src/renderers/opentui/image/loader.ts +12 -2
  284. package/src/renderers/opentui/input-host.tsx +7 -49
  285. package/src/renderers/opentui/start.tsx +2 -0
  286. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  287. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  288. package/src/sources/gloomberb-cloud/index.ts +42 -20
  289. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  290. package/src/sources/provider-router/cache.ts +1 -1
  291. package/src/sources/provider-router/document-routes.ts +10 -5
  292. package/src/sources/provider-router/financials.test.ts +14 -0
  293. package/src/sources/provider-router/financials.ts +6 -1
  294. package/src/sources/provider-router/history.test.ts +163 -1
  295. package/src/sources/provider-router/history.ts +278 -347
  296. package/src/sources/provider-router/index.ts +2 -2
  297. package/src/sources/provider-router/routing.test.ts +39 -0
  298. package/src/sources/provider-router/routing.ts +24 -11
  299. package/src/sources/provider-router/supplemental.ts +37 -17
  300. package/src/sources/sec-edgar/content.ts +2 -12
  301. package/src/sources/sec-edgar.ts +2 -13
  302. package/src/sources/yahoo-finance/history.ts +2 -2
  303. package/src/sources/yahoo-finance/options.ts +5 -0
  304. package/src/sources/yahoo-finance.ts +2 -2
  305. package/src/state/app/bootstrap.ts +2 -2
  306. package/src/state/app/context/index.tsx +2 -2
  307. package/src/state/hooks/quote-streaming.ts +66 -1
  308. package/src/state/hooks/session-snapshot.ts +2 -2
  309. package/src/state/selectors.ts +0 -9
  310. package/src/sync/controller.test.ts +98 -0
  311. package/src/sync/controller.ts +29 -5
  312. package/src/sync/react.ts +2 -2
  313. package/src/sync/types.ts +2 -2
  314. package/src/theme/colors.test.ts +10 -0
  315. package/src/theme/colors.ts +75 -78
  316. package/src/theme/theme-context.test.tsx +67 -0
  317. package/src/theme/theme-context.tsx +9 -5
  318. package/src/tickers/open-target.ts +2 -2
  319. package/src/tickers/search/upsert.ts +2 -2
  320. package/src/time-series/alignment.ts +0 -7
  321. package/src/time-series/chart-data.ts +145 -0
  322. package/src/time-series/date-window.ts +44 -0
  323. package/src/time-series/history-window.ts +21 -0
  324. package/src/time-series/hooks.test.ts +59 -0
  325. package/src/time-series/hooks.ts +33 -12
  326. package/src/time-series/market-resolution.ts +2 -2
  327. package/src/time-series/market.ts +1 -1
  328. package/src/time-series/range.ts +9 -0
  329. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  330. package/src/time-series/resolve.test.ts +203 -1
  331. package/src/time-series/resolve.ts +96 -18
  332. package/src/time-series/spec-studies.test.ts +15 -0
  333. package/src/time-series/spec.ts +2 -2
  334. package/src/time-series/studies.ts +5 -2
  335. package/src/time-series/types.ts +3 -1
  336. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  337. package/src/time-series/use-chart-resolution.ts +46 -4
  338. package/src/types/broker.ts +2 -2
  339. package/src/types/data-provider.ts +3 -3
  340. package/src/types/financials.ts +12 -0
  341. package/src/types/plugin.ts +2 -2
  342. package/src/ui/context-menu.tsx +3 -2
  343. package/src/ui/dialog.tsx +2 -2
  344. package/src/utils/html-entities.ts +3 -5
  345. package/src/utils/main-thread-monitor.ts +1 -1
  346. package/src/utils/price-history.ts +5 -5
  347. package/src/version.ts +3 -1
  348. package/src/components/chart/core/viewport.test.ts +0 -8
  349. package/src/components/chart/core/viewport.ts +0 -24
  350. package/src/time-series/index.ts +0 -14
  351. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -1,15 +1,18 @@
1
- import type { MarketDataRequestContext } from "../../types/data-provider";
1
+ import type { BrokerCandidate } from "./brokers";
2
+ import { withBrokerTimeout } from "./brokers";
3
+ import type { DataProvider, MarketDataRequestContext } from "../../types/data-provider";
2
4
  import type { PricePoint } from "../../types/financials";
3
- import type { TimeRange } from "../../components/chart/core/types";
5
+ import type { TimeRange } from "../../time-series/range";
4
6
  import {
5
7
  isIntradayResolution,
6
8
  normalizeChartResolutionSupport,
7
9
  type ChartResolutionSupport,
8
10
  type ManualChartResolution,
9
- } from "../../components/chart/core/resolution";
11
+ } from "../../time-series/resolution";
10
12
  import { canonicalExchange } from "../../utils/exchanges";
11
13
  import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
12
14
  import { isPriceHistoryStaleForCurrentWindow, normalizePriceHistory } from "../../utils/price-history";
15
+ import { shouldLogProviderError } from "../provider-errors";
13
16
  import {
14
17
  buildVariantKey,
15
18
  compactDate,
@@ -17,53 +20,105 @@ import {
17
20
  isIntradayRange,
18
21
  isStaleIntradayHistory,
19
22
  selectCachedArrayResource,
23
+ type ProviderRouterCachePolicyKey,
20
24
  } from "./cache";
21
- import { shouldLogProviderError } from "../provider-errors";
22
- import { withBrokerTimeout } from "./brokers";
23
25
  import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
26
+ import { makeRouterRequestIdentity, type RouterRequestIdentity } from "./routing";
27
+
28
+ type PriceHistoryCachePolicyKey = Extract<
29
+ ProviderRouterCachePolicyKey,
30
+ "priceHistoryIntraday" | "priceHistoryDaily"
31
+ >;
32
+ const PRICE_HISTORY_CACHE_VERSION = 3;
33
+
34
+ interface HistoryRequestDescriptor {
35
+ identity: RouterRequestIdentity;
36
+ cacheVariantKeys: string[];
37
+ context?: MarketDataRequestContext;
38
+ cachePolicyKey: PriceHistoryCachePolicyKey;
39
+ missingProviderError?: string;
40
+ isCachedValueStale(value: PricePoint[]): boolean;
41
+ isFetchedValueStale(value: PricePoint[]): boolean;
42
+ fetchBroker(candidate: BrokerCandidate): Promise<PricePoint[] | null>;
43
+ fetchProvider(provider: DataProvider): Promise<PricePoint[] | null>;
44
+ }
24
45
 
25
- function buildPriceHistoryVariantKey(
46
+ function priceHistoryVariantParts(
26
47
  parts: Array<[string, string | number | undefined | null]>,
27
48
  exchange: string,
28
- ): string {
49
+ ): Array<[string, string | number | undefined | null]> {
29
50
  const unit = resolvePriceHistoryCurrencyUnit(null, exchange);
30
- const unitPart: Array<[string, string]> = unit.divisor === 1 ? [] : [["unit", unit.currency]];
31
- return buildVariantKey([...parts, ...unitPart]);
51
+ const versionedParts: Array<[string, string | number | undefined | null]> = [
52
+ ...parts,
53
+ ["version", PRICE_HISTORY_CACHE_VERSION],
54
+ ];
55
+ return unit.divisor === 1
56
+ ? versionedParts
57
+ : [...versionedParts, ["unit", unit.currency]];
58
+ }
59
+
60
+ function makeHistoryRequestIdentity(
61
+ deps: Pick<ProviderRouterCoreDeps, "getEntityKey">,
62
+ input: {
63
+ kind: string;
64
+ ticker: string;
65
+ exchange: string;
66
+ context?: MarketDataRequestContext;
67
+ variantParts: Array<[string, string | number | undefined | null]>;
68
+ fallbackVariantParts: Array<[string, string | number | undefined | null]>;
69
+ },
70
+ ): Pick<HistoryRequestDescriptor, "identity" | "cacheVariantKeys"> {
71
+ const identity = makeRouterRequestIdentity(deps, {
72
+ kind: input.kind,
73
+ ticker: input.ticker,
74
+ context: input.context,
75
+ variantParts: priceHistoryVariantParts(input.variantParts, input.exchange),
76
+ });
77
+ return {
78
+ identity,
79
+ cacheVariantKeys: [
80
+ identity.variantKey,
81
+ buildVariantKey(priceHistoryVariantParts(input.fallbackVariantParts, input.exchange)),
82
+ ],
83
+ };
32
84
  }
33
85
 
34
86
  export class ProviderRouterHistoryRoutes {
35
87
  constructor(private readonly deps: ProviderRouterCoreDeps) {}
36
88
 
37
- async getPriceHistory(ticker: string, exchange: string, range: TimeRange, context?: MarketDataRequestContext): Promise<PricePoint[]> {
38
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
39
- const variantKeys = [
40
- buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["range", range]], exchange),
41
- buildPriceHistoryVariantKey([["range", range]], exchange),
42
- ];
43
- const sourceKeys = [
44
- ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
45
- ...this.deps.getProviderSourceKeys(),
46
- ];
47
- const cached = selectCachedArrayResource<PricePoint>(this.deps.resources, "price-history", entityKey, variantKeys, sourceKeys, false);
48
- const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
49
- const cachedHistoryStale = isStaleIntradayHistory(cachedValue, isIntradayRange(range), exchange);
50
- const forceRefresh = context?.cacheMode === "refresh";
51
- if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale && !cachedHistoryStale) {
52
- return cachedValue;
53
- }
54
-
55
- const brokerHistory = await withBrokerTimeout(this.fetchBrokerPriceHistory(ticker, exchange, range, context));
56
- if (brokerHistory && brokerHistory.value.length > 0) return brokerHistory.value;
57
-
58
- const providerHistory = await this.fetchProviderPriceHistory(ticker, exchange, range, context);
59
- if (providerHistory && providerHistory.value.length > 0) {
60
- return providerHistory.value;
61
- }
62
- if (cachedValue.length > 0 && !cachedHistoryStale) return cachedValue;
63
- if (!providerHistory) {
64
- throw new Error(`No history provider available for ${ticker}`);
65
- }
66
- return providerHistory.value;
89
+ async getPriceHistory(
90
+ ticker: string,
91
+ exchange: string,
92
+ range: TimeRange,
93
+ context?: MarketDataRequestContext,
94
+ ): Promise<PricePoint[]> {
95
+ const identity = makeHistoryRequestIdentity(this.deps, {
96
+ kind: "price-history",
97
+ ticker,
98
+ exchange,
99
+ context,
100
+ variantParts: [["exchange", canonicalExchange(exchange)], ["range", range]],
101
+ fallbackVariantParts: [["range", range]],
102
+ });
103
+ const intraday = isIntradayRange(range);
104
+ return this.executeHistoryRequest({
105
+ ...identity,
106
+ context,
107
+ cachePolicyKey: intraday ? "priceHistoryIntraday" : "priceHistoryDaily",
108
+ missingProviderError: `No history provider available for ${ticker}`,
109
+ isCachedValueStale: (value) => isStaleIntradayHistory(value, intraday, exchange),
110
+ isFetchedValueStale: (value) => isStaleIntradayHistory(value, intraday, exchange),
111
+ fetchBroker: async (candidate) => candidate.broker.getPriceHistory
112
+ ? candidate.broker.getPriceHistory(
113
+ ticker,
114
+ candidate.instance,
115
+ exchange,
116
+ range,
117
+ context?.instrument ?? null,
118
+ )
119
+ : null,
120
+ fetchProvider: (provider) => provider.getPriceHistory(ticker, exchange, range, context),
121
+ });
67
122
  }
68
123
 
69
124
  async getPriceHistoryForResolution(
@@ -73,35 +128,40 @@ export class ProviderRouterHistoryRoutes {
73
128
  resolution: ManualChartResolution,
74
129
  context?: MarketDataRequestContext,
75
130
  ): Promise<PricePoint[]> {
76
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
77
- const variantKeys = [
78
- buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["range", bufferRange], ["resolution", resolution]], exchange),
79
- buildPriceHistoryVariantKey([["range", bufferRange], ["resolution", resolution]], exchange),
80
- ];
81
- const sourceKeys = [
82
- ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
83
- ...this.deps.getProviderSourceKeys(),
84
- ];
85
- const cached = selectCachedArrayResource<PricePoint>(this.deps.resources, "price-history", entityKey, variantKeys, sourceKeys, false);
86
- const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
87
- const cachedHistoryStale = isStaleIntradayHistory(cachedValue, isIntradayResolution(resolution), exchange);
88
- const forceRefresh = context?.cacheMode === "refresh";
89
- if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale && !cachedHistoryStale) {
90
- return cachedValue;
91
- }
92
-
93
- const brokerHistory = await withBrokerTimeout(this.fetchBrokerPriceHistoryForResolution(ticker, exchange, bufferRange, resolution, context));
94
- if (brokerHistory && brokerHistory.value.length > 0) return brokerHistory.value;
95
-
96
- const providerHistory = await this.fetchProviderPriceHistoryForResolution(ticker, exchange, bufferRange, resolution, context);
97
- if (providerHistory && providerHistory.value.length > 0) {
98
- return providerHistory.value;
99
- }
100
- if (cachedValue.length > 0 && !cachedHistoryStale) return cachedValue;
101
- if (!providerHistory) {
102
- throw new Error(`No resolution-aware history provider available for ${ticker}`);
103
- }
104
- return providerHistory.value;
131
+ const identity = makeHistoryRequestIdentity(this.deps, {
132
+ kind: "price-history",
133
+ ticker,
134
+ exchange,
135
+ context,
136
+ variantParts: [
137
+ ["exchange", canonicalExchange(exchange)],
138
+ ["range", bufferRange],
139
+ ["resolution", resolution],
140
+ ],
141
+ fallbackVariantParts: [["range", bufferRange], ["resolution", resolution]],
142
+ });
143
+ const intraday = isIntradayResolution(resolution);
144
+ return this.executeHistoryRequest({
145
+ ...identity,
146
+ context,
147
+ cachePolicyKey: intraday ? "priceHistoryIntraday" : "priceHistoryDaily",
148
+ missingProviderError: `No resolution-aware history provider available for ${ticker}`,
149
+ isCachedValueStale: (value) => isStaleIntradayHistory(value, intraday, exchange),
150
+ isFetchedValueStale: (value) => isStaleIntradayHistory(value, intraday, exchange),
151
+ fetchBroker: async (candidate) => candidate.broker.getPriceHistoryForResolution
152
+ ? candidate.broker.getPriceHistoryForResolution(
153
+ ticker,
154
+ candidate.instance,
155
+ exchange,
156
+ bufferRange,
157
+ resolution,
158
+ context?.instrument ?? null,
159
+ )
160
+ : null,
161
+ fetchProvider: async (provider) => provider.getPriceHistoryForResolution
162
+ ? provider.getPriceHistoryForResolution(ticker, exchange, bufferRange, resolution, context)
163
+ : null,
164
+ });
105
165
  }
106
166
 
107
167
  async getChartResolutionSupport(
@@ -109,11 +169,44 @@ export class ProviderRouterHistoryRoutes {
109
169
  exchange?: string,
110
170
  context?: MarketDataRequestContext,
111
171
  ): Promise<ChartResolutionSupport[]> {
112
- const brokerSupport = await withBrokerTimeout(this.fetchBrokerChartResolutionSupport(ticker, exchange, context));
113
- if (brokerSupport && brokerSupport.value.length > 0) {
114
- return brokerSupport.value;
115
- }
116
- const providerSupport = await this.fetchProviderChartResolutionSupport(ticker, exchange, context);
172
+ const candidates = this.deps.getBrokerCandidatesForContext(context, false);
173
+ const brokerSupport = await withBrokerTimeout(this.firstBrokerResult(candidates, async (candidate) => {
174
+ const result = candidate.broker.getChartResolutionSupport
175
+ ? normalizeChartResolutionSupport(await candidate.broker.getChartResolutionSupport(
176
+ ticker,
177
+ candidate.instance,
178
+ exchange,
179
+ context?.instrument ?? null,
180
+ ))
181
+ : candidate.broker.getChartResolutionCapabilities
182
+ ? normalizeChartResolutionSupport(
183
+ (await candidate.broker.getChartResolutionCapabilities(
184
+ ticker,
185
+ candidate.instance,
186
+ exchange,
187
+ context?.instrument ?? null,
188
+ )).map((resolution) => ({ resolution, maxRange: "ALL" })),
189
+ )
190
+ : null;
191
+ return result && result.length > 0 ? result : null;
192
+ }));
193
+ if (brokerSupport) return brokerSupport.value;
194
+
195
+ const providerSupport = await this.firstProviderArrayResult(async (provider) => {
196
+ if (provider.canProvide && !await provider.canProvide(ticker, exchange, context)) {
197
+ return null;
198
+ }
199
+ if (provider.getChartResolutionSupport) {
200
+ return normalizeChartResolutionSupport(await provider.getChartResolutionSupport(ticker, exchange, context));
201
+ }
202
+ if (provider.getChartResolutionCapabilities) {
203
+ return normalizeChartResolutionSupport(
204
+ (await provider.getChartResolutionCapabilities(ticker, exchange, context))
205
+ .map((resolution) => ({ resolution, maxRange: "ALL" })),
206
+ );
207
+ }
208
+ return null;
209
+ });
117
210
  return providerSupport?.value ?? [];
118
211
  }
119
212
 
@@ -134,315 +227,153 @@ export class ProviderRouterHistoryRoutes {
134
227
  barSize: string,
135
228
  context?: MarketDataRequestContext,
136
229
  ): Promise<PricePoint[]> {
137
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
138
- const variantKeys = [
139
- buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]], exchange),
140
- buildPriceHistoryVariantKey([["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]], exchange),
230
+ const primaryParts: Array<[string, string | number | undefined | null]> = [
231
+ ["exchange", canonicalExchange(exchange)],
232
+ ["start", compactDate(startDate)],
233
+ ["end", compactDate(endDate)],
234
+ ["bar", barSize],
141
235
  ];
236
+ const fallbackParts = primaryParts.slice(1);
237
+ const identity = makeHistoryRequestIdentity(this.deps, {
238
+ kind: "detailed-price-history",
239
+ ticker,
240
+ exchange,
241
+ context,
242
+ variantParts: primaryParts,
243
+ fallbackVariantParts: fallbackParts,
244
+ });
245
+ const currentWindowAtLookup = isCurrentHistoryWindow(endDate);
246
+ return this.executeHistoryRequest({
247
+ ...identity,
248
+ context,
249
+ cachePolicyKey: "priceHistoryIntraday",
250
+ isCachedValueStale: (value) => currentWindowAtLookup
251
+ && isPriceHistoryStaleForCurrentWindow(value, Date.now(), { exchange }),
252
+ isFetchedValueStale: (value) => isCurrentHistoryWindow(endDate)
253
+ && isPriceHistoryStaleForCurrentWindow(value, Date.now(), { exchange }),
254
+ fetchBroker: async (candidate) => candidate.broker.getDetailedPriceHistory
255
+ ? candidate.broker.getDetailedPriceHistory(
256
+ ticker,
257
+ candidate.instance,
258
+ exchange,
259
+ startDate,
260
+ endDate,
261
+ barSize,
262
+ context?.instrument ?? null,
263
+ )
264
+ : null,
265
+ fetchProvider: async (provider) => provider.getDetailedPriceHistory
266
+ ? provider.getDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context)
267
+ : null,
268
+ });
269
+ }
270
+
271
+ private async executeHistoryRequest(request: HistoryRequestDescriptor): Promise<PricePoint[]> {
272
+ const brokerCandidates = this.deps.getBrokerCandidatesForContext(request.context, false);
142
273
  const sourceKeys = [
143
- ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
274
+ ...brokerCandidates.map((candidate) => this.deps.brokerSourceKey(candidate)),
144
275
  ...this.deps.getProviderSourceKeys(),
145
276
  ];
146
- const cached = selectCachedArrayResource<PricePoint>(this.deps.resources, "detailed-price-history", entityKey, variantKeys, sourceKeys, false);
277
+ const cached = selectCachedArrayResource<PricePoint>(
278
+ this.deps.resources,
279
+ request.identity.kind,
280
+ request.identity.entityKey,
281
+ request.cacheVariantKeys,
282
+ sourceKeys,
283
+ false,
284
+ );
147
285
  const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
148
- const isCurrentWindow = isCurrentHistoryWindow(endDate);
149
- const cachedHistoryStale = isCurrentWindow && isPriceHistoryStaleForCurrentWindow(cachedValue, Date.now(), { exchange });
150
- const forceRefresh = context?.cacheMode === "refresh";
286
+ const cachedHistoryStale = request.isCachedValueStale(cachedValue);
287
+ const forceRefresh = request.context?.cacheMode === "refresh";
151
288
  if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale && !cachedHistoryStale) {
152
289
  return cachedValue;
153
290
  }
154
291
 
155
- const brokerResult = await withBrokerTimeout(this.fetchBrokerDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context));
292
+ const brokerResult = await withBrokerTimeout(this.fetchBrokerHistory(request, brokerCandidates));
156
293
  if (brokerResult && brokerResult.value.length > 0) return brokerResult.value;
157
294
 
158
- const providerResult = await this.fetchProviderDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context);
159
- if (providerResult && providerResult.value.length > 0) {
160
- return providerResult.value;
161
- }
162
- return cachedValue.length > 0 && !cachedHistoryStale ? cachedValue : (providerResult?.value ?? []);
163
- }
164
-
165
- private async fetchBrokerPriceHistory(
166
- ticker: string,
167
- exchange: string,
168
- range: TimeRange,
169
- context?: MarketDataRequestContext,
170
- ): Promise<SourceResult<PricePoint[]> | null> {
171
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
172
- const variantKey = buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["range", range]], exchange);
173
- for (const candidate of this.deps.getBrokerCandidatesForContext(context, false)) {
174
- if (!candidate.broker.getPriceHistory) continue;
175
- try {
176
- const result = normalizePriceHistory(await candidate.broker.getPriceHistory(
177
- ticker,
178
- candidate.instance,
179
- exchange,
180
- range,
181
- context?.instrument ?? null,
182
- ));
183
- if (isStaleIntradayHistory(result, isIntradayRange(range), exchange)) continue;
184
- this.deps.cacheResource(
185
- "price-history",
186
- entityKey,
187
- variantKey,
188
- this.deps.brokerSourceKey(candidate),
189
- result,
190
- this.deps.resolveBrokerPolicy(isIntradayRange(range) ? "priceHistoryIntraday" : "priceHistoryDaily", candidate.broker),
191
- );
192
- return { sourceKey: this.deps.brokerSourceKey(candidate), value: result };
193
- } catch {
194
- // continue
195
- }
196
- }
197
- return null;
198
- }
199
-
200
- private async fetchBrokerPriceHistoryForResolution(
201
- ticker: string,
202
- exchange: string,
203
- bufferRange: TimeRange,
204
- resolution: ManualChartResolution,
205
- context?: MarketDataRequestContext,
206
- ): Promise<SourceResult<PricePoint[]> | null> {
207
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
208
- const variantKey = buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["range", bufferRange], ["resolution", resolution]], exchange);
209
- for (const candidate of this.deps.getBrokerCandidatesForContext(context, false)) {
210
- if (!candidate.broker.getPriceHistoryForResolution) continue;
211
- try {
212
- const result = normalizePriceHistory(await candidate.broker.getPriceHistoryForResolution(
213
- ticker,
214
- candidate.instance,
215
- exchange,
216
- bufferRange,
217
- resolution,
218
- context?.instrument ?? null,
219
- ));
220
- if (isStaleIntradayHistory(result, isIntradayResolution(resolution), exchange)) continue;
221
- this.deps.cacheResource(
222
- "price-history",
223
- entityKey,
224
- variantKey,
225
- this.deps.brokerSourceKey(candidate),
226
- result,
227
- this.deps.resolveBrokerPolicy(isIntradayResolution(resolution) ? "priceHistoryIntraday" : "priceHistoryDaily", candidate.broker),
228
- );
229
- return { sourceKey: this.deps.brokerSourceKey(candidate), value: result };
230
- } catch {
231
- // continue
232
- }
233
- }
234
- return null;
235
- }
236
-
237
- private async fetchProviderPriceHistory(
238
- ticker: string,
239
- exchange: string,
240
- range: TimeRange,
241
- context?: MarketDataRequestContext,
242
- ): Promise<SourceResult<PricePoint[]> | null> {
243
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
244
- const variantKey = buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["range", range]], exchange);
245
- let firstEmptyResult: SourceResult<PricePoint[]> | null = null;
246
-
247
- for (const provider of this.deps.providersInPriorityOrder()) {
248
- try {
249
- const value = normalizePriceHistory(await provider.getPriceHistory(ticker, exchange, range, context));
250
- if (isStaleIntradayHistory(value, isIntradayRange(range), exchange)) continue;
251
- const sourceKey = this.deps.providerSourceKey(provider);
252
- this.deps.cacheResource(
253
- "price-history",
254
- entityKey,
255
- variantKey,
256
- sourceKey,
257
- value,
258
- this.deps.resolveProviderPolicy(isIntradayRange(range) ? "priceHistoryIntraday" : "priceHistoryDaily", provider),
259
- );
260
- if (value.length > 0) {
261
- return { sourceKey, value };
262
- }
263
- firstEmptyResult ??= { sourceKey, value };
264
- } catch (error) {
265
- if (shouldLogProviderError(error)) {
266
- this.deps.logProviderError(`${provider.id} failed: ${error}`);
267
- }
268
- }
295
+ const providerResult = await this.fetchProviderHistory(request);
296
+ if (providerResult && providerResult.value.length > 0) return providerResult.value;
297
+ if (cachedValue.length > 0 && !cachedHistoryStale) return cachedValue;
298
+ if (!providerResult && request.missingProviderError) {
299
+ throw new Error(request.missingProviderError);
269
300
  }
270
-
271
- return firstEmptyResult;
301
+ return providerResult?.value ?? [];
272
302
  }
273
303
 
274
- private async fetchProviderPriceHistoryForResolution(
275
- ticker: string,
276
- exchange: string,
277
- bufferRange: TimeRange,
278
- resolution: ManualChartResolution,
279
- context?: MarketDataRequestContext,
304
+ private fetchBrokerHistory(
305
+ request: HistoryRequestDescriptor,
306
+ candidates: BrokerCandidate[],
280
307
  ): Promise<SourceResult<PricePoint[]> | null> {
281
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
282
- const variantKey = buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["range", bufferRange], ["resolution", resolution]], exchange);
283
- let firstEmptyResult: SourceResult<PricePoint[]> | null = null;
284
-
285
- for (const provider of this.deps.providersInPriorityOrder()) {
286
- if (!provider.getPriceHistoryForResolution) continue;
287
- try {
288
- const value = normalizePriceHistory(await provider.getPriceHistoryForResolution(ticker, exchange, bufferRange, resolution, context));
289
- if (isStaleIntradayHistory(value, isIntradayResolution(resolution), exchange)) continue;
290
- const sourceKey = this.deps.providerSourceKey(provider);
291
- this.deps.cacheResource(
292
- "price-history",
293
- entityKey,
294
- variantKey,
295
- sourceKey,
296
- value,
297
- this.deps.resolveProviderPolicy(isIntradayResolution(resolution) ? "priceHistoryIntraday" : "priceHistoryDaily", provider),
298
- );
299
- if (value.length > 0) {
300
- return { sourceKey, value };
301
- }
302
- firstEmptyResult ??= { sourceKey, value };
303
- } catch (error) {
304
- if (shouldLogProviderError(error)) {
305
- this.deps.logProviderError(`${provider.id} failed: ${error}`);
306
- }
307
- }
308
- }
309
-
310
- return firstEmptyResult;
308
+ return this.firstBrokerResult(candidates, async (candidate) => {
309
+ const fetched = await request.fetchBroker(candidate);
310
+ if (fetched === null) return null;
311
+ const value = normalizePriceHistory(fetched);
312
+ if (request.isFetchedValueStale(value)) return null;
313
+ this.deps.cacheResource(
314
+ request.identity.kind,
315
+ request.identity.entityKey,
316
+ request.identity.variantKey,
317
+ this.deps.brokerSourceKey(candidate),
318
+ value,
319
+ this.deps.resolveBrokerPolicy(request.cachePolicyKey, candidate.broker),
320
+ );
321
+ return value;
322
+ });
311
323
  }
312
324
 
313
- private async fetchBrokerChartResolutionSupport(
314
- ticker: string,
315
- exchange?: string,
316
- context?: MarketDataRequestContext,
317
- ): Promise<SourceResult<ChartResolutionSupport[]> | null> {
318
- for (const candidate of this.deps.getBrokerCandidatesForContext(context, false)) {
319
- if (!candidate.broker.getChartResolutionSupport && !candidate.broker.getChartResolutionCapabilities) continue;
320
- try {
321
- const result = candidate.broker.getChartResolutionSupport
322
- ? normalizeChartResolutionSupport(await candidate.broker.getChartResolutionSupport(
323
- ticker,
324
- candidate.instance,
325
- exchange,
326
- context?.instrument ?? null,
327
- ))
328
- : normalizeChartResolutionSupport(
329
- (await candidate.broker.getChartResolutionCapabilities!(
330
- ticker,
331
- candidate.instance,
332
- exchange,
333
- context?.instrument ?? null,
334
- )).map((resolution) => ({ resolution, maxRange: "ALL" })),
335
- );
336
- if (result.length === 0) continue;
337
- return { sourceKey: this.deps.brokerSourceKey(candidate), value: result };
338
- } catch {
339
- // continue
340
- }
341
- }
342
- return null;
325
+ private fetchProviderHistory(request: HistoryRequestDescriptor): Promise<SourceResult<PricePoint[]> | null> {
326
+ return this.firstProviderArrayResult(async (provider) => {
327
+ const fetched = await request.fetchProvider(provider);
328
+ if (fetched === null) return null;
329
+ const value = normalizePriceHistory(fetched);
330
+ if (request.isFetchedValueStale(value)) return null;
331
+ this.deps.cacheResource(
332
+ request.identity.kind,
333
+ request.identity.entityKey,
334
+ request.identity.variantKey,
335
+ this.deps.providerSourceKey(provider),
336
+ value,
337
+ this.deps.resolveProviderPolicy(request.cachePolicyKey, provider),
338
+ );
339
+ return value;
340
+ });
343
341
  }
344
342
 
345
- private async fetchProviderChartResolutionSupport(
346
- ticker: string,
347
- exchange?: string,
348
- context?: MarketDataRequestContext,
349
- ): Promise<SourceResult<ChartResolutionSupport[]> | null> {
350
- for (const provider of this.deps.providersInPriorityOrder()) {
351
- if (!provider.getChartResolutionSupport && !provider.getChartResolutionCapabilities) continue;
343
+ private async firstBrokerResult<T>(
344
+ candidates: BrokerCandidate[],
345
+ fetch: (candidate: BrokerCandidate) => Promise<T | null>,
346
+ ): Promise<SourceResult<T> | null> {
347
+ for (const candidate of candidates) {
352
348
  try {
353
- const result = provider.getChartResolutionSupport
354
- ? normalizeChartResolutionSupport(await provider.getChartResolutionSupport(ticker, exchange, context))
355
- : normalizeChartResolutionSupport(
356
- (await provider.getChartResolutionCapabilities!(ticker, exchange, context)).map((resolution) => ({ resolution, maxRange: "ALL" })),
357
- );
358
- if (result.length === 0) continue;
359
- return { sourceKey: this.deps.providerSourceKey(provider), value: result };
360
- } catch (error) {
361
- if (shouldLogProviderError(error)) {
362
- this.deps.logProviderError(`${provider.id} failed: ${error}`);
349
+ const value = await fetch(candidate);
350
+ if (value !== null) {
351
+ return { sourceKey: this.deps.brokerSourceKey(candidate), value };
363
352
  }
364
- }
365
- }
366
-
367
- return null;
368
- }
369
-
370
- private async fetchBrokerDetailedPriceHistory(
371
- ticker: string,
372
- exchange: string,
373
- startDate: Date,
374
- endDate: Date,
375
- barSize: string,
376
- context?: MarketDataRequestContext,
377
- ): Promise<SourceResult<PricePoint[]> | null> {
378
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
379
- const variantKey = buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]], exchange);
380
- for (const candidate of this.deps.getBrokerCandidatesForContext(context, false)) {
381
- if (!candidate.broker.getDetailedPriceHistory) continue;
382
- try {
383
- const result = normalizePriceHistory(await candidate.broker.getDetailedPriceHistory(
384
- ticker,
385
- candidate.instance,
386
- exchange,
387
- startDate,
388
- endDate,
389
- barSize,
390
- context?.instrument ?? null,
391
- ));
392
- if (isCurrentHistoryWindow(endDate) && isPriceHistoryStaleForCurrentWindow(result, Date.now(), { exchange })) continue;
393
- this.deps.cacheResource(
394
- "detailed-price-history",
395
- entityKey,
396
- variantKey,
397
- this.deps.brokerSourceKey(candidate),
398
- result,
399
- this.deps.resolveBrokerPolicy("priceHistoryIntraday", candidate.broker),
400
- );
401
- return { sourceKey: this.deps.brokerSourceKey(candidate), value: result };
402
353
  } catch {
403
- // continue
354
+ // Continue through the broker candidates.
404
355
  }
405
356
  }
406
357
  return null;
407
358
  }
408
359
 
409
- private async fetchProviderDetailedPriceHistory(
410
- ticker: string,
411
- exchange: string,
412
- startDate: Date,
413
- endDate: Date,
414
- barSize: string,
415
- context?: MarketDataRequestContext,
416
- ): Promise<SourceResult<PricePoint[]> | null> {
417
- const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
418
- const variantKey = buildPriceHistoryVariantKey([["exchange", canonicalExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]], exchange);
419
- let firstEmptyResult: SourceResult<PricePoint[]> | null = null;
420
-
360
+ private async firstProviderArrayResult<T>(
361
+ fetch: (provider: DataProvider) => Promise<T[] | null>,
362
+ ): Promise<SourceResult<T[]> | null> {
363
+ let firstEmptyResult: SourceResult<T[]> | null = null;
421
364
  for (const provider of this.deps.providersInPriorityOrder()) {
422
- if (!provider.getDetailedPriceHistory) continue;
423
365
  try {
424
- const value = normalizePriceHistory(await provider.getDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context));
425
- if (isCurrentHistoryWindow(endDate) && isPriceHistoryStaleForCurrentWindow(value, Date.now(), { exchange })) continue;
426
- const sourceKey = this.deps.providerSourceKey(provider);
427
- this.deps.cacheResource(
428
- "detailed-price-history",
429
- entityKey,
430
- variantKey,
431
- sourceKey,
432
- value,
433
- this.deps.resolveProviderPolicy("priceHistoryIntraday", provider),
434
- );
435
- if (value.length > 0) {
436
- return { sourceKey, value };
437
- }
438
- firstEmptyResult ??= { sourceKey, value };
366
+ const value = await fetch(provider);
367
+ if (value === null) continue;
368
+ const result = { sourceKey: this.deps.providerSourceKey(provider), value };
369
+ if (value.length > 0) return result;
370
+ firstEmptyResult ??= result;
439
371
  } catch (error) {
440
372
  if (shouldLogProviderError(error)) {
441
373
  this.deps.logProviderError(`${provider.id} failed: ${error}`);
442
374
  }
443
375
  }
444
376
  }
445
-
446
377
  return firstEmptyResult;
447
378
  }
448
379
  }