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,842 @@
1
+ import type {
2
+ FinancialStatement,
3
+ PricePoint,
4
+ TickerFinancials,
5
+ } from "../types/financials";
6
+ import { areNearbyFinancialPeriodEnds } from "../utils/financial-statements";
7
+ import { canonicalTimeSeriesFieldId } from "./field-catalog";
8
+ import type { SecuritySeriesSource, SeriesPeriod, TimeSeriesPoint } from "./types";
9
+
10
+ type NumericStatementField =
11
+ | "totalRevenue"
12
+ | "grossProfit"
13
+ | "operatingIncome"
14
+ | "netIncome"
15
+ | "ebitda"
16
+ | "operatingCashFlow"
17
+ | "capitalExpenditure"
18
+ | "freeCashFlow"
19
+ | "eps"
20
+ | "totalAssets"
21
+ | "cashAndCashEquivalents"
22
+ | "cashCashEquivalentsAndShortTermInvestments"
23
+ | "totalDebt"
24
+ | "totalEquity"
25
+ | "basicShares"
26
+ | "dilutedShares"
27
+ | "shareIssued"
28
+ | "ordinarySharesNumber";
29
+
30
+ type InternalStatement = FinancialStatement & {
31
+ __timeSeriesDerivedFields?: NumericStatementField[];
32
+ __timeSeriesTtm?: boolean;
33
+ };
34
+
35
+ const DAY_MS = 24 * 60 * 60 * 1_000;
36
+
37
+ export const QUARTERLY_FLOW_FIELDS: readonly NumericStatementField[] = [
38
+ "totalRevenue",
39
+ "grossProfit",
40
+ "operatingIncome",
41
+ "netIncome",
42
+ "ebitda",
43
+ "operatingCashFlow",
44
+ "capitalExpenditure",
45
+ "freeCashFlow",
46
+ "eps",
47
+ ];
48
+
49
+ export const QUARTERLY_SNAPSHOT_FIELDS: readonly NumericStatementField[] = [
50
+ "totalAssets",
51
+ "cashAndCashEquivalents",
52
+ "cashCashEquivalentsAndShortTermInvestments",
53
+ "totalDebt",
54
+ "totalEquity",
55
+ "basicShares",
56
+ "dilutedShares",
57
+ "shareIssued",
58
+ "ordinarySharesNumber",
59
+ ];
60
+
61
+ const NUMERIC_STATEMENT_FIELDS: readonly NumericStatementField[] = [
62
+ ...QUARTERLY_FLOW_FIELDS,
63
+ ...QUARTERLY_SNAPSHOT_FIELDS,
64
+ ];
65
+
66
+ const FUNDAMENTAL_IDS = new Set([
67
+ "totalRevenue",
68
+ "grossProfit",
69
+ "grossMargin",
70
+ "operatingIncome",
71
+ "operatingMargin",
72
+ "netIncome",
73
+ "netMargin",
74
+ "operatingCashFlow",
75
+ "freeCashFlow",
76
+ "freeCashFlowMargin",
77
+ "totalAssets",
78
+ "totalDebt",
79
+ "totalEquity",
80
+ "eps",
81
+ ]);
82
+
83
+ const VALUATION_IDS = new Set([
84
+ "trailingPE",
85
+ "forwardPE",
86
+ "pegRatio",
87
+ "priceSales",
88
+ "evSales",
89
+ "evEbitda",
90
+ "priceFcf",
91
+ ]);
92
+
93
+ const QUOTE_DERIVED_VALUATION_IDS = new Set([
94
+ "trailingPE",
95
+ "priceSales",
96
+ "evSales",
97
+ "evEbitda",
98
+ "priceFcf",
99
+ ]);
100
+
101
+ function finiteNumber(value: unknown): value is number {
102
+ return typeof value === "number" && Number.isFinite(value);
103
+ }
104
+
105
+ function validDate(value: unknown): Date | null {
106
+ if (value instanceof Date) {
107
+ return Number.isFinite(value.getTime()) ? new Date(value.getTime()) : null;
108
+ }
109
+ if (typeof value !== "string" && typeof value !== "number") return null;
110
+ const parsed = new Date(value);
111
+ return Number.isFinite(parsed.getTime()) ? parsed : null;
112
+ }
113
+
114
+ function latestDateString(values: Array<string | null | undefined>): string | undefined {
115
+ let latest: { value: string; time: number } | undefined;
116
+ for (const value of values) {
117
+ if (!value) continue;
118
+ const time = Date.parse(value);
119
+ if (!Number.isFinite(time)) continue;
120
+ if (!latest || time > latest.time) latest = { value, time };
121
+ }
122
+ return latest?.value;
123
+ }
124
+
125
+ function statementTime(statement: FinancialStatement): number {
126
+ return Date.parse(statement.date);
127
+ }
128
+
129
+ function statementNumber(statement: FinancialStatement, field: NumericStatementField): number | null {
130
+ const value = statement[field];
131
+ return finiteNumber(value) ? value : null;
132
+ }
133
+
134
+ function setStatementNumber(
135
+ statement: InternalStatement,
136
+ field: NumericStatementField,
137
+ value: number,
138
+ availableAt?: string,
139
+ ): void {
140
+ (statement as unknown as Record<string, unknown>)[field] = value;
141
+ statement.__timeSeriesDerivedFields = [
142
+ ...new Set([...(statement.__timeSeriesDerivedFields ?? []), field]),
143
+ ];
144
+ if (availableAt) {
145
+ statement.fieldAvailability = { ...statement.fieldAvailability, [field]: availableAt };
146
+ }
147
+ }
148
+
149
+ function periodCategory(date: string, period: "annual" | "quarterly"): string {
150
+ const parsedYear = Number(date.slice(0, 4));
151
+ let year = Number.isFinite(parsedYear) ? parsedYear : 0;
152
+ if (period === "annual") return year > 0 ? `FY${year}` : date;
153
+
154
+ let month = Number(date.slice(5, 7));
155
+ const day = Number(date.slice(8, 10));
156
+ if (Number.isFinite(month) && Number.isFinite(day) && day <= 7 && (month - 1) % 3 === 0) {
157
+ month -= 1;
158
+ if (month === 0) {
159
+ month = 12;
160
+ year -= 1;
161
+ }
162
+ }
163
+ const quarter = Number.isFinite(month) && month > 0 ? Math.ceil(month / 3) : 0;
164
+ return quarter > 0 && year > 0 ? `${year} Q${quarter}` : date;
165
+ }
166
+
167
+ function statementAvailabilityScore(statement: FinancialStatement): number {
168
+ return Object.values(statement.fieldAvailability ?? {})
169
+ .filter((value) => Number.isFinite(Date.parse(value)))
170
+ .length + (Number.isFinite(Date.parse(statement.availableAt ?? "")) ? 1 : 0);
171
+ }
172
+
173
+ interface StatementFieldCandidate {
174
+ statement: InternalStatement;
175
+ value: number;
176
+ availableAt?: string;
177
+ derived: boolean;
178
+ }
179
+
180
+ function selectFieldCandidate(candidates: StatementFieldCandidate[]): StatementFieldCandidate {
181
+ const reported = candidates.filter((candidate) => !candidate.derived);
182
+ const qualityCandidates = reported.length > 0 ? reported : candidates;
183
+ const dated = qualityCandidates.filter((candidate) => (
184
+ Number.isFinite(Date.parse(candidate.availableAt ?? ""))
185
+ ));
186
+ const chronological = [...(dated.length > 0 ? dated : qualityCandidates)].sort((left, right) => (
187
+ Date.parse(left.availableAt ?? "") - Date.parse(right.availableAt ?? "")
188
+ || left.statement.date.localeCompare(right.statement.date)
189
+ ));
190
+ let selected = chronological[0]!;
191
+ for (const candidate of chronological.slice(1)) {
192
+ if (!Object.is(candidate.value, selected.value)) selected = candidate;
193
+ }
194
+ return selected;
195
+ }
196
+
197
+ function periodDateSource(statements: readonly InternalStatement[]): InternalStatement {
198
+ return [...statements].sort((left, right) => {
199
+ const availabilityDifference = statementAvailabilityScore(right) - statementAvailabilityScore(left);
200
+ return availabilityDifference || left.date.localeCompare(right.date);
201
+ })[0]!;
202
+ }
203
+
204
+ function mergeStatementPeriodGroup(statements: readonly InternalStatement[]): InternalStatement {
205
+ const dateSource = periodDateSource(statements);
206
+ const merged: InternalStatement = { date: dateSource.date };
207
+ const derivedFields: NumericStatementField[] = [];
208
+ const fieldAvailability: Record<string, string> = {};
209
+ const record = merged as unknown as Record<string, unknown>;
210
+
211
+ for (const field of NUMERIC_STATEMENT_FIELDS) {
212
+ const candidates = statements.flatMap((statement): StatementFieldCandidate[] => {
213
+ const value = statementNumber(statement, field);
214
+ if (value === null) return [];
215
+ return [{
216
+ statement,
217
+ value,
218
+ availableAt: statement.fieldAvailability?.[field] ?? statement.availableAt,
219
+ derived: statement.__timeSeriesDerivedFields?.includes(field) === true,
220
+ }];
221
+ });
222
+ if (candidates.length === 0) continue;
223
+ const selected = selectFieldCandidate(candidates);
224
+ record[field] = selected.value;
225
+ if (selected.availableAt) fieldAvailability[field] = selected.availableAt;
226
+ if (selected.derived) derivedFields.push(field);
227
+ }
228
+
229
+ if (Object.keys(fieldAvailability).length > 0) {
230
+ merged.fieldAvailability = fieldAvailability;
231
+ merged.availableAt = latestDateString(Object.values(fieldAvailability));
232
+ }
233
+ if (derivedFields.length > 0) merged.__timeSeriesDerivedFields = derivedFields;
234
+ return merged;
235
+ }
236
+
237
+ function mergeStatementsByPeriod(
238
+ statements: readonly FinancialStatement[],
239
+ ): InternalStatement[] {
240
+ const groups: InternalStatement[][] = [];
241
+ const sorted = [...statements].sort((left, right) => left.date.localeCompare(right.date));
242
+ for (const statement of sorted) {
243
+ const group = groups.find((candidate) => (
244
+ areNearbyFinancialPeriodEnds(candidate[0]!.date, statement.date)
245
+ ));
246
+ if (group) group.push(statement as InternalStatement);
247
+ else groups.push([statement as InternalStatement]);
248
+ }
249
+ return groups
250
+ .map(mergeStatementPeriodGroup)
251
+ .sort((left, right) => left.date.localeCompare(right.date));
252
+ }
253
+
254
+ interface PrecedingQuarterInput {
255
+ date: string;
256
+ time: number;
257
+ value: number;
258
+ availableAt?: string;
259
+ }
260
+
261
+ function precedingQuarterInputs(
262
+ quarterlyStatements: readonly FinancialStatement[],
263
+ annualStatement: FinancialStatement,
264
+ field: NumericStatementField,
265
+ ): PrecedingQuarterInput[] {
266
+ const annualTime = statementTime(annualStatement);
267
+ if (!Number.isFinite(annualTime)) return [];
268
+
269
+ const annualCategory = periodCategory(annualStatement.date, "quarterly");
270
+ const byCategory = new Map<string, PrecedingQuarterInput>();
271
+ for (const statement of quarterlyStatements) {
272
+ const time = statementTime(statement);
273
+ if (!Number.isFinite(time) || time >= annualTime || annualTime - time > 370 * DAY_MS) continue;
274
+ const category = periodCategory(statement.date, "quarterly");
275
+ if (category === annualCategory) continue;
276
+ const value = statementNumber(statement, field);
277
+ if (value === null) continue;
278
+ const previous = byCategory.get(category);
279
+ if (!previous || statement.date.localeCompare(previous.date) > 0) {
280
+ byCategory.set(category, {
281
+ date: statement.date,
282
+ time,
283
+ value,
284
+ availableAt: statement.fieldAvailability?.[field] ?? statement.availableAt,
285
+ });
286
+ }
287
+ }
288
+
289
+ return [...byCategory.values()]
290
+ .sort((left, right) => right.time - left.time)
291
+ .slice(0, 3)
292
+ .sort((left, right) => left.time - right.time);
293
+ }
294
+
295
+ /**
296
+ * Completes missing fiscal Q4 rows from the annual total and the three reported
297
+ * quarters. Snapshot fields are copied from the annual balance sheet. Derived
298
+ * values become available only after the annual total and every quarterly
299
+ * input used in the derivation are public.
300
+ */
301
+ export function deriveQuarterlyStatements(
302
+ quarterlyStatements: readonly FinancialStatement[],
303
+ annualStatements: readonly FinancialStatement[],
304
+ ): FinancialStatement[] {
305
+ const mergedQuarterly = mergeStatementsByPeriod(quarterlyStatements);
306
+ const byDate = new Map(mergedQuarterly.map((statement) => [statement.date, { ...statement }]));
307
+
308
+ for (const annualStatement of mergeStatementsByPeriod(annualStatements)) {
309
+ let target: InternalStatement = byDate.get(annualStatement.date) ?? { date: annualStatement.date };
310
+ let changed = false;
311
+
312
+ for (const field of QUARTERLY_FLOW_FIELDS) {
313
+ if (statementNumber(target, field) !== null) continue;
314
+ const annualValue = statementNumber(annualStatement, field);
315
+ if (annualValue === null) continue;
316
+ const previousInputs = precedingQuarterInputs(mergedQuarterly, annualStatement, field);
317
+ if (previousInputs.length !== 3) continue;
318
+ const derived = annualValue - previousInputs.reduce((sum, input) => sum + input.value, 0);
319
+ if (!Number.isFinite(derived)) continue;
320
+ const availableAt = latestDateString([
321
+ annualStatement.fieldAvailability?.[field] ?? annualStatement.availableAt,
322
+ ...previousInputs.map((input) => input.availableAt),
323
+ ]);
324
+ setStatementNumber(target, field, derived, availableAt);
325
+ changed = true;
326
+ }
327
+
328
+ for (const field of QUARTERLY_SNAPSHOT_FIELDS) {
329
+ if (statementNumber(target, field) !== null) continue;
330
+ const annualValue = statementNumber(annualStatement, field);
331
+ if (annualValue === null) continue;
332
+ const availableAt = annualStatement.fieldAvailability?.[field] ?? annualStatement.availableAt;
333
+ setStatementNumber(target, field, annualValue, availableAt);
334
+ changed = true;
335
+ }
336
+
337
+ if (changed) {
338
+ target.availableAt = latestDateString([
339
+ target.availableAt,
340
+ annualStatement.availableAt,
341
+ ...Object.values(target.fieldAvailability ?? {}),
342
+ ]);
343
+ byDate.set(target.date, target);
344
+ }
345
+ }
346
+
347
+ return mergeStatementsByPeriod([...byDate.values()]);
348
+ }
349
+
350
+ function buildTtmStatements(statements: readonly FinancialStatement[]): InternalStatement[] {
351
+ const sorted = mergeStatementsByPeriod(statements);
352
+ const result: InternalStatement[] = [];
353
+ for (let index = 3; index < sorted.length; index += 1) {
354
+ const window = sorted.slice(index - 3, index + 1);
355
+ const firstTime = statementTime(window[0]!);
356
+ const lastTime = statementTime(window.at(-1)!);
357
+ if (!Number.isFinite(firstTime) || !Number.isFinite(lastTime) || lastTime - firstTime > 400 * DAY_MS) {
358
+ continue;
359
+ }
360
+
361
+ const latest = window.at(-1)!;
362
+ const ttm: InternalStatement = {
363
+ date: latest.date,
364
+ availableAt: latestDateString(window.map((statement) => statement.availableAt)),
365
+ fieldAvailability: {},
366
+ __timeSeriesTtm: true,
367
+ __timeSeriesDerivedFields: [],
368
+ };
369
+
370
+ for (const field of QUARTERLY_FLOW_FIELDS) {
371
+ const values = window.map((statement) => statementNumber(statement, field));
372
+ if (!values.every((value): value is number => value !== null)) continue;
373
+ (ttm as unknown as Record<string, unknown>)[field] = values.reduce((sum, value) => sum + value, 0);
374
+ ttm.__timeSeriesDerivedFields!.push(field);
375
+ const availableAt = latestDateString(window.map((statement) => (
376
+ statement.fieldAvailability?.[field] ?? statement.availableAt
377
+ )));
378
+ if (availableAt) ttm.fieldAvailability![field] = availableAt;
379
+ }
380
+
381
+ for (const field of QUARTERLY_SNAPSHOT_FIELDS) {
382
+ const value = statementNumber(latest, field);
383
+ if (value === null) continue;
384
+ (ttm as unknown as Record<string, unknown>)[field] = value;
385
+ const availableAt = latest.fieldAvailability?.[field] ?? latest.availableAt;
386
+ if (availableAt) ttm.fieldAvailability![field] = availableAt;
387
+ }
388
+ ttm.availableAt = latestDateString([
389
+ ttm.availableAt,
390
+ ...Object.values(ttm.fieldAvailability ?? {}),
391
+ ]);
392
+ result.push(ttm);
393
+ }
394
+ return result;
395
+ }
396
+
397
+ function normalizeFundamentalPeriod(period: SeriesPeriod | undefined): "annual" | "quarterly" | "ttm" | "auto" {
398
+ return period === "annual" || period === "quarterly" || period === "ttm" ? period : "auto";
399
+ }
400
+
401
+ function sourceStatements(
402
+ financials: TickerFinancials,
403
+ period: SeriesPeriod | undefined,
404
+ valuationMetric?: string,
405
+ ): { statements: InternalStatement[]; period: "annual" | "quarterly" | "ttm" } {
406
+ const normalized = normalizeFundamentalPeriod(period);
407
+ const quarterly = deriveQuarterlyStatements(financials.quarterlyStatements, financials.annualStatements);
408
+ if (normalized === "annual") {
409
+ return { statements: mergeStatementsByPeriod(financials.annualStatements), period: "annual" };
410
+ }
411
+ if (normalized === "ttm" || valuationMetric) {
412
+ const ttm = buildTtmStatements(quarterly);
413
+ if (normalized === "ttm" && ttm.length > 0) return { statements: ttm, period: "ttm" };
414
+ if (valuationMetric && ttm.some((statement) => (
415
+ valuationAtPrice(statement, valuationMetric, 1) !== null
416
+ ))) {
417
+ return { statements: ttm, period: "ttm" };
418
+ }
419
+ if (normalized === "ttm") return { statements: [], period: "ttm" };
420
+ if (valuationMetric) {
421
+ return { statements: mergeStatementsByPeriod(financials.annualStatements), period: "annual" };
422
+ }
423
+ }
424
+ if (normalized === "quarterly" || (normalized === "auto" && quarterly.length > 0)) {
425
+ return { statements: quarterly, period: "quarterly" };
426
+ }
427
+ return { statements: mergeStatementsByPeriod(financials.annualStatements), period: "annual" };
428
+ }
429
+
430
+ function ratio(numerator: unknown, denominator: unknown): number | null {
431
+ return finiteNumber(numerator) && finiteNumber(denominator) && denominator !== 0
432
+ ? numerator / denominator
433
+ : null;
434
+ }
435
+
436
+ function freeCashFlow(statement: FinancialStatement): { value: number | null; derived: boolean } {
437
+ if (finiteNumber(statement.freeCashFlow)) return { value: statement.freeCashFlow, derived: false };
438
+ if (finiteNumber(statement.operatingCashFlow) && finiteNumber(statement.capitalExpenditure)) {
439
+ return { value: statement.operatingCashFlow + statement.capitalExpenditure, derived: true };
440
+ }
441
+ return { value: null, derived: false };
442
+ }
443
+
444
+ type SelectedStatementField = {
445
+ field: NumericStatementField;
446
+ value: number;
447
+ };
448
+
449
+ function selectStatementField(
450
+ statement: FinancialStatement,
451
+ fields: readonly NumericStatementField[],
452
+ accepts: (value: number) => boolean = () => true,
453
+ ): SelectedStatementField | null {
454
+ for (const field of fields) {
455
+ const value = statementNumber(statement, field);
456
+ if (value !== null && accepts(value)) return { field, value };
457
+ }
458
+ return null;
459
+ }
460
+
461
+ function selectedShares(statement: FinancialStatement): SelectedStatementField | null {
462
+ return selectStatementField(
463
+ statement,
464
+ ["dilutedShares", "basicShares", "ordinarySharesNumber", "shareIssued"],
465
+ (value) => value > 0,
466
+ );
467
+ }
468
+
469
+ function selectedCash(statement: FinancialStatement): SelectedStatementField | null {
470
+ return selectStatementField(statement, [
471
+ "cashCashEquivalentsAndShortTermInvestments",
472
+ "cashAndCashEquivalents",
473
+ ]);
474
+ }
475
+
476
+ function selectedEps(
477
+ statement: FinancialStatement,
478
+ ): { value: number; dependencies: NumericStatementField[] } | null {
479
+ if (finiteNumber(statement.eps) && statement.eps !== 0) {
480
+ return { value: statement.eps, dependencies: ["eps"] };
481
+ }
482
+ const shares = selectedShares(statement);
483
+ if (!shares || !finiteNumber(statement.netIncome)) return null;
484
+ return {
485
+ value: statement.netIncome / shares.value,
486
+ dependencies: ["netIncome", shares.field],
487
+ };
488
+ }
489
+
490
+ function uniqueDependencies(
491
+ fields: Array<NumericStatementField | null | undefined>,
492
+ ): NumericStatementField[] {
493
+ return [...new Set(fields.filter((field): field is NumericStatementField => !!field))];
494
+ }
495
+
496
+ function metricDependencies(metric: string, statement: FinancialStatement): NumericStatementField[] {
497
+ if (metric === "grossMargin") return ["grossProfit", "totalRevenue"];
498
+ if (metric === "operatingMargin") return ["operatingIncome", "totalRevenue"];
499
+ if (metric === "netMargin") return ["netIncome", "totalRevenue"];
500
+ if (metric === "freeCashFlowMargin") {
501
+ return finiteNumber(statement.freeCashFlow)
502
+ ? ["freeCashFlow", "totalRevenue"]
503
+ : ["operatingCashFlow", "capitalExpenditure", "totalRevenue"];
504
+ }
505
+ if (metric === "freeCashFlow") {
506
+ return finiteNumber(statement.freeCashFlow)
507
+ ? ["freeCashFlow"]
508
+ : ["operatingCashFlow", "capitalExpenditure"];
509
+ }
510
+ if (metric === "trailingPE") return selectedEps(statement)?.dependencies ?? ["eps"];
511
+ if (metric === "priceSales") {
512
+ return uniqueDependencies(["totalRevenue", selectedShares(statement)?.field]);
513
+ }
514
+ if (metric === "evSales") {
515
+ return uniqueDependencies([
516
+ "totalRevenue",
517
+ selectedShares(statement)?.field,
518
+ finiteNumber(statement.totalDebt) ? "totalDebt" : undefined,
519
+ selectedCash(statement)?.field,
520
+ ]);
521
+ }
522
+ if (metric === "evEbitda") {
523
+ return uniqueDependencies([
524
+ "ebitda",
525
+ selectedShares(statement)?.field,
526
+ finiteNumber(statement.totalDebt) ? "totalDebt" : undefined,
527
+ selectedCash(statement)?.field,
528
+ ]);
529
+ }
530
+ if (metric === "priceFcf") {
531
+ return uniqueDependencies([
532
+ ...(finiteNumber(statement.freeCashFlow)
533
+ ? ["freeCashFlow" as const]
534
+ : ["operatingCashFlow" as const, "capitalExpenditure" as const]),
535
+ selectedShares(statement)?.field,
536
+ ]);
537
+ }
538
+ return [metric as NumericStatementField];
539
+ }
540
+
541
+ function metricAvailability(statement: FinancialStatement, metric: string): string | undefined {
542
+ const dependencies = metricDependencies(metric, statement);
543
+ if (dependencies.length === 0) return statement.availableAt;
544
+ return latestDateString(dependencies.map((field) => (
545
+ latestDateString([statement.fieldAvailability?.[field]]) ?? statement.availableAt
546
+ )));
547
+ }
548
+
549
+ function fundamentalValue(
550
+ statement: InternalStatement,
551
+ metric: string,
552
+ ): { value: number | null; derived: boolean } {
553
+ if (metric === "grossMargin") {
554
+ const value = ratio(statement.grossProfit, statement.totalRevenue);
555
+ return { value: value === null ? null : value * 100, derived: true };
556
+ }
557
+ if (metric === "operatingMargin") {
558
+ const value = ratio(statement.operatingIncome, statement.totalRevenue);
559
+ return { value: value === null ? null : value * 100, derived: true };
560
+ }
561
+ if (metric === "netMargin") {
562
+ const value = ratio(statement.netIncome, statement.totalRevenue);
563
+ return { value: value === null ? null : value * 100, derived: true };
564
+ }
565
+ if (metric === "freeCashFlow" || metric === "freeCashFlowMargin") {
566
+ const fcf = freeCashFlow(statement);
567
+ if (metric === "freeCashFlow") return fcf;
568
+ const value = ratio(fcf.value, statement.totalRevenue);
569
+ return { value: value === null ? null : value * 100, derived: true };
570
+ }
571
+ const value = statementNumber(statement, metric as NumericStatementField);
572
+ const derived = statement.__timeSeriesTtm === true
573
+ || statement.__timeSeriesDerivedFields?.includes(metric as NumericStatementField) === true;
574
+ return { value, derived };
575
+ }
576
+
577
+ function pointForStatement(
578
+ statement: FinancialStatement,
579
+ metric: string,
580
+ value: number,
581
+ period: "annual" | "quarterly" | "ttm",
582
+ timestampMode: SecuritySeriesSource["timestampMode"],
583
+ derived: boolean,
584
+ ): TimeSeriesPoint | null {
585
+ const observedAt = validDate(statement.date);
586
+ if (!observedAt) return null;
587
+ const availableAtString = metricAvailability(statement, metric);
588
+ const availableAt = validDate(availableAtString);
589
+ const date = timestampMode !== "period-end" && availableAt ? availableAt : observedAt;
590
+ return {
591
+ date,
592
+ observedAt,
593
+ availableAt: availableAt ?? undefined,
594
+ value,
595
+ periodLabel: period === "annual"
596
+ ? periodCategory(statement.date, "annual")
597
+ : period === "ttm"
598
+ ? `TTM ${periodCategory(statement.date, "quarterly")}`
599
+ : periodCategory(statement.date, "quarterly"),
600
+ provenance: { quality: derived ? "derived" : "reported" },
601
+ };
602
+ }
603
+
604
+ function priceAtOrBefore(priceHistory: readonly PricePoint[], date: string): number | null {
605
+ const target = Date.parse(date);
606
+ if (!Number.isFinite(target)) return null;
607
+ let closest: { time: number; close: number } | undefined;
608
+ for (const point of priceHistory) {
609
+ const time = point.date.getTime();
610
+ if (!Number.isFinite(time) || time > target || !finiteNumber(point.close) || point.close <= 0) continue;
611
+ if (!closest || time > closest.time) closest = { time, close: point.close };
612
+ }
613
+ return closest?.close ?? null;
614
+ }
615
+
616
+ function historicalValuation(
617
+ financials: TickerFinancials,
618
+ statement: FinancialStatement,
619
+ metric: string,
620
+ ): number | null {
621
+ const priceDate = metricAvailability(statement, metric) ?? statement.date;
622
+ const price = priceAtOrBefore(financials.priceHistory, priceDate);
623
+ return price === null ? null : valuationAtPrice(statement, metric, price);
624
+ }
625
+
626
+ function valuationAtPrice(
627
+ statement: FinancialStatement,
628
+ metric: string,
629
+ price: number,
630
+ ): number | null {
631
+ const shares = selectedShares(statement);
632
+ const marketCap = shares ? price * shares.value : null;
633
+ const cash = selectedCash(statement)?.value ?? 0;
634
+ const debt = finiteNumber(statement.totalDebt) ? statement.totalDebt : 0;
635
+ const enterpriseValue = marketCap !== null
636
+ ? marketCap + debt - (finiteNumber(cash) ? cash : 0)
637
+ : null;
638
+ if (metric === "trailingPE") {
639
+ const eps = selectedEps(statement)?.value;
640
+ return eps && eps > 0 ? price / eps : null;
641
+ }
642
+ if (metric === "priceSales") return ratio(marketCap, statement.totalRevenue);
643
+ if (metric === "evSales") return ratio(enterpriseValue, statement.totalRevenue);
644
+ if (metric === "evEbitda") return ratio(enterpriseValue, statement.ebitda);
645
+ if (metric === "priceFcf") return ratio(marketCap, freeCashFlow(statement).value);
646
+ return null;
647
+ }
648
+
649
+ function providerCurrentValuationPoint(
650
+ financials: TickerFinancials,
651
+ metric: string,
652
+ ): TimeSeriesPoint | null {
653
+ const value = metric === "forwardPE"
654
+ ? financials.fundamentals?.forwardPE
655
+ : metric === "pegRatio"
656
+ ? financials.fundamentals?.pegRatio
657
+ : undefined;
658
+ if (!finiteNumber(value)) return null;
659
+ const quoteTime = financials.quote?.lastUpdated;
660
+ const date = validDate(finiteNumber(quoteTime) && quoteTime > 0 ? quoteTime : null);
661
+ if (!date) return null;
662
+ return {
663
+ date,
664
+ observedAt: date,
665
+ availableAt: date,
666
+ value,
667
+ periodLabel: "Current",
668
+ provenance: {
669
+ providerId: financials.quote?.providerId,
670
+ quality: "estimated",
671
+ },
672
+ };
673
+ }
674
+
675
+ function currentDerivedValuationPoint(
676
+ financials: TickerFinancials,
677
+ statements: readonly FinancialStatement[],
678
+ metric: string,
679
+ ): TimeSeriesPoint | null {
680
+ const quote = financials.quote;
681
+ const quoteDate = validDate(quote?.lastUpdated);
682
+ if (!quoteDate || !finiteNumber(quote?.price) || quote.price <= 0) return null;
683
+ const quoteTime = quoteDate.getTime();
684
+ const statement = statements
685
+ .flatMap((candidate) => {
686
+ const availableAt = validDate(metricAvailability(candidate, metric) ?? candidate.date);
687
+ const observedAt = validDate(candidate.date);
688
+ if (!availableAt || !observedAt || availableAt.getTime() > quoteTime) return [];
689
+ if (valuationAtPrice(candidate, metric, quote.price) === null) return [];
690
+ return [{ candidate, observedAt: observedAt.getTime(), availableAt: availableAt.getTime() }];
691
+ })
692
+ .sort((left, right) => (
693
+ left.observedAt - right.observedAt || left.availableAt - right.availableAt
694
+ ))
695
+ .at(-1)?.candidate;
696
+ if (!statement) return null;
697
+ const value = valuationAtPrice(statement, metric, quote.price);
698
+ if (value === null) return null;
699
+ return {
700
+ date: quoteDate,
701
+ observedAt: quoteDate,
702
+ availableAt: quoteDate,
703
+ value,
704
+ periodLabel: "Current",
705
+ provenance: {
706
+ providerId: quote.providerId,
707
+ quality: "derived",
708
+ },
709
+ };
710
+ }
711
+
712
+ /** Whether a valuation field derives a current point from the latest quote. */
713
+ export function valuationSeriesUsesLiveQuote(fieldId: string): boolean {
714
+ const [namespace, metric = ""] = canonicalTimeSeriesFieldId(fieldId).split(".");
715
+ return namespace === "valuation" && QUOTE_DERIVED_VALUATION_IDS.has(metric);
716
+ }
717
+
718
+ function dedupeAndSortPoints(points: readonly TimeSeriesPoint[]): TimeSeriesPoint[] {
719
+ const byTimestamp = new Map<number, TimeSeriesPoint>();
720
+ for (const point of points) {
721
+ const time = point.date.getTime();
722
+ if (Number.isFinite(time)) byTimestamp.set(time, point);
723
+ }
724
+ return [...byTimestamp.values()].sort((left, right) => left.date.getTime() - right.date.getTime());
725
+ }
726
+
727
+ function preferredPeriodPoint(
728
+ existing: TimeSeriesPoint,
729
+ candidate: TimeSeriesPoint,
730
+ ): TimeSeriesPoint {
731
+ if (existing.provenance?.quality !== candidate.provenance?.quality) {
732
+ return candidate.provenance?.quality === "reported" ? candidate : existing;
733
+ }
734
+ const existingAvailableAt = existing.availableAt?.getTime();
735
+ const candidateAvailableAt = candidate.availableAt?.getTime();
736
+ const existingHasAvailability = Number.isFinite(existingAvailableAt);
737
+ const candidateHasAvailability = Number.isFinite(candidateAvailableAt);
738
+ if (existingHasAvailability !== candidateHasAvailability) {
739
+ return candidateHasAvailability ? candidate : existing;
740
+ }
741
+ if (
742
+ existingHasAvailability
743
+ && candidateHasAvailability
744
+ && candidateAvailableAt! !== existingAvailableAt!
745
+ ) {
746
+ const preferLater = !Object.is(existing.value, candidate.value);
747
+ const candidateWins = preferLater
748
+ ? candidateAvailableAt! > existingAvailableAt!
749
+ : candidateAvailableAt! < existingAvailableAt!;
750
+ return candidateWins ? candidate : existing;
751
+ }
752
+ return candidate.observedAt.getTime() < existing.observedAt.getTime()
753
+ ? candidate
754
+ : existing;
755
+ }
756
+
757
+ /**
758
+ * Provider snapshots can contain both an issuer fiscal-period row and a
759
+ * calendar-normalized row for the same observation. Their period ends and
760
+ * display timestamps differ slightly, so timestamp-only deduplication renders
761
+ * paired bars. A financial period is the observation identity; publication
762
+ * time only decides when that observation became available.
763
+ */
764
+ function dedupeFundamentalPeriods(points: readonly TimeSeriesPoint[]): TimeSeriesPoint[] {
765
+ const groups: TimeSeriesPoint[][] = [];
766
+ const sorted = [...points].sort((left, right) => (
767
+ left.observedAt.getTime() - right.observedAt.getTime()
768
+ ));
769
+ for (const point of sorted) {
770
+ const group = groups.find((candidate) => (
771
+ areNearbyFinancialPeriodEnds(candidate[0]!.observedAt, point.observedAt)
772
+ ));
773
+ if (group) group.push(point);
774
+ else groups.push([point]);
775
+ }
776
+ return groups
777
+ .map((group) => group.slice(1).reduce(preferredPeriodPoint, group[0]!))
778
+ .sort((left, right) => (
779
+ left.date.getTime() - right.date.getTime()
780
+ || left.observedAt.getTime() - right.observedAt.getTime()
781
+ ));
782
+ }
783
+
784
+ /** Extracts a point-in-time-safe fundamental or valuation series from a snapshot. */
785
+ export function extractFundamentalSeries(
786
+ financials: TickerFinancials | null,
787
+ source: SecuritySeriesSource,
788
+ ): TimeSeriesPoint[] {
789
+ if (!financials) return [];
790
+ const canonicalId = canonicalTimeSeriesFieldId(source.fieldId);
791
+ const [namespace, metric = ""] = canonicalId.split(".");
792
+ if (namespace === "fundamental" && FUNDAMENTAL_IDS.has(metric)) {
793
+ const selected = sourceStatements(financials, source.period);
794
+ const points = selected.statements.flatMap((statement) => {
795
+ const result = fundamentalValue(statement, metric);
796
+ if (result.value === null) return [];
797
+ const point = pointForStatement(
798
+ statement,
799
+ metric,
800
+ result.value,
801
+ selected.period,
802
+ source.timestampMode,
803
+ result.derived,
804
+ );
805
+ return point ? [point] : [];
806
+ });
807
+ return dedupeFundamentalPeriods(points);
808
+ }
809
+
810
+ if (namespace !== "valuation" || !VALUATION_IDS.has(metric)) return [];
811
+ if (metric === "forwardPE" || metric === "pegRatio") {
812
+ const current = providerCurrentValuationPoint(financials, metric);
813
+ return current ? [current] : [];
814
+ }
815
+
816
+ const selected = sourceStatements(financials, source.period, metric);
817
+ const historical = selected.statements.flatMap((statement) => {
818
+ const value = historicalValuation(financials, statement, metric);
819
+ if (value === null) return [];
820
+ const point = pointForStatement(
821
+ statement,
822
+ metric,
823
+ value,
824
+ selected.period,
825
+ source.timestampMode,
826
+ true,
827
+ );
828
+ return point ? [point] : [];
829
+ });
830
+ const current = currentDerivedValuationPoint(financials, selected.statements, metric);
831
+ const dedupedHistorical = dedupeFundamentalPeriods(historical);
832
+ return dedupeAndSortPoints(current ? [...dedupedHistorical, current] : dedupedHistorical);
833
+ }
834
+
835
+ export function fundamentalSeriesUsesAvailabilityFallback(
836
+ financials: TickerFinancials | null,
837
+ source: SecuritySeriesSource,
838
+ ): boolean {
839
+ if (!financials || source.timestampMode === "period-end") return false;
840
+ const points = extractFundamentalSeries(financials, source);
841
+ return points.some((point) => point.availableAt === undefined);
842
+ }