gloomberb 0.9.9 → 0.10.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 (519) hide show
  1. package/README.md +41 -3
  2. package/README.zh-CN.md +294 -0
  3. package/package.json +10 -4
  4. package/patches/@opentui%2Fcore@0.3.2.patch +26 -69
  5. package/src/api-client/auth.ts +2 -0
  6. package/src/api-client/index.test.ts +4 -0
  7. package/src/api-client/types.ts +3 -0
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -0
  9. package/src/app/pane-template-dialog-wizard.test.tsx +36 -0
  10. package/src/app/pane-template-dialog-wizard.tsx +10 -3
  11. package/src/app/runtime/desktop-deeplink.test.ts +15 -0
  12. package/src/app/runtime/desktop-deeplink.ts +21 -6
  13. package/src/app.tsx +40 -33
  14. package/src/capabilities/service-capabilities.ts +7 -2
  15. package/src/cli/commands/automation.test.ts +202 -0
  16. package/src/cli/commands/automation.ts +153 -36
  17. package/src/cli/commands/market.ts +1 -14
  18. package/src/cli/commands/system.ts +4 -1
  19. package/src/cli/context.ts +1 -1
  20. package/src/cli/desktop-pane-shot.ts +65 -4
  21. package/src/cli/entry.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +532 -0
  23. package/src/cli/pane-functions/catalog.ts +39 -4
  24. package/src/cli/pane-functions/data.test.ts +17 -0
  25. package/src/cli/pane-functions/data.ts +55 -13
  26. package/src/cli/pane-functions/discovery.ts +36 -9
  27. package/src/cli/pane-functions/index.test.ts +104 -1
  28. package/src/cli/pane-functions/index.ts +64 -5
  29. package/src/cli/pane-functions/options.ts +21 -6
  30. package/src/cli/pane-functions/report.ts +735 -72
  31. package/src/cli/pane-functions/resolver.ts +70 -6
  32. package/src/cli/pane-functions/screenshot.test.ts +431 -0
  33. package/src/cli/pane-functions/screenshot.ts +952 -8
  34. package/src/components/chart/composite/column-layout.ts +215 -0
  35. package/src/components/chart/composite/composite-chart.test.tsx +630 -0
  36. package/src/components/chart/composite/composite-chart.tsx +1148 -0
  37. package/src/components/chart/composite/format.test.ts +137 -0
  38. package/src/components/chart/composite/format.ts +135 -0
  39. package/src/components/chart/composite/index.ts +38 -0
  40. package/src/components/chart/composite/interactions.test.ts +109 -0
  41. package/src/components/chart/composite/interactions.ts +247 -0
  42. package/src/components/chart/composite/price-series.test.ts +39 -0
  43. package/src/components/chart/composite/price-series.ts +78 -0
  44. package/src/components/chart/composite/rasterizer.ts +359 -0
  45. package/src/components/chart/composite/renderers.test.ts +610 -0
  46. package/src/components/chart/composite/scene.test.ts +464 -0
  47. package/src/components/chart/composite/scene.ts +553 -0
  48. package/src/components/chart/composite/text-renderer.ts +285 -0
  49. package/src/components/chart/composite/time-scale.test.ts +88 -0
  50. package/src/components/chart/composite/time-scale.ts +217 -0
  51. package/src/components/chart/composite/types.ts +141 -0
  52. package/src/components/chart/core/data.ts +81 -5
  53. package/src/components/chart/core/index.test.ts +128 -43
  54. package/src/components/chart/core/resolution.ts +0 -6
  55. package/src/components/chart/native/chart-rasterizer.ts +1 -3
  56. package/src/components/chart/native/kitty/index.test.ts +99 -120
  57. package/src/components/chart/native/kitty/manager.ts +3 -0
  58. package/src/components/chart/native/kitty/protocol.ts +4 -1
  59. package/src/components/chart/native/raster/crosshair.ts +79 -46
  60. package/src/components/chart/native/raster/types.ts +2 -11
  61. package/src/components/chart/native/surface/manager.ts +48 -3
  62. package/src/components/chart/price-axis-labels.tsx +18 -4
  63. package/src/components/command-bar/commands/direct/index.ts +25 -0
  64. package/src/components/command-bar/commands/registry.ts +9 -0
  65. package/src/components/command-bar/helpers.test.ts +4 -1
  66. package/src/components/command-bar/list/view.tsx +18 -6
  67. package/src/components/command-bar/multi-select-picker.tsx +6 -5
  68. package/src/components/command-bar/pane-settings.test.ts +141 -0
  69. package/src/components/command-bar/pane-settings.ts +61 -1
  70. package/src/components/command-bar/panel/body.tsx +7 -6
  71. package/src/components/command-bar/routes/root/command-items.ts +4 -4
  72. package/src/components/command-bar/routes/root/results.ts +10 -1
  73. package/src/components/command-bar/routes/root/selection.test.ts +54 -0
  74. package/src/components/command-bar/routes/root/selection.ts +13 -0
  75. package/src/components/command-bar/routing/actions.ts +18 -2
  76. package/src/components/command-bar/routing/list-state.ts +2 -1
  77. package/src/components/command-bar/surface/index.test.tsx +36 -4
  78. package/src/components/command-bar/surface/pane.test.tsx +93 -0
  79. package/src/components/command-bar/theme-picker.tsx +2 -1
  80. package/src/components/command-bar/view-model.ts +19 -20
  81. package/src/components/command-bar/workflow/body.tsx +2 -1
  82. package/src/components/command-bar/workflow/field-row.tsx +3 -2
  83. package/src/components/command-bar/workflow/fields.ts +4 -2
  84. package/src/components/command-bar/workflow/ops.test.ts +161 -0
  85. package/src/components/command-bar/workflow/ops.ts +20 -50
  86. package/src/components/command-bar/workflow/route-actions.test.ts +34 -0
  87. package/src/components/command-bar/workflow/route-actions.ts +4 -0
  88. package/src/components/command-bar/workflow/types.ts +1 -0
  89. package/src/components/feed-data-table/stack-view.tsx +6 -5
  90. package/src/components/index.ts +13 -1
  91. package/src/components/layout/detached-pane-shell.tsx +2 -1
  92. package/src/components/layout/floating-pane.tsx +3 -0
  93. package/src/components/layout/header.tsx +15 -8
  94. package/src/components/layout/pane/footer/index.test.tsx +22 -3
  95. package/src/components/layout/pane/footer/index.tsx +3 -1
  96. package/src/components/layout/pane/footer/model.ts +9 -2
  97. package/src/components/layout/pane/header.tsx +4 -6
  98. package/src/components/layout/pane/index.tsx +3 -0
  99. package/src/components/layout/pane/sidebar.test.tsx +100 -0
  100. package/src/components/layout/pane/sidebar.tsx +239 -0
  101. package/src/components/layout/pane/title.ts +4 -3
  102. package/src/components/layout/shell/action-menu-overlay.tsx +5 -3
  103. package/src/components/layout/shell/drag/runtime.ts +2 -0
  104. package/src/components/layout/shell/menu.test.ts +19 -0
  105. package/src/components/layout/shell/menu.ts +8 -5
  106. package/src/components/layout/shell/native/pointer-runtime.ts +4 -1
  107. package/src/components/layout/shell/pane/layers.tsx +2 -2
  108. package/src/components/layout/status-bar.tsx +8 -7
  109. package/src/components/layout/window-controls.tsx +4 -3
  110. package/src/components/metric-treemap/index.tsx +3 -2
  111. package/src/components/onboarding/onboarding-steps.tsx +30 -29
  112. package/src/components/onboarding/onboarding-wizard.tsx +5 -4
  113. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +15 -12
  114. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +17 -16
  115. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +6 -5
  116. package/src/components/onboarding/portfolio-step/choose-panel.tsx +5 -4
  117. package/src/components/pane-settings-dialog/desktop.tsx +25 -3
  118. package/src/components/pane-settings-dialog/field-dialogs.tsx +5 -4
  119. package/src/components/pane-settings-dialog/tui.tsx +11 -9
  120. package/src/components/pane-settings-dialog/value.ts +8 -5
  121. package/src/components/pane-settings-dialog.test.tsx +235 -0
  122. package/src/components/pane-settings-dialog.tsx +24 -0
  123. package/src/components/pane-template-wizard.tsx +6 -2
  124. package/src/components/ticker/list-table-view.tsx +3 -2
  125. package/src/components/ui/button.tsx +3 -1
  126. package/src/components/ui/choice-dialog.tsx +6 -1
  127. package/src/components/ui/confirm-dialog.tsx +3 -2
  128. package/src/components/ui/fields.tsx +10 -1
  129. package/src/components/ui/frame.tsx +4 -1
  130. package/src/components/ui/index.ts +5 -0
  131. package/src/components/ui/inline-quick-add.tsx +104 -0
  132. package/src/components/ui/list-view.tsx +5 -4
  133. package/src/components/ui/loading.tsx +2 -1
  134. package/src/components/ui/multi-select/dialog.tsx +188 -44
  135. package/src/components/ui/popover.tsx +10 -0
  136. package/src/components/ui/shortcut-hint.tsx +2 -0
  137. package/src/components/ui/status.tsx +4 -3
  138. package/src/components/ui/tabs.tsx +19 -8
  139. package/src/components/ui/toggle.tsx +44 -3
  140. package/src/components/ui/ui.test.tsx +67 -5
  141. package/src/core/app-services.ts +4 -6
  142. package/src/core/state/session-persistence.ts +3 -2
  143. package/src/data/config/chart-settings.ts +712 -0
  144. package/src/data/config/layout.ts +24 -5
  145. package/src/data/config/store/index.test.ts +377 -14
  146. package/src/data/config/store/normalize.ts +79 -65
  147. package/src/data/fred-series.ts +137 -0
  148. package/src/i18n/index.test.ts +114 -0
  149. package/src/i18n/index.ts +147 -0
  150. package/src/i18n/ja.ts +1013 -0
  151. package/src/i18n/ko.ts +1011 -0
  152. package/src/i18n/languages.test.ts +31 -0
  153. package/src/i18n/languages.ts +72 -0
  154. package/src/i18n/zh-cn.ts +1013 -0
  155. package/src/i18n/zh-tw.ts +1013 -0
  156. package/src/plugins/builtin/account-management/footer.ts +3 -2
  157. package/src/plugins/builtin/account-management/form-components.tsx +3 -2
  158. package/src/plugins/builtin/account-management/keyboard.ts +6 -0
  159. package/src/plugins/builtin/account-management/model.ts +25 -21
  160. package/src/plugins/builtin/account-management/navigation.test.ts +27 -0
  161. package/src/plugins/builtin/account-management/navigation.ts +24 -0
  162. package/src/plugins/builtin/account-management/pane.tsx +120 -85
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +10 -9
  164. package/src/plugins/builtin/ai/agent-history.ts +139 -0
  165. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +133 -51
  166. package/src/plugins/builtin/ai/ask-ai.test.tsx +83 -22
  167. package/src/plugins/builtin/ai/index.test.ts +302 -0
  168. package/src/plugins/builtin/ai/index.tsx +374 -40
  169. package/src/plugins/builtin/ai/pane-settings.test.ts +155 -0
  170. package/src/plugins/builtin/ai/pane-settings.ts +319 -0
  171. package/src/plugins/builtin/ai/pi/credential-store.test.ts +80 -0
  172. package/src/plugins/builtin/ai/pi/credential-store.ts +138 -0
  173. package/src/plugins/builtin/ai/pi/file-store.ts +138 -0
  174. package/src/plugins/builtin/ai/pi/host.test.ts +500 -0
  175. package/src/plugins/builtin/ai/pi/host.ts +564 -0
  176. package/src/plugins/builtin/ai/pi/index.ts +5 -0
  177. package/src/plugins/builtin/ai/pi/models-store.test.ts +44 -0
  178. package/src/plugins/builtin/ai/pi/models-store.ts +141 -0
  179. package/src/plugins/builtin/ai/pi/providers.ts +68 -0
  180. package/src/plugins/builtin/ai/pi/runtime.test.ts +240 -0
  181. package/src/plugins/builtin/ai/pi/runtime.ts +635 -0
  182. package/src/plugins/builtin/ai/providers.test.ts +68 -0
  183. package/src/plugins/builtin/ai/providers.ts +163 -38
  184. package/src/plugins/builtin/ai/runner-selection.test.ts +174 -0
  185. package/src/plugins/builtin/ai/runner-selection.ts +193 -0
  186. package/src/plugins/builtin/ai/runner-selector.tsx +153 -0
  187. package/src/plugins/builtin/ai/runner.test.ts +198 -0
  188. package/src/plugins/builtin/ai/runner.ts +276 -73
  189. package/src/plugins/builtin/ai/screener/contract.test.ts +26 -73
  190. package/src/plugins/builtin/ai/screener/contract.ts +14 -52
  191. package/src/plugins/builtin/ai/screener/editor-runtime.ts +55 -5
  192. package/src/plugins/builtin/ai/screener/editor.tsx +42 -36
  193. package/src/plugins/builtin/ai/screener/footer.ts +82 -67
  194. package/src/plugins/builtin/ai/screener/keyboard.ts +27 -11
  195. package/src/plugins/builtin/ai/screener/model.test.ts +24 -0
  196. package/src/plugins/builtin/ai/screener/model.ts +9 -3
  197. package/src/plugins/builtin/ai/screener/pane.test.tsx +142 -55
  198. package/src/plugins/builtin/ai/screener/pane.tsx +95 -97
  199. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -41
  200. package/src/plugins/builtin/ai/screener/runner.ts +41 -25
  201. package/src/plugins/builtin/ai/screener/tabs-bar.tsx +2 -2
  202. package/src/plugins/builtin/ai/use-runtime-providers.ts +28 -0
  203. package/src/plugins/builtin/ai/workspace/model.test.ts +227 -0
  204. package/src/plugins/builtin/ai/workspace/model.ts +308 -0
  205. package/src/plugins/builtin/ai/workspace/pane.test.tsx +197 -0
  206. package/src/plugins/builtin/ai/workspace/pane.tsx +896 -0
  207. package/src/plugins/builtin/analytics/index.test.tsx +3 -3
  208. package/src/plugins/builtin/analytics/index.tsx +3 -8
  209. package/src/plugins/builtin/broker-manager/detail.tsx +25 -24
  210. package/src/plugins/builtin/broker-manager/index.test.tsx +1 -1
  211. package/src/plugins/builtin/broker-manager/index.tsx +9 -11
  212. package/src/plugins/builtin/broker-manager/model.ts +11 -10
  213. package/src/plugins/builtin/broker-manager/pane-actions.tsx +22 -21
  214. package/src/plugins/builtin/broker-manager/table.ts +9 -8
  215. package/src/plugins/builtin/changelog/index.tsx +3 -8
  216. package/src/plugins/builtin/chart-composer/chart-spec.ts +89 -0
  217. package/src/plugins/builtin/chart-composer/cli-options.ts +98 -0
  218. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +120 -0
  219. package/src/plugins/builtin/chart-composer/editor.test.tsx +332 -0
  220. package/src/plugins/builtin/chart-composer/editor.tsx +1107 -0
  221. package/src/plugins/builtin/chart-composer/index.tsx +228 -0
  222. package/src/plugins/builtin/chart-composer/pane.tsx +645 -0
  223. package/src/plugins/builtin/chart-composer/presets.test.ts +561 -0
  224. package/src/plugins/builtin/chart-composer/presets.ts +759 -0
  225. package/src/plugins/builtin/chart-composer/quick-add.test.tsx +245 -0
  226. package/src/plugins/builtin/chart-composer/quick-add.tsx +409 -0
  227. package/src/plugins/builtin/chart-composer/semantic.ts +86 -0
  228. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +65 -0
  229. package/src/plugins/builtin/chart-composer/series-catalog.ts +295 -0
  230. package/src/plugins/builtin/chart-composer/settings.test.ts +163 -0
  231. package/src/plugins/builtin/chart-composer/settings.ts +397 -0
  232. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +44 -0
  233. package/src/plugins/builtin/chart-composer/shortcuts.ts +35 -0
  234. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +103 -0
  235. package/src/plugins/builtin/chat/channel-labels.ts +5 -4
  236. package/src/plugins/builtin/chat/channels.ts +12 -11
  237. package/src/plugins/builtin/chat/chat.test.tsx +32 -0
  238. package/src/plugins/builtin/chat/content/composer-runtime.ts +6 -1
  239. package/src/plugins/builtin/chat/content/composer.tsx +9 -8
  240. package/src/plugins/builtin/chat/content/index.tsx +36 -1
  241. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +8 -7
  242. package/src/plugins/builtin/chat/content/transcript.tsx +9 -3
  243. package/src/plugins/builtin/chat/controller/utils.ts +10 -9
  244. package/src/plugins/builtin/chat/message/desktop.tsx +17 -10
  245. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +122 -3
  246. package/src/plugins/builtin/chat/message/profile-popover.tsx +38 -1
  247. package/src/plugins/builtin/chat/message/terminal.tsx +19 -10
  248. package/src/plugins/builtin/chat/pane-state.test.ts +31 -0
  249. package/src/plugins/builtin/chat/pane-state.ts +30 -0
  250. package/src/plugins/builtin/chat/pane.tsx +27 -4
  251. package/src/plugins/builtin/chat/profile-popover.ts +62 -5
  252. package/src/plugins/builtin/chat/sidebar.test.tsx +4 -7
  253. package/src/plugins/builtin/chat/sidebar.tsx +107 -168
  254. package/src/plugins/builtin/chat/test-harness.tsx +6 -0
  255. package/src/plugins/builtin/cloud/auth-actions.tsx +5 -4
  256. package/src/plugins/builtin/cloud/plugin.tsx +150 -127
  257. package/src/plugins/builtin/composite-plugins.ts +71 -0
  258. package/src/plugins/builtin/correlation/index.test.tsx +4 -4
  259. package/src/plugins/builtin/correlation/index.tsx +3 -8
  260. package/src/plugins/builtin/earnings/index.tsx +37 -21
  261. package/src/plugins/builtin/earnings/model.test.ts +52 -0
  262. package/src/plugins/builtin/earnings/model.ts +32 -0
  263. package/src/plugins/builtin/econ/detail-view.tsx +17 -4
  264. package/src/plugins/builtin/econ/fred-cache.test.ts +57 -11
  265. package/src/plugins/builtin/econ/index.tsx +16 -20
  266. package/src/plugins/builtin/fear-greed/index.tsx +2 -8
  267. package/src/plugins/builtin/fx-matrix/index.tsx +3 -8
  268. package/src/plugins/builtin/help/components.tsx +5 -4
  269. package/src/plugins/builtin/help/index.tsx +14 -17
  270. package/src/plugins/builtin/holders/index.tsx +2 -8
  271. package/src/plugins/builtin/insider/index.tsx +2 -7
  272. package/src/plugins/builtin/kelly-sizer/index.test.tsx +2 -2
  273. package/src/plugins/builtin/kelly-sizer/index.tsx +3 -7
  274. package/src/plugins/builtin/kelly-sizer/test-support.tsx +3 -3
  275. package/src/plugins/builtin/layout-manager/index.tsx +3 -7
  276. package/src/plugins/builtin/market-heatmap/index.tsx +3 -8
  277. package/src/plugins/builtin/market-movers/index.tsx +3 -8
  278. package/src/plugins/builtin/news/index.tsx +12 -16
  279. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +3 -3
  280. package/src/plugins/builtin/news/wire/index.tsx +83 -73
  281. package/src/plugins/builtin/options/index.tsx +2 -8
  282. package/src/plugins/builtin/plugin-module.test.ts +89 -0
  283. package/src/plugins/builtin/plugin-module.ts +116 -0
  284. package/src/plugins/builtin/portfolio-list/header.tsx +4 -3
  285. package/src/plugins/builtin/portfolio-list/index.test.ts +17 -0
  286. package/src/plugins/builtin/portfolio-list/index.tsx +3 -5
  287. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +3 -3
  288. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +53 -53
  289. package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
  290. package/src/plugins/builtin/portfolio-list/summary/index.tsx +4 -3
  291. package/src/plugins/builtin/research/index.tsx +2 -8
  292. package/src/plugins/builtin/sec/index.tsx +2 -8
  293. package/src/plugins/builtin/sectors/index.tsx +3 -8
  294. package/src/plugins/builtin/thirteenf/index.tsx +3 -8
  295. package/src/plugins/builtin/thirteenf/pane.test.tsx +1 -1
  296. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +49 -23
  297. package/src/plugins/builtin/ticker-detail/index.test.tsx +15 -136
  298. package/src/plugins/builtin/ticker-detail/index.tsx +2 -93
  299. package/src/plugins/builtin/ticker-detail/overview/components.tsx +6 -5
  300. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -17
  301. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -3
  302. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +1 -1
  303. package/src/plugins/builtin/ticker-detail/settings.ts +3 -50
  304. package/src/plugins/builtin/ticker-research-backend-plugin.ts +17 -0
  305. package/src/plugins/builtin/ticker-research-plugin.ts +27 -0
  306. package/src/plugins/builtin/tv/channels.ts +27 -0
  307. package/src/plugins/builtin/tv/index.tsx +40 -0
  308. package/src/plugins/builtin/tv/pane.tsx +286 -0
  309. package/src/plugins/builtin/tv/youtube-stream.ts +100 -0
  310. package/src/plugins/builtin/world-indices/index.tsx +3 -8
  311. package/src/plugins/builtin/yield-curve/index.tsx +8 -8
  312. package/src/plugins/catalog-backend.test.ts +21 -0
  313. package/src/plugins/catalog-backend.ts +42 -0
  314. package/src/plugins/catalog-ui.ts +6 -14
  315. package/src/plugins/ibkr/instance-selection.test.ts +0 -1
  316. package/src/plugins/ownership.ts +143 -0
  317. package/src/plugins/prediction-markets/backend-plugin.ts +19 -0
  318. package/src/plugins/prediction-markets/chart.test.tsx +7 -5
  319. package/src/plugins/prediction-markets/chart.tsx +25 -7
  320. package/src/plugins/registry/index.test.ts +51 -0
  321. package/src/plugins/registry/index.ts +8 -0
  322. package/src/plugins/registry/pane-settings.ts +1 -1
  323. package/src/plugins/runtime/index.test.tsx +2 -2
  324. package/src/remote/app-host.tsx +16 -3
  325. package/src/remote/controller.test.ts +102 -1
  326. package/src/remote/controller.ts +122 -0
  327. package/src/remote/types.ts +11 -0
  328. package/src/renderers/electrobun/bun/core-capabilities.ts +123 -27
  329. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +5 -1
  330. package/src/renderers/electrobun/bun/desktop/initialization.ts +5 -1
  331. package/src/renderers/electrobun/bun/desktop/media.ts +15 -0
  332. package/src/renderers/electrobun/bun/desktop/window-style.ts +4 -1
  333. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +32 -1
  334. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +10 -0
  335. package/src/renderers/electrobun/bun/index.ts +5 -0
  336. package/src/renderers/electrobun/bun/tui-entry.ts +1 -6
  337. package/src/renderers/electrobun/view/ai-host.ts +124 -19
  338. package/src/renderers/electrobun/view/backend-rpc.ts +4 -0
  339. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +151 -39
  340. package/src/renderers/electrobun/view/desktop/popover.tsx +166 -0
  341. package/src/renderers/electrobun/view/dialog-host.test.ts +25 -0
  342. package/src/renderers/electrobun/view/dialog-host.tsx +75 -12
  343. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  344. package/src/renderers/electrobun/view/host/chart-surface.tsx +10 -9
  345. package/src/renderers/electrobun/view/host/input.tsx +9 -19
  346. package/src/renderers/electrobun/view/host/media-surface.tsx +170 -0
  347. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  348. package/src/renderers/electrobun/view/host/style.ts +1 -1
  349. package/src/renderers/electrobun/view/host/tabs.tsx +4 -3
  350. package/src/renderers/electrobun/view/main.tsx +10 -3
  351. package/src/renderers/electrobun/view/styles.css +17 -0
  352. package/src/renderers/electrobun/view/ui-host.tsx +7 -0
  353. package/src/renderers/opentui/chart-surface.test.tsx +52 -6
  354. package/src/renderers/opentui/chart-surface.tsx +95 -11
  355. package/src/renderers/opentui/dialog-host.tsx +36 -5
  356. package/src/renderers/opentui/host.tsx +39 -0
  357. package/src/renderers/opentui/start.tsx +30 -0
  358. package/src/renderers/opentui/ui-host.tsx +2 -0
  359. package/src/sources/provider-router/index.test.ts +43 -0
  360. package/src/sources/provider-router/index.ts +4 -0
  361. package/src/sources/provider-router/supplemental.ts +46 -0
  362. package/src/sources/sec-edgar.test.ts +203 -2
  363. package/src/sources/sec-edgar.ts +70 -15
  364. package/src/state/app/context/index.tsx +13 -1
  365. package/src/state/app/context/selector.test.tsx +62 -1
  366. package/src/state/session-persistence.test.ts +13 -2
  367. package/src/sync/controller.test.ts +190 -3
  368. package/src/sync/controller.ts +76 -45
  369. package/src/sync/core-contributors.test.ts +121 -0
  370. package/src/sync/core-contributors.ts +104 -27
  371. package/src/sync/react.ts +6 -8
  372. package/src/sync/types.ts +3 -0
  373. package/src/time-series/alignment.ts +158 -0
  374. package/src/time-series/economic.ts +49 -0
  375. package/src/time-series/field-catalog.ts +306 -0
  376. package/src/time-series/fundamentals.test.ts +505 -0
  377. package/src/time-series/fundamentals.ts +842 -0
  378. package/src/time-series/hooks.ts +45 -0
  379. package/src/time-series/index.ts +14 -0
  380. package/src/time-series/live-quotes.test.ts +219 -0
  381. package/src/time-series/live-quotes.ts +203 -0
  382. package/src/time-series/market-resolution.ts +53 -0
  383. package/src/time-series/market.ts +125 -0
  384. package/src/time-series/reporting.ts +98 -0
  385. package/src/time-series/resolve.test.ts +1281 -0
  386. package/src/time-series/resolve.ts +783 -0
  387. package/src/time-series/spec-studies.test.ts +400 -0
  388. package/src/time-series/spec.ts +487 -0
  389. package/src/time-series/studies.ts +563 -0
  390. package/src/time-series/transforms.test.ts +105 -0
  391. package/src/time-series/transforms.ts +152 -0
  392. package/src/time-series/types.ts +162 -0
  393. package/src/time-series/use-chart-resolution.ts +166 -0
  394. package/src/types/config.ts +9 -11
  395. package/src/types/desktop-window.ts +1 -0
  396. package/src/types/financials.ts +4 -0
  397. package/src/types/media.ts +17 -0
  398. package/src/types/plugin.ts +32 -1
  399. package/src/ui/context-menu.tsx +15 -3
  400. package/src/ui/dialog.tsx +14 -2
  401. package/src/ui/host.tsx +40 -1
  402. package/src/ui/index.tsx +9 -0
  403. package/src/utils/exchanges.test.ts +13 -1
  404. package/src/utils/exchanges.ts +10 -0
  405. package/src/utils/financial-statements.test.ts +138 -0
  406. package/src/utils/financial-statements.ts +115 -8
  407. package/src/utils/format.test.ts +11 -1
  408. package/src/utils/format.ts +7 -0
  409. package/src/version.ts +1 -1
  410. package/src/components/chart/bar-chart-renderer.test.ts +0 -233
  411. package/src/components/chart/bar-chart-renderer.ts +0 -445
  412. package/src/components/chart/comparison/data.test.ts +0 -159
  413. package/src/components/chart/comparison/data.ts +0 -302
  414. package/src/components/chart/comparison/renderer.test.ts +0 -187
  415. package/src/components/chart/comparison/renderer.ts +0 -282
  416. package/src/components/chart/comparison/scene.ts +0 -178
  417. package/src/components/chart/comparison-stock-chart/axis-width.ts +0 -99
  418. package/src/components/chart/comparison-stock-chart/canvas.ts +0 -148
  419. package/src/components/chart/comparison-stock-chart/controls.ts +0 -252
  420. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -101
  421. package/src/components/chart/comparison-stock-chart/interactions.ts +0 -208
  422. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +0 -62
  423. package/src/components/chart/comparison-stock-chart/layout.tsx +0 -155
  424. package/src/components/chart/comparison-stock-chart/legend.tsx +0 -74
  425. package/src/components/chart/comparison-stock-chart/native-surfaces.ts +0 -89
  426. package/src/components/chart/comparison-stock-chart/render-data.ts +0 -227
  427. package/src/components/chart/comparison-stock-chart/render-output.ts +0 -140
  428. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +0 -128
  429. package/src/components/chart/comparison-stock-chart/sources.ts +0 -51
  430. package/src/components/chart/comparison-stock-chart/surface-runtime.ts +0 -159
  431. package/src/components/chart/comparison-stock-chart/toolbar.tsx +0 -117
  432. package/src/components/chart/comparison-stock-chart/types.ts +0 -22
  433. package/src/components/chart/comparison-stock-chart/view.tsx +0 -572
  434. package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +0 -239
  435. package/src/components/chart/comparison-stock-chart.tsx +0 -24
  436. package/src/components/chart/core/axis-measure.ts +0 -33
  437. package/src/components/chart/core/controller.test.ts +0 -157
  438. package/src/components/chart/core/controller.ts +0 -349
  439. package/src/components/chart/core/indicator-selector.tsx +0 -75
  440. package/src/components/chart/core/keyboard.ts +0 -24
  441. package/src/components/chart/core/pane-settings.test.ts +0 -52
  442. package/src/components/chart/core/pane-settings.ts +0 -141
  443. package/src/components/chart/core/scroll.test.ts +0 -64
  444. package/src/components/chart/core/scroll.ts +0 -82
  445. package/src/components/chart/core/window-viewport.test.ts +0 -69
  446. package/src/components/chart/core/window-viewport.ts +0 -162
  447. package/src/components/chart/display-cursor.ts +0 -153
  448. package/src/components/chart/indicators/bands.test.ts +0 -36
  449. package/src/components/chart/indicators/bands.ts +0 -29
  450. package/src/components/chart/indicators/moving-averages.test.ts +0 -39
  451. package/src/components/chart/indicators/moving-averages.ts +0 -42
  452. package/src/components/chart/indicators/options.test.ts +0 -32
  453. package/src/components/chart/indicators/options.ts +0 -107
  454. package/src/components/chart/indicators/oscillators.test.ts +0 -58
  455. package/src/components/chart/indicators/oscillators.ts +0 -92
  456. package/src/components/chart/native/canvas-bitmaps.ts +0 -140
  457. package/src/components/chart/native/raster/comparison-chart.ts +0 -162
  458. package/src/components/chart/native/surface/hooks.ts +0 -144
  459. package/src/components/chart/native/surface/rendering.ts +0 -211
  460. package/src/components/chart/static/bar-chart-surface.test.tsx +0 -101
  461. package/src/components/chart/static/bar-chart-surface.tsx +0 -292
  462. package/src/components/chart/stock-chart/auto.ts +0 -216
  463. package/src/components/chart/stock-chart/axis-width.ts +0 -146
  464. package/src/components/chart/stock-chart/bitmaps.ts +0 -22
  465. package/src/components/chart/stock-chart/canvas.ts +0 -202
  466. package/src/components/chart/stock-chart/controls.tsx +0 -149
  467. package/src/components/chart/stock-chart/cursor.ts +0 -60
  468. package/src/components/chart/stock-chart/data-runtime.ts +0 -128
  469. package/src/components/chart/stock-chart/footer.test.ts +0 -65
  470. package/src/components/chart/stock-chart/footer.ts +0 -303
  471. package/src/components/chart/stock-chart/geometry.ts +0 -160
  472. package/src/components/chart/stock-chart/index.test.ts +0 -532
  473. package/src/components/chart/stock-chart/index.tsx +0 -29
  474. package/src/components/chart/stock-chart/indicators.ts +0 -226
  475. package/src/components/chart/stock-chart/interaction-runtime.ts +0 -44
  476. package/src/components/chart/stock-chart/interactions.ts +0 -300
  477. package/src/components/chart/stock-chart/keyboard.ts +0 -373
  478. package/src/components/chart/stock-chart/layout-metrics.ts +0 -62
  479. package/src/components/chart/stock-chart/layout.tsx +0 -177
  480. package/src/components/chart/stock-chart/native-surfaces.ts +0 -121
  481. package/src/components/chart/stock-chart/presentation.ts +0 -67
  482. package/src/components/chart/stock-chart/renderer-switch.test.tsx +0 -263
  483. package/src/components/chart/stock-chart/rendering/bounds.ts +0 -116
  484. package/src/components/chart/stock-chart/rendering/candidates.ts +0 -145
  485. package/src/components/chart/stock-chart/rendering/data.ts +0 -370
  486. package/src/components/chart/stock-chart/rendering/invalidation.ts +0 -68
  487. package/src/components/chart/stock-chart/rendering/output.ts +0 -304
  488. package/src/components/chart/stock-chart/rendering/planning.ts +0 -122
  489. package/src/components/chart/stock-chart/rendering/projection.test.tsx +0 -98
  490. package/src/components/chart/stock-chart/rendering/projection.ts +0 -112
  491. package/src/components/chart/stock-chart/rendering/state.test.ts +0 -70
  492. package/src/components/chart/stock-chart/rendering/state.ts +0 -283
  493. package/src/components/chart/stock-chart/requests.ts +0 -337
  494. package/src/components/chart/stock-chart/resolution-support.ts +0 -96
  495. package/src/components/chart/stock-chart/resolved/runtime.ts +0 -289
  496. package/src/components/chart/stock-chart/resolved.tsx +0 -359
  497. package/src/components/chart/stock-chart/settings.ts +0 -81
  498. package/src/components/chart/stock-chart/surface-runtime.ts +0 -62
  499. package/src/components/chart/stock-chart/types.ts +0 -25
  500. package/src/components/chart/stock-chart/view.tsx +0 -155
  501. package/src/components/chart/stock-chart/viewport/effects.ts +0 -293
  502. package/src/components/chart/stock-chart/viewport/index.ts +0 -144
  503. package/src/components/chart/stock-chart/viewport/runtime.ts +0 -375
  504. package/src/components/chart/time-axis-label.tsx +0 -175
  505. package/src/plugins/builtin/ai/gloomberb-cli.ts +0 -71
  506. package/src/plugins/builtin/ai/screener/action-bar.tsx +0 -58
  507. package/src/plugins/builtin/comparison-chart/index.test.tsx +0 -848
  508. package/src/plugins/builtin/comparison-chart/index.tsx +0 -194
  509. package/src/plugins/builtin/econ/fred-cache.ts +0 -106
  510. package/src/plugins/builtin/plugin-groups.ts +0 -130
  511. package/src/plugins/builtin/ticker-detail/chart-pane.tsx +0 -91
  512. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -100
  513. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +0 -296
  514. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +0 -62
  515. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +0 -608
  516. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +0 -65
  517. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +0 -58
  518. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +0 -138
  519. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +0 -220
@@ -0,0 +1,45 @@
1
+ import { useMemo } from "react";
2
+ import { apiClient } from "../api-client";
3
+ import { loadCachedFredSeries } from "../data/fred-series";
4
+ import { instrumentFromTicker } from "../market-data/request-types";
5
+ import { useAssetData } from "../plugins/runtime";
6
+ import { useAppSelector } from "../state/app/context";
7
+ import type { FredSeriesRequest } from "../data/fred-series";
8
+ import type { ChartSpec } from "./types";
9
+ import {
10
+ useChartResolution,
11
+ type UseChartResolutionOptions,
12
+ type UseChartResolutionResult,
13
+ } from "./use-chart-resolution";
14
+
15
+ async function loadFred(request: FredSeriesRequest) {
16
+ return loadCachedFredSeries(
17
+ request,
18
+ () => apiClient.getCloudFredSeries(request.seriesId, {
19
+ startDate: request.startDate,
20
+ sortOrder: request.sortOrder,
21
+ }),
22
+ );
23
+ }
24
+
25
+ export function useResolvedChartSpec(
26
+ spec: ChartSpec,
27
+ options: UseChartResolutionOptions = {},
28
+ ): UseChartResolutionResult {
29
+ const dataProvider = useAssetData();
30
+ const tickers = useAppSelector((state) => state.tickers);
31
+ const hydratedSpec = useMemo<ChartSpec>(() => ({
32
+ ...spec,
33
+ series: spec.series.map((entry) => {
34
+ if (entry.source.kind !== "security" || entry.source.instrument.exchange) return entry;
35
+ const symbol = entry.source.instrument.symbol.trim().toUpperCase();
36
+ const ticker = tickers.get(symbol);
37
+ const instrument = instrumentFromTicker(ticker, symbol);
38
+ return instrument
39
+ ? { ...entry, source: { ...entry.source, instrument: { ...instrument, ...entry.source.instrument } } }
40
+ : entry;
41
+ }),
42
+ }), [spec, tickers]);
43
+ const sources = useMemo(() => ({ dataProvider, loadFredSeries: loadFred }), [dataProvider]);
44
+ return useChartResolution(hydratedSpec, sources, options);
45
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./alignment";
2
+ export * from "./economic";
3
+ export * from "./field-catalog";
4
+ export * from "./fundamentals";
5
+ export * from "./hooks";
6
+ export * from "./live-quotes";
7
+ export * from "./market";
8
+ export * from "./reporting";
9
+ export * from "./resolve";
10
+ export * from "./spec";
11
+ export * from "./studies";
12
+ export * from "./transforms";
13
+ export * from "./types";
14
+ export * from "./use-chart-resolution";
@@ -0,0 +1,219 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createTestDataProvider } from "../test-support/data-provider";
3
+ import type { Quote } from "../types/financials";
4
+ import { CHART_SPEC_VERSION, type ChartSeriesSpec, type ChartSpec } from "./types";
5
+ import {
6
+ chartQuoteOverrideKeyForTarget,
7
+ getLiveChartQuoteTargets,
8
+ subscribeToLiveChartQuotes,
9
+ } from "./live-quotes";
10
+
11
+ function securitySeries(
12
+ id: string,
13
+ symbol: string,
14
+ fieldId: string,
15
+ visible = true,
16
+ ): ChartSeriesSpec {
17
+ return {
18
+ id,
19
+ source: { kind: "security", instrument: { symbol }, fieldId },
20
+ style: "line",
21
+ transform: "raw",
22
+ axis: "auto",
23
+ panelId: "main",
24
+ interpolation: "none",
25
+ visible,
26
+ };
27
+ }
28
+
29
+ function specWithSeries(series: ChartSeriesSpec[]): ChartSpec {
30
+ return {
31
+ version: CHART_SPEC_VERSION,
32
+ viewport: { range: "1Y", resolution: "1d" },
33
+ panels: [{ id: "main" }],
34
+ series,
35
+ studies: [],
36
+ };
37
+ }
38
+
39
+ function quote(symbol: string, price: number, lastUpdated: number): Quote {
40
+ return {
41
+ symbol,
42
+ price,
43
+ currency: "USD",
44
+ change: 0,
45
+ changePercent: 0,
46
+ lastUpdated,
47
+ };
48
+ }
49
+
50
+ async function waitFor(predicate: () => boolean): Promise<void> {
51
+ for (let attempt = 0; attempt < 100; attempt += 1) {
52
+ if (predicate()) return;
53
+ await new Promise((resolve) => setTimeout(resolve, 2));
54
+ }
55
+ throw new Error("Timed out waiting for live quote refresh.");
56
+ }
57
+
58
+ describe("live chart quotes", () => {
59
+ test("subscribes only to visible quote-sensitive instruments and deduplicates them", () => {
60
+ const spec = specWithSeries([
61
+ securitySeries("aapl-close", "AAPL", "market.close"),
62
+ securitySeries("aapl-volume", "AAPL", "market.volume"),
63
+ securitySeries("hidden", "MSFT", "market.close", false),
64
+ securitySeries("fundamental", "GOOG", "fundamental.totalRevenue"),
65
+ securitySeries("valuation", "TSLA", "pe"),
66
+ securitySeries("price-sales", "SHOP", "valuation.priceSales"),
67
+ securitySeries("forward-pe", "NVDA", "valuation.forwardPE"),
68
+ securitySeries("peg", "META", "valuation.pegRatio"),
69
+ {
70
+ id: "fred",
71
+ source: { kind: "economic", provider: "fred", seriesId: "CPIAUCSL" },
72
+ style: "line",
73
+ transform: "raw",
74
+ axis: "auto",
75
+ panelId: "main",
76
+ interpolation: "none",
77
+ },
78
+ ]);
79
+
80
+ expect(getLiveChartQuoteTargets(spec).map((target) => target.symbol)).toEqual(["AAPL", "TSLA", "SHOP"]);
81
+ });
82
+
83
+ test("subscribes to a hidden quote series when a visible study depends on it", () => {
84
+ const spec = specWithSeries([
85
+ securitySeries("hidden-price", "MSFT", "market.close", false),
86
+ ]);
87
+ spec.studies = [{
88
+ id: "sma",
89
+ kind: "sma",
90
+ inputSeriesIds: ["hidden-price"],
91
+ parameters: { period: 20 },
92
+ panelId: "main",
93
+ axis: "auto",
94
+ }];
95
+
96
+ expect(getLiveChartQuoteTargets(spec).map((target) => target.symbol)).toEqual(["MSFT"]);
97
+ });
98
+
99
+ test("coalesces bursts, serializes refreshes, and stops cleanly", async () => {
100
+ const spec = specWithSeries([securitySeries("price", "AAPL", "market.close")]);
101
+ let handler: Parameters<NonNullable<ReturnType<typeof createTestDataProvider>["subscribeQuotes"]>>[1]
102
+ | undefined;
103
+ let subscribedTarget: Parameters<NonNullable<ReturnType<typeof createTestDataProvider>["subscribeQuotes"]>>[0][number]
104
+ | undefined;
105
+ let unsubscribeCalls = 0;
106
+ const provider = createTestDataProvider({
107
+ subscribeQuotes: (targets, onQuote) => {
108
+ subscribedTarget = targets[0];
109
+ handler = onQuote;
110
+ return () => {
111
+ unsubscribeCalls += 1;
112
+ };
113
+ },
114
+ });
115
+ let releaseFirst!: () => void;
116
+ const firstRefresh = new Promise<void>((resolve) => {
117
+ releaseFirst = resolve;
118
+ });
119
+ const snapshots: Array<ReadonlyMap<string, Quote>> = [];
120
+ const dispose = subscribeToLiveChartQuotes({
121
+ spec,
122
+ dataProvider: provider,
123
+ refreshIntervalMs: 0,
124
+ onRefresh: async (overrides) => {
125
+ snapshots.push(overrides);
126
+ if (snapshots.length === 1) await firstRefresh;
127
+ },
128
+ });
129
+ const key = chartQuoteOverrideKeyForTarget(subscribedTarget!);
130
+
131
+ handler!(subscribedTarget!, quote("AAPL", 100, 100));
132
+ handler!(subscribedTarget!, quote("AAPL", 101, 101));
133
+ await waitFor(() => snapshots.length === 1);
134
+ expect(snapshots[0]?.get(key)?.price).toBe(101);
135
+
136
+ handler!(subscribedTarget!, quote("AAPL", 102, 102));
137
+ handler!(subscribedTarget!, quote("AAPL", 99, 99));
138
+ handler!(subscribedTarget!, quote("AAPL", 103, 103));
139
+ await new Promise((resolve) => setTimeout(resolve, 5));
140
+ expect(snapshots).toHaveLength(1);
141
+
142
+ releaseFirst();
143
+ await waitFor(() => snapshots.length === 2);
144
+ expect(snapshots[1]?.get(key)?.price).toBe(103);
145
+
146
+ dispose();
147
+ dispose();
148
+ expect(unsubscribeCalls).toBe(1);
149
+ handler!(subscribedTarget!, quote("AAPL", 104, 104));
150
+ await new Promise((resolve) => setTimeout(resolve, 5));
151
+ expect(snapshots).toHaveLength(2);
152
+ });
153
+
154
+ test("does not refresh resolved charts for receivedAt-only quote updates", async () => {
155
+ const spec = specWithSeries([securitySeries("price", "AAPL", "market.close")]);
156
+ let handler: Parameters<NonNullable<ReturnType<typeof createTestDataProvider>["subscribeQuotes"]>>[1]
157
+ | undefined;
158
+ let target: Parameters<NonNullable<ReturnType<typeof createTestDataProvider>["subscribeQuotes"]>>[0][number]
159
+ | undefined;
160
+ const provider = createTestDataProvider({
161
+ subscribeQuotes: (targets, onQuote) => {
162
+ target = targets[0];
163
+ handler = onQuote;
164
+ return () => {};
165
+ },
166
+ });
167
+ let refreshCalls = 0;
168
+ const dispose = subscribeToLiveChartQuotes({
169
+ spec,
170
+ dataProvider: provider,
171
+ refreshIntervalMs: 0,
172
+ onRefresh: () => {
173
+ refreshCalls += 1;
174
+ },
175
+ });
176
+
177
+ handler!(target!, { ...quote("AAPL", 100, 100), receivedAt: 100 });
178
+ await waitFor(() => refreshCalls === 1);
179
+ handler!(target!, { ...quote("AAPL", 100, 100), receivedAt: 101 });
180
+ await new Promise((resolve) => setTimeout(resolve, 5));
181
+ expect(refreshCalls).toBe(1);
182
+
183
+ handler!(target!, { ...quote("AAPL", 101, 100), receivedAt: 102 });
184
+ await waitFor(() => refreshCalls === 2);
185
+ dispose();
186
+ });
187
+
188
+ test("continues after a synchronous background refresh failure", async () => {
189
+ const spec = specWithSeries([securitySeries("price", "AAPL", "market.close")]);
190
+ let handler: Parameters<NonNullable<ReturnType<typeof createTestDataProvider>["subscribeQuotes"]>>[1]
191
+ | undefined;
192
+ let target: Parameters<NonNullable<ReturnType<typeof createTestDataProvider>["subscribeQuotes"]>>[0][number]
193
+ | undefined;
194
+ const provider = createTestDataProvider({
195
+ subscribeQuotes: (targets, onQuote) => {
196
+ target = targets[0];
197
+ handler = onQuote;
198
+ return () => {};
199
+ },
200
+ });
201
+ let refreshCalls = 0;
202
+ const dispose = subscribeToLiveChartQuotes({
203
+ spec,
204
+ dataProvider: provider,
205
+ refreshIntervalMs: 0,
206
+ onRefresh: () => {
207
+ refreshCalls += 1;
208
+ if (refreshCalls === 1) throw new Error("temporary failure");
209
+ },
210
+ });
211
+
212
+ handler!(target!, quote("AAPL", 100, 100));
213
+ await waitFor(() => refreshCalls === 1);
214
+ handler!(target!, quote("AAPL", 101, 101));
215
+ await waitFor(() => refreshCalls === 2);
216
+
217
+ dispose();
218
+ });
219
+ });
@@ -0,0 +1,203 @@
1
+ import { canonicalTimeSeriesFieldId, isMarketFieldId } from "./field-catalog";
2
+ import type { DataProvider, QuoteSubscriptionTarget } from "../types/data-provider";
3
+ import type { Quote } from "../types/financials";
4
+ import type { BrokerContractRef } from "../types/instrument";
5
+ import type { ChartSeriesSpec, ChartSpec, SecuritySeriesSource } from "./types";
6
+ import { activeStudyInputSeriesIds } from "./studies";
7
+ import { valuationSeriesUsesLiveQuote } from "./fundamentals";
8
+
9
+ export const LIVE_CHART_REFRESH_INTERVAL_MS = 1_000;
10
+
11
+ function normalized(value: string | undefined): string {
12
+ return value?.trim().toUpperCase() ?? "";
13
+ }
14
+
15
+ function quoteIdentityKey(identity: {
16
+ symbol: string;
17
+ exchange?: string;
18
+ brokerId?: string;
19
+ brokerInstanceId?: string;
20
+ instrument?: BrokerContractRef | null;
21
+ }): string {
22
+ const contractKey = identity.instrument?.conId
23
+ ?? identity.instrument?.localSymbol
24
+ ?? identity.instrument?.symbol
25
+ ?? "";
26
+ return [
27
+ normalized(identity.symbol),
28
+ normalized(identity.exchange),
29
+ identity.brokerId ?? "",
30
+ identity.brokerInstanceId ?? "",
31
+ contractKey,
32
+ ].join("|");
33
+ }
34
+
35
+ export function chartQuoteOverrideKeyForSource(source: SecuritySeriesSource): string {
36
+ return quoteIdentityKey({
37
+ symbol: source.instrument.symbol,
38
+ exchange: source.instrument.exchange,
39
+ brokerId: source.instrument.brokerId,
40
+ brokerInstanceId: source.instrument.brokerInstanceId,
41
+ instrument: source.instrument.instrument,
42
+ });
43
+ }
44
+
45
+ export function chartQuoteOverrideKeyForTarget(target: QuoteSubscriptionTarget): string {
46
+ return quoteIdentityKey({
47
+ symbol: target.symbol,
48
+ exchange: target.exchange,
49
+ brokerId: target.context?.brokerId,
50
+ brokerInstanceId: target.context?.brokerInstanceId,
51
+ instrument: target.context?.instrument,
52
+ });
53
+ }
54
+
55
+ function supportsLiveQuote(
56
+ series: ChartSeriesSpec,
57
+ activeStudyInputs: ReadonlySet<string>,
58
+ ): series is ChartSeriesSpec & {
59
+ source: SecuritySeriesSource;
60
+ } {
61
+ if ((series.visible === false && !activeStudyInputs.has(series.id)) || series.source.kind !== "security") {
62
+ return false;
63
+ }
64
+ const fieldId = canonicalTimeSeriesFieldId(series.source.fieldId);
65
+ return isMarketFieldId(fieldId) || valuationSeriesUsesLiveQuote(fieldId);
66
+ }
67
+
68
+ /** Displayed or study-required quote-sensitive instruments, deduplicated by routing identity. */
69
+ export function getLiveChartQuoteTargets(spec: ChartSpec): QuoteSubscriptionTarget[] {
70
+ const targets = new Map<string, QuoteSubscriptionTarget>();
71
+ const activeStudyInputs = activeStudyInputSeriesIds(spec.studies);
72
+ for (const series of spec.series) {
73
+ if (!supportsLiveQuote(series, activeStudyInputs)) continue;
74
+ const target: QuoteSubscriptionTarget = {
75
+ symbol: series.source.instrument.symbol,
76
+ exchange: series.source.instrument.exchange,
77
+ context: {
78
+ brokerId: series.source.instrument.brokerId,
79
+ brokerInstanceId: series.source.instrument.brokerInstanceId,
80
+ instrument: series.source.instrument.instrument ?? null,
81
+ },
82
+ surface: "detail",
83
+ visible: true,
84
+ weight: 1,
85
+ };
86
+ targets.set(chartQuoteOverrideKeyForTarget(target), target);
87
+ }
88
+ return [...targets.values()];
89
+ }
90
+
91
+ export function liveChartQuoteTargetSignature(spec: ChartSpec): string {
92
+ return getLiveChartQuoteTargets(spec)
93
+ .map(chartQuoteOverrideKeyForTarget)
94
+ .sort()
95
+ .join("\n");
96
+ }
97
+
98
+ function isNewerQuote(next: Quote, current: Quote | undefined): boolean {
99
+ if (!current) return true;
100
+ if (next.lastUpdated !== current.lastUpdated) return next.lastUpdated > current.lastUpdated;
101
+ return (next.receivedAt ?? 0) > (current.receivedAt ?? 0);
102
+ }
103
+
104
+ function hasResolutionRelevantChange(next: Quote, current: Quote | undefined): boolean {
105
+ if (!current) return true;
106
+ return next.lastUpdated !== current.lastUpdated
107
+ || next.price !== current.price
108
+ || next.currency !== current.currency
109
+ || next.providerId !== current.providerId
110
+ || next.marketState !== current.marketState
111
+ || next.preMarketPrice !== current.preMarketPrice
112
+ || next.postMarketPrice !== current.postMarketPrice
113
+ || next.exchangeName !== current.exchangeName
114
+ || next.listingExchangeName !== current.listingExchangeName;
115
+ }
116
+
117
+ export interface LiveChartQuoteSubscriptionOptions {
118
+ spec: ChartSpec;
119
+ dataProvider: DataProvider | null;
120
+ onRefresh: (quoteOverrides: ReadonlyMap<string, Quote>) => Promise<void> | void;
121
+ refreshIntervalMs?: number;
122
+ }
123
+
124
+ /**
125
+ * Coalesces streaming quote bursts and serializes engine refreshes. A slow
126
+ * refresh can have at most one follow-up queued, using the latest quote per
127
+ * instrument, so streaming never fans out into overlapping history requests.
128
+ */
129
+ export function subscribeToLiveChartQuotes({
130
+ spec,
131
+ dataProvider,
132
+ onRefresh,
133
+ refreshIntervalMs = LIVE_CHART_REFRESH_INTERVAL_MS,
134
+ }: LiveChartQuoteSubscriptionOptions): () => void {
135
+ const targets = getLiveChartQuoteTargets(spec);
136
+ if (!dataProvider?.subscribeQuotes || targets.length === 0) return () => {};
137
+
138
+ const subscribedKeys = new Set(targets.map(chartQuoteOverrideKeyForTarget));
139
+ const quoteOverrides = new Map<string, Quote>();
140
+ const interval = Math.max(0, refreshIntervalMs);
141
+ let disposed = false;
142
+ let pending = false;
143
+ let inFlight = false;
144
+ let lastStartedAt = Number.NEGATIVE_INFINITY;
145
+ let timer: ReturnType<typeof setTimeout> | null = null;
146
+
147
+ const schedule = () => {
148
+ if (disposed || inFlight || timer !== null || !pending) return;
149
+ const delay = Math.max(0, interval - (Date.now() - lastStartedAt));
150
+ timer = setTimeout(() => {
151
+ timer = null;
152
+ if (disposed || inFlight || !pending) return;
153
+ pending = false;
154
+ inFlight = true;
155
+ lastStartedAt = Date.now();
156
+ const snapshot = new Map(quoteOverrides);
157
+ Promise.resolve()
158
+ .then(() => onRefresh(snapshot))
159
+ .catch(() => {
160
+ // A background refresh failure must not stop the live subscription.
161
+ })
162
+ .finally(() => {
163
+ inFlight = false;
164
+ if (pending) schedule();
165
+ });
166
+ }, delay);
167
+ };
168
+
169
+ let unsubscribe: () => void;
170
+ try {
171
+ unsubscribe = dataProvider.subscribeQuotes(targets, (target, quote) => {
172
+ if (disposed) return;
173
+ const key = chartQuoteOverrideKeyForTarget(target);
174
+ const previous = quoteOverrides.get(key);
175
+ if (!subscribedKeys.has(key) || !isNewerQuote(quote, previous)) return;
176
+ quoteOverrides.set(key, quote);
177
+ // receivedAt-only updates keep freshness metadata current without
178
+ // rebuilding unchanged series, studies, and chart bitmaps.
179
+ if (!hasResolutionRelevantChange(quote, previous)) return;
180
+ pending = true;
181
+ schedule();
182
+ });
183
+ } catch {
184
+ disposed = true;
185
+ pending = false;
186
+ if (timer !== null) clearTimeout(timer);
187
+ timer = null;
188
+ return () => {};
189
+ }
190
+
191
+ return () => {
192
+ if (disposed) return;
193
+ disposed = true;
194
+ pending = false;
195
+ if (timer !== null) clearTimeout(timer);
196
+ timer = null;
197
+ try {
198
+ unsubscribe();
199
+ } catch {
200
+ // Cleanup remains idempotent even if a provider has already torn down.
201
+ }
202
+ };
203
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ CHART_RESOLUTION_STEP_MS,
3
+ type ManualChartResolution,
4
+ } from "../components/chart/core/resolution";
5
+ import { isMarketFieldId } from "./field-catalog";
6
+ import type { ChartSeriesSpec, SeriesPeriod } from "./types";
7
+
8
+ const MARKET_PERIOD_MAX_RESOLUTION: Partial<Record<SeriesPeriod, ManualChartResolution>> = {
9
+ daily: "1d",
10
+ weekly: "1wk",
11
+ monthly: "1mo",
12
+ };
13
+
14
+ export function maximumResolutionForMarketPeriod(
15
+ period: SeriesPeriod | undefined,
16
+ ): ManualChartResolution | null {
17
+ return period ? MARKET_PERIOD_MAX_RESOLUTION[period] ?? null : null;
18
+ }
19
+
20
+ export function isResolutionFineEnoughForMarketPeriod(
21
+ resolution: ManualChartResolution,
22
+ period: SeriesPeriod | undefined,
23
+ ): boolean {
24
+ const maximum = maximumResolutionForMarketPeriod(period);
25
+ return maximum === null
26
+ || CHART_RESOLUTION_STEP_MS[resolution] <= CHART_RESOLUTION_STEP_MS[maximum];
27
+ }
28
+
29
+ /** Returns the finest fetch cadence required by explicit market periods. */
30
+ export function finestExplicitMarketPeriodResolution(
31
+ series: readonly ChartSeriesSpec[],
32
+ ): ManualChartResolution | null {
33
+ let finest: ManualChartResolution | null = null;
34
+ for (const entry of series) {
35
+ if (entry.source.kind !== "security" || !isMarketFieldId(entry.source.fieldId)) continue;
36
+ const required = maximumResolutionForMarketPeriod(entry.source.period);
37
+ if (!required) continue;
38
+ if (!finest || CHART_RESOLUTION_STEP_MS[required] < CHART_RESOLUTION_STEP_MS[finest]) {
39
+ finest = required;
40
+ }
41
+ }
42
+ return finest;
43
+ }
44
+
45
+ export function resolutionForExplicitMarketPeriods(
46
+ preferred: ManualChartResolution,
47
+ series: readonly ChartSeriesSpec[],
48
+ ): ManualChartResolution {
49
+ const required = finestExplicitMarketPeriodResolution(series);
50
+ return required && CHART_RESOLUTION_STEP_MS[preferred] > CHART_RESOLUTION_STEP_MS[required]
51
+ ? required
52
+ : preferred;
53
+ }
@@ -0,0 +1,125 @@
1
+ import type { PricePoint, TickerFinancials } from "../types/financials";
2
+ import { canonicalTimeSeriesFieldId, isFundamentalFieldId, isMarketFieldId } from "./field-catalog";
3
+ import { extractFundamentalSeries } from "./fundamentals";
4
+ import type { SecuritySeriesSource, SeriesPeriod, TimeSeriesPoint } from "./types";
5
+
6
+ function finiteNumber(value: unknown): value is number {
7
+ return typeof value === "number" && Number.isFinite(value);
8
+ }
9
+
10
+ function pricePointDate(value: unknown): Date | null {
11
+ const date = value instanceof Date ? new Date(value) : new Date(value as string | number);
12
+ return Number.isFinite(date.getTime()) ? date : null;
13
+ }
14
+
15
+ function utcDay(date: Date): string {
16
+ return date.toISOString().slice(0, 10);
17
+ }
18
+
19
+ function periodKey(date: Date, period: SeriesPeriod): string {
20
+ const year = date.getUTCFullYear();
21
+ const month = date.getUTCMonth();
22
+ if (period === "annual") return String(year);
23
+ if (period === "quarterly") return `${year}-Q${Math.floor(month / 3) + 1}`;
24
+ if (period === "monthly") return `${year}-${String(month + 1).padStart(2, "0")}`;
25
+ if (period === "weekly") {
26
+ const monday = new Date(Date.UTC(year, month, date.getUTCDate()));
27
+ const weekday = monday.getUTCDay();
28
+ monday.setUTCDate(monday.getUTCDate() - (weekday === 0 ? 6 : weekday - 1));
29
+ return utcDay(monday);
30
+ }
31
+ return utcDay(date);
32
+ }
33
+
34
+ interface AggregatedPricePoint {
35
+ date: Date;
36
+ open: number;
37
+ high: number;
38
+ low: number;
39
+ close: number;
40
+ volume?: number;
41
+ }
42
+
43
+ /** Aggregates OHLCV correctly: first open, max high, min low, last close, summed volume. */
44
+ export function aggregatePriceHistory(
45
+ points: readonly PricePoint[],
46
+ period: SeriesPeriod,
47
+ ): PricePoint[] {
48
+ const sorted = points
49
+ .flatMap((point) => {
50
+ const date = pricePointDate(point.date);
51
+ return date && finiteNumber(point.close) ? [{ ...point, date }] : [];
52
+ })
53
+ .sort((left, right) => left.date.getTime() - right.date.getTime());
54
+ if (period === "auto") return sorted.map((point) => ({ ...point, date: new Date(point.date) }));
55
+ if (period === "ttm") return [];
56
+
57
+ const buckets = new Map<string, AggregatedPricePoint>();
58
+ for (const point of sorted) {
59
+ const key = periodKey(point.date, period);
60
+ const open = finiteNumber(point.open) ? point.open : point.close;
61
+ const high = finiteNumber(point.high) ? point.high : point.close;
62
+ const low = finiteNumber(point.low) ? point.low : point.close;
63
+ const current = buckets.get(key);
64
+ if (!current) {
65
+ buckets.set(key, {
66
+ date: new Date(point.date),
67
+ open,
68
+ high,
69
+ low,
70
+ close: point.close,
71
+ volume: finiteNumber(point.volume) ? point.volume : undefined,
72
+ });
73
+ continue;
74
+ }
75
+ current.date = new Date(point.date);
76
+ current.high = Math.max(current.high, high);
77
+ current.low = Math.min(current.low, low);
78
+ current.close = point.close;
79
+ if (finiteNumber(point.volume)) current.volume = (current.volume ?? 0) + point.volume;
80
+ }
81
+ return [...buckets.values()];
82
+ }
83
+
84
+ export function extractPriceSeries(
85
+ priceHistory: readonly PricePoint[],
86
+ source: SecuritySeriesSource,
87
+ ): TimeSeriesPoint[] {
88
+ const fieldId = canonicalTimeSeriesFieldId(source.fieldId);
89
+ if (!isMarketFieldId(fieldId)) return [];
90
+ const aggregated = aggregatePriceHistory(priceHistory, source.period ?? "auto");
91
+ return aggregated.map((point) => {
92
+ const value = fieldId === "market.open"
93
+ ? point.open
94
+ : fieldId === "market.high"
95
+ ? point.high
96
+ : fieldId === "market.low"
97
+ ? point.low
98
+ : fieldId === "market.volume"
99
+ ? point.volume
100
+ : point.close;
101
+ return {
102
+ date: new Date(point.date),
103
+ observedAt: new Date(point.date),
104
+ availableAt: new Date(point.date),
105
+ value: finiteNumber(value) ? value : null,
106
+ open: finiteNumber(point.open) ? point.open : null,
107
+ high: finiteNumber(point.high) ? point.high : null,
108
+ low: finiteNumber(point.low) ? point.low : null,
109
+ close: finiteNumber(point.close) ? point.close : null,
110
+ volume: finiteNumber(point.volume) ? point.volume : null,
111
+ provenance: { quality: "reported" as const },
112
+ };
113
+ });
114
+ }
115
+
116
+ /** Pure security-source coordinator used by runtime hooks after data loading. */
117
+ export function extractSecuritySeries(
118
+ financials: TickerFinancials | null,
119
+ source: SecuritySeriesSource,
120
+ ): TimeSeriesPoint[] {
121
+ if (!financials) return [];
122
+ if (isMarketFieldId(source.fieldId)) return extractPriceSeries(financials.priceHistory, source);
123
+ if (isFundamentalFieldId(source.fieldId)) return extractFundamentalSeries(financials, source);
124
+ return [];
125
+ }