gloomberb 0.5.1 → 0.6.0

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 (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -14
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +99 -9
  440. package/src/utils/api-client.ts +378 -18
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,5 +1,6 @@
1
1
  import type { CachedResourceRecord, ResourceStore } from "../data/resource-store";
2
2
  import type { PluginRegistry } from "../plugins/registry";
3
+ import type { AssetDataCapability, NewsCapability } from "../capabilities";
3
4
  import type { BrokerAdapter } from "../types/broker";
4
5
  import type { AppConfig } from "../types/config";
5
6
  import { createDefaultConfig } from "../types/config";
@@ -7,12 +8,14 @@ import type {
7
8
  CachedFinancialsTarget,
8
9
  DataProvider,
9
10
  MarketDataRequestContext,
10
- NewsItem,
11
11
  QuoteSubscriptionTarget,
12
12
  SearchRequestContext,
13
13
  SecFilingItem,
14
14
  } from "../types/data-provider";
15
- import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
15
+ import type { CapabilityRouteSource } from "../types/capability-route-source";
16
+ import { routeSourcePriority } from "../types/capability-route-source";
17
+ import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
18
+ import type { NewsArticle, NewsQuery } from "../news/types";
16
19
  import type { BrokerContractRef, InstrumentSearchResult } from "../types/instrument";
17
20
  import type { CachePolicy, CachePolicyMap } from "../types/persistence";
18
21
  import type { TimeRange } from "../components/chart/chart-types";
@@ -24,6 +27,7 @@ import {
24
27
  } from "../components/chart/chart-resolution";
25
28
  import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
26
29
  import { debugLog } from "../utils/debug-log";
30
+ import { canonicalExchange } from "../utils/exchanges";
27
31
  import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../utils/price-history";
28
32
  import { isQuoteStaleForCurrentSession } from "../utils/quote-freshness";
29
33
  import {
@@ -34,7 +38,7 @@ import {
34
38
  } from "../utils/quote-resolution";
35
39
  import { isProviderMiss } from "./provider-errors";
36
40
 
37
- const providerLog = debugLog.createLogger("provider-router");
41
+ const providerLog = debugLog.createLogger("asset-data-router");
38
42
 
39
43
  const BROKER_ATTEMPT_TIMEOUT = 10_000;
40
44
  const EXPECTED_PROVIDER_MISS = /No data found|symbol may be delisted|"code":"Not Found"|No history for /i;
@@ -47,6 +51,9 @@ const DEFAULT_CACHE_POLICIES: Record<string, CachePolicy> = {
47
51
  priceHistoryIntraday: { staleMs: 5 * 60_000, expireMs: 2 * 24 * 60 * 60_000 },
48
52
  priceHistoryDaily: { staleMs: 24 * 60 * 60_000, expireMs: 30 * 24 * 60 * 60_000 },
49
53
  news: { staleMs: 15 * 60_000, expireMs: 2 * 24 * 60 * 60_000 },
54
+ holders: { staleMs: 24 * 60 * 60_000, expireMs: 7 * 24 * 60 * 60_000 },
55
+ analystResearch: { staleMs: 24 * 60 * 60_000, expireMs: 7 * 24 * 60 * 60_000 },
56
+ corporateActions: { staleMs: 24 * 60 * 60_000, expireMs: 14 * 24 * 60 * 60_000 },
50
57
  secFilings: { staleMs: 15 * 60_000, expireMs: 2 * 24 * 60 * 60_000 },
51
58
  secFilingContent: { staleMs: 30 * 24 * 60 * 60_000, expireMs: 365 * 24 * 60 * 60_000 },
52
59
  articleSummary: { staleMs: 30 * 24 * 60 * 60_000, expireMs: 90 * 24 * 60 * 60_000 },
@@ -74,10 +81,6 @@ function normalizeTicker(ticker: string): string {
74
81
  return ticker.trim().toUpperCase();
75
82
  }
76
83
 
77
- function normalizeExchange(exchange?: string): string {
78
- return (exchange ?? "").trim().toUpperCase();
79
- }
80
-
81
84
  function sanitizeCachedFinancials(financials: TickerFinancials): TickerFinancials {
82
85
  if (!isQuoteStaleForCurrentSession(financials.quote)) return financials;
83
86
  return {
@@ -143,6 +146,20 @@ function hasMeaningfulProfile(data: TickerFinancials | null | undefined): boolea
143
146
  );
144
147
  }
145
148
 
149
+ function hasAnalystResearchValue(data: AnalystResearchData): boolean {
150
+ return !!data.priceTarget
151
+ || data.recommendations.length > 0
152
+ || data.ratings.length > 0
153
+ || data.earningsEstimates.length > 0
154
+ || data.revenueEstimates.length > 0;
155
+ }
156
+
157
+ function hasCorporateActionsValue(data: CorporateActionsData): boolean {
158
+ return data.dividends.length > 0
159
+ || data.splits.length > 0
160
+ || data.earnings.length > 0;
161
+ }
162
+
146
163
  function hasLikelyPriceUnitMismatch(primary: TickerFinancials, fallback: TickerFinancials): boolean {
147
164
  return hasLikelyQuoteUnitMismatch(primary.quote, fallback.quote);
148
165
  }
@@ -229,20 +246,63 @@ interface SourceResult<T> {
229
246
  value: T;
230
247
  }
231
248
 
232
- export class ProviderRouter implements DataProvider {
233
- readonly id = "provider-router";
234
- readonly name = "Provider Router";
249
+ type RouteSourceCapability = AssetDataCapability | NewsCapability;
250
+
251
+ function routeSourceFromMarketProvider(provider: DataProvider): CapabilityRouteSource {
252
+ return {
253
+ id: provider.id,
254
+ name: provider.name,
255
+ priority: provider.priority,
256
+ cachePolicy: provider.cachePolicy,
257
+ market: provider,
258
+ };
259
+ }
260
+
261
+ function normalizeRouteSource(source: CapabilityRouteSource | DataProvider): CapabilityRouteSource {
262
+ return "market" in source || "news" in source
263
+ ? source as CapabilityRouteSource
264
+ : routeSourceFromMarketProvider(source as DataProvider);
265
+ }
266
+
267
+ function capabilityRouteSourceId(capability: RouteSourceCapability): string {
268
+ return capability.sourceId ?? capability.id;
269
+ }
270
+
271
+ function mergeCapabilityRouteSource(current: CapabilityRouteSource | undefined, capability: RouteSourceCapability): CapabilityRouteSource {
272
+ const priority = Math.min(
273
+ current?.priority ?? Number.MAX_SAFE_INTEGER,
274
+ capability.priority ?? 1000,
275
+ );
276
+ return {
277
+ id: capabilityRouteSourceId(capability),
278
+ name: current?.name ?? capability.name,
279
+ priority,
280
+ cachePolicy: capability.cachePolicy ?? current?.cachePolicy,
281
+ isEnabled: capability.isEnabled ?? current?.isEnabled,
282
+ market: capability.kind === "asset-data" ? capability.provider : current?.market,
283
+ news: capability.kind === "news" ? capability.provider : current?.news,
284
+ };
285
+ }
286
+
287
+ export class AssetDataRouter implements DataProvider {
288
+ readonly id = "asset-data-router";
289
+ readonly name = "Asset Data Router";
235
290
  readonly priority = Number.MAX_SAFE_INTEGER;
236
291
 
237
292
  private registry: PluginRegistry | null = null;
238
293
  private readonly revalidationInFlight = new Map<string, Promise<unknown>>();
239
294
  private getConfigFn: () => AppConfig = () => createDefaultConfig("");
295
+ private readonly fallbackSource: CapabilityRouteSource | null;
296
+ private readonly extraSources: CapabilityRouteSource[];
240
297
 
241
298
  constructor(
242
- private readonly fallbackProvider: DataProvider | null = null,
243
- private readonly extraProviders: DataProvider[] = [],
299
+ fallbackSource: CapabilityRouteSource | DataProvider | null = null,
300
+ extraSources: Array<CapabilityRouteSource | DataProvider> = [],
244
301
  private readonly resources?: ResourceStore,
245
- ) {}
302
+ ) {
303
+ this.fallbackSource = fallbackSource ? normalizeRouteSource(fallbackSource) : null;
304
+ this.extraSources = extraSources.map(normalizeRouteSource);
305
+ }
246
306
 
247
307
  attachRegistry(registry: PluginRegistry): void {
248
308
  this.registry = registry;
@@ -417,36 +477,88 @@ export class ProviderRouter implements DataProvider {
417
477
  return results;
418
478
  }
419
479
 
420
- async getNews(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<NewsItem[]> {
480
+ async getNews(query: NewsQuery): Promise<NewsArticle[]> {
481
+ const sources = this.newsSourcesInPriorityOrder()
482
+ .filter((source) => source.news?.supports?.(query) ?? true);
483
+ const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
484
+ if (feed === "ticker") {
485
+ let firstEmpty: NewsArticle[] | null = null;
486
+ for (const source of sources) {
487
+ try {
488
+ const articles = await source.news!.fetchNews(query);
489
+ if (articles.length > 0) return articles;
490
+ firstEmpty ??= articles;
491
+ } catch (error) {
492
+ if (shouldLogProviderError(error)) {
493
+ providerLog.error(`${source.id} failed: ${error}`);
494
+ }
495
+ }
496
+ }
497
+ return firstEmpty ?? [];
498
+ }
499
+
500
+ const settled = await Promise.allSettled(sources.map((source) => source.news!.fetchNews(query)));
501
+ return settled.flatMap((result) => result.status === "fulfilled" ? result.value : []);
502
+ }
503
+
504
+ async getHolders(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<HolderData> {
421
505
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
422
- const variantKeys = [
423
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["count", count]]),
424
- buildVariantKey([["count", count]]),
425
- "",
426
- ];
427
- const cached = this.selectCachedResource<NewsItem[]>("news", entityKey, variantKeys, this.getProviderSourceKeys(), false);
428
- if (cached) {
429
- this.scheduleRevalidation(this.makeRevalidationKey("news", ticker, exchange, context, count), async () => {
430
- await this.revalidateNews(ticker, count, exchange, context);
506
+ const variantKeys = this.getTickerVariantCandidates(exchange);
507
+ const cached = this.selectCachedResource<HolderData>("holders", entityKey, variantKeys, this.getProviderSourceKeys(), false);
508
+ const forceRefresh = context?.cacheMode === "refresh";
509
+ if (cached && !forceRefresh) {
510
+ this.scheduleRevalidation(this.makeRevalidationKey("holders", ticker, exchange, context), async () => {
511
+ await this.revalidateHolders(ticker, exchange, context);
431
512
  });
432
513
  return cached.value;
433
514
  }
434
515
 
435
- const result = await this.firstProvider((provider) => provider.getNews(ticker, count, exchange, context));
436
- if (!result) {
437
- throw new Error(`No news provider available for ${ticker}`);
516
+ const result = await this.fetchProviderHolders(ticker, exchange, context);
517
+ if (result) return result.value;
518
+ if (cached) return cached.value;
519
+ throw new Error(`No holder data provider available for ${ticker}`);
520
+ }
521
+
522
+ async getAnalystResearch(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<AnalystResearchData> {
523
+ const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
524
+ const variantKeys = this.getTickerVariantCandidates(exchange);
525
+ const cached = this.selectCachedResource<AnalystResearchData>("analystResearch", entityKey, variantKeys, this.getProviderSourceKeys(), false);
526
+ const forceRefresh = context?.cacheMode === "refresh";
527
+ if (cached && !forceRefresh) {
528
+ this.scheduleRevalidation(this.makeRevalidationKey("analystResearch", ticker, exchange, context), async () => {
529
+ await this.revalidateAnalystResearch(ticker, exchange, context);
530
+ });
531
+ return cached.value;
438
532
  }
439
- const provider = this.resolveProviderBySourceKey(result.sourceKey);
440
- if (provider) {
441
- this.cacheResource("news", entityKey, variantKeys[0] ?? "", result.sourceKey, result.value, this.resolveProviderPolicy("news", provider));
533
+
534
+ const result = await this.fetchProviderAnalystResearch(ticker, exchange, context);
535
+ if (result) return result.value;
536
+ if (cached) return cached.value;
537
+ throw new Error(`No analyst research provider available for ${ticker}`);
538
+ }
539
+
540
+ async getCorporateActions(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<CorporateActionsData> {
541
+ const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
542
+ const variantKeys = this.getTickerVariantCandidates(exchange);
543
+ const cached = this.selectCachedResource<CorporateActionsData>("corporateActions", entityKey, variantKeys, this.getProviderSourceKeys(), false);
544
+ const forceRefresh = context?.cacheMode === "refresh";
545
+ if (cached && !forceRefresh) {
546
+ this.scheduleRevalidation(this.makeRevalidationKey("corporateActions", ticker, exchange, context), async () => {
547
+ await this.revalidateCorporateActions(ticker, exchange, context);
548
+ });
549
+ return cached.value;
442
550
  }
443
- return result.value;
551
+
552
+ const result = await this.fetchProviderCorporateActions(ticker, exchange, context);
553
+ if (result) return result.value;
554
+ if (cached) return cached.value;
555
+ throw new Error(`No corporate actions provider available for ${ticker}`);
444
556
  }
445
557
 
446
558
  async getSecFilings(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
447
559
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
448
560
  const variantKeys = [
449
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["count", count]]),
561
+ buildVariantKey([["exchange", canonicalExchange(exchange)], ["count", count]]),
450
562
  buildVariantKey([["count", count]]),
451
563
  "",
452
564
  ];
@@ -506,7 +618,7 @@ export class ProviderRouter implements DataProvider {
506
618
  async getPriceHistory(ticker: string, exchange: string, range: TimeRange, context?: MarketDataRequestContext): Promise<PricePoint[]> {
507
619
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
508
620
  const variantKeys = [
509
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["range", range]]),
621
+ buildVariantKey([["exchange", canonicalExchange(exchange)], ["range", range]]),
510
622
  buildVariantKey([["range", range]]),
511
623
  ];
512
624
  const sourceKeys = [
@@ -543,7 +655,7 @@ export class ProviderRouter implements DataProvider {
543
655
  ): Promise<PricePoint[]> {
544
656
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
545
657
  const variantKeys = [
546
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["range", bufferRange], ["resolution", resolution]]),
658
+ buildVariantKey([["exchange", canonicalExchange(exchange)], ["range", bufferRange], ["resolution", resolution]]),
547
659
  buildVariantKey([["range", bufferRange], ["resolution", resolution]]),
548
660
  ];
549
661
  const sourceKeys = [
@@ -603,7 +715,7 @@ export class ProviderRouter implements DataProvider {
603
715
  ): Promise<PricePoint[]> {
604
716
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
605
717
  const variantKeys = [
606
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]),
718
+ buildVariantKey([["exchange", canonicalExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]),
607
719
  buildVariantKey([["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]),
608
720
  ];
609
721
  const sourceKeys = [
@@ -630,7 +742,7 @@ export class ProviderRouter implements DataProvider {
630
742
  async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, context?: MarketDataRequestContext): Promise<OptionsChain> {
631
743
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
632
744
  const variantKeys = [
633
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["expiration", expirationDate ?? "default"]]),
745
+ buildVariantKey([["exchange", canonicalExchange(exchange)], ["expiration", expirationDate ?? "default"]]),
634
746
  buildVariantKey([["expiration", expirationDate ?? "default"]]),
635
747
  "",
636
748
  ];
@@ -682,7 +794,7 @@ export class ProviderRouter implements DataProvider {
682
794
  }
683
795
 
684
796
  private getTickerVariantCandidates(exchange?: string): string[] {
685
- const normalizedExchange = normalizeExchange(exchange);
797
+ const normalizedExchange = canonicalExchange(exchange);
686
798
  return [
687
799
  buildVariantKey([["exchange", normalizedExchange]]),
688
800
  "",
@@ -701,10 +813,6 @@ export class ProviderRouter implements DataProvider {
701
813
  return this.providersInPriorityOrder().map((provider) => this.providerSourceKey(provider));
702
814
  }
703
815
 
704
- private firstAvailableProvider(): DataProvider | null {
705
- return this.providersInPriorityOrder()[0] ?? null;
706
- }
707
-
708
816
  private resolvePolicy(overrides: CachePolicyMap | undefined, key: keyof typeof DEFAULT_CACHE_POLICIES): CachePolicy {
709
817
  return overrides?.[key] ?? DEFAULT_CACHE_POLICIES[key]!;
710
818
  }
@@ -928,27 +1036,48 @@ export class ProviderRouter implements DataProvider {
928
1036
  }));
929
1037
  }
930
1038
 
931
- private sortedProviders(): DataProvider[] {
932
- const providers = [...this.extraProviders];
1039
+ private sortedSources(): CapabilityRouteSource[] {
1040
+ const sources = [...this.extraSources];
933
1041
  if (this.registry) {
934
- const registryProviders = typeof this.registry.getEnabledDataProviders === "function"
935
- ? this.registry.getEnabledDataProviders()
936
- : [...this.registry.dataProviders.values()];
937
- providers.push(...registryProviders);
1042
+ const capabilitySources = new Map<string, CapabilityRouteSource>();
1043
+ for (const capability of this.registry.getEnabledCapabilities("asset-data") as AssetDataCapability[]) {
1044
+ const sourceId = capabilityRouteSourceId(capability);
1045
+ capabilitySources.set(sourceId, mergeCapabilityRouteSource(capabilitySources.get(sourceId), capability));
1046
+ }
1047
+ for (const capability of this.registry.getEnabledCapabilities("news") as NewsCapability[]) {
1048
+ const sourceId = capabilityRouteSourceId(capability);
1049
+ capabilitySources.set(sourceId, mergeCapabilityRouteSource(capabilitySources.get(sourceId), capability));
1050
+ }
1051
+ sources.push(...capabilitySources.values());
938
1052
  }
939
- return providers
940
- .filter((provider) => provider.id !== this.id && provider.id !== this.fallbackProvider?.id)
941
- .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
1053
+ return sources
1054
+ .filter((source) => source.id !== this.id && source.id !== this.fallbackSource?.id)
1055
+ .sort((a, b) => routeSourcePriority(a) - routeSourcePriority(b));
1056
+ }
1057
+
1058
+ private sortedProviders(): DataProvider[] {
1059
+ return this.sortedSources()
1060
+ .map((source) => source.market)
1061
+ .filter((provider): provider is DataProvider => !!provider);
942
1062
  }
943
1063
 
944
1064
  private providersInPriorityOrder(): DataProvider[] {
945
1065
  const providers = [...this.sortedProviders()];
946
- if (this.fallbackProvider && !providers.some((provider) => provider.id === this.fallbackProvider?.id)) {
947
- providers.push(this.fallbackProvider);
1066
+ const fallbackProvider = this.fallbackSource?.market ?? null;
1067
+ if (fallbackProvider && !providers.some((provider) => provider.id === fallbackProvider.id)) {
1068
+ providers.push(fallbackProvider);
948
1069
  }
949
1070
  return providers;
950
1071
  }
951
1072
 
1073
+ private newsSourcesInPriorityOrder(): CapabilityRouteSource[] {
1074
+ const sources = this.sortedSources().filter((source) => !!source.news);
1075
+ if (this.fallbackSource?.news && !sources.some((source) => source.id === this.fallbackSource?.id)) {
1076
+ sources.push(this.fallbackSource);
1077
+ }
1078
+ return sources;
1079
+ }
1080
+
952
1081
  private async firstProvider<T>(fn: (provider: DataProvider) => Promise<T | null | undefined>): Promise<SourceResult<T> | null> {
953
1082
  for (const provider of this.providersInPriorityOrder()) {
954
1083
  try {
@@ -1090,7 +1219,7 @@ export class ProviderRouter implements DataProvider {
1090
1219
  context?: MarketDataRequestContext,
1091
1220
  ): Promise<SourceResult<PricePoint[]> | null> {
1092
1221
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1093
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["range", range]]);
1222
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["range", range]]);
1094
1223
  for (const candidate of this.getBrokerCandidatesForContext(context, false)) {
1095
1224
  if (!candidate.broker.getPriceHistory) continue;
1096
1225
  try {
@@ -1125,7 +1254,7 @@ export class ProviderRouter implements DataProvider {
1125
1254
  context?: MarketDataRequestContext,
1126
1255
  ): Promise<SourceResult<PricePoint[]> | null> {
1127
1256
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1128
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["range", bufferRange], ["resolution", resolution]]);
1257
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["range", bufferRange], ["resolution", resolution]]);
1129
1258
  for (const candidate of this.getBrokerCandidatesForContext(context, false)) {
1130
1259
  if (!candidate.broker.getPriceHistoryForResolution) continue;
1131
1260
  try {
@@ -1160,7 +1289,7 @@ export class ProviderRouter implements DataProvider {
1160
1289
  context?: MarketDataRequestContext,
1161
1290
  ): Promise<SourceResult<PricePoint[]> | null> {
1162
1291
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1163
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["range", range]]);
1292
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["range", range]]);
1164
1293
  let firstEmptyResult: SourceResult<PricePoint[]> | null = null;
1165
1294
 
1166
1295
  for (const provider of this.providersInPriorityOrder()) {
@@ -1197,7 +1326,7 @@ export class ProviderRouter implements DataProvider {
1197
1326
  context?: MarketDataRequestContext,
1198
1327
  ): Promise<SourceResult<PricePoint[]> | null> {
1199
1328
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1200
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["range", bufferRange], ["resolution", resolution]]);
1329
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["range", bufferRange], ["resolution", resolution]]);
1201
1330
  let firstEmptyResult: SourceResult<PricePoint[]> | null = null;
1202
1331
 
1203
1332
  for (const provider of this.providersInPriorityOrder()) {
@@ -1293,7 +1422,7 @@ export class ProviderRouter implements DataProvider {
1293
1422
  context?: MarketDataRequestContext,
1294
1423
  ): Promise<SourceResult<PricePoint[]> | null> {
1295
1424
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1296
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]);
1425
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]);
1297
1426
  for (const candidate of this.getBrokerCandidatesForContext(context, false)) {
1298
1427
  if (!candidate.broker.getDetailedPriceHistory) continue;
1299
1428
  try {
@@ -1331,7 +1460,7 @@ export class ProviderRouter implements DataProvider {
1331
1460
  context?: MarketDataRequestContext,
1332
1461
  ): Promise<SourceResult<PricePoint[]> | null> {
1333
1462
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1334
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]);
1463
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["start", compactDate(startDate)], ["end", compactDate(endDate)], ["bar", barSize]]);
1335
1464
  let firstEmptyResult: SourceResult<PricePoint[]> | null = null;
1336
1465
 
1337
1466
  for (const provider of this.providersInPriorityOrder()) {
@@ -1368,7 +1497,7 @@ export class ProviderRouter implements DataProvider {
1368
1497
  context?: MarketDataRequestContext,
1369
1498
  ): Promise<SourceResult<OptionsChain> | null> {
1370
1499
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1371
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["expiration", expirationDate ?? "default"]]);
1500
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["expiration", expirationDate ?? "default"]]);
1372
1501
  for (const candidate of this.getBrokerCandidatesForContext(context, false)) {
1373
1502
  if (!candidate.broker.getOptionsChain) continue;
1374
1503
  try {
@@ -1402,7 +1531,7 @@ export class ProviderRouter implements DataProvider {
1402
1531
  context?: MarketDataRequestContext,
1403
1532
  ): Promise<SourceResult<OptionsChain> | null> {
1404
1533
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1405
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["expiration", expirationDate ?? "default"]]);
1534
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["expiration", expirationDate ?? "default"]]);
1406
1535
  const result = await this.firstProvider(async (provider) => {
1407
1536
  if (!provider.getOptionsChain) return null;
1408
1537
  return provider.getOptionsChain(ticker, exchange, expirationDate, context);
@@ -1415,6 +1544,99 @@ export class ProviderRouter implements DataProvider {
1415
1544
  return result;
1416
1545
  }
1417
1546
 
1547
+ private async fetchProviderHolders(
1548
+ ticker: string,
1549
+ exchange?: string,
1550
+ context?: MarketDataRequestContext,
1551
+ ): Promise<SourceResult<HolderData> | null> {
1552
+ const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1553
+ const variantKey = this.getTickerVariantCandidates(exchange)[0] ?? "";
1554
+ let firstEmptyResult: SourceResult<HolderData> | null = null;
1555
+ let lastError: unknown = null;
1556
+
1557
+ for (const provider of this.providersInPriorityOrder()) {
1558
+ if (!provider.getHolders) continue;
1559
+ try {
1560
+ const value = await provider.getHolders(ticker, exchange, context);
1561
+ const sourceKey = this.providerSourceKey(provider);
1562
+ this.cacheResource("holders", entityKey, variantKey, sourceKey, value, this.resolveProviderPolicy("holders", provider));
1563
+ if (value.holders.length > 0) return { sourceKey, value };
1564
+ firstEmptyResult ??= { sourceKey, value };
1565
+ } catch (error) {
1566
+ lastError = error;
1567
+ if (shouldLogProviderError(error)) {
1568
+ providerLog.error(`${provider.id} failed: ${error}`);
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ if (firstEmptyResult) return firstEmptyResult;
1574
+ if (lastError) throw lastError;
1575
+ return null;
1576
+ }
1577
+
1578
+ private async fetchProviderAnalystResearch(
1579
+ ticker: string,
1580
+ exchange?: string,
1581
+ context?: MarketDataRequestContext,
1582
+ ): Promise<SourceResult<AnalystResearchData> | null> {
1583
+ const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1584
+ const variantKey = this.getTickerVariantCandidates(exchange)[0] ?? "";
1585
+ let firstEmptyResult: SourceResult<AnalystResearchData> | null = null;
1586
+ let lastError: unknown = null;
1587
+
1588
+ for (const provider of this.providersInPriorityOrder()) {
1589
+ if (!provider.getAnalystResearch) continue;
1590
+ try {
1591
+ const value = await provider.getAnalystResearch(ticker, exchange, context);
1592
+ const sourceKey = this.providerSourceKey(provider);
1593
+ this.cacheResource("analystResearch", entityKey, variantKey, sourceKey, value, this.resolveProviderPolicy("analystResearch", provider));
1594
+ if (hasAnalystResearchValue(value)) return { sourceKey, value };
1595
+ firstEmptyResult ??= { sourceKey, value };
1596
+ } catch (error) {
1597
+ lastError = error;
1598
+ if (shouldLogProviderError(error)) {
1599
+ providerLog.error(`${provider.id} failed: ${error}`);
1600
+ }
1601
+ }
1602
+ }
1603
+
1604
+ if (firstEmptyResult) return firstEmptyResult;
1605
+ if (lastError) throw lastError;
1606
+ return null;
1607
+ }
1608
+
1609
+ private async fetchProviderCorporateActions(
1610
+ ticker: string,
1611
+ exchange?: string,
1612
+ context?: MarketDataRequestContext,
1613
+ ): Promise<SourceResult<CorporateActionsData> | null> {
1614
+ const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1615
+ const variantKey = this.getTickerVariantCandidates(exchange)[0] ?? "";
1616
+ let firstEmptyResult: SourceResult<CorporateActionsData> | null = null;
1617
+ let lastError: unknown = null;
1618
+
1619
+ for (const provider of this.providersInPriorityOrder()) {
1620
+ if (!provider.getCorporateActions) continue;
1621
+ try {
1622
+ const value = await provider.getCorporateActions(ticker, exchange, context);
1623
+ const sourceKey = this.providerSourceKey(provider);
1624
+ this.cacheResource("corporateActions", entityKey, variantKey, sourceKey, value, this.resolveProviderPolicy("corporateActions", provider));
1625
+ if (hasCorporateActionsValue(value)) return { sourceKey, value };
1626
+ firstEmptyResult ??= { sourceKey, value };
1627
+ } catch (error) {
1628
+ lastError = error;
1629
+ if (shouldLogProviderError(error)) {
1630
+ providerLog.error(`${provider.id} failed: ${error}`);
1631
+ }
1632
+ }
1633
+ }
1634
+
1635
+ if (firstEmptyResult) return firstEmptyResult;
1636
+ if (lastError) throw lastError;
1637
+ return null;
1638
+ }
1639
+
1418
1640
  private async fetchProviderSecFilings(
1419
1641
  ticker: string,
1420
1642
  count = 15,
@@ -1422,7 +1644,7 @@ export class ProviderRouter implements DataProvider {
1422
1644
  context?: MarketDataRequestContext,
1423
1645
  ): Promise<SourceResult<SecFilingItem[]> | null> {
1424
1646
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1425
- const variantKey = buildVariantKey([["exchange", normalizeExchange(exchange)], ["count", count]]);
1647
+ const variantKey = buildVariantKey([["exchange", canonicalExchange(exchange)], ["count", count]]);
1426
1648
  let lastError: unknown = null;
1427
1649
 
1428
1650
  for (const provider of this.providersInPriorityOrder()) {
@@ -1474,21 +1696,6 @@ export class ProviderRouter implements DataProvider {
1474
1696
  return null;
1475
1697
  }
1476
1698
 
1477
- private async revalidateFinancials(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1478
- const brokerResult = await withBrokerTimeout(this.fetchBrokerFinancials(ticker, exchange, context));
1479
- const needsProvider = !brokerResult || !hasMeaningfulFundamentals(brokerResult.value) || !hasMeaningfulProfile(brokerResult.value);
1480
- if (needsProvider) {
1481
- await this.fetchProviderFinancials(ticker, exchange, context);
1482
- }
1483
- }
1484
-
1485
- private async revalidateQuote(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1486
- const brokerQuote = await withBrokerTimeout(this.fetchBrokerQuote(ticker, exchange, context));
1487
- if (!brokerQuote) {
1488
- await this.fetchProviderQuote(ticker, exchange, context);
1489
- }
1490
- }
1491
-
1492
1699
  private async revalidateExchangeRate(fromCurrency: string): Promise<void> {
1493
1700
  const result = await this.firstProvider(async (provider) => {
1494
1701
  const rate = await provider.getExchangeRate(fromCurrency);
@@ -1498,25 +1705,22 @@ export class ProviderRouter implements DataProvider {
1498
1705
  this.cacheExchangeRate(fromCurrency, result.value.rate, result.value.provider);
1499
1706
  }
1500
1707
 
1501
- private async revalidateNews(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1502
- const result = await this.firstProvider((provider) => provider.getNews(ticker, count, exchange, context));
1503
- if (!result) return;
1504
- const provider = this.resolveProviderBySourceKey(result.sourceKey);
1505
- if (!provider) return;
1506
- this.cacheResource(
1507
- "news",
1508
- this.getEntityKey(ticker, exchange, context?.instrument),
1509
- buildVariantKey([["exchange", normalizeExchange(exchange)], ["count", count]]),
1510
- result.sourceKey,
1511
- result.value,
1512
- this.resolveProviderPolicy("news", provider),
1513
- );
1514
- }
1515
-
1516
1708
  private async revalidateSecFilings(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1517
1709
  await this.fetchProviderSecFilings(ticker, count, exchange, context);
1518
1710
  }
1519
1711
 
1712
+ private async revalidateHolders(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1713
+ await this.fetchProviderHolders(ticker, exchange, context);
1714
+ }
1715
+
1716
+ private async revalidateAnalystResearch(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1717
+ await this.fetchProviderAnalystResearch(ticker, exchange, context);
1718
+ }
1719
+
1720
+ private async revalidateCorporateActions(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<void> {
1721
+ await this.fetchProviderCorporateActions(ticker, exchange, context);
1722
+ }
1723
+
1520
1724
  private async revalidateSecFilingContent(filing: SecFilingItem): Promise<void> {
1521
1725
  await this.fetchProviderSecFilingContent(filing);
1522
1726
  }
@@ -1530,27 +1734,6 @@ export class ProviderRouter implements DataProvider {
1530
1734
  }
1531
1735
  }
1532
1736
 
1533
- private async revalidatePriceHistory(ticker: string, exchange: string, range: TimeRange, context?: MarketDataRequestContext): Promise<void> {
1534
- const brokerHistory = await withBrokerTimeout(this.fetchBrokerPriceHistory(ticker, exchange, range, context));
1535
- if (!brokerHistory || brokerHistory.value.length === 0) {
1536
- await this.fetchProviderPriceHistory(ticker, exchange, range, context);
1537
- }
1538
- }
1539
-
1540
- private async revalidateDetailedPriceHistory(
1541
- ticker: string,
1542
- exchange: string,
1543
- startDate: Date,
1544
- endDate: Date,
1545
- barSize: string,
1546
- context?: MarketDataRequestContext,
1547
- ): Promise<void> {
1548
- const brokerHistory = await withBrokerTimeout(this.fetchBrokerDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context));
1549
- if (!brokerHistory || brokerHistory.value.length === 0) {
1550
- await this.fetchProviderDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context);
1551
- }
1552
- }
1553
-
1554
1737
  private async revalidateOptionsChain(
1555
1738
  ticker: string,
1556
1739
  exchange?: string,