gloomberb 0.10.0 → 0.10.2

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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -3,6 +3,7 @@ import {
3
3
  applyTheme,
4
4
  clearTransientThemePreview,
5
5
  getCurrentThemeId,
6
+ getThemeColors,
6
7
  previewTheme,
7
8
  syncTheme,
8
9
  } from "./colors";
@@ -43,6 +44,15 @@ afterEach(() => {
43
44
  });
44
45
 
45
46
  describe("theme colors", () => {
47
+ test("provides immutable palette snapshots while the compatibility facade follows the active theme", () => {
48
+ const amber = getThemeColors(DEFAULT_THEME);
49
+ const midnight = getThemeColors("midnight");
50
+
51
+ expect(Object.isFrozen(amber)).toBe(true);
52
+ expect(Object.isFrozen(midnight)).toBe(true);
53
+ expect(amber).not.toBe(midnight);
54
+ });
55
+
46
56
  test("syncs CSS variables when a theme is applied", () => {
47
57
  const values = installDocumentStyleMock();
48
58
  const theme = getTheme("midnight");
@@ -1,9 +1,24 @@
1
1
  import { blendForContrast, blendHex, higherContrast } from "./color-utils";
2
2
  import { getTheme, DEFAULT_THEME, type Theme } from "./themes";
3
3
 
4
- // Mutable colors object — properties are updated in-place when the theme changes.
5
- // React components re-read these on each render triggered by the SET_THEME action.
6
- export const colors: Omit<Theme, "name" | "description"> = { ...getTheme(DEFAULT_THEME) };
4
+ export type ThemeColors = Readonly<Omit<Theme, "name" | "description">>;
5
+
6
+ export function getThemeColors(id: string): ThemeColors {
7
+ const { name: _name, description: _description, ...palette } = getTheme(id);
8
+ return Object.freeze(palette);
9
+ }
10
+
11
+ let currentColors = getThemeColors(DEFAULT_THEME);
12
+
13
+ /** Compatibility facade for non-React formatting and rendering helpers. */
14
+ export const colors = new Proxy({ ...currentColors } as ThemeColors, {
15
+ get: (_target, property) => currentColors[property as keyof ThemeColors],
16
+ getOwnPropertyDescriptor: (_target, property) => ({
17
+ configurable: true,
18
+ enumerable: true,
19
+ value: currentColors[property as keyof ThemeColors],
20
+ }),
21
+ }) as ThemeColors;
7
22
 
8
23
  type ColorKey = keyof typeof colors;
9
24
 
@@ -38,10 +53,8 @@ export function getCurrentThemeId(): string {
38
53
  }
39
54
 
40
55
  export function applyTheme(id: string): void {
41
- const theme = getTheme(id);
42
56
  currentThemeId = id;
43
- // Mutate in-place so every existing import sees the new values
44
- Object.assign(colors, theme);
57
+ currentColors = getThemeColors(id);
45
58
  syncThemeCssVariables();
46
59
  }
47
60
 
@@ -73,22 +86,6 @@ export function syncTheme(id: string): void {
73
86
 
74
87
  export { blendHex } from "./color-utils";
75
88
 
76
- export function getComparisonSeriesColor(index: number): string {
77
- const seriesColors = [
78
- colors.borderFocused,
79
- colors.warning,
80
- colors.positive,
81
- colors.negative,
82
- colors.textBright,
83
- colors.neutral,
84
- blendHex(colors.borderFocused, colors.positive, 0.45),
85
- blendHex(colors.warning, colors.negative, 0.35),
86
- blendHex(colors.textBright, colors.positive, 0.35),
87
- blendHex(colors.borderFocused, colors.negative, 0.35),
88
- ];
89
- return seriesColors[((index % seriesColors.length) + seriesColors.length) % seriesColors.length]!;
90
- }
91
-
92
89
  export function getChartIndicatorColor(index: number): string {
93
90
  const accent = higherContrast(colors.warning, colors.borderFocused, colors.bg);
94
91
  const indicatorColors = [
@@ -112,8 +109,8 @@ export function getChartIndicatorColor(index: number): string {
112
109
  }
113
110
 
114
111
  /** Returns a hover background color derived from bg and selected */
115
- export function hoverBg(): string {
116
- return blendHex(colors.bg, colors.selected, 0.5);
112
+ export function hoverBg(palette: ThemeColors = colors): string {
113
+ return blendHex(palette.bg, palette.selected, 0.5);
117
114
  }
118
115
 
119
116
  function syncThemeCssVariables(): void {
@@ -126,62 +123,62 @@ function syncThemeCssVariables(): void {
126
123
  cssThemeId = currentThemeId;
127
124
  cssThemeDocument = documentLike;
128
125
  for (const [key, name] of THEME_CSS_VARIABLES) {
129
- style.setProperty(name, colors[key]);
126
+ style.setProperty(name, currentColors[key]);
130
127
  }
131
128
  style.setProperty("--gloom-hover-bg", hoverBg());
132
129
  }
133
130
 
134
- export function commandBarBg(): string {
135
- const base = higherContrast(colors.commandBg, colors.panel, colors.bg);
136
- const accent = higherContrast(colors.textBright, colors.borderFocused, base);
137
- return blendForContrast(base, colors.bg, accent, 1.45);
131
+ export function commandBarBg(palette: ThemeColors = colors): string {
132
+ const base = higherContrast(palette.commandBg, palette.panel, palette.bg);
133
+ const accent = higherContrast(palette.textBright, palette.borderFocused, base);
134
+ return blendForContrast(base, palette.bg, accent, 1.45);
138
135
  }
139
136
 
140
- export function commandBarPanelBg(): string {
141
- return blendHex(commandBarBg(), colors.panel, 0.28);
137
+ export function commandBarPanelBg(palette: ThemeColors = colors): string {
138
+ return blendHex(commandBarBg(palette), palette.panel, 0.28);
142
139
  }
143
140
 
144
- export function commandBarInputBg(): string {
145
- return blendHex(commandBarPanelBg(), colors.bg, 0.22);
141
+ export function commandBarInputBg(palette: ThemeColors = colors): string {
142
+ return blendHex(commandBarPanelBg(palette), palette.bg, 0.22);
146
143
  }
147
144
 
148
- export function commandBarSelectedBg(): string {
149
- const base = commandBarBg();
150
- const accent = higherContrast(colors.selectedText, colors.textBright, colors.selected);
151
- return blendForContrast(colors.selected, base, accent, 1.45);
145
+ export function commandBarSelectedBg(palette: ThemeColors = colors): string {
146
+ const base = commandBarBg(palette);
147
+ const accent = higherContrast(palette.selectedText, palette.textBright, palette.selected);
148
+ return blendForContrast(palette.selected, base, accent, 1.45);
152
149
  }
153
150
 
154
- export function commandBarHoverBg(): string {
155
- return blendHex(commandBarBg(), commandBarSelectedBg(), 0.45);
151
+ export function commandBarHoverBg(palette: ThemeColors = colors): string {
152
+ return blendHex(commandBarBg(palette), commandBarSelectedBg(palette), 0.45);
156
153
  }
157
154
 
158
- export function commandBarText(): string {
159
- const base = commandBarBg();
160
- const fallback = higherContrast(colors.textBright, "#f2f2f2", base);
161
- return blendForContrast(colors.text, base, fallback, 5.4);
155
+ export function commandBarText(palette: ThemeColors = colors): string {
156
+ const base = commandBarBg(palette);
157
+ const fallback = higherContrast(palette.textBright, "#f2f2f2", base);
158
+ return blendForContrast(palette.text, base, fallback, 5.4);
162
159
  }
163
160
 
164
- export function commandBarHeadingText(): string {
165
- const base = commandBarBg();
161
+ export function commandBarHeadingText(palette: ThemeColors = colors): string {
162
+ const base = commandBarBg(palette);
166
163
  const fallback = higherContrast(
167
- higherContrast(colors.textDim, colors.text, base),
168
- higherContrast(colors.textBright, colors.selectedText, base),
164
+ higherContrast(palette.textDim, palette.text, base),
165
+ higherContrast(palette.textBright, palette.selectedText, base),
169
166
  base,
170
167
  );
171
- return blendForContrast(colors.textMuted, base, fallback, 3.6);
168
+ return blendForContrast(palette.textMuted, base, fallback, 3.6);
172
169
  }
173
170
 
174
- export function commandBarSubtleText(): string {
175
- const base = commandBarBg();
176
- const fallback = higherContrast(commandBarText(), "#d8d8d8", base);
177
- return blendForContrast(colors.textDim, base, fallback, 4.1);
171
+ export function commandBarSubtleText(palette: ThemeColors = colors): string {
172
+ const base = commandBarBg(palette);
173
+ const fallback = higherContrast(commandBarText(palette), "#d8d8d8", base);
174
+ return blendForContrast(palette.textDim, base, fallback, 4.1);
178
175
  }
179
176
 
180
- export function commandBarSelectedText(): string {
181
- const base = commandBarSelectedBg();
182
- const preferred = higherContrast(colors.selectedText, colors.text, base);
177
+ export function commandBarSelectedText(palette: ThemeColors = colors): string {
178
+ const base = commandBarSelectedBg(palette);
179
+ const preferred = higherContrast(palette.selectedText, palette.text, base);
183
180
  const fallback = higherContrast(
184
- higherContrast(colors.textBright, colors.text, base),
181
+ higherContrast(palette.textBright, palette.text, base),
185
182
  higherContrast("#ffffff", "#000000", base),
186
183
  base,
187
184
  );
@@ -189,44 +186,44 @@ export function commandBarSelectedText(): string {
189
186
  }
190
187
 
191
188
  /** Background for docked pane bodies */
192
- export function paneBg(focused: boolean): string {
193
- if (focused) return blendHex(colors.bg, colors.borderFocused, 0.06);
194
- return blendHex(colors.panel, colors.border, 0.08);
189
+ export function paneBg(focused: boolean, palette: ThemeColors = colors): string {
190
+ if (focused) return blendHex(palette.bg, palette.borderFocused, 0.06);
191
+ return blendHex(palette.panel, palette.border, 0.08);
195
192
  }
196
193
 
197
194
  /** Background for floating pane bodies — elevated above docked panes */
198
- export function floatingPaneBg(focused: boolean): string {
199
- if (focused) return blendHex(colors.bg, colors.borderFocused, 0.08);
200
- return blendHex(colors.panel, colors.border, 0.18);
195
+ export function floatingPaneBg(focused: boolean, palette: ThemeColors = colors): string {
196
+ if (focused) return blendHex(palette.bg, palette.borderFocused, 0.08);
197
+ return blendHex(palette.panel, palette.border, 0.18);
201
198
  }
202
199
 
203
200
  /** Background for pane title bars */
204
- export function paneTitleBg(focused: boolean): string {
205
- if (focused) return blendHex(colors.bg, colors.borderFocused, 0.22);
206
- return blendHex(colors.panel, colors.border, 0.15);
201
+ export function paneTitleBg(focused: boolean, palette: ThemeColors = colors): string {
202
+ if (focused) return blendHex(palette.bg, palette.borderFocused, 0.22);
203
+ return blendHex(palette.panel, palette.border, 0.15);
207
204
  }
208
205
 
209
206
  /** Background for floating pane title bars */
210
- export function floatingPaneTitleBg(focused: boolean): string {
211
- if (focused) return blendHex(colors.bg, colors.borderFocused, 0.25);
212
- return blendHex(colors.panel, colors.border, 0.25);
207
+ export function floatingPaneTitleBg(focused: boolean, palette: ThemeColors = colors): string {
208
+ if (focused) return blendHex(palette.bg, palette.borderFocused, 0.25);
209
+ return blendHex(palette.panel, palette.border, 0.25);
213
210
  }
214
211
 
215
212
  /** Title text color for panes */
216
- export function paneTitleText(focused: boolean, floating = false): string {
217
- const background = floating ? floatingPaneTitleBg(focused) : paneTitleBg(focused);
213
+ export function paneTitleText(focused: boolean, floating = false, palette: ThemeColors = colors): string {
214
+ const background = floating ? floatingPaneTitleBg(focused, palette) : paneTitleBg(focused, palette);
218
215
  const preferred = focused
219
- ? higherContrast(colors.textBright, colors.headerText, background)
220
- : colors.textDim;
216
+ ? higherContrast(palette.textBright, palette.headerText, background)
217
+ : palette.textDim;
221
218
  const fallback = focused
222
- ? higherContrast(colors.text, "#f2f2f2", background)
223
- : higherContrast(colors.text, colors.textBright, background);
219
+ ? higherContrast(palette.text, "#f2f2f2", background)
220
+ : higherContrast(palette.text, palette.textBright, background);
224
221
  return blendForContrast(preferred, background, fallback, focused ? 5.2 : 4.5);
225
222
  }
226
223
 
227
224
  /** Returns green for positive, red for negative, neutral for zero */
228
- export function priceColor(value: number): string {
229
- if (value > 0) return colors.positive;
230
- if (value < 0) return colors.negative;
231
- return colors.neutral;
225
+ export function priceColor(value: number, palette: ThemeColors = colors): string {
226
+ if (value > 0) return palette.positive;
227
+ if (value < 0) return palette.negative;
228
+ return palette.neutral;
232
229
  }
@@ -0,0 +1,67 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { createTestRenderer } from "@opentui/core/testing";
3
+ import { act } from "react";
4
+ import { createOpenTuiTestRoot as createRoot } from "../renderers/opentui/test-utils";
5
+ import { commandBarBg, getThemeColors, paneBg, syncTheme } from "./colors";
6
+ import { ThemeProvider, useThemeColors } from "./theme-context";
7
+ import { DEFAULT_THEME } from "./themes";
8
+
9
+ let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
10
+ let root: ReturnType<typeof createRoot> | undefined;
11
+ const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
12
+ actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
13
+
14
+ function ThemeProbe() {
15
+ const colors = useThemeColors();
16
+ return <text>{`${colors.bg}|${commandBarBg(colors)}|${paneBg(true, colors)}`}</text>;
17
+ }
18
+
19
+ afterEach(async () => {
20
+ if (root) {
21
+ await act(async () => {
22
+ root?.unmount();
23
+ await Promise.resolve();
24
+ });
25
+ root = undefined;
26
+ }
27
+ testSetup?.renderer.destroy();
28
+ testSetup = undefined;
29
+ syncTheme(DEFAULT_THEME);
30
+ });
31
+
32
+ describe("ThemeProvider", () => {
33
+ test("render helpers switch with the context palette in the same render", async () => {
34
+ testSetup = await createTestRenderer({ width: 120, height: 2 });
35
+ root = createRoot(testSetup.renderer);
36
+
37
+ act(() => {
38
+ root?.render(
39
+ <ThemeProvider themeId={DEFAULT_THEME}>
40
+ <ThemeProbe />
41
+ </ThemeProvider>,
42
+ );
43
+ });
44
+ await act(async () => {
45
+ await testSetup?.renderOnce();
46
+ await testSetup?.renderOnce();
47
+ });
48
+
49
+ const nextThemeId = "midnight";
50
+ const nextColors = getThemeColors(nextThemeId);
51
+ act(() => {
52
+ root?.render(
53
+ <ThemeProvider themeId={nextThemeId}>
54
+ <ThemeProbe />
55
+ </ThemeProvider>,
56
+ );
57
+ });
58
+ await act(async () => {
59
+ await testSetup?.renderOnce();
60
+ await testSetup?.renderOnce();
61
+ });
62
+
63
+ expect(testSetup.captureCharFrame()).toContain(
64
+ `${nextColors.bg}|${commandBarBg(nextColors)}|${paneBg(true, nextColors)}`,
65
+ );
66
+ });
67
+ });
@@ -1,8 +1,11 @@
1
1
  import { createContext, useContext, useMemo, type ReactNode } from "react";
2
- import { colors, getCurrentThemeId, syncTheme } from "./colors";
3
- import type { Theme } from "./themes";
4
-
5
- type ThemeColors = Omit<Theme, "name" | "description">;
2
+ import {
3
+ colors,
4
+ getCurrentThemeId,
5
+ getThemeColors,
6
+ syncTheme,
7
+ type ThemeColors,
8
+ } from "./colors";
6
9
 
7
10
  interface ThemeContextValue {
8
11
  themeId: string;
@@ -13,7 +16,8 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
13
16
 
14
17
  export function ThemeProvider({ themeId, children }: { themeId: string; children: ReactNode }) {
15
18
  syncTheme(themeId);
16
- const value = useMemo(() => ({ themeId, colors }), [themeId]);
19
+ const themeColors = useMemo(() => getThemeColors(themeId), [themeId]);
20
+ const value = useMemo(() => ({ themeId, colors: themeColors }), [themeColors, themeId]);
17
21
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
18
22
  }
19
23
 
@@ -1,4 +1,4 @@
1
- import type { TickerRepository } from "../data/ticker-repository";
1
+ import type { AppTickerRepositoryPort } from "../core/app-service-ports";
2
2
  import type { SearchRequestContext, DataProvider } from "../types/data-provider";
3
3
  import type { TickerRecord } from "../types/ticker";
4
4
  import {
@@ -21,7 +21,7 @@ export async function resolveTickerOpenTarget({
21
21
  query: string;
22
22
  tickers: ReadonlyMap<string, TickerRecord>;
23
23
  dataProvider: DataProvider;
24
- tickerRepository: TickerRepository;
24
+ tickerRepository: AppTickerRepositoryPort;
25
25
  searchContext?: SearchRequestContext;
26
26
  }): Promise<TickerOpenTarget | null> {
27
27
  const symbol = normalizeTickerInput(null, query);
@@ -1,6 +1,6 @@
1
1
  import type { InstrumentSearchResult } from "../../types/instrument";
2
2
  import type { TickerRecord, TickerMetadata } from "../../types/ticker";
3
- import type { TickerRepository } from "../../data/ticker-repository";
3
+ import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
4
4
  import {
5
5
  classifyInstrumentKind,
6
6
  } from "./ranking";
@@ -10,7 +10,7 @@ import {
10
10
  } from "./result";
11
11
 
12
12
  export async function upsertTickerFromSearchResult(
13
- tickerRepository: TickerRepository,
13
+ tickerRepository: AppTickerRepositoryPort,
14
14
  result: InstrumentSearchResult,
15
15
  ): Promise<{ ticker: TickerRecord; created: boolean }> {
16
16
  const symbol = getSearchResultSymbol(result);
@@ -127,13 +127,6 @@ export function alignTimeSeries(
127
127
  return rows;
128
128
  }
129
129
 
130
- export function firstCommonValueDate(series: readonly ResolvedSeries[]): Date | null {
131
- const row = alignTimeSeries(series, { mode: "intersection" }).find((entry) => (
132
- series.every(({ id }) => finiteNumber(entry.values[id]?.value))
133
- ));
134
- return row ? new Date(row.date) : null;
135
- }
136
-
137
130
  /** Returns visible points and optionally the prior step anchor needed to draw continuity. */
138
131
  export function clipSeriesToWindow(
139
132
  series: ResolvedSeries,
@@ -0,0 +1,145 @@
1
+ import type { PricePoint, Quote } from "../types/financials";
2
+ import { isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
3
+ import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
4
+ import {
5
+ CHART_RESOLUTION_STEP_MS,
6
+ type ManualChartResolution,
7
+ } from "./resolution";
8
+
9
+ const MAX_LIVE_QUOTE_TAIL_AGE_MS = 7 * 24 * 60 * 60_000;
10
+ const MAX_LIVE_QUOTE_CLOCK_SKEW_MS = 5 * 60_000;
11
+ const MAX_INTRADAY_BAR_INTERVAL_MS = 6 * 60 * 60_000;
12
+ const MIN_LIVE_QUOTE_TAIL_GAP_MS = 5 * 60_000;
13
+
14
+ export type AppendLiveQuotePointOptions =
15
+ | {
16
+ now?: number;
17
+ mode?: "scalar";
18
+ }
19
+ | {
20
+ now?: number;
21
+ mode: "ohlc";
22
+ resolution: ManualChartResolution;
23
+ };
24
+
25
+ function coerceDate(value: Date | string | number): Date {
26
+ return value instanceof Date ? value : new Date(value);
27
+ }
28
+
29
+ function getPointTime(point: Pick<PricePoint, "date">): number {
30
+ return coerceDate(point.date as Date | string | number).getTime();
31
+ }
32
+
33
+ function getActiveQuotePrice(quote: Quote): number {
34
+ if ((quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice != null) {
35
+ return quote.preMarketPrice;
36
+ }
37
+ if ((quote.marketState === "POST" || quote.marketState === "POSTPOST") && quote.postMarketPrice != null) {
38
+ return quote.postMarketPrice;
39
+ }
40
+ return quote.price;
41
+ }
42
+
43
+ function quoteBelongsToLatestBar(
44
+ latestTime: number,
45
+ quoteTime: number,
46
+ resolution: ManualChartResolution,
47
+ ): boolean {
48
+ if (quoteTime < latestTime) return false;
49
+ if (resolution === "1mo") {
50
+ const latestDate = new Date(latestTime);
51
+ const quoteDate = new Date(quoteTime);
52
+ return latestDate.getUTCFullYear() === quoteDate.getUTCFullYear()
53
+ && latestDate.getUTCMonth() === quoteDate.getUTCMonth();
54
+ }
55
+ return quoteTime - latestTime < CHART_RESOLUTION_STEP_MS[resolution];
56
+ }
57
+
58
+ function finiteOrFallback(value: number | undefined, fallback: number): number {
59
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
60
+ }
61
+
62
+ function mergeQuoteIntoLatestBar(latest: PricePoint, quotePrice: number): PricePoint {
63
+ const open = finiteOrFallback(latest.open, latest.close);
64
+ const high = finiteOrFallback(latest.high, Math.max(open, latest.close));
65
+ const low = finiteOrFallback(latest.low, Math.min(open, latest.close));
66
+ return {
67
+ ...latest,
68
+ open,
69
+ high: Math.max(high, open, latest.close, quotePrice),
70
+ low: Math.min(low, open, latest.close, quotePrice),
71
+ close: quotePrice,
72
+ };
73
+ }
74
+
75
+ export function appendLiveQuotePoint(
76
+ points: PricePoint[],
77
+ quote: Quote | null | undefined,
78
+ options: AppendLiveQuotePointOptions = {},
79
+ ): PricePoint[] {
80
+ const now = options.now ?? Date.now();
81
+ if (!quote || isQuoteStaleForCurrentSession(quote, now)) return points;
82
+
83
+ const quoteTime = quote.lastUpdated;
84
+ const quotePrice = getActiveQuotePrice(quote);
85
+ if (
86
+ !Number.isFinite(quoteTime)
87
+ || !Number.isFinite(quotePrice)
88
+ || quotePrice <= 0
89
+ || quoteTime > now + MAX_LIVE_QUOTE_CLOCK_SKEW_MS
90
+ || now - quoteTime > MAX_LIVE_QUOTE_TAIL_AGE_MS
91
+ ) {
92
+ return points;
93
+ }
94
+
95
+ const latest = points.at(-1);
96
+ if (!latest) return points;
97
+
98
+ const latestTime = getPointTime(latest);
99
+ if (!Number.isFinite(latestTime) || quoteTime < latestTime) return points;
100
+
101
+ const previous = points.at(-2);
102
+ const latestInterval = previous ? latestTime - getPointTime(previous) : Number.NaN;
103
+ if (
104
+ latestInterval > 0
105
+ && latestInterval <= MAX_INTRADAY_BAR_INTERVAL_MS
106
+ && quoteTime - latestTime > Math.max(MIN_LIVE_QUOTE_TAIL_GAP_MS, latestInterval * 3)
107
+ ) {
108
+ return points;
109
+ }
110
+
111
+ const latestClose = latest.close;
112
+ if (hasLikelyQuoteUnitMismatch(
113
+ { currency: quote.currency, price: latestClose },
114
+ { currency: quote.currency, price: quotePrice },
115
+ )) {
116
+ return points;
117
+ }
118
+
119
+ if (options.mode === "ohlc") {
120
+ if (quoteBelongsToLatestBar(latestTime, quoteTime, options.resolution)) {
121
+ const merged = mergeQuoteIntoLatestBar(latest, quotePrice);
122
+ return [...points.slice(0, -1), merged];
123
+ }
124
+ return [
125
+ ...points,
126
+ {
127
+ date: new Date(quoteTime),
128
+ open: quotePrice,
129
+ high: quotePrice,
130
+ low: quotePrice,
131
+ close: quotePrice,
132
+ },
133
+ ];
134
+ }
135
+
136
+ if (quoteTime === latestTime) return points;
137
+
138
+ return [
139
+ ...points,
140
+ {
141
+ date: new Date(quoteTime),
142
+ close: quotePrice,
143
+ },
144
+ ];
145
+ }
@@ -0,0 +1,44 @@
1
+ import { TIME_RANGES, type ChartDateWindow, type TimeRange } from "./range";
2
+
3
+ export interface DateWindowRange extends ChartDateWindow {}
4
+
5
+ export function subtractTimeRange(endDate: Date, range: TimeRange): Date {
6
+ const startDate = new Date(endDate);
7
+ switch (range) {
8
+ case "1D":
9
+ startDate.setDate(startDate.getDate() - 1);
10
+ break;
11
+ case "1W":
12
+ startDate.setDate(startDate.getDate() - 7);
13
+ break;
14
+ case "1M":
15
+ startDate.setMonth(startDate.getMonth() - 1);
16
+ break;
17
+ case "3M":
18
+ startDate.setMonth(startDate.getMonth() - 3);
19
+ break;
20
+ case "6M":
21
+ startDate.setMonth(startDate.getMonth() - 6);
22
+ break;
23
+ case "1Y":
24
+ startDate.setFullYear(startDate.getFullYear() - 1);
25
+ break;
26
+ case "5Y":
27
+ startDate.setFullYear(startDate.getFullYear() - 5);
28
+ break;
29
+ case "ALL":
30
+ startDate.setFullYear(startDate.getFullYear() - 50);
31
+ break;
32
+ }
33
+ return startDate;
34
+ }
35
+
36
+ export function isDateWindowWithinTimeRange(startDate: Date, endDate: Date, maxRange: TimeRange): boolean {
37
+ if (maxRange === "ALL") return true;
38
+ return startDate.getTime() >= subtractTimeRange(endDate, maxRange).getTime();
39
+ }
40
+
41
+ export function getTimeRangeForDateWindow(window: DateWindowRange | null): TimeRange {
42
+ if (!window?.start || !window.end) return "ALL";
43
+ return TIME_RANGES.find((candidate) => isDateWindowWithinTimeRange(window.start!, window.end!, candidate)) ?? "ALL";
44
+ }
@@ -0,0 +1,21 @@
1
+ import { subtractTimeRange } from "./date-window";
2
+ import type { TimeRange } from "./range";
3
+ import type { PricePoint } from "../types/financials";
4
+ import { getPricePointTimestamp } from "../utils/price-history";
5
+
6
+ export function clipPriceHistoryToRange(points: PricePoint[], range: TimeRange): PricePoint[] {
7
+ if (range === "ALL" || points.length === 0) return points;
8
+ const dated = points.flatMap((point) => {
9
+ const timestamp = getPricePointTimestamp(point);
10
+ return Number.isFinite(timestamp) ? [{ point, timestamp }] : [];
11
+ });
12
+ const endTimestamp = dated.reduce<number | null>((latest, entry) => (
13
+ latest == null || entry.timestamp > latest ? entry.timestamp : latest
14
+ ), null);
15
+ if (endTimestamp == null) return [];
16
+ const end = new Date(endTimestamp);
17
+ const start = subtractTimeRange(end, range);
18
+ return dated
19
+ .filter(({ timestamp }) => timestamp >= start.getTime() && timestamp <= endTimestamp)
20
+ .map(({ point }) => point);
21
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildComparisonChartPreset } from "../plugins/builtin/chart-composer/presets";
3
+ import type { TickerRecord } from "../types/ticker";
4
+ import { hydrateChartSpecInstruments } from "./hooks";
5
+
6
+ function ticker(symbol: string, exchange: string): TickerRecord {
7
+ return {
8
+ metadata: {
9
+ ticker: symbol,
10
+ exchange,
11
+ currency: "USD",
12
+ name: symbol,
13
+ portfolios: [],
14
+ watchlists: [],
15
+ positions: [],
16
+ custom: {},
17
+ tags: [],
18
+ },
19
+ };
20
+ }
21
+
22
+ describe("hydrateChartSpecInstruments", () => {
23
+ test("fills missing exchanges without replacing explicit chart identities", () => {
24
+ const initial = buildComparisonChartPreset(["SNDK", "SAP:XETR"]);
25
+ const spec = {
26
+ ...initial,
27
+ series: initial.series.map((entry, index) => (
28
+ index === 0 && entry.source.kind === "security"
29
+ ? {
30
+ ...entry,
31
+ source: {
32
+ ...entry.source,
33
+ instrument: { ...entry.source.instrument, exchange: "" },
34
+ },
35
+ }
36
+ : entry
37
+ )),
38
+ };
39
+
40
+ const hydrated = hydrateChartSpecInstruments(spec, new Map([
41
+ ["SNDK", ticker("SNDK", "NASDAQ")],
42
+ ["SAP", ticker("SAP", "JSE")],
43
+ ]));
44
+
45
+ expect(hydrated.series[0]?.source).toMatchObject({
46
+ kind: "security",
47
+ instrument: { symbol: "SNDK", exchange: "NASDAQ" },
48
+ });
49
+ expect(hydrated.series[1]?.source).toMatchObject({
50
+ kind: "security",
51
+ instrument: { symbol: "SAP", exchange: "XETRA" },
52
+ });
53
+ expect(spec.series[0]?.source).toMatchObject({
54
+ kind: "security",
55
+ instrument: { symbol: "SNDK", exchange: "" },
56
+ });
57
+ expect(hydrateChartSpecInstruments(hydrated, new Map())).toBe(hydrated);
58
+ });
59
+ });