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,1281 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { FredSeriesData, FredSeriesLoadResult } from "../data/fred-series";
3
+ import { createTestDataProvider } from "../test-support/data-provider";
4
+ import type { TickerFinancials } from "../types/financials";
5
+ import { CHART_SPEC_VERSION, type ChartSpec } from "./types";
6
+ import { ChartResolveCache, resolveChartSpecData } from "./resolve";
7
+ import { chartQuoteOverrideKeyForSource } from "./live-quotes";
8
+
9
+ const emptyFinancials = (): TickerFinancials => ({
10
+ annualStatements: [],
11
+ quarterlyStatements: [],
12
+ priceHistory: [],
13
+ });
14
+
15
+ const fredLoad = (
16
+ data: FredSeriesData = { observations: [], info: null },
17
+ overrides: Partial<FredSeriesLoadResult> = {},
18
+ ): FredSeriesLoadResult => ({
19
+ data,
20
+ fetchedAt: Date.parse("2026-03-01T00:00:00Z"),
21
+ stale: false,
22
+ source: "network",
23
+ ...overrides,
24
+ });
25
+
26
+ describe("resolveChartSpecData", () => {
27
+ test("derives Auto fetch resolution from the finest explicit market period", async () => {
28
+ const cases = [
29
+ { range: "ALL" as const, periods: ["daily", "monthly"] as const, expected: "1d" },
30
+ { range: "ALL" as const, periods: ["weekly", "monthly"] as const, expected: "1wk" },
31
+ { range: "1Y" as const, periods: ["monthly"] as const, expected: "1d" },
32
+ ];
33
+
34
+ for (const scenario of cases) {
35
+ const requestedResolutions: string[] = [];
36
+ const provider = createTestDataProvider({
37
+ getTickerFinancials: async () => emptyFinancials(),
38
+ getPriceHistoryForResolution: async (_symbol, _exchange, _range, resolution) => {
39
+ requestedResolutions.push(resolution);
40
+ return [{ date: new Date("2025-01-07T16:00:00Z"), close: 100 }];
41
+ },
42
+ });
43
+ const spec: ChartSpec = {
44
+ version: CHART_SPEC_VERSION,
45
+ viewport: { range: scenario.range, resolution: "auto" },
46
+ panels: [{ id: "main" }],
47
+ series: scenario.periods.map((period, index) => ({
48
+ id: `price-${index}`,
49
+ source: {
50
+ kind: "security" as const,
51
+ instrument: { symbol: `TEST${index}` },
52
+ fieldId: "market.close",
53
+ period,
54
+ },
55
+ style: "line" as const,
56
+ transform: "raw" as const,
57
+ axis: "left" as const,
58
+ panelId: "main",
59
+ interpolation: "none" as const,
60
+ })),
61
+ studies: [],
62
+ };
63
+
64
+ const result = await resolveChartSpecData(spec, {
65
+ dataProvider: provider,
66
+ now: new Date("2025-01-08T00:00:00Z"),
67
+ loadFredSeries: async () => fredLoad(),
68
+ });
69
+
70
+ expect(result.errors).toEqual([]);
71
+ expect(new Set(requestedResolutions)).toEqual(new Set([scenario.expected]));
72
+ }
73
+ });
74
+
75
+ test("does not relabel provider-default history as a manually requested resolution", async () => {
76
+ let genericHistoryCalls = 0;
77
+ const provider = createTestDataProvider({
78
+ getTickerFinancials: async () => emptyFinancials(),
79
+ getPriceHistoryForResolution: async () => [],
80
+ getPriceHistory: async () => {
81
+ genericHistoryCalls += 1;
82
+ return [{ date: new Date("2025-01-07T16:00:00Z"), close: 100 }];
83
+ },
84
+ });
85
+ const spec: ChartSpec = {
86
+ version: CHART_SPEC_VERSION,
87
+ viewport: { range: "1W", resolution: "5m" },
88
+ panels: [{ id: "main" }],
89
+ series: [{
90
+ id: "price",
91
+ source: { kind: "security", instrument: { symbol: "TEST" }, fieldId: "market.close" },
92
+ style: "line",
93
+ transform: "raw",
94
+ axis: "left",
95
+ panelId: "main",
96
+ interpolation: "none",
97
+ }],
98
+ studies: [],
99
+ };
100
+
101
+ const result = await resolveChartSpecData(spec, {
102
+ dataProvider: provider,
103
+ now: new Date("2025-01-08T00:00:00Z"),
104
+ loadFredSeries: async () => fredLoad(),
105
+ });
106
+
107
+ expect(genericHistoryCalls).toBe(0);
108
+ expect(result.series).toEqual([]);
109
+ expect(result.errors).toEqual([
110
+ "price: Requested 5m price history is unavailable for TEST. Choose Auto or a supported interval.",
111
+ ]);
112
+ });
113
+
114
+ test("uses provider-default history as a valid fallback for Auto resolution", async () => {
115
+ let genericHistoryCalls = 0;
116
+ const provider = createTestDataProvider({
117
+ getTickerFinancials: async () => emptyFinancials(),
118
+ getPriceHistoryForResolution: async () => {
119
+ throw new Error("5m is unavailable");
120
+ },
121
+ getPriceHistory: async () => {
122
+ genericHistoryCalls += 1;
123
+ return [{ date: new Date("2025-01-07T16:00:00Z"), close: 100 }];
124
+ },
125
+ });
126
+ const spec: ChartSpec = {
127
+ version: CHART_SPEC_VERSION,
128
+ viewport: { range: "1W", resolution: "auto" },
129
+ panels: [{ id: "main" }],
130
+ series: [{
131
+ id: "price",
132
+ source: { kind: "security", instrument: { symbol: "TEST" }, fieldId: "market.close" },
133
+ style: "line",
134
+ transform: "raw",
135
+ axis: "left",
136
+ panelId: "main",
137
+ interpolation: "none",
138
+ }],
139
+ studies: [],
140
+ };
141
+
142
+ const result = await resolveChartSpecData(spec, {
143
+ dataProvider: provider,
144
+ now: new Date("2025-01-08T00:00:00Z"),
145
+ loadFredSeries: async () => fredLoad(),
146
+ });
147
+
148
+ expect(genericHistoryCalls).toBe(1);
149
+ expect(result.errors).toEqual([]);
150
+ expect(result.series[0]?.points.map((point) => point.value)).toEqual([100]);
151
+ });
152
+
153
+ test("clamps preload history to the provider limit without coarsening the visible interval", async () => {
154
+ const requests: Array<{ range: string; resolution: string }> = [];
155
+ const provider = createTestDataProvider({
156
+ getTickerFinancials: async () => emptyFinancials(),
157
+ getChartResolutionSupport: () => [{ resolution: "5m", maxRange: "1W" }],
158
+ getPriceHistoryForResolution: async (_symbol, _exchange, range, resolution) => {
159
+ requests.push({ range, resolution });
160
+ return [{ date: new Date("2025-01-07T16:00:00Z"), close: 100 }];
161
+ },
162
+ });
163
+ const spec: ChartSpec = {
164
+ version: CHART_SPEC_VERSION,
165
+ viewport: { range: "1W", resolution: "auto" },
166
+ panels: [{ id: "main" }],
167
+ series: [{
168
+ id: "price",
169
+ source: { kind: "security", instrument: { symbol: "TEST" }, fieldId: "market.close" },
170
+ style: "line",
171
+ transform: "raw",
172
+ axis: "left",
173
+ panelId: "main",
174
+ interpolation: "none",
175
+ }],
176
+ studies: [],
177
+ };
178
+
179
+ const result = await resolveChartSpecData(spec, {
180
+ dataProvider: provider,
181
+ now: new Date("2025-01-08T00:00:00Z"),
182
+ loadFredSeries: async () => fredLoad(),
183
+ });
184
+
185
+ expect(result.errors).toEqual([]);
186
+ expect(requests).toEqual([{ range: "1W", resolution: "5m" }]);
187
+ });
188
+
189
+ test("adapts Auto to a zoomed historical window without changing the authored viewport", async () => {
190
+ const detailedRequests: Array<{
191
+ start: string;
192
+ end: string;
193
+ resolution: string;
194
+ }> = [];
195
+ const provider = createTestDataProvider({
196
+ getTickerFinancials: async () => emptyFinancials(),
197
+ getChartResolutionSupport: () => [
198
+ { resolution: "15m", maxRange: "1M" },
199
+ { resolution: "1h", maxRange: "6M" },
200
+ { resolution: "1d", maxRange: "5Y" },
201
+ ],
202
+ getDetailedPriceHistory: async (_symbol, _exchange, start, end, resolution) => {
203
+ detailedRequests.push({
204
+ start: start.toISOString(),
205
+ end: end.toISOString(),
206
+ resolution,
207
+ });
208
+ return [{ date: new Date("2025-01-15T16:00:00.000Z"), close: 100 }];
209
+ },
210
+ });
211
+ const spec: ChartSpec = {
212
+ version: CHART_SPEC_VERSION,
213
+ viewport: { range: "3M", resolution: "auto" },
214
+ panels: [{ id: "main" }],
215
+ series: [{
216
+ id: "price",
217
+ source: {
218
+ kind: "security",
219
+ instrument: { symbol: "TEST", exchange: "NASDAQ" },
220
+ fieldId: "market.close",
221
+ },
222
+ style: "line",
223
+ transform: "raw",
224
+ axis: "left",
225
+ panelId: "main",
226
+ interpolation: "none",
227
+ }],
228
+ studies: [],
229
+ };
230
+
231
+ const result = await resolveChartSpecData(
232
+ spec,
233
+ {
234
+ dataProvider: provider,
235
+ now: new Date("2025-04-01T00:00:00.000Z"),
236
+ loadFredSeries: async () => fredLoad(),
237
+ },
238
+ new ChartResolveCache(),
239
+ {
240
+ autoViewport: {
241
+ start: new Date("2025-01-10T00:00:00.000Z"),
242
+ end: new Date("2025-01-17T00:00:00.000Z"),
243
+ },
244
+ targetPointCount: 100,
245
+ },
246
+ );
247
+
248
+ expect(spec.viewport.resolution).toBe("auto");
249
+ expect(detailedRequests).toHaveLength(1);
250
+ expect(detailedRequests[0]?.resolution).toBe("15m");
251
+ expect(detailedRequests[0]?.end).toBe("2025-01-17T00:00:00.001Z");
252
+ expect(result.viewport?.start.toISOString()).toBe("2025-01-01T00:00:00.000Z");
253
+ expect(result.viewport?.end.toISOString()).toBe("2025-04-01T00:00:00.000Z");
254
+ expect(result.series[0]?.timeBasis).toMatchObject({
255
+ kind: "market",
256
+ timeZone: "America/New_York",
257
+ cadenceMs: 15 * 60 * 1_000,
258
+ });
259
+ });
260
+
261
+ test("resolves unrelated price, filed fundamental, and economic series on one chart", async () => {
262
+ const provider = createTestDataProvider({
263
+ getTickerFinancials: async (symbol) => symbol === "MSFT"
264
+ ? {
265
+ ...emptyFinancials(),
266
+ annualStatements: [{
267
+ date: "2025-12-31",
268
+ availableAt: "2026-02-15",
269
+ fieldAvailability: { totalRevenue: "2026-02-15" },
270
+ totalRevenue: 250,
271
+ }],
272
+ }
273
+ : emptyFinancials(),
274
+ getPriceHistoryForResolution: async (symbol, _exchange, _range, resolution) => {
275
+ expect(symbol).toBe("AAPL");
276
+ expect(resolution).toBe("1d");
277
+ return [
278
+ { date: new Date("2024-12-31T00:00:00Z"), close: 80 },
279
+ { date: new Date("2025-04-01T00:00:00Z"), close: 100 },
280
+ { date: new Date("2026-03-01T00:00:00Z"), close: 125 },
281
+ ];
282
+ },
283
+ });
284
+ const spec: ChartSpec = {
285
+ version: CHART_SPEC_VERSION,
286
+ viewport: { range: "1Y", resolution: "1d" },
287
+ panels: [{ id: "main" }, { id: "macro", height: 0.4 }],
288
+ series: [
289
+ {
290
+ id: "aapl-price",
291
+ source: { kind: "security", instrument: { symbol: "AAPL" }, fieldId: "market.close" },
292
+ style: "line",
293
+ transform: "percent",
294
+ axis: "auto",
295
+ panelId: "main",
296
+ interpolation: "none",
297
+ },
298
+ {
299
+ id: "msft-revenue",
300
+ source: {
301
+ kind: "security",
302
+ instrument: { symbol: "MSFT" },
303
+ fieldId: "fundamental.totalRevenue",
304
+ period: "annual",
305
+ timestampMode: "available-at",
306
+ },
307
+ style: "columns",
308
+ transform: "raw",
309
+ axis: "auto",
310
+ panelId: "main",
311
+ interpolation: "step-after",
312
+ },
313
+ {
314
+ id: "cpi",
315
+ source: { kind: "economic", provider: "fred", seriesId: "CPIAUCSL" },
316
+ style: "step",
317
+ transform: "raw",
318
+ axis: "auto",
319
+ panelId: "macro",
320
+ interpolation: "step-after",
321
+ },
322
+ ],
323
+ studies: [],
324
+ };
325
+
326
+ const result = await resolveChartSpecData(spec, {
327
+ dataProvider: provider,
328
+ now: new Date("2026-03-31T00:00:00Z"),
329
+ loadFredSeries: async () => fredLoad({
330
+ observations: [
331
+ { date: "2025-03-01", value: 310 },
332
+ { date: "2026-03-01", value: 320 },
333
+ ],
334
+ info: {
335
+ id: "CPIAUCSL",
336
+ title: "Consumer Price Index",
337
+ units: "Index 1982-1984=100",
338
+ frequency: "Monthly",
339
+ seasonalAdjustment: "Seasonally Adjusted",
340
+ source: "FRED",
341
+ notes: "",
342
+ },
343
+ }, {
344
+ fetchedAt: Date.parse("2026-03-15T00:00:00Z"),
345
+ stale: true,
346
+ source: "stale-fallback",
347
+ refreshError: "network unavailable",
348
+ }),
349
+ });
350
+
351
+ expect(result.errors).toEqual([]);
352
+ expect(result.series).toHaveLength(3);
353
+ const price = result.series.find((entry) => entry.id === "aapl-price")!;
354
+ expect(price.points.map((point) => point.value)).toEqual([0, 25]);
355
+ expect(price.axis).toBe("left");
356
+ const bufferedPrice = result.bufferedSeries?.find((entry) => entry.id === "aapl-price");
357
+ expect(bufferedPrice?.points.map((point) => point.value)).toEqual([-20, 0, 25]);
358
+ const revenue = result.series.find((entry) => entry.id === "msft-revenue")!;
359
+ expect(revenue.points[0]?.date.toISOString().slice(0, 10)).toBe("2026-02-15");
360
+ expect(revenue.axis).toBe("right");
361
+ expect(result.series.find((entry) => entry.id === "cpi")?.panelId).toBe("macro");
362
+ expect(result.warnings[0]).toContain("FRED refresh failed (network unavailable)");
363
+ expect(result.warnings[0]).toContain("cached data fetched 2026-03-15");
364
+ expect(result.warnings.some((warning) => warning.includes("FRED vintage dates"))).toBe(true);
365
+ expect(result.viewport).toEqual({
366
+ start: new Date("2025-03-31T00:00:00.000Z"),
367
+ end: new Date("2026-03-31T00:00:00.000Z"),
368
+ });
369
+ });
370
+
371
+ test("uses an exact custom window, derives its auto resolution, and warms studies before clipping", async () => {
372
+ let detailRequest: {
373
+ start: Date;
374
+ end: Date;
375
+ barSize: string;
376
+ } | null = null;
377
+ const provider = createTestDataProvider({
378
+ getTickerFinancials: async () => emptyFinancials(),
379
+ getDetailedPriceHistory: async (_symbol, _exchange, start, end, barSize) => {
380
+ detailRequest = { start, end, barSize };
381
+ return [
382
+ { date: new Date("2024-12-30T16:00:00Z"), close: 10 },
383
+ { date: new Date("2024-12-31T16:00:00Z"), close: 20 },
384
+ { date: new Date("2025-01-01T16:00:00Z"), close: 30 },
385
+ { date: new Date("2025-01-31T16:00:00Z"), close: 40 },
386
+ ];
387
+ },
388
+ });
389
+ const spec: ChartSpec = {
390
+ version: CHART_SPEC_VERSION,
391
+ viewport: {
392
+ range: "1Y",
393
+ resolution: "auto",
394
+ dateWindow: { start: "2025-01-01", end: "2025-01-31" },
395
+ },
396
+ panels: [{ id: "main" }],
397
+ series: [{
398
+ id: "price",
399
+ source: { kind: "security", instrument: { symbol: "TEST" }, fieldId: "market.close" },
400
+ style: "line",
401
+ transform: "raw",
402
+ axis: "left",
403
+ panelId: "main",
404
+ interpolation: "none",
405
+ }],
406
+ studies: [{
407
+ id: "sma",
408
+ kind: "sma",
409
+ inputSeriesIds: ["price"],
410
+ parameters: { period: 3 },
411
+ panelId: "main",
412
+ axis: "left",
413
+ }],
414
+ };
415
+
416
+ const result = await resolveChartSpecData(spec, {
417
+ dataProvider: provider,
418
+ now: new Date("2026-03-01T00:00:00Z"),
419
+ loadFredSeries: async () => fredLoad(),
420
+ });
421
+
422
+ expect(detailRequest).not.toBeNull();
423
+ expect(detailRequest!.barSize).toBe("15m");
424
+ expect(detailRequest!.start.getTime()).toBeLessThan(new Date("2025-01-01T00:00:00Z").getTime());
425
+ expect(detailRequest!.end.toISOString()).toBe("2025-02-01T00:00:00.000Z");
426
+ expect(result.series.find((entry) => entry.id === "price")?.points.map((point) => point.date.toISOString())).toEqual([
427
+ "2025-01-01T16:00:00.000Z",
428
+ "2025-01-31T16:00:00.000Z",
429
+ ]);
430
+ expect(result.series.find((entry) => entry.id === "sma")?.points.map((point) => point.value)).toEqual([20, 30]);
431
+ expect(result.viewport).toEqual({
432
+ start: new Date("2025-01-01T00:00:00.000Z"),
433
+ end: new Date("2025-01-31T23:59:59.999Z"),
434
+ });
435
+ });
436
+
437
+ test("falls back to a trailing range that reaches a historical custom window", async () => {
438
+ let detailAttempted = false;
439
+ let requestedRange = "";
440
+ let requestedResolution = "";
441
+ const provider = createTestDataProvider({
442
+ getTickerFinancials: async () => emptyFinancials(),
443
+ getDetailedPriceHistory: async () => {
444
+ detailAttempted = true;
445
+ return [];
446
+ },
447
+ getPriceHistoryForResolution: async (_symbol, _exchange, range, resolution) => {
448
+ requestedRange = range;
449
+ requestedResolution = resolution;
450
+ return [{ date: new Date("2025-01-31T16:00:00Z"), close: 40 }];
451
+ },
452
+ });
453
+ const spec: ChartSpec = {
454
+ version: CHART_SPEC_VERSION,
455
+ viewport: {
456
+ range: "1D",
457
+ resolution: "auto",
458
+ dateWindow: { start: "2025-01-01", end: "2025-01-31" },
459
+ },
460
+ panels: [{ id: "main" }],
461
+ series: [{
462
+ id: "price",
463
+ source: { kind: "security", instrument: { symbol: "TEST" }, fieldId: "market.close" },
464
+ style: "line",
465
+ transform: "raw",
466
+ axis: "left",
467
+ panelId: "main",
468
+ interpolation: "none",
469
+ }],
470
+ studies: [],
471
+ };
472
+
473
+ const result = await resolveChartSpecData(spec, {
474
+ dataProvider: provider,
475
+ now: new Date("2026-03-01T00:00:00Z"),
476
+ loadFredSeries: async () => fredLoad(),
477
+ });
478
+
479
+ expect(detailAttempted).toBe(true);
480
+ expect(requestedRange).toBe("5Y");
481
+ expect(requestedResolution).toBe("15m");
482
+ expect(result.series[0]?.points).toHaveLength(1);
483
+ });
484
+
485
+ test("retains the prior step observation as the custom window's left-edge anchor", async () => {
486
+ const provider = createTestDataProvider({
487
+ getTickerFinancials: async () => ({
488
+ ...emptyFinancials(),
489
+ annualStatements: [{
490
+ date: "2024-09-30",
491
+ availableAt: "2024-12-15",
492
+ fieldAvailability: { totalRevenue: "2024-12-15" },
493
+ totalRevenue: 90,
494
+ }],
495
+ }),
496
+ });
497
+ const spec: ChartSpec = {
498
+ version: CHART_SPEC_VERSION,
499
+ viewport: {
500
+ range: "1M",
501
+ resolution: "auto",
502
+ dateWindow: { start: "2025-01-01", end: "2025-01-31" },
503
+ },
504
+ panels: [{ id: "main" }],
505
+ series: [{
506
+ id: "revenue",
507
+ source: {
508
+ kind: "security",
509
+ instrument: { symbol: "TEST" },
510
+ fieldId: "fundamental.totalRevenue",
511
+ period: "annual",
512
+ timestampMode: "available-at",
513
+ },
514
+ style: "step",
515
+ transform: "raw",
516
+ axis: "left",
517
+ panelId: "main",
518
+ interpolation: "step-after",
519
+ }],
520
+ studies: [],
521
+ };
522
+
523
+ const result = await resolveChartSpecData(spec, {
524
+ dataProvider: provider,
525
+ now: new Date("2025-02-01T00:00:00Z"),
526
+ loadFredSeries: async () => fredLoad(),
527
+ });
528
+
529
+ const anchor = result.series[0]?.points[0];
530
+ expect(result.series[0]?.points).toHaveLength(1);
531
+ expect(anchor?.date.toISOString()).toBe("2024-12-15T00:00:00.000Z");
532
+ expect(anchor?.value).toBe(90);
533
+ });
534
+
535
+ test("retains an as-of anchor for ratio and spread formulas across a custom window", async () => {
536
+ const provider = createTestDataProvider({
537
+ getTickerFinancials: async (symbol) => ({
538
+ ...emptyFinancials(),
539
+ quarterlyStatements: symbol === "LEFT"
540
+ ? [
541
+ {
542
+ date: "2024-12-31",
543
+ availableAt: "2025-02-10",
544
+ fieldAvailability: { totalRevenue: "2025-02-10" },
545
+ totalRevenue: 100,
546
+ },
547
+ {
548
+ date: "2025-03-31",
549
+ availableAt: "2025-05-10",
550
+ fieldAvailability: { totalRevenue: "2025-05-10" },
551
+ totalRevenue: 120,
552
+ },
553
+ ]
554
+ : [
555
+ {
556
+ date: "2024-12-31",
557
+ availableAt: "2025-02-20",
558
+ fieldAvailability: { totalRevenue: "2025-02-20" },
559
+ totalRevenue: 50,
560
+ },
561
+ {
562
+ date: "2025-03-31",
563
+ availableAt: "2025-06-10",
564
+ fieldAvailability: { totalRevenue: "2025-06-10" },
565
+ totalRevenue: 40,
566
+ },
567
+ ],
568
+ }),
569
+ });
570
+ const fundamental = (id: string, symbol: string): ChartSpec["series"][number] => ({
571
+ id,
572
+ source: {
573
+ kind: "security",
574
+ instrument: { symbol },
575
+ fieldId: "fundamental.totalRevenue",
576
+ period: "quarterly",
577
+ timestampMode: "available-at",
578
+ },
579
+ // The built-in fundamental comparison preset uses columns without
580
+ // display interpolation; formula calculation still uses as-of values.
581
+ style: "columns",
582
+ transform: "raw",
583
+ axis: "left",
584
+ panelId: "main",
585
+ interpolation: "none",
586
+ });
587
+ const spec: ChartSpec = {
588
+ version: CHART_SPEC_VERSION,
589
+ viewport: {
590
+ range: "1Y",
591
+ resolution: "auto",
592
+ dateWindow: { start: "2025-04-01", end: "2025-07-31" },
593
+ },
594
+ panels: [{ id: "main" }, { id: "formula" }],
595
+ series: [fundamental("left", "LEFT"), fundamental("right", "RIGHT")],
596
+ studies: [
597
+ {
598
+ id: "ratio",
599
+ kind: "ratio",
600
+ inputSeriesIds: ["left", "right"],
601
+ parameters: {},
602
+ panelId: "formula",
603
+ axis: "left",
604
+ },
605
+ {
606
+ id: "spread",
607
+ kind: "spread",
608
+ inputSeriesIds: ["left", "right"],
609
+ parameters: {},
610
+ panelId: "formula",
611
+ axis: "left",
612
+ },
613
+ ],
614
+ };
615
+
616
+ const result = await resolveChartSpecData(spec, {
617
+ dataProvider: provider,
618
+ now: new Date("2025-08-01T00:00:00Z"),
619
+ loadFredSeries: async () => fredLoad(),
620
+ });
621
+
622
+ expect(result.errors).toEqual([]);
623
+ expect(result.series.find(({ id }) => id === "ratio")).toMatchObject({
624
+ style: "step",
625
+ interpolation: "step-after",
626
+ });
627
+ expect(result.series.find(({ id }) => id === "ratio")?.points.map((point) => ({
628
+ date: point.date.toISOString().slice(0, 10),
629
+ value: point.value,
630
+ }))).toEqual([
631
+ { date: "2025-02-20", value: 2 },
632
+ { date: "2025-05-10", value: 2.4 },
633
+ { date: "2025-06-10", value: 3 },
634
+ ]);
635
+ expect(result.series.find(({ id }) => id === "spread")?.points[0]).toMatchObject({
636
+ date: new Date("2025-02-20T00:00:00Z"),
637
+ value: 50,
638
+ });
639
+ });
640
+
641
+ test("keeps an explicit viewport when a latest-observation cap is also set", async () => {
642
+ const provider = createTestDataProvider({
643
+ getTickerFinancials: async () => ({
644
+ ...emptyFinancials(),
645
+ annualStatements: [
646
+ {
647
+ date: "2023-12-31",
648
+ availableAt: "2025-01-10",
649
+ fieldAvailability: { totalRevenue: "2025-01-10" },
650
+ totalRevenue: 100,
651
+ },
652
+ {
653
+ date: "2024-12-31",
654
+ availableAt: "2025-02-10",
655
+ fieldAvailability: { totalRevenue: "2025-02-10" },
656
+ totalRevenue: 120,
657
+ },
658
+ ],
659
+ }),
660
+ });
661
+ const spec: ChartSpec = {
662
+ version: CHART_SPEC_VERSION,
663
+ viewport: {
664
+ range: "ALL",
665
+ resolution: "auto",
666
+ dateWindow: { start: "2025-01-01", end: "2025-02-28" },
667
+ maxPoints: 1,
668
+ },
669
+ panels: [{ id: "main" }],
670
+ series: [{
671
+ id: "revenue",
672
+ source: {
673
+ kind: "security",
674
+ instrument: { symbol: "TEST" },
675
+ fieldId: "fundamental.totalRevenue",
676
+ period: "annual",
677
+ timestampMode: "available-at",
678
+ },
679
+ style: "step",
680
+ transform: "raw",
681
+ axis: "left",
682
+ panelId: "main",
683
+ interpolation: "step-after",
684
+ }],
685
+ studies: [],
686
+ };
687
+
688
+ const result = await resolveChartSpecData(spec, {
689
+ dataProvider: provider,
690
+ now: new Date("2025-03-01T00:00:00Z"),
691
+ loadFredSeries: async () => fredLoad(),
692
+ });
693
+
694
+ expect(result.series[0]?.points.map((point) => point.value)).toEqual([120]);
695
+ expect(result.viewport).toEqual({
696
+ start: new Date("2025-01-01T00:00:00.000Z"),
697
+ end: new Date("2025-02-28T23:59:59.999Z"),
698
+ });
699
+ });
700
+
701
+ test("keeps hidden base series available to visible studies without rendering the base", async () => {
702
+ const provider = createTestDataProvider({
703
+ getTickerFinancials: async () => emptyFinancials(),
704
+ getPriceHistoryForResolution: async () => [
705
+ { date: new Date("2025-01-01T16:00:00Z"), close: 10 },
706
+ { date: new Date("2025-01-02T16:00:00Z"), close: 20 },
707
+ { date: new Date("2025-01-03T16:00:00Z"), close: 30 },
708
+ ],
709
+ });
710
+ const spec: ChartSpec = {
711
+ version: CHART_SPEC_VERSION,
712
+ viewport: { range: "1Y", resolution: "1d" },
713
+ panels: [{ id: "main" }],
714
+ series: [{
715
+ id: "price",
716
+ source: {
717
+ kind: "security",
718
+ instrument: { symbol: "TEST", exchange: "NASDAQ" },
719
+ fieldId: "market.close",
720
+ },
721
+ style: "line",
722
+ transform: "raw",
723
+ axis: "left",
724
+ panelId: "main",
725
+ interpolation: "none",
726
+ visible: false,
727
+ }],
728
+ studies: [{
729
+ id: "sma",
730
+ kind: "sma",
731
+ inputSeriesIds: ["price"],
732
+ parameters: { period: 2 },
733
+ panelId: "main",
734
+ axis: "left",
735
+ }],
736
+ };
737
+
738
+ const result = await resolveChartSpecData(spec, {
739
+ dataProvider: provider,
740
+ now: new Date("2025-01-04T00:00:00Z"),
741
+ loadFredSeries: async () => fredLoad(),
742
+ });
743
+
744
+ expect(result.errors).toEqual([]);
745
+ expect(result.series.map((series) => series.id)).toEqual(["sma"]);
746
+ expect(result.legendSeries?.map((series) => series.id)).toEqual(["price", "sma"]);
747
+ expect(result.series[0]?.points.map((point) => point.value)).toEqual([15, 25]);
748
+ expect(result.legendSeries?.find((series) => series.id === "price")?.timeBasis)
749
+ .toMatchObject({ kind: "market", timeZone: "America/New_York" });
750
+ expect(result.series[0]?.timeBasis)
751
+ .toMatchObject({ kind: "market", timeZone: "America/New_York" });
752
+ });
753
+
754
+ test("applies a streamed quote override before recomputing transforms and studies", async () => {
755
+ const now = Date.now();
756
+ const source = {
757
+ kind: "security" as const,
758
+ instrument: { symbol: "LIVE" },
759
+ fieldId: "market.close",
760
+ };
761
+ const provider = createTestDataProvider({
762
+ getTickerFinancials: async () => ({
763
+ ...emptyFinancials(),
764
+ quote: {
765
+ symbol: "LIVE",
766
+ price: 115,
767
+ currency: "USD",
768
+ change: 5,
769
+ changePercent: 4.5,
770
+ lastUpdated: now - 500,
771
+ },
772
+ }),
773
+ getPriceHistoryForResolution: async () => [
774
+ { date: new Date(now - 2 * 86_400_000), close: 100 },
775
+ { date: new Date(now - 86_400_000), close: 110 },
776
+ ],
777
+ });
778
+ const spec: ChartSpec = {
779
+ version: CHART_SPEC_VERSION,
780
+ viewport: { range: "1Y", resolution: "1d" },
781
+ panels: [{ id: "main" }],
782
+ series: [{
783
+ id: "price",
784
+ source,
785
+ style: "line",
786
+ transform: "percent",
787
+ axis: "left",
788
+ panelId: "main",
789
+ interpolation: "none",
790
+ }],
791
+ studies: [{
792
+ id: "sma",
793
+ kind: "sma",
794
+ inputSeriesIds: ["price"],
795
+ parameters: { period: 2 },
796
+ panelId: "main",
797
+ axis: "left",
798
+ }],
799
+ };
800
+ const quoteOverrides = new Map([[
801
+ chartQuoteOverrideKeyForSource(source),
802
+ {
803
+ symbol: "LIVE",
804
+ price: 121,
805
+ currency: "USD",
806
+ change: 11,
807
+ changePercent: 10,
808
+ lastUpdated: now,
809
+ },
810
+ ]]);
811
+
812
+ const result = await resolveChartSpecData(spec, {
813
+ dataProvider: provider,
814
+ now: new Date(now),
815
+ quoteOverrides,
816
+ loadFredSeries: async () => fredLoad(),
817
+ });
818
+
819
+ expect(result.errors).toEqual([]);
820
+ const price = result.series.find((entry) => entry.id === "price")!;
821
+ expect(price.points).toHaveLength(3);
822
+ expect(price.points.at(-1)?.date.getTime()).toBe(now);
823
+ expect(price.points.at(-1)?.value).toBeCloseTo(21);
824
+ const sma = result.series.find((entry) => entry.id === "sma")!;
825
+ expect(sma.points).toHaveLength(2);
826
+ expect(sma.points.at(-1)?.date.getTime()).toBe(now);
827
+ expect(sma.points.at(-1)?.value).toBeCloseTo(15.5);
828
+ });
829
+
830
+ test("reuses raw source loads while live quotes recompute the chart tail", async () => {
831
+ const now = Date.parse("2026-05-15T20:30:00Z");
832
+ const source = {
833
+ kind: "security" as const,
834
+ instrument: { symbol: "LIVE" },
835
+ fieldId: "market.close",
836
+ };
837
+ let financialCalls = 0;
838
+ let historyCalls = 0;
839
+ let fredCalls = 0;
840
+ const provider = createTestDataProvider({
841
+ getTickerFinancials: async () => {
842
+ financialCalls += 1;
843
+ return emptyFinancials();
844
+ },
845
+ getPriceHistoryForResolution: async () => {
846
+ historyCalls += 1;
847
+ return [{ date: new Date(now - 86_400_000), close: 100 }];
848
+ },
849
+ });
850
+ const spec: ChartSpec = {
851
+ version: CHART_SPEC_VERSION,
852
+ viewport: { range: "1Y", resolution: "1d" },
853
+ panels: [{ id: "main" }],
854
+ series: [{
855
+ id: "price",
856
+ source,
857
+ style: "line",
858
+ transform: "raw",
859
+ axis: "left",
860
+ panelId: "main",
861
+ interpolation: "none",
862
+ }, {
863
+ id: "cpi",
864
+ source: { kind: "economic", provider: "fred", seriesId: "CPIAUCSL" },
865
+ style: "line",
866
+ transform: "raw",
867
+ axis: "right",
868
+ panelId: "main",
869
+ interpolation: "none",
870
+ }],
871
+ studies: [],
872
+ };
873
+ const loadFredSeries = async () => {
874
+ fredCalls += 1;
875
+ return fredLoad({
876
+ observations: [{ date: "2026-05-01", value: 320 }],
877
+ info: null,
878
+ });
879
+ };
880
+ const cache = new ChartResolveCache();
881
+
882
+ await resolveChartSpecData(spec, {
883
+ dataProvider: provider,
884
+ now: new Date(now),
885
+ loadFredSeries,
886
+ }, cache);
887
+ const live = await resolveChartSpecData(spec, {
888
+ dataProvider: provider,
889
+ now: new Date(now + 1_000),
890
+ loadFredSeries,
891
+ quoteOverrides: new Map([[
892
+ chartQuoteOverrideKeyForSource(source),
893
+ {
894
+ symbol: "LIVE",
895
+ price: 105,
896
+ currency: "USD",
897
+ change: 5,
898
+ changePercent: 5,
899
+ lastUpdated: now,
900
+ },
901
+ ]]),
902
+ }, cache);
903
+
904
+ expect(financialCalls).toBe(1);
905
+ expect(historyCalls).toBe(1);
906
+ expect(fredCalls).toBe(1);
907
+ expect(live.errors).toEqual([]);
908
+ expect(live.series.find((entry) => entry.id === "price")?.points.at(-1)?.value).toBe(105);
909
+ });
910
+
911
+ test("derives current trailing multiples from the live quote and latest public TTM inputs", async () => {
912
+ const quoteTime = Date.parse("2025-05-01T16:00:00Z");
913
+ const statements = [
914
+ {
915
+ date: "2024-03-31",
916
+ availableAt: "2024-05-01",
917
+ totalRevenue: 100,
918
+ ebitda: 20,
919
+ freeCashFlow: 10,
920
+ eps: 1,
921
+ basicShares: 10,
922
+ totalDebt: 50,
923
+ cashAndCashEquivalents: 20,
924
+ },
925
+ {
926
+ date: "2024-06-30",
927
+ availableAt: "2024-08-01",
928
+ totalRevenue: 110,
929
+ ebitda: 22,
930
+ freeCashFlow: 11,
931
+ eps: 1.1,
932
+ basicShares: 10,
933
+ totalDebt: 50,
934
+ cashAndCashEquivalents: 20,
935
+ },
936
+ {
937
+ date: "2024-09-30",
938
+ availableAt: "2024-11-01",
939
+ totalRevenue: 120,
940
+ ebitda: 24,
941
+ freeCashFlow: 12,
942
+ eps: 1.2,
943
+ basicShares: 10,
944
+ totalDebt: 50,
945
+ cashAndCashEquivalents: 20,
946
+ },
947
+ {
948
+ date: "2024-12-31",
949
+ availableAt: "2025-02-15",
950
+ totalRevenue: 130,
951
+ ebitda: 26,
952
+ freeCashFlow: 13,
953
+ eps: 1.3,
954
+ basicShares: 10,
955
+ totalDebt: 50,
956
+ cashAndCashEquivalents: 20,
957
+ },
958
+ {
959
+ date: "2025-03-31",
960
+ // This newer TTM window is not public at the quote timestamp.
961
+ availableAt: "2025-05-15",
962
+ totalRevenue: 1_000,
963
+ ebitda: 500,
964
+ freeCashFlow: 400,
965
+ eps: 20,
966
+ basicShares: 20,
967
+ totalDebt: 100,
968
+ cashAndCashEquivalents: 5,
969
+ },
970
+ ];
971
+ const provider = createTestDataProvider({
972
+ getTickerFinancials: async () => ({
973
+ ...emptyFinancials(),
974
+ quarterlyStatements: statements,
975
+ quote: {
976
+ symbol: "LIVE",
977
+ price: 50,
978
+ currency: "USD",
979
+ change: 0,
980
+ changePercent: 0,
981
+ lastUpdated: quoteTime - 1_000,
982
+ },
983
+ }),
984
+ getPriceHistoryForResolution: async () => [
985
+ { date: new Date("2024-04-30T16:00:00Z"), close: 40 },
986
+ { date: new Date("2024-07-31T16:00:00Z"), close: 45 },
987
+ { date: new Date("2024-10-31T16:00:00Z"), close: 48 },
988
+ { date: new Date("2025-02-14T16:00:00Z"), close: 50 },
989
+ ],
990
+ });
991
+ const valuationFields = [
992
+ "trailingPE",
993
+ "priceSales",
994
+ "evSales",
995
+ "evEbitda",
996
+ "priceFcf",
997
+ ] as const;
998
+ const makeSeries = (metric: typeof valuationFields[number]): ChartSpec["series"][number] => ({
999
+ id: metric,
1000
+ source: {
1001
+ kind: "security",
1002
+ instrument: { symbol: "LIVE" },
1003
+ fieldId: `valuation.${metric}`,
1004
+ period: "quarterly",
1005
+ timestampMode: "available-at",
1006
+ },
1007
+ style: "step",
1008
+ transform: "raw",
1009
+ axis: "left",
1010
+ panelId: "main",
1011
+ interpolation: "step-after",
1012
+ });
1013
+ const spec: ChartSpec = {
1014
+ version: CHART_SPEC_VERSION,
1015
+ viewport: { range: "1Y", resolution: "1d" },
1016
+ panels: [{ id: "main" }],
1017
+ series: valuationFields.map(makeSeries),
1018
+ studies: [],
1019
+ };
1020
+ const firstSource = spec.series[0]!.source;
1021
+ if (firstSource.kind !== "security") throw new Error("expected security source");
1022
+ const quoteOverrides = new Map([[
1023
+ chartQuoteOverrideKeyForSource(firstSource),
1024
+ {
1025
+ symbol: "LIVE",
1026
+ price: 60,
1027
+ currency: "USD",
1028
+ change: 10,
1029
+ changePercent: 20,
1030
+ lastUpdated: quoteTime,
1031
+ },
1032
+ ]]);
1033
+
1034
+ const result = await resolveChartSpecData(spec, {
1035
+ dataProvider: provider,
1036
+ now: new Date(quoteTime),
1037
+ quoteOverrides,
1038
+ loadFredSeries: async () => fredLoad(),
1039
+ });
1040
+
1041
+ expect(result.errors).toEqual([]);
1042
+ const currentValue = (id: string) => {
1043
+ const point = result.series.find((series) => series.id === id)?.points.at(-1);
1044
+ expect(point).toMatchObject({
1045
+ date: new Date(quoteTime),
1046
+ periodLabel: "Current",
1047
+ provenance: { quality: "derived" },
1048
+ });
1049
+ return point?.value;
1050
+ };
1051
+ expect(currentValue("trailingPE")).toBeCloseTo(60 / 4.6, 10);
1052
+ expect(currentValue("priceSales")).toBeCloseTo(600 / 460, 10);
1053
+ expect(currentValue("evSales")).toBeCloseTo(630 / 460, 10);
1054
+ expect(currentValue("evEbitda")).toBeCloseTo(630 / 92, 10);
1055
+ expect(currentValue("priceFcf")).toBeCloseTo(600 / 46, 10);
1056
+ });
1057
+
1058
+ test("does not re-timestamp provider forward PE or PEG on quote overrides", async () => {
1059
+ const snapshotTime = Date.parse("2025-04-30T16:00:00Z");
1060
+ const overrideTime = Date.parse("2025-05-01T16:00:00Z");
1061
+ let historyCalls = 0;
1062
+ const provider = createTestDataProvider({
1063
+ getTickerFinancials: async () => ({
1064
+ ...emptyFinancials(),
1065
+ fundamentals: { forwardPE: 15, pegRatio: 1.5 },
1066
+ quote: {
1067
+ symbol: "STATIC",
1068
+ price: 50,
1069
+ currency: "USD",
1070
+ change: 0,
1071
+ changePercent: 0,
1072
+ lastUpdated: snapshotTime,
1073
+ },
1074
+ }),
1075
+ getPriceHistoryForResolution: async () => {
1076
+ historyCalls += 1;
1077
+ return [];
1078
+ },
1079
+ });
1080
+ const makeSeries = (metric: "forwardPE" | "pegRatio"): ChartSpec["series"][number] => ({
1081
+ id: metric,
1082
+ source: {
1083
+ kind: "security",
1084
+ instrument: { symbol: "STATIC" },
1085
+ fieldId: `valuation.${metric}`,
1086
+ timestampMode: "available-at",
1087
+ },
1088
+ style: "step",
1089
+ transform: "raw",
1090
+ axis: "left",
1091
+ panelId: "main",
1092
+ interpolation: "step-after",
1093
+ });
1094
+ const spec: ChartSpec = {
1095
+ version: CHART_SPEC_VERSION,
1096
+ viewport: { range: "1Y", resolution: "1d" },
1097
+ panels: [{ id: "main" }],
1098
+ series: [makeSeries("forwardPE"), makeSeries("pegRatio")],
1099
+ studies: [],
1100
+ };
1101
+ const firstSource = spec.series[0]!.source;
1102
+ if (firstSource.kind !== "security") throw new Error("expected security source");
1103
+ const quoteOverrides = new Map([[
1104
+ chartQuoteOverrideKeyForSource(firstSource),
1105
+ {
1106
+ symbol: "STATIC",
1107
+ price: 60,
1108
+ currency: "USD",
1109
+ change: 10,
1110
+ changePercent: 20,
1111
+ lastUpdated: overrideTime,
1112
+ },
1113
+ ]]);
1114
+
1115
+ const result = await resolveChartSpecData(spec, {
1116
+ dataProvider: provider,
1117
+ now: new Date(overrideTime),
1118
+ quoteOverrides,
1119
+ loadFredSeries: async () => fredLoad(),
1120
+ });
1121
+
1122
+ expect(result.errors).toEqual([]);
1123
+ expect(historyCalls).toBe(0);
1124
+ expect(result.series.find(({ id }) => id === "forwardPE")?.points).toEqual([
1125
+ expect.objectContaining({ date: new Date(snapshotTime), value: 15 }),
1126
+ ]);
1127
+ expect(result.series.find(({ id }) => id === "pegRatio")?.points).toEqual([
1128
+ expect.objectContaining({ date: new Date(snapshotTime), value: 1.5 }),
1129
+ ]);
1130
+ });
1131
+
1132
+ test("merges streamed quotes into the active OHLC resolution bucket", async () => {
1133
+ const now = Date.now();
1134
+ const source = {
1135
+ kind: "security" as const,
1136
+ instrument: { symbol: "LIVE" },
1137
+ fieldId: "market.ohlcv",
1138
+ };
1139
+ const provider = createTestDataProvider({
1140
+ getTickerFinancials: async () => emptyFinancials(),
1141
+ getPriceHistoryForResolution: async () => [
1142
+ {
1143
+ date: new Date(now - 10 * 60_000),
1144
+ open: 100,
1145
+ high: 110,
1146
+ low: 99,
1147
+ close: 108,
1148
+ volume: 1_000,
1149
+ },
1150
+ {
1151
+ date: new Date(now - 4 * 60_000),
1152
+ open: 108,
1153
+ high: 120,
1154
+ low: 105,
1155
+ close: 115,
1156
+ volume: 800,
1157
+ },
1158
+ ],
1159
+ });
1160
+ const spec: ChartSpec = {
1161
+ version: CHART_SPEC_VERSION,
1162
+ viewport: { range: "1D", resolution: "5m" },
1163
+ panels: [{ id: "main" }],
1164
+ series: [{
1165
+ id: "price",
1166
+ source,
1167
+ style: "candles",
1168
+ transform: "raw",
1169
+ axis: "left",
1170
+ panelId: "main",
1171
+ interpolation: "none",
1172
+ }],
1173
+ studies: [],
1174
+ };
1175
+ const quoteOverrides = new Map([[
1176
+ chartQuoteOverrideKeyForSource(source),
1177
+ {
1178
+ symbol: "LIVE",
1179
+ price: 121,
1180
+ currency: "USD",
1181
+ change: 6,
1182
+ changePercent: 5.2,
1183
+ lastUpdated: now,
1184
+ },
1185
+ ]]);
1186
+
1187
+ const result = await resolveChartSpecData(spec, {
1188
+ dataProvider: provider,
1189
+ now: new Date(now),
1190
+ quoteOverrides,
1191
+ loadFredSeries: async () => fredLoad(),
1192
+ });
1193
+
1194
+ expect(result.errors).toEqual([]);
1195
+ const price = result.series.find((entry) => entry.id === "price")!;
1196
+ expect(price.points).toHaveLength(2);
1197
+ expect(price.points.at(-1)).toMatchObject({
1198
+ date: new Date(now - 4 * 60_000),
1199
+ value: 121,
1200
+ open: 108,
1201
+ high: 121,
1202
+ low: 105,
1203
+ close: 121,
1204
+ volume: 800,
1205
+ });
1206
+ });
1207
+
1208
+ test("calculates formulas and volume from raw inputs before display transforms", async () => {
1209
+ const provider = createTestDataProvider({
1210
+ getTickerFinancials: async () => emptyFinancials(),
1211
+ getPriceHistoryForResolution: async (symbol) => symbol === "LEFT"
1212
+ ? [
1213
+ { date: new Date("2025-01-01T00:00:00Z"), close: 100, volume: 1_000 },
1214
+ { date: new Date("2025-01-02T00:00:00Z"), close: 120, volume: 1_200 },
1215
+ ]
1216
+ : [
1217
+ { date: new Date("2025-01-01T00:00:00Z"), close: 200, volume: 2_000 },
1218
+ { date: new Date("2025-01-02T00:00:00Z"), close: 240, volume: 2_400 },
1219
+ ],
1220
+ });
1221
+ const makeSeries = (id: string, symbol: string): ChartSpec["series"][number] => ({
1222
+ id,
1223
+ source: { kind: "security", instrument: { symbol }, fieldId: "market.ohlcv" },
1224
+ style: "line",
1225
+ transform: "percent",
1226
+ axis: "left",
1227
+ panelId: "main",
1228
+ interpolation: "none",
1229
+ });
1230
+ const spec: ChartSpec = {
1231
+ version: CHART_SPEC_VERSION,
1232
+ viewport: { range: "1Y", resolution: "1d" },
1233
+ panels: [{ id: "main" }, { id: "formula" }, { id: "volume" }],
1234
+ series: [makeSeries("left", "LEFT"), makeSeries("right", "RIGHT")],
1235
+ studies: [
1236
+ {
1237
+ id: "ratio",
1238
+ kind: "ratio",
1239
+ inputSeriesIds: ["left", "right"],
1240
+ parameters: {},
1241
+ panelId: "formula",
1242
+ axis: "left",
1243
+ },
1244
+ {
1245
+ id: "volume",
1246
+ kind: "volume",
1247
+ inputSeriesIds: ["left"],
1248
+ parameters: {},
1249
+ panelId: "volume",
1250
+ axis: "left",
1251
+ },
1252
+ {
1253
+ id: "sma",
1254
+ kind: "sma",
1255
+ inputSeriesIds: ["left"],
1256
+ parameters: { period: 2 },
1257
+ panelId: "main",
1258
+ axis: "left",
1259
+ },
1260
+ ],
1261
+ };
1262
+
1263
+ const result = await resolveChartSpecData(spec, {
1264
+ dataProvider: provider,
1265
+ now: new Date("2025-01-02T00:00:00Z"),
1266
+ loadFredSeries: async () => fredLoad(),
1267
+ });
1268
+
1269
+ expect(result.errors).toEqual([]);
1270
+ expect(result.series.find((series) => series.id === "ratio")?.points.map((point) => point.value))
1271
+ .toEqual([0.5, 0.5]);
1272
+ expect(result.series.find((series) => series.id === "volume")?.points.map((point) => point.value))
1273
+ .toEqual([1_000, 1_200]);
1274
+ expect(result.series.find((series) => series.id === "sma")).toMatchObject({
1275
+ transform: "percent",
1276
+ unit: "%",
1277
+ });
1278
+ expect(result.series.find((series) => series.id === "sma")?.points.map((point) => point.value))
1279
+ .toEqual([10]);
1280
+ });
1281
+ });