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
@@ -1,6 +1,7 @@
1
1
  import type { DataProvider, QuoteSubscriptionTarget } from "../../types/data-provider";
2
2
  import type { Quote } from "../../types/financials";
3
3
  import type { InstrumentRef } from "../request-types";
4
+ import { mergeQuoteSubscriptionTargets } from "../quote-subscription-target";
4
5
  import { QueryStore } from "../query-store";
5
6
  import type { QueryEntry } from "../result-types";
6
7
  import { buildQuoteKey, toMarketDataContext } from "../selectors";
@@ -61,47 +62,10 @@ const STREAM_QUOTE_FIELDS: Array<keyof Quote> = [
61
62
  "low",
62
63
  "mark",
63
64
  "dataSource",
65
+ "delivery",
66
+ "stale",
64
67
  ];
65
68
 
66
- function quoteSubscriptionPriorityScore(target: QuoteSubscriptionTarget): number {
67
- let score = Number.isFinite(target.weight) ? Math.max(0, target.weight ?? 0) : 0;
68
- if (target.selected) score += 10_000;
69
- if (target.visible) score += 5_000;
70
- if (target.surface === "detail" || target.surface === "monitor") score += 4_000;
71
- if (target.surface === "portfolio" || target.surface === "watchlist") score += 1_000;
72
- if (target.surface === "screener") score += 700;
73
- if (target.surface === "inline") score += 200;
74
- return score;
75
- }
76
-
77
- function mergeQuoteSubscriptionTargets(targets: Iterable<QuoteSubscriptionTarget>): QuoteSubscriptionTarget | null {
78
- let selectedTarget: QuoteSubscriptionTarget | null = null;
79
- let selectedScore = -1;
80
- let visible = false;
81
- let selected = false;
82
- let weight = 0;
83
-
84
- for (const target of targets) {
85
- const score = quoteSubscriptionPriorityScore(target);
86
- if (!selectedTarget || score > selectedScore) {
87
- selectedTarget = target;
88
- selectedScore = score;
89
- }
90
- visible ||= target.visible === true;
91
- selected ||= target.selected === true;
92
- weight = Math.max(weight, Number.isFinite(target.weight) ? Math.max(0, target.weight ?? 0) : 0);
93
- }
94
-
95
- return selectedTarget
96
- ? {
97
- ...selectedTarget,
98
- visible,
99
- selected,
100
- weight,
101
- }
102
- : null;
103
- }
104
-
105
69
  function quoteTargetFromInstrument(
106
70
  instrument: InstrumentRef,
107
71
  priority: QuoteSubscriptionPriority = {},
@@ -289,7 +253,7 @@ export class QuoteSubscriptionManager {
289
253
  if (!this.dataProvider.subscribeQuotes) return;
290
254
 
291
255
  const activeEntries = [...this.quoteSubscriptions.entries()]
292
- .filter(([, entry]) => entry.targets.size > 0 || entry.removeTimer)
256
+ .filter(([, entry]) => entry.targets.size > 0)
293
257
  .sort(([left], [right]) => left.localeCompare(right));
294
258
  const nextSignature = activeEntries.map(([key, entry]) => [
295
259
  key,
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, jest, test } from "bun:test";
2
2
  import { MarketDataCoordinator } from "./index";
3
3
  import { buildQuoteKey } from "../selectors";
4
4
  import { createTestDataProvider } from "../../test-support/data-provider";
@@ -117,6 +117,43 @@ describe("MarketDataCoordinator key subscriptions", () => {
117
117
  });
118
118
  });
119
119
 
120
+ test("replaces a capped target window without retaining pending removals", () => {
121
+ jest.useFakeTimers();
122
+ try {
123
+ const subscriptions: string[][] = [];
124
+ let disposals = 0;
125
+ const provider = createTestDataProvider({
126
+ id: "test-provider",
127
+ subscribeQuotes: (targets) => {
128
+ subscriptions.push(targets.map((target) => target.symbol));
129
+ return () => {
130
+ disposals += 1;
131
+ };
132
+ },
133
+ });
134
+ const coordinator = new MarketDataCoordinator(provider);
135
+ const oldTargets = Array.from({ length: 16 }, (_, index) => ({
136
+ instrument: { symbol: `OPT${index}`, exchange: "OPTIONS" },
137
+ }));
138
+ const newTargets = Array.from({ length: 16 }, (_, index) => ({
139
+ instrument: { symbol: `OPT${index + 2}`, exchange: "OPTIONS" },
140
+ }));
141
+
142
+ const unsubscribeOld = coordinator.subscribeQuotes(oldTargets);
143
+ unsubscribeOld();
144
+ const unsubscribeNew = coordinator.subscribeQuotes(newTargets);
145
+
146
+ expect(disposals).toBe(1);
147
+ expect(subscriptions).toHaveLength(2);
148
+ expect(subscriptions[1]).toEqual(newTargets.map(({ instrument }) => instrument.symbol).sort());
149
+
150
+ unsubscribeNew();
151
+ jest.runAllTimers();
152
+ } finally {
153
+ jest.useRealTimers();
154
+ }
155
+ });
156
+
120
157
  test("notifies listeners for changed keys only", async () => {
121
158
  const { provider, emitQuote } = createProvider();
122
159
  const coordinator = new MarketDataCoordinator(provider);
@@ -207,4 +244,55 @@ describe("MarketDataCoordinator key subscriptions", () => {
207
244
  Date.now = realDateNow;
208
245
  }
209
246
  });
247
+
248
+ test("does not refresh receipt time from an explicitly stale polled quote", async () => {
249
+ const realDateNow = Date.now;
250
+ const { provider, emitQuote } = createProvider();
251
+ const coordinator = new MarketDataCoordinator(provider);
252
+ const option = { symbol: "AAPL260731C00110000", exchange: "OPTIONS" };
253
+ const firstTimestamp = 1_800_000_000_000;
254
+ try {
255
+ coordinator.subscribeQuotes([{ instrument: option }]);
256
+
257
+ Date.now = () => firstTimestamp;
258
+ emitQuote(
259
+ option,
260
+ quote(option.symbol, 2.5, {
261
+ lastUpdated: firstTimestamp,
262
+ dataSource: "live",
263
+ delivery: "stream",
264
+ stale: false,
265
+ }),
266
+ );
267
+ await flushCoordinator();
268
+
269
+ expect(coordinator.getQuoteEntry(option).data).toMatchObject({
270
+ delivery: "stream",
271
+ stale: false,
272
+ receivedAt: firstTimestamp,
273
+ });
274
+
275
+ Date.now = () => firstTimestamp + 10_000;
276
+ emitQuote(
277
+ option,
278
+ quote(option.symbol, 2.5, {
279
+ lastUpdated: firstTimestamp,
280
+ dataSource: "live",
281
+ delivery: "poll",
282
+ stale: true,
283
+ }),
284
+ );
285
+ await flushCoordinator();
286
+
287
+ const entry = coordinator.getQuoteEntry(option);
288
+ expect(entry.data).toBeNull();
289
+ expect(entry.lastGoodData).toMatchObject({
290
+ delivery: "stream",
291
+ stale: false,
292
+ receivedAt: firstTimestamp,
293
+ });
294
+ } finally {
295
+ Date.now = realDateNow;
296
+ }
297
+ });
210
298
  });
@@ -30,6 +30,10 @@ interface ChartQueryOptions {
30
30
  refreshIntervalMs?: number;
31
31
  }
32
32
 
33
+ interface OptionsQueryOptions {
34
+ refreshIntervalMs?: number;
35
+ }
36
+
33
37
  type SharedCoordinator = NonNullable<ReturnType<typeof getSharedMarketDataCoordinator>>;
34
38
 
35
39
  function loadChartRequests(
@@ -180,6 +184,34 @@ export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerR
180
184
  return entry;
181
185
  }
182
186
 
187
+ /**
188
+ * Observe quote entries already populated by the shared stream without issuing
189
+ * one HTTP snapshot request per instrument.
190
+ */
191
+ export function useQuoteEntries(
192
+ instruments: readonly InstrumentRef[],
193
+ ): Map<string, QueryEntry<Quote>> {
194
+ const instrumentKey = instruments
195
+ .map((instrument) => buildQuoteKey(instrument))
196
+ .join("\u001f");
197
+ const keys = useMemo(
198
+ () => instruments.map((instrument) => buildQuoteKey(instrument)),
199
+ [instrumentKey],
200
+ );
201
+ const keysVersion = useCoordinatorKeysVersion(keys);
202
+ const coordinator = getSharedMarketDataCoordinator();
203
+
204
+ return useMemo(() => {
205
+ if (!coordinator) return new Map<string, QueryEntry<Quote>>();
206
+ return new Map(
207
+ instruments.map((instrument) => [
208
+ buildQuoteKey(instrument),
209
+ coordinator.getQuoteEntry(instrument),
210
+ ]),
211
+ );
212
+ }, [coordinator, instrumentKey, keysVersion]);
213
+ }
214
+
183
215
  export function useChartQuery(
184
216
  request: ChartRequest | null | undefined,
185
217
  options: ChartQueryOptions = {},
@@ -275,11 +307,16 @@ export function useChartQueries(
275
307
  return useMemo(() => new Map(entries), [entries]);
276
308
  }
277
309
 
278
- export function useOptionsQuery(request: OptionsRequest | null | undefined): QueryEntry<OptionsChain> | null {
310
+ export function useOptionsQuery(
311
+ request: OptionsRequest | null | undefined,
312
+ options: OptionsQueryOptions = {},
313
+ ): QueryEntry<OptionsChain> | null {
279
314
  const requestKey = request ? buildOptionsKey(request) : null;
280
315
  useCoordinatorKeysVersion(requestKey ? [requestKey] : []);
281
316
  const coordinator = getSharedMarketDataCoordinator();
282
317
  const entry = coordinator && request ? coordinator.getOptionsEntry(request) : null;
318
+ const appActive = useAppActive();
319
+ const refreshIntervalMs = Math.max(0, options.refreshIntervalMs ?? 0);
283
320
 
284
321
  useEffect(() => {
285
322
  const coordinator = getSharedMarketDataCoordinator();
@@ -287,6 +324,15 @@ export function useOptionsQuery(request: OptionsRequest | null | undefined): Que
287
324
  void coordinator.loadOptions(request).catch(() => {});
288
325
  }, [requestKey]);
289
326
 
327
+ useEffect(() => {
328
+ const coordinator = getSharedMarketDataCoordinator();
329
+ if (!coordinator || !request || !appActive || refreshIntervalMs <= 0) return;
330
+ const interval = setInterval(() => {
331
+ void coordinator.loadOptions(request, { forceRefresh: true }).catch(() => {});
332
+ }, refreshIntervalMs);
333
+ return () => clearInterval(interval);
334
+ }, [appActive, refreshIntervalMs, requestKey]);
335
+
290
336
  return entry;
291
337
  }
292
338
 
@@ -1,5 +1,5 @@
1
1
  import type { MarketState, Quote, QuoteFieldProvenance } from "../../types/financials";
2
- import { blendHex, colors, priceColor } from "../../theme/colors";
2
+ import { blendHex, colors, priceColor, type ThemeColors } from "../../theme/colors";
3
3
 
4
4
  const CLOSED_CHANGE_MUTING_RATIO = 0.55;
5
5
 
@@ -20,14 +20,14 @@ export function marketStateLabel(state: MarketState): string {
20
20
  }
21
21
  }
22
22
 
23
- export function marketStateColor(state: MarketState): string {
23
+ export function marketStateColor(state: MarketState, palette: ThemeColors = colors): string {
24
24
  switch (state) {
25
- case "REGULAR": return colors.positive;
25
+ case "REGULAR": return palette.positive;
26
26
  case "PRE":
27
- case "POST": return colors.textBright;
27
+ case "POST": return palette.textBright;
28
28
  case "PREPRE":
29
29
  case "POSTPOST":
30
- case "CLOSED": return colors.textDim;
30
+ case "CLOSED": return palette.textDim;
31
31
  }
32
32
  }
33
33
 
@@ -47,7 +47,7 @@ export function marketStateDot(state?: MarketState): string {
47
47
  }
48
48
  }
49
49
 
50
- export function isClosedMarketState(state?: MarketState): boolean {
50
+ function isClosedMarketState(state?: MarketState): boolean {
51
51
  return state === "CLOSED" || state === "PREPRE" || state === "POSTPOST";
52
52
  }
53
53
 
@@ -27,7 +27,7 @@ function coercePointDate(value: Date | string | number): Date | null {
27
27
  return Number.isFinite(date.getTime()) ? date : null;
28
28
  }
29
29
 
30
- export function normalizePriceReturnHistory(points: readonly PricePoint[]): PricePoint[] {
30
+ function normalizePriceReturnHistory(points: readonly PricePoint[]): PricePoint[] {
31
31
  const byTimestamp = new Map<number, PricePoint>();
32
32
  for (const point of points) {
33
33
  const date = coercePointDate(point.date as Date | string | number);
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mergeQuoteSubscriptionTargets } from "./quote-subscription-target";
3
+
4
+ describe("mergeQuoteSubscriptionTargets", () => {
5
+ test("does not synthesize optional priority fields", () => {
6
+ const merged = mergeQuoteSubscriptionTargets([
7
+ { symbol: "AAPL", exchange: "NASDAQ" },
8
+ { symbol: "AAPL", exchange: "NASDAQ" },
9
+ ]);
10
+
11
+ expect(merged).toEqual({ symbol: "AAPL", exchange: "NASDAQ" });
12
+ expect(merged).not.toHaveProperty("visible");
13
+ expect(merged).not.toHaveProperty("selected");
14
+ expect(merged).not.toHaveProperty("weight");
15
+ });
16
+
17
+ test("combines priority fields that callers explicitly provide", () => {
18
+ const merged = mergeQuoteSubscriptionTargets([
19
+ { symbol: "TSLA", surface: "watchlist" as const, visible: true, selected: false, weight: 5 },
20
+ { symbol: "TSLA", surface: "detail" as const, visible: false, selected: true, weight: 20 },
21
+ ]);
22
+
23
+ expect(merged).toEqual({
24
+ symbol: "TSLA",
25
+ surface: "detail",
26
+ visible: true,
27
+ selected: true,
28
+ weight: 20,
29
+ });
30
+ });
31
+
32
+ test("preserves explicit false and zero values", () => {
33
+ expect(mergeQuoteSubscriptionTargets([
34
+ { symbol: "MSFT", visible: false, selected: false, weight: 0 },
35
+ ])).toEqual({ symbol: "MSFT", visible: false, selected: false, weight: 0 });
36
+ });
37
+ });
@@ -0,0 +1,60 @@
1
+ export interface QuoteSubscriptionPriorityTarget {
2
+ surface?: "portfolio" | "watchlist" | "detail" | "monitor" | "inline" | "options" | "screener" | "unknown";
3
+ visible?: boolean;
4
+ selected?: boolean;
5
+ weight?: number;
6
+ }
7
+
8
+ function quoteSubscriptionPriorityScore(target: QuoteSubscriptionPriorityTarget): number {
9
+ let score = Number.isFinite(target.weight) ? Math.max(0, target.weight ?? 0) : 0;
10
+ if (target.selected) score += 10_000;
11
+ if (target.visible) score += 5_000;
12
+ if (target.surface === "detail" || target.surface === "monitor" || target.surface === "options") score += 4_000;
13
+ if (target.surface === "portfolio" || target.surface === "watchlist") score += 1_000;
14
+ if (target.surface === "screener") score += 700;
15
+ if (target.surface === "inline") score += 200;
16
+ return score;
17
+ }
18
+
19
+ export function mergeQuoteSubscriptionTargets<T extends QuoteSubscriptionPriorityTarget>(
20
+ targets: Iterable<T>,
21
+ ): T | null {
22
+ let selectedTarget: T | null = null;
23
+ let selectedScore = -1;
24
+ let visible = false;
25
+ let selected = false;
26
+ let weight = 0;
27
+ let hasVisible = false;
28
+ let hasSelected = false;
29
+ let hasWeight = false;
30
+
31
+ for (const target of targets) {
32
+ const score = quoteSubscriptionPriorityScore(target);
33
+ if (!selectedTarget || score > selectedScore) {
34
+ selectedTarget = target;
35
+ selectedScore = score;
36
+ }
37
+ if (target.visible !== undefined) {
38
+ hasVisible = true;
39
+ visible ||= target.visible;
40
+ }
41
+ if (target.selected !== undefined) {
42
+ hasSelected = true;
43
+ selected ||= target.selected;
44
+ }
45
+ if (Number.isFinite(target.weight)) {
46
+ hasWeight = true;
47
+ weight = Math.max(weight, Math.max(0, target.weight ?? 0));
48
+ }
49
+ }
50
+
51
+ if (!selectedTarget) return null;
52
+ const merged = { ...selectedTarget };
53
+ if (hasVisible) merged.visible = visible;
54
+ else delete merged.visible;
55
+ if (hasSelected) merged.selected = selected;
56
+ else delete merged.selected;
57
+ if (hasWeight) merged.weight = weight;
58
+ else delete merged.weight;
59
+ return merged;
60
+ }
@@ -21,6 +21,7 @@ function isQuoteMissingActiveSessionPrice(quote: Quote): boolean {
21
21
 
22
22
  export function isQuoteStaleForCurrentSession(quote: Quote | null | undefined, now = Date.now()): boolean {
23
23
  if (!quote) return false;
24
+ if (quote.stale === true) return true;
24
25
  if (isQuoteMissingActiveSessionPrice(quote)) return true;
25
26
 
26
27
  return isTimestampStaleForExchangeSession(
@@ -41,6 +41,8 @@ const PRICE_FIELD_KEYS = [
41
41
  "mark",
42
42
  "lastUpdated",
43
43
  "receivedAt",
44
+ "delivery",
45
+ "stale",
44
46
  ] as const;
45
47
 
46
48
  const SESSION_FIELD_KEYS = [
@@ -441,6 +443,8 @@ export function resolveCanonicalQuote(
441
443
  name: resolved.name as Quote["name"],
442
444
  lastUpdated: Number(resolved.lastUpdated ?? priceProvider?.lastUpdated ?? sessionProvider?.lastUpdated ?? now),
443
445
  receivedAt: resolved.receivedAt as Quote["receivedAt"],
446
+ delivery: resolved.delivery as Quote["delivery"],
447
+ stale: resolved.stale as Quote["stale"],
444
448
  exchangeName: resolved.exchangeName as Quote["exchangeName"],
445
449
  fullExchangeName: resolved.fullExchangeName as Quote["fullExchangeName"],
446
450
  listingExchangeName: resolved.listingExchangeName as Quote["listingExchangeName"],
@@ -1,5 +1,5 @@
1
- import type { TimeRange } from "../components/chart/core/types";
2
- import type { ManualChartResolution } from "../components/chart/core/resolution";
1
+ import type { TimeRange } from "../time-series/range";
2
+ import type { ManualChartResolution } from "../time-series/resolution";
3
3
  import type { QuoteSubscriptionTarget } from "../types/data-provider";
4
4
  import type { BrokerContractRef } from "../types/instrument";
5
5
  import type { TickerRecord } from "../types/ticker";
@@ -95,7 +95,7 @@ export function quoteSubscriptionTargetFromTicker(
95
95
  return quoteSubscriptionTargetFromInstrument(instrument, route);
96
96
  }
97
97
 
98
- export function quoteSubscriptionTargetFromInstrument(
98
+ function quoteSubscriptionTargetFromInstrument(
99
99
  instrument: InstrumentRef | null | undefined,
100
100
  route: QuoteSubscriptionTarget["route"] = "auto",
101
101
  ): QuoteSubscriptionTarget | null {
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, mock } from "bun:test";
1
+ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
2
2
  import { NewsService } from "./aggregator";
3
3
  import { newsProvider, type NewsCapability } from "../capabilities";
4
4
  import type { MarketNewsItem } from "../types/news-source";
@@ -69,6 +69,10 @@ describe("NewsService", () => {
69
69
  agg = new NewsService();
70
70
  });
71
71
 
72
+ afterEach(() => {
73
+ agg.stop();
74
+ });
75
+
72
76
  it("deduplicates by URL, keeping higher importance", async () => {
73
77
  const low = makeItem({ url: "https://example.com/1", importance: 40 });
74
78
  const high = makeItem({ url: "https://example.com/1", importance: 80 });
@@ -224,6 +228,98 @@ describe("NewsService", () => {
224
228
  expect(states).toHaveLength(callCount);
225
229
  });
226
230
 
231
+ it("polls referenced queries only and stops after the last watcher leaves", async () => {
232
+ const query = { feed: "latest" as const, limit: 20 };
233
+ let fetchCount = 0;
234
+ const source = newsProvider({
235
+ id: "poll-lifecycle",
236
+ name: "poll-lifecycle",
237
+ provider: {
238
+ fetchNews: async () => {
239
+ fetchCount++;
240
+ return [];
241
+ },
242
+ },
243
+ });
244
+ agg.register(source);
245
+ await agg.load(query);
246
+ expect(fetchCount).toBe(1);
247
+
248
+ agg.start();
249
+ agg.register(makeSource("inactive-trigger", []));
250
+ await new Promise((resolve) => setTimeout(resolve, 0));
251
+ expect(fetchCount).toBe(1);
252
+
253
+ const dispose = agg.watchQuery(query, () => {});
254
+ await new Promise((resolve) => setTimeout(resolve, 0));
255
+ expect(fetchCount).toBe(2);
256
+
257
+ const disposeSecond = agg.watchQuery(query, () => {});
258
+ await new Promise((resolve) => setTimeout(resolve, 0));
259
+ expect(fetchCount).toBe(3);
260
+
261
+ agg.register(makeSource("active-trigger", []));
262
+ await new Promise((resolve) => setTimeout(resolve, 0));
263
+ expect(fetchCount).toBe(4);
264
+
265
+ dispose();
266
+ agg.register(makeSource("single-watcher-trigger", []));
267
+ await new Promise((resolve) => setTimeout(resolve, 0));
268
+ expect(fetchCount).toBe(5);
269
+
270
+ disposeSecond();
271
+ agg.register(makeSource("disposed-trigger", []));
272
+ await new Promise((resolve) => setTimeout(resolve, 0));
273
+ expect(fetchCount).toBe(5);
274
+ });
275
+
276
+ it("retains inactive query state for remounts", async () => {
277
+ const item = makeItem({ url: "https://remount.example.com/1" });
278
+ const query = { feed: "latest" as const, limit: 20 };
279
+ agg.register(makeSource("remount", [item]));
280
+ await agg.load(query);
281
+
282
+ const firstStates: string[][] = [];
283
+ const dispose = agg.watchQuery(query, (state) => {
284
+ firstStates.push(state.articles.map((article) => article.url));
285
+ });
286
+ dispose();
287
+
288
+ const remountedStates: string[][] = [];
289
+ const disposeRemount = agg.watchQuery(query, (state) => {
290
+ remountedStates.push(state.articles.map((article) => article.url));
291
+ });
292
+
293
+ expect(firstStates[0]).toEqual([item.url]);
294
+ expect(remountedStates[0]).toEqual([item.url]);
295
+ disposeRemount();
296
+ });
297
+
298
+ it("bounds inactive query state by LRU and TTL", async () => {
299
+ let now = 0;
300
+ agg = new NewsService({
301
+ inactiveQueryTtlMs: 10,
302
+ maxInactiveQueries: 2,
303
+ now: () => now,
304
+ });
305
+ agg.register(makeSource("bounded", [makeItem({ url: "https://bounded.example.com/1" })]));
306
+ const first = { feed: "latest" as const, topics: ["first"], limit: 20 };
307
+ const second = { feed: "latest" as const, topics: ["second"], limit: 20 };
308
+ const third = { feed: "latest" as const, topics: ["third"], limit: 20 };
309
+
310
+ await agg.load(first);
311
+ now = 1;
312
+ await agg.load(second);
313
+ now = 2;
314
+ await agg.load(third);
315
+
316
+ expect(agg.getQueryState(first).phase).toBe("idle");
317
+ expect(agg.getQueryState(third).phase).toBe("ready");
318
+
319
+ now = 20;
320
+ expect(agg.getQueryState(third).phase).toBe("idle");
321
+ });
322
+
227
323
  it("seeds cached source items immediately on register", () => {
228
324
  const cached = makeItem({ url: "https://cached.example.com/1", importance: 70 });
229
325
  let callCount = 0;