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,487 @@
1
+ import type { ChartResolution, TimeRange } from "../components/chart/core/types";
2
+ import { getChartResolutionLabel } from "../components/chart/core/resolution";
3
+ import {
4
+ canonicalTimeSeriesFieldId,
5
+ getTimeSeriesField,
6
+ isFundamentalFieldId,
7
+ } from "./field-catalog";
8
+ import {
9
+ isResolutionFineEnoughForMarketPeriod,
10
+ maximumResolutionForMarketPeriod,
11
+ } from "./market-resolution";
12
+ import {
13
+ CHART_SPEC_VERSION,
14
+ type ChartPanelSpec,
15
+ type ChartSeriesSource,
16
+ type ChartSeriesSpec,
17
+ type ChartSpec,
18
+ type ChartStudyKind,
19
+ type ChartStudySpec,
20
+ type PanelScale,
21
+ type SeriesAxis,
22
+ type SeriesInterpolation,
23
+ type SeriesPeriod,
24
+ type SeriesStyle,
25
+ type SeriesTimestampMode,
26
+ type SeriesTransform,
27
+ type SecuritySeriesSource,
28
+ } from "./types";
29
+
30
+ const TIME_RANGES = new Set<TimeRange>(["1D", "1W", "1M", "3M", "6M", "1Y", "5Y", "ALL"]);
31
+ const RESOLUTIONS = new Set<ChartResolution>(["auto", "1m", "5m", "15m", "30m", "45m", "1h", "1d", "1wk", "1mo"]);
32
+ const PERIODS = new Set<SeriesPeriod>(["auto", "daily", "weekly", "monthly", "quarterly", "annual", "ttm"]);
33
+ const STYLES = new Set<SeriesStyle>(["line", "area", "step", "columns", "points", "candles", "ohlc", "hlc"]);
34
+ const ECONOMIC_STYLES = new Set<SeriesStyle>(["line", "area", "step", "columns", "points"]);
35
+ const TRANSFORMS = new Set<SeriesTransform>(["raw", "percent", "index100", "yoy", "qoq", "log"]);
36
+ const AXES = new Set<SeriesAxis>(["auto", "left", "right"]);
37
+ const SCALES = new Set<PanelScale>(["linear", "log"]);
38
+ const STUDIES = new Set<ChartStudyKind>([
39
+ "volume",
40
+ "sma",
41
+ "ema",
42
+ "bollinger",
43
+ "rsi",
44
+ "macd",
45
+ "ratio",
46
+ "spread",
47
+ "correlation",
48
+ ]);
49
+
50
+ export function isOhlcSeriesStyle(style: SeriesStyle): boolean {
51
+ return style === "candles" || style === "ohlc" || style === "hlc";
52
+ }
53
+
54
+ export function coerceSeriesTransformForStyle(
55
+ style: SeriesStyle,
56
+ transform: SeriesTransform,
57
+ ): SeriesTransform {
58
+ return isOhlcSeriesStyle(style) ? "raw" : transform;
59
+ }
60
+
61
+ export function coerceSeriesInterpolationForStyle(
62
+ style: SeriesStyle,
63
+ ): SeriesInterpolation {
64
+ return style === "step" ? "step-after" : "none";
65
+ }
66
+
67
+ export const MAX_CHART_SERIES = 10;
68
+
69
+ export const DEFAULT_CHART_SPEC: ChartSpec = Object.freeze({
70
+ version: CHART_SPEC_VERSION,
71
+ viewport: Object.freeze({ range: "1Y", resolution: "auto" }),
72
+ panels: Object.freeze([Object.freeze({ id: "main", height: 1, scale: "linear" })]) as unknown as ChartPanelSpec[],
73
+ series: Object.freeze([]) as unknown as ChartSeriesSpec[],
74
+ studies: Object.freeze([]) as unknown as ChartStudySpec[],
75
+ });
76
+
77
+ export interface ChartSpecIssue {
78
+ path: string;
79
+ code: string;
80
+ message: string;
81
+ }
82
+
83
+ export interface ChartSpecValidationResult {
84
+ valid: boolean;
85
+ errors: ChartSpecIssue[];
86
+ warnings: ChartSpecIssue[];
87
+ }
88
+
89
+ function record(value: unknown): Record<string, unknown> | null {
90
+ return typeof value === "object" && value !== null && !Array.isArray(value)
91
+ ? value as Record<string, unknown>
92
+ : null;
93
+ }
94
+
95
+ function nonEmptyString(value: unknown): string | null {
96
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
97
+ }
98
+
99
+ function uniqueId(candidate: unknown, prefix: string, index: number, seen: Set<string>): string {
100
+ const base = nonEmptyString(candidate) ?? `${prefix}-${index + 1}`;
101
+ let id = base;
102
+ let suffix = 2;
103
+ while (seen.has(id)) id = `${base}-${suffix++}`;
104
+ seen.add(id);
105
+ return id;
106
+ }
107
+
108
+ function cloneSpec(spec: ChartSpec): ChartSpec {
109
+ return {
110
+ version: CHART_SPEC_VERSION,
111
+ viewport: {
112
+ ...spec.viewport,
113
+ dateWindow: spec.viewport.dateWindow ? { ...spec.viewport.dateWindow } : undefined,
114
+ },
115
+ panels: spec.panels.map((panel) => ({ ...panel })),
116
+ series: spec.series.map((entry) => ({
117
+ ...entry,
118
+ source: entry.source.kind === "security"
119
+ ? { ...entry.source, instrument: { ...entry.source.instrument } }
120
+ : { ...entry.source },
121
+ })),
122
+ studies: spec.studies.map((study) => ({
123
+ ...study,
124
+ inputSeriesIds: [...study.inputSeriesIds],
125
+ parameters: { ...study.parameters },
126
+ })),
127
+ };
128
+ }
129
+
130
+ function normalizeSource(value: unknown): ChartSeriesSource | null {
131
+ const source = record(value);
132
+ if (!source) return null;
133
+ if (source.kind === "economic") {
134
+ const seriesId = nonEmptyString(source.seriesId);
135
+ if (!seriesId || source.provider !== "fred") return null;
136
+ return { kind: "economic", provider: "fred", seriesId };
137
+ }
138
+ if (source.kind !== "security") return null;
139
+ const instrument = record(source.instrument);
140
+ const symbol = nonEmptyString(instrument?.symbol);
141
+ const fieldId = nonEmptyString(source.fieldId);
142
+ if (!instrument || !symbol || !fieldId) return null;
143
+ const period = PERIODS.has(source.period as SeriesPeriod) ? source.period as SeriesPeriod : undefined;
144
+ const timestampMode = source.timestampMode === "period-end" || source.timestampMode === "available-at"
145
+ ? source.timestampMode as SeriesTimestampMode
146
+ : undefined;
147
+ return {
148
+ kind: "security",
149
+ instrument: {
150
+ ...(instrument as ChartSeriesSource & Record<string, unknown>),
151
+ symbol: symbol.toUpperCase(),
152
+ } as unknown as SecuritySeriesSource["instrument"],
153
+ fieldId: canonicalTimeSeriesFieldId(fieldId),
154
+ period,
155
+ ...(timestampMode ? { timestampMode } : {}),
156
+ };
157
+ }
158
+
159
+ function normalizedDateWindow(value: unknown): { start: string; end: string } | undefined {
160
+ const window = record(value);
161
+ const start = nonEmptyString(window?.start);
162
+ const end = nonEmptyString(window?.end);
163
+ if (!start || !end) return undefined;
164
+ const startTime = Date.parse(start);
165
+ const endTime = Date.parse(end);
166
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime > endTime) return undefined;
167
+ return { start, end };
168
+ }
169
+
170
+ function normalizePanel(value: unknown, index: number, seen: Set<string>): ChartPanelSpec | null {
171
+ const panel = record(value);
172
+ if (!panel) return null;
173
+ const id = uniqueId(panel.id, "panel", index, seen);
174
+ const height = typeof panel.height === "number" && Number.isFinite(panel.height) && panel.height > 0
175
+ ? Math.min(10, Math.max(0.1, panel.height))
176
+ : undefined;
177
+ return {
178
+ id,
179
+ label: nonEmptyString(panel.label) ?? undefined,
180
+ height,
181
+ scale: SCALES.has(panel.scale as PanelScale) ? panel.scale as PanelScale : "linear",
182
+ };
183
+ }
184
+
185
+ function normalizeSeries(
186
+ value: unknown,
187
+ index: number,
188
+ seen: Set<string>,
189
+ defaultPanelId: string,
190
+ ): ChartSeriesSpec | null {
191
+ const entry = record(value);
192
+ if (!entry) return null;
193
+ const source = normalizeSource(entry.source);
194
+ if (!source) return null;
195
+ const definition = source.kind === "security" ? getTimeSeriesField(source.fieldId) : undefined;
196
+ const requestedStyle = STYLES.has(entry.style as SeriesStyle) ? entry.style as SeriesStyle : undefined;
197
+ const requestedTransform = TRANSFORMS.has(entry.transform as SeriesTransform)
198
+ ? entry.transform as SeriesTransform
199
+ : undefined;
200
+ const style = definition
201
+ ? requestedStyle && definition.styles.includes(requestedStyle)
202
+ ? requestedStyle
203
+ : definition.defaultStyle
204
+ : source.kind === "economic"
205
+ ? requestedStyle && ECONOMIC_STYLES.has(requestedStyle) ? requestedStyle : "step"
206
+ : requestedStyle ?? "line";
207
+ const transform = definition
208
+ ? requestedTransform && definition.transforms.includes(requestedTransform)
209
+ ? requestedTransform
210
+ : "raw"
211
+ : requestedTransform ?? "raw";
212
+ const normalizedSource = source.kind === "security" && isFundamentalFieldId(source.fieldId)
213
+ ? {
214
+ ...source,
215
+ // V1 specs created before timing became an independent control may not
216
+ // contain a timestamp mode. Preserve their legacy visual semantics once
217
+ // during normalization, then persist the explicit choice.
218
+ timestampMode: source.timestampMode
219
+ ?? (style === "columns" ? "period-end" as const : "available-at" as const),
220
+ }
221
+ : source;
222
+ return {
223
+ id: uniqueId(entry.id, "series", index, seen),
224
+ source: normalizedSource,
225
+ label: nonEmptyString(entry.label) ?? undefined,
226
+ style,
227
+ transform,
228
+ axis: AXES.has(entry.axis as SeriesAxis) ? entry.axis as SeriesAxis : "auto",
229
+ panelId: nonEmptyString(entry.panelId) ?? defaultPanelId,
230
+ interpolation: coerceSeriesInterpolationForStyle(style),
231
+ color: nonEmptyString(entry.color) ?? undefined,
232
+ visible: typeof entry.visible === "boolean" ? entry.visible : true,
233
+ };
234
+ }
235
+
236
+ function normalizeStudy(
237
+ value: unknown,
238
+ index: number,
239
+ seen: Set<string>,
240
+ defaultPanelId: string,
241
+ ): ChartStudySpec | null {
242
+ const study = record(value);
243
+ if (!study || !STUDIES.has(study.kind as ChartStudyKind)) return null;
244
+ const parameters: Record<string, number> = {};
245
+ const rawParameters = record(study.parameters);
246
+ for (const [key, parameter] of Object.entries(rawParameters ?? {})) {
247
+ if (typeof parameter === "number" && Number.isFinite(parameter)) parameters[key] = parameter;
248
+ }
249
+ return {
250
+ id: uniqueId(study.id, "study", index, seen),
251
+ kind: study.kind as ChartStudyKind,
252
+ inputSeriesIds: Array.isArray(study.inputSeriesIds)
253
+ ? [...new Set(study.inputSeriesIds.map(nonEmptyString).filter((id): id is string => id !== null))]
254
+ : [],
255
+ parameters,
256
+ panelId: nonEmptyString(study.panelId) ?? defaultPanelId,
257
+ axis: AXES.has(study.axis as SeriesAxis) ? study.axis as SeriesAxis : "auto",
258
+ color: nonEmptyString(study.color) ?? undefined,
259
+ visible: typeof study.visible === "boolean" ? study.visible : true,
260
+ };
261
+ }
262
+
263
+ /** Normalizes persisted or user-authored data into the current versioned chart spec. */
264
+ export function normalizeChartSpec(value: unknown, fallback: ChartSpec = DEFAULT_CHART_SPEC): ChartSpec {
265
+ const input = record(value);
266
+ if (!input) return cloneSpec(fallback);
267
+ const fallbackCopy = cloneSpec(fallback);
268
+ const viewport = record(input.viewport);
269
+ const panelIds = new Set<string>();
270
+ let panels = (Array.isArray(input.panels) ? input.panels : fallbackCopy.panels)
271
+ .map((panel, index) => normalizePanel(panel, index, panelIds))
272
+ .filter((panel): panel is ChartPanelSpec => panel !== null);
273
+ if (panels.length === 0) panels = [{ id: "main", height: 1, scale: "linear" }];
274
+ const defaultPanelId = panels[0]!.id;
275
+
276
+ const seriesIds = new Set<string>();
277
+ const series = (Array.isArray(input.series) ? input.series : fallbackCopy.series)
278
+ .slice(0, MAX_CHART_SERIES)
279
+ .map((entry, index) => normalizeSeries(entry, index, seriesIds, defaultPanelId))
280
+ .filter((entry): entry is ChartSeriesSpec => entry !== null);
281
+ const studyIds = new Set<string>();
282
+ const studies = (Array.isArray(input.studies) ? input.studies : fallbackCopy.studies)
283
+ .map((study, index) => normalizeStudy(study, index, studyIds, defaultPanelId))
284
+ .filter((study): study is ChartStudySpec => study !== null);
285
+
286
+ const referencedPanelIds = new Set([
287
+ ...series.map((entry) => entry.panelId),
288
+ ...studies.map((study) => study.panelId),
289
+ ]);
290
+ for (const panelId of referencedPanelIds) {
291
+ if (!panels.some((panel) => panel.id === panelId)) panels.push({ id: panelId, height: 0.35, scale: "linear" });
292
+ }
293
+
294
+ return {
295
+ version: CHART_SPEC_VERSION,
296
+ viewport: {
297
+ range: TIME_RANGES.has(viewport?.range as TimeRange)
298
+ ? viewport!.range as TimeRange
299
+ : fallbackCopy.viewport.range,
300
+ resolution: RESOLUTIONS.has(viewport?.resolution as ChartResolution)
301
+ ? viewport!.resolution as ChartResolution
302
+ : fallbackCopy.viewport.resolution,
303
+ dateWindow: normalizedDateWindow(viewport?.dateWindow),
304
+ maxPoints: typeof viewport?.maxPoints === "number"
305
+ && Number.isFinite(viewport.maxPoints)
306
+ && viewport.maxPoints > 0
307
+ ? Math.min(10_000, Math.floor(viewport.maxPoints))
308
+ : undefined,
309
+ },
310
+ panels,
311
+ series,
312
+ studies,
313
+ };
314
+ }
315
+
316
+ function issue(path: string, code: string, message: string): ChartSpecIssue {
317
+ return { path, code, message };
318
+ }
319
+
320
+ function requiredInputCount(kind: ChartStudyKind): number {
321
+ return kind === "ratio" || kind === "spread" || kind === "correlation" ? 2 : 1;
322
+ }
323
+
324
+ /** Validates semantic constraints that normalization cannot safely guess. */
325
+ export function validateChartSpec(spec: ChartSpec): ChartSpecValidationResult {
326
+ const errors: ChartSpecIssue[] = [];
327
+ const warnings: ChartSpecIssue[] = [];
328
+ if (spec.version !== CHART_SPEC_VERSION) {
329
+ errors.push(issue("version", "unsupported-version", `Unsupported chart spec version ${String(spec.version)}.`));
330
+ }
331
+ if (!TIME_RANGES.has(spec.viewport.range)) errors.push(issue("viewport.range", "invalid-range", "Invalid date range."));
332
+ if (!RESOLUTIONS.has(spec.viewport.resolution)) {
333
+ errors.push(issue("viewport.resolution", "invalid-resolution", "Invalid chart resolution."));
334
+ }
335
+ if (spec.viewport.dateWindow) {
336
+ const start = Date.parse(spec.viewport.dateWindow.start);
337
+ const end = Date.parse(spec.viewport.dateWindow.end);
338
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) {
339
+ errors.push(issue("viewport.dateWindow", "invalid-window", "Custom date window must contain valid ordered dates."));
340
+ }
341
+ }
342
+ if (spec.viewport.maxPoints !== undefined
343
+ && (!Number.isInteger(spec.viewport.maxPoints) || spec.viewport.maxPoints <= 0 || spec.viewport.maxPoints > 10_000)) {
344
+ errors.push(issue("viewport.maxPoints", "invalid-max-points", "Latest-observation count must be an integer from 1 to 10000."));
345
+ }
346
+ if (spec.panels.length === 0) errors.push(issue("panels", "missing-panel", "At least one chart panel is required."));
347
+ const panelIds = new Set<string>();
348
+ spec.panels.forEach((panel, index) => {
349
+ if (!panel.id.trim()) errors.push(issue(`panels.${index}.id`, "missing-id", "Panel ID is required."));
350
+ if (panelIds.has(panel.id)) errors.push(issue(`panels.${index}.id`, "duplicate-id", `Duplicate panel ID ${panel.id}.`));
351
+ panelIds.add(panel.id);
352
+ if (panel.height !== undefined && (!Number.isFinite(panel.height) || panel.height <= 0)) {
353
+ errors.push(issue(`panels.${index}.height`, "invalid-height", "Panel height must be positive."));
354
+ }
355
+ });
356
+ if (spec.series.length > MAX_CHART_SERIES) {
357
+ errors.push(issue("series", "too-many-series", `A chart supports at most ${MAX_CHART_SERIES} base series.`));
358
+ }
359
+
360
+ const seriesIds = new Set<string>();
361
+ const candleCountByPanel = new Map<string, number>();
362
+ const unitGroupsByPanel = new Map<string, Set<string>>();
363
+ const panelScaleById = new Map(spec.panels.map((panel) => [panel.id, panel.scale ?? "linear"] as const));
364
+ spec.series.forEach((entry, index) => {
365
+ const path = `series.${index}`;
366
+ if (!entry.id.trim()) errors.push(issue(`${path}.id`, "missing-id", "Series ID is required."));
367
+ if (seriesIds.has(entry.id)) errors.push(issue(`${path}.id`, "duplicate-id", `Duplicate series ID ${entry.id}.`));
368
+ seriesIds.add(entry.id);
369
+ if (!panelIds.has(entry.panelId)) {
370
+ errors.push(issue(`${path}.panelId`, "missing-panel", `Panel ${entry.panelId} does not exist.`));
371
+ }
372
+ if (entry.transform === "log" && panelScaleById.get(entry.panelId) === "log") {
373
+ errors.push(issue(
374
+ `${path}.transform`,
375
+ "double-log",
376
+ "Choose either a log transform or a logarithmic panel scale, not both.",
377
+ ));
378
+ }
379
+ if (entry.source.kind === "security") {
380
+ if (!entry.source.instrument.symbol.trim()) {
381
+ errors.push(issue(`${path}.source.instrument.symbol`, "missing-symbol", "Security symbol is required."));
382
+ }
383
+ const definition = getTimeSeriesField(entry.source.fieldId);
384
+ if (!definition) {
385
+ errors.push(issue(`${path}.source.fieldId`, "unknown-field", `Unknown field ${entry.source.fieldId}.`));
386
+ } else {
387
+ if (definition.sourceKind !== entry.source.kind) {
388
+ errors.push(issue(`${path}.source.fieldId`, "wrong-source", `${definition.label} is not valid for this source.`));
389
+ }
390
+ if (!definition.styles.includes(entry.style)) {
391
+ errors.push(issue(`${path}.style`, "unsupported-style", `${entry.style} is not valid for ${definition.label}.`));
392
+ }
393
+ if (!definition.transforms.includes(entry.transform)) {
394
+ errors.push(issue(`${path}.transform`, "unsupported-transform", `${entry.transform} is not valid for ${definition.label}.`));
395
+ }
396
+ if (entry.transform === "qoq" && entry.source.period === "annual") {
397
+ errors.push(issue(`${path}.transform`, "qoq-annual", "Quarter-over-quarter change requires quarterly or finer data."));
398
+ }
399
+ if (definition.id.startsWith("market.") && entry.source.period === "ttm") {
400
+ errors.push(issue(`${path}.source.period`, "unsupported-period", "TTM is not a valid market-price aggregation period."));
401
+ }
402
+ const marketPeriod = definition.id.startsWith("market.") ? entry.source.period : undefined;
403
+ const maximumMarketResolution = maximumResolutionForMarketPeriod(marketPeriod);
404
+ if (marketPeriod
405
+ && maximumMarketResolution
406
+ && spec.viewport.resolution !== "auto"
407
+ && !isResolutionFineEnoughForMarketPeriod(spec.viewport.resolution, marketPeriod)) {
408
+ const period = marketPeriod.charAt(0).toUpperCase() + marketPeriod.slice(1);
409
+ errors.push(issue(
410
+ `${path}.source.period`,
411
+ "market-period-resolution",
412
+ `${period} market series cannot use the coarser ${getChartResolutionLabel(spec.viewport.resolution)} viewport interval. Choose Auto or ${getChartResolutionLabel(maximumMarketResolution)} (or finer).`,
413
+ ));
414
+ }
415
+ if ((definition.id.startsWith("fundamental.") || definition.id.startsWith("valuation."))
416
+ && (entry.source.period === "daily" || entry.source.period === "weekly" || entry.source.period === "monthly")) {
417
+ errors.push(issue(`${path}.source.period`, "unsupported-period", "Financial fields support automatic, quarterly, annual, or TTM periods."));
418
+ }
419
+ const groups = unitGroupsByPanel.get(entry.panelId) ?? new Set<string>();
420
+ groups.add(entry.transform === "percent" || entry.transform === "yoy" || entry.transform === "qoq"
421
+ ? "percent"
422
+ : entry.transform === "index100"
423
+ ? "index"
424
+ : definition.unitGroup);
425
+ unitGroupsByPanel.set(entry.panelId, groups);
426
+ }
427
+ } else {
428
+ if (!entry.source.seriesId.trim()) {
429
+ errors.push(issue(`${path}.source.seriesId`, "missing-series", "Economic series ID is required."));
430
+ }
431
+ if (!ECONOMIC_STYLES.has(entry.style)) {
432
+ errors.push(issue(`${path}.style`, "unsupported-style", `${entry.style} is not valid for an economic scalar series.`));
433
+ }
434
+ }
435
+ if (isOhlcSeriesStyle(entry.style)) {
436
+ const count = (candleCountByPanel.get(entry.panelId) ?? 0) + 1;
437
+ candleCountByPanel.set(entry.panelId, count);
438
+ if (entry.transform !== "raw") {
439
+ errors.push(issue(`${path}.transform`, "transformed-ohlc", "OHLC styles require raw values."));
440
+ }
441
+ }
442
+ });
443
+ for (const [panelId, count] of candleCountByPanel) {
444
+ if (count > 1) {
445
+ errors.push(issue("series", "multiple-ohlc", `Panel ${panelId} may contain only one candle or OHLC series.`));
446
+ }
447
+ }
448
+ for (const [panelId, groups] of unitGroupsByPanel) {
449
+ if (groups.size > 2) {
450
+ warnings.push(issue("series", "too-many-unit-groups", `Panel ${panelId} has more than two unit groups; normalize a series or use another panel.`));
451
+ }
452
+ }
453
+
454
+ const studyIds = new Set<string>();
455
+ spec.studies.forEach((study, index) => {
456
+ const path = `studies.${index}`;
457
+ if (!study.id.trim()) errors.push(issue(`${path}.id`, "missing-id", "Study ID is required."));
458
+ if (studyIds.has(study.id)) errors.push(issue(`${path}.id`, "duplicate-id", `Duplicate study ID ${study.id}.`));
459
+ studyIds.add(study.id);
460
+ if (!panelIds.has(study.panelId)) {
461
+ errors.push(issue(`${path}.panelId`, "missing-panel", `Panel ${study.panelId} does not exist.`));
462
+ }
463
+ const inputCount = requiredInputCount(study.kind);
464
+ if (study.inputSeriesIds.length !== inputCount) {
465
+ errors.push(issue(`${path}.inputSeriesIds`, "wrong-input-count", `${study.kind} requires ${inputCount} input series.`));
466
+ }
467
+ for (const inputId of study.inputSeriesIds) {
468
+ if (!seriesIds.has(inputId)) {
469
+ errors.push(issue(`${path}.inputSeriesIds`, "missing-input", `Input series ${inputId} does not exist.`));
470
+ }
471
+ }
472
+ for (const [name, value] of Object.entries(study.parameters)) {
473
+ if (!Number.isFinite(value)) errors.push(issue(`${path}.parameters.${name}`, "invalid-parameter", "Study parameters must be finite."));
474
+ if ((name === "period" || name === "fast" || name === "slow" || name === "signal") && value <= 0) {
475
+ errors.push(issue(`${path}.parameters.${name}`, "invalid-period", "Study periods must be positive."));
476
+ }
477
+ }
478
+ if (study.kind === "macd") {
479
+ const fast = study.parameters.fast ?? 12;
480
+ const slow = study.parameters.slow ?? 26;
481
+ if (fast >= slow) errors.push(issue(`${path}.parameters`, "invalid-macd-periods", "MACD fast period must be shorter than slow period."));
482
+ }
483
+ });
484
+
485
+ if (spec.series.length === 0) warnings.push(issue("series", "empty-chart", "Chart has no data series."));
486
+ return { valid: errors.length === 0, errors, warnings };
487
+ }