gloomberb 0.17.4 → 0.17.6

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 (384) hide show
  1. package/package.json +3 -1
  2. package/patches/electrobun@1.18.1.patch +80 -0
  3. package/src/api-client/chat.ts +9 -0
  4. package/src/api-client/commodity-balances.ts +77 -0
  5. package/src/api-client/data.ts +35 -3
  6. package/src/api-client/earnings.ts +10 -0
  7. package/src/api-client/index.ts +5 -0
  8. package/src/api-client/insider-signals.ts +75 -0
  9. package/src/api-client/market-discovery.ts +1 -1
  10. package/src/api-client/normalizers.ts +1 -0
  11. package/src/api-client/paths.ts +9 -0
  12. package/src/api-client/research-activity.ts +10 -3
  13. package/src/api-client/socket.ts +10 -0
  14. package/src/api-client/types.ts +25 -1
  15. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
  16. package/src/app/runtime/broker-import.ts +23 -21
  17. package/src/app/runtime/plugin-bindings.ts +13 -7
  18. package/src/app/runtime/update.ts +64 -4
  19. package/src/app.tsx +2 -0
  20. package/src/architecture/core-builtin-allowlist.json +2 -10
  21. package/src/ask-gloom/pane-context.ts +11 -1
  22. package/src/brokers/broker-portfolio-sync.ts +66 -0
  23. package/src/brokers/sync-broker-instance.ts +75 -41
  24. package/src/cli/commands/market.ts +101 -34
  25. package/src/cli/commands/options-chain.ts +20 -6
  26. package/src/cli/commands/overview.ts +54 -13
  27. package/src/cli/commands/search.ts +1 -1
  28. package/src/cli/commands/system.ts +19 -1
  29. package/src/cli/commands/ticker.ts +24 -4
  30. package/src/cli/context.ts +13 -0
  31. package/src/cli/errors.ts +15 -2
  32. package/src/cli/fx-pair.ts +77 -0
  33. package/src/cli/glossary.ts +34 -4
  34. package/src/cli/history-rows.ts +25 -11
  35. package/src/cli/index.ts +30 -12
  36. package/src/cli/listing-arg.ts +61 -138
  37. package/src/cli/listing-suggestion.ts +3 -21
  38. package/src/cli/pane-functions/catalog.ts +50 -0
  39. package/src/cli/pane-functions/freshness.ts +44 -9
  40. package/src/cli/pane-functions/function-help.ts +9 -9
  41. package/src/cli/pane-functions/headless.ts +19 -7
  42. package/src/cli/pane-functions/index.ts +63 -5
  43. package/src/cli/pane-functions/market-session.ts +18 -3
  44. package/src/cli/pane-functions/resolver.ts +29 -8
  45. package/src/cli/pane-functions/screenshot.ts +2 -0
  46. package/src/cli/report-tables.ts +11 -4
  47. package/src/cli/result.ts +2 -0
  48. package/src/cli/series-search.ts +6 -1
  49. package/src/cli/ticker-resolution.ts +1 -2
  50. package/src/cli/unknown-command.ts +18 -0
  51. package/src/components/chart/composite/composite-chart.tsx +80 -19
  52. package/src/components/chart/composite/format.ts +17 -0
  53. package/src/components/chart/composite/legend.tsx +26 -15
  54. package/src/components/chart/composite/panel-labels.ts +38 -0
  55. package/src/components/chart/composite/panel-series.ts +1 -0
  56. package/src/components/chart/composite/panel-surface.tsx +25 -7
  57. package/src/components/chart/composite/price-axis-labels.tsx +16 -3
  58. package/src/components/chart/composite/scene.ts +60 -2
  59. package/src/components/chart/composite/series-axis-tags.tsx +67 -0
  60. package/src/components/chart/composite/tools.ts +2 -2
  61. package/src/components/chart/composite/types.ts +28 -0
  62. package/src/components/chart/core/view-reset.ts +46 -0
  63. package/src/components/command-bar/commands/collection.ts +1 -1
  64. package/src/components/command-bar/commands/direct/index.ts +48 -1
  65. package/src/components/command-bar/commands/registry.ts +8 -0
  66. package/src/components/command-bar/list/model.ts +7 -0
  67. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  68. package/src/components/command-bar/panel/prompt-binding.ts +8 -0
  69. package/src/components/command-bar/routes/root/command-items.ts +17 -2
  70. package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
  71. package/src/components/command-bar/routes/root/provider-search.ts +6 -0
  72. package/src/components/command-bar/routes/root/runtime.ts +3 -0
  73. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
  74. package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
  75. package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
  76. package/src/components/command-bar/surface/index.tsx +7 -1
  77. package/src/components/command-bar/workflow/collection-actions.ts +6 -5
  78. package/src/components/command-bar/workflow/ops.ts +8 -5
  79. package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
  80. package/src/components/command-bar/workflow/tickers.ts +1 -1
  81. package/src/components/form-modal/confirm.tsx +1 -0
  82. package/src/components/form-modal/request.ts +2 -0
  83. package/src/components/layout/detached-pane-shell.tsx +6 -1
  84. package/src/components/layout/pane/content.tsx +11 -8
  85. package/src/components/layout/pane/sidebar.tsx +24 -11
  86. package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
  87. package/src/components/layout/shell/index.tsx +5 -1
  88. package/src/components/layout/shell/menu.ts +8 -0
  89. package/src/components/layout/shell/pane/capture.ts +29 -12
  90. package/src/components/onboarding/wizard-model.ts +1 -1
  91. package/src/components/onboarding/wizard-positions.ts +18 -10
  92. package/src/components/ticker/badge/format.ts +64 -15
  93. package/src/components/ticker/badge/index.tsx +32 -11
  94. package/src/components/ticker/badge/list.tsx +4 -3
  95. package/src/components/ticker/badge/text.tsx +2 -2
  96. package/src/components/ticker/list-table-view.tsx +8 -1
  97. package/src/components/ui/confirm-dialog.tsx +30 -4
  98. package/src/components/ui/data-table/opentui/index.tsx +61 -20
  99. package/src/components/ui/data-table/opentui/model.ts +104 -0
  100. package/src/components/ui/fields.tsx +5 -0
  101. package/src/components/ui/inline-quick-add.tsx +4 -1
  102. package/src/components/ui/query-bar-keys.tsx +29 -5
  103. package/src/components/ui/query-bar.tsx +29 -4
  104. package/src/components/ui/windowed-list.tsx +27 -0
  105. package/src/data/config/store/normalize.ts +8 -0
  106. package/src/docs/snapshot.json +248 -5
  107. package/src/i18n/es.ts +3 -1
  108. package/src/i18n/ja.ts +3 -1
  109. package/src/i18n/ko.ts +3 -1
  110. package/src/i18n/zh-cn.ts +3 -1
  111. package/src/i18n/zh-tw.ts +3 -1
  112. package/src/layout-marketplace/cloud.ts +16 -5
  113. package/src/layout-marketplace/gallery.tsx +4 -4
  114. package/src/layout-marketplace/linked-sync.ts +9 -3
  115. package/src/layout-marketplace/linked.ts +24 -3
  116. package/src/layout-marketplace/model.ts +39 -4
  117. package/src/market-data/coordinator/events.ts +5 -1
  118. package/src/market-data/coordinator/index.ts +38 -1
  119. package/src/market-data/coordinator/quotes.ts +4 -0
  120. package/src/market-data/futures-contract.ts +53 -0
  121. package/src/market-data/hooks.tsx +225 -30
  122. package/src/market-data/market/bar-period.ts +71 -0
  123. package/src/market-data/market/format.ts +14 -2
  124. package/src/market-data/market/freshness.ts +28 -1
  125. package/src/market-data/market/status.ts +152 -13
  126. package/src/market-data/model-dividend-yield.ts +76 -0
  127. package/src/market-data/quotes/freshness.ts +30 -1
  128. package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
  129. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  130. package/src/plugins/builtin/analytics/metrics.ts +8 -4
  131. package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
  132. package/src/plugins/builtin/analytics/risk-model.ts +74 -15
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
  134. package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
  135. package/src/plugins/builtin/broker-manager/model.ts +2 -2
  136. package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
  137. package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
  138. package/src/plugins/builtin/chart-composer/formula.ts +233 -0
  139. package/src/plugins/builtin/chart-composer/headless.ts +27 -2
  140. package/src/plugins/builtin/chart-composer/index.tsx +3 -2
  141. package/src/plugins/builtin/chart-composer/pane.tsx +41 -10
  142. package/src/plugins/builtin/chart-composer/presets.ts +91 -2
  143. package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
  144. package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
  145. package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
  146. package/src/plugins/builtin/chart-composer/settings.ts +16 -4
  147. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  148. package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
  149. package/src/plugins/builtin/chat/content/footer.tsx +5 -2
  150. package/src/plugins/builtin/chat/content/index.tsx +12 -5
  151. package/src/plugins/builtin/chat/content/mentions.ts +50 -21
  152. package/src/plugins/builtin/chat/content/scroll.ts +12 -5
  153. package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
  154. package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
  155. package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
  156. package/src/plugins/builtin/chat/controller/index.ts +88 -17
  157. package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
  158. package/src/plugins/builtin/chat/controller/messages.ts +14 -2
  159. package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
  160. package/src/plugins/builtin/chat/controller/state.ts +25 -0
  161. package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
  162. package/src/plugins/builtin/chat/controller/view.ts +4 -0
  163. package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
  164. package/src/plugins/builtin/chat/conversations.ts +12 -5
  165. package/src/plugins/builtin/chat/index.tsx +1 -1
  166. package/src/plugins/builtin/chat/layout.ts +23 -5
  167. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  168. package/src/plugins/builtin/chat/share/flow.tsx +18 -9
  169. package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
  170. package/src/plugins/builtin/chat/sidebar.tsx +6 -1
  171. package/src/plugins/builtin/chat/unread-badge.ts +38 -7
  172. package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
  173. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
  174. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
  175. package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
  176. package/src/plugins/builtin/commodity-balances/client.ts +51 -193
  177. package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
  178. package/src/plugins/builtin/commodity-balances/model.ts +5 -180
  179. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
  180. package/src/plugins/builtin/custom-view/index.tsx +1 -1
  181. package/src/plugins/builtin/custom-view/loader.ts +57 -7
  182. package/src/plugins/builtin/custom-view/sources.ts +146 -0
  183. package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
  184. package/src/plugins/builtin/earnings/board-model.ts +33 -3
  185. package/src/plugins/builtin/earnings/board-table.ts +59 -14
  186. package/src/plugins/builtin/earnings/board.tsx +36 -3
  187. package/src/plugins/builtin/earnings/headless.ts +24 -5
  188. package/src/plugins/builtin/earnings/history-model.ts +9 -1
  189. package/src/plugins/builtin/earnings/history-table.ts +16 -5
  190. package/src/plugins/builtin/earnings/history.tsx +40 -10
  191. package/src/plugins/builtin/earnings/index.tsx +3 -1
  192. package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
  193. package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
  194. package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
  195. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  196. package/src/plugins/builtin/econ-statistics/view.ts +4 -0
  197. package/src/plugins/builtin/futures/model.ts +5 -4
  198. package/src/plugins/builtin/futures/series.ts +38 -1
  199. package/src/plugins/builtin/futures-curve/client.ts +4 -4
  200. package/src/plugins/builtin/futures-curve/headless.ts +33 -8
  201. package/src/plugins/builtin/futures-curve/index.tsx +4 -4
  202. package/src/plugins/builtin/futures-curve/model.ts +109 -6
  203. package/src/plugins/builtin/futures-curve/pane.tsx +24 -13
  204. package/src/plugins/builtin/insider/headless.ts +74 -4
  205. package/src/plugins/builtin/insider/index.tsx +99 -5
  206. package/src/plugins/builtin/insider/signals-client.ts +69 -0
  207. package/src/plugins/builtin/insider/signals-model.ts +146 -0
  208. package/src/plugins/builtin/insider/signals.tsx +176 -0
  209. package/src/plugins/builtin/iv-history/client.ts +41 -2
  210. package/src/plugins/builtin/iv-history/headless.ts +24 -16
  211. package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
  212. package/src/plugins/builtin/jodi/client.ts +42 -205
  213. package/src/plugins/builtin/jodi/headless.ts +13 -4
  214. package/src/plugins/builtin/jodi/model.ts +18 -43
  215. package/src/plugins/builtin/jodi/pane.tsx +5 -0
  216. package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
  217. package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
  218. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  219. package/src/plugins/builtin/macro-day/release-table.ts +3 -3
  220. package/src/plugins/builtin/market-movers/client.ts +32 -6
  221. package/src/plugins/builtin/market-movers/headless.ts +7 -2
  222. package/src/plugins/builtin/market-movers/model.ts +64 -3
  223. package/src/plugins/builtin/market-movers/screener.ts +20 -9
  224. package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
  225. package/src/plugins/builtin/market-valuation/headless.ts +17 -4
  226. package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
  227. package/src/plugins/builtin/market-valuation/view.ts +10 -0
  228. package/src/plugins/builtin/mna/model.ts +2 -1
  229. package/src/plugins/builtin/options-positioning/client.ts +10 -0
  230. package/src/plugins/builtin/options-positioning/headless.ts +88 -13
  231. package/src/plugins/builtin/options-positioning/index.tsx +2 -2
  232. package/src/plugins/builtin/options-positioning/model.ts +47 -0
  233. package/src/plugins/builtin/options-positioning/pane.tsx +55 -26
  234. package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
  235. package/src/plugins/builtin/options-scenario/client.ts +111 -17
  236. package/src/plugins/builtin/options-scenario/collar.ts +141 -0
  237. package/src/plugins/builtin/options-scenario/csp.ts +1 -1
  238. package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
  239. package/src/plugins/builtin/options-scenario/headless.ts +28 -12
  240. package/src/plugins/builtin/options-scenario/index.tsx +1 -1
  241. package/src/plugins/builtin/options-scenario/model.ts +55 -5
  242. package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
  243. package/src/plugins/builtin/perps/client.ts +77 -6
  244. package/src/plugins/builtin/perps/headless.ts +25 -14
  245. package/src/plugins/builtin/perps/index.tsx +1 -1
  246. package/src/plugins/builtin/perps/model.ts +32 -0
  247. package/src/plugins/builtin/perps/pane.tsx +5 -4
  248. package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
  249. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
  250. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  251. package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
  252. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
  253. package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
  254. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  255. package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
  256. package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
  257. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  258. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
  259. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  260. package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
  261. package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
  262. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  263. package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
  264. package/src/plugins/builtin/rate-path/model.ts +1 -1
  265. package/src/plugins/builtin/rate-path/pane.tsx +2 -1
  266. package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
  267. package/src/plugins/builtin/research/event-model.ts +41 -49
  268. package/src/plugins/builtin/research/event-sources.ts +1 -1
  269. package/src/plugins/builtin/research/headless.ts +1 -1
  270. package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
  271. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
  272. package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
  273. package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
  274. package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
  275. package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
  276. package/src/plugins/builtin/sectors/client.ts +46 -19
  277. package/src/plugins/builtin/sectors/sector-model.ts +22 -10
  278. package/src/plugins/builtin/shared/earnings-report.ts +48 -1
  279. package/src/plugins/builtin/shared/history-span.ts +55 -0
  280. package/src/plugins/builtin/shared/report-freshness.ts +20 -11
  281. package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
  282. package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
  283. package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
  284. package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
  285. package/src/plugins/builtin/themes/headless.ts +15 -2
  286. package/src/plugins/builtin/thirteenf/api.ts +60 -43
  287. package/src/plugins/builtin/thirteenf/data.ts +76 -35
  288. package/src/plugins/builtin/thirteenf/headless.ts +2 -0
  289. package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
  290. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
  291. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
  292. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
  293. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
  294. package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
  295. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  296. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  297. package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
  298. package/src/plugins/builtin/yield-curve/curves.ts +15 -1
  299. package/src/plugins/builtin/yield-curve/headless.ts +17 -7
  300. package/src/plugins/builtin/yield-curve/history.ts +2 -0
  301. package/src/plugins/builtin/yield-curve/index.tsx +23 -18
  302. package/src/plugins/builtin/yield-curve/series.ts +19 -9
  303. package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
  304. package/src/plugins/builtin/yield-curve/world.ts +4 -0
  305. package/src/plugins/registry/host-actions.ts +3 -0
  306. package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
  307. package/src/portfolio/valuation-basis.ts +4 -21
  308. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  309. package/src/remote/controller.ts +27 -7
  310. package/src/remote/schema.ts +25 -9
  311. package/src/renderers/browser/app-services.ts +7 -1
  312. package/src/renderers/browser/config-host.ts +55 -2
  313. package/src/renderers/browser/first-screen.ts +193 -0
  314. package/src/renderers/browser/main.tsx +59 -7
  315. package/src/renderers/cloudflare/http-proxy.ts +21 -13
  316. package/src/renderers/dom/desktop/controls.tsx +2 -0
  317. package/src/renderers/dom/desktop/query-bar.tsx +1 -1
  318. package/src/renderers/dom/dom-ui-host.tsx +2 -0
  319. package/src/renderers/dom/windowed-list.tsx +201 -0
  320. package/src/renderers/dom/windowed-rows.ts +65 -0
  321. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
  322. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -7
  323. package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
  324. package/src/renderers/electrobun/bun/desktop/update.ts +132 -14
  325. package/src/renderers/electrobun/shared/protocol.ts +7 -4
  326. package/src/renderers/electrobun/view/http-fetch.ts +12 -1
  327. package/src/renderers/electrobun/view/update-host.ts +2 -2
  328. package/src/shares/bug-report-dialog.tsx +162 -0
  329. package/src/shares/bug-report-flow.tsx +84 -0
  330. package/src/shares/bug-report.ts +42 -0
  331. package/src/shares/capture-flow.ts +19 -0
  332. package/src/shares/capture-preview.tsx +129 -0
  333. package/src/shares/chat.ts +10 -1
  334. package/src/shares/dialog.tsx +16 -91
  335. package/src/shares/flow.tsx +12 -16
  336. package/src/shares/personal-pane.ts +35 -1
  337. package/src/sources/listing-symbols.ts +8 -4
  338. package/src/state/app/context/index.tsx +13 -0
  339. package/src/state/app/tab-cursor.ts +85 -0
  340. package/src/sync/core-contributors.ts +206 -59
  341. package/src/telemetry/usage-counts.ts +16 -0
  342. package/src/test-support/config-store.ts +31 -0
  343. package/src/test-support/moved-symbols.ts +64 -0
  344. package/src/theme/colors.ts +30 -0
  345. package/src/tickers/listing-fate.ts +242 -0
  346. package/src/tickers/open-target.ts +61 -11
  347. package/src/tickers/search/index.ts +45 -3
  348. package/src/tickers/search/ranking.ts +19 -2
  349. package/src/tickers/search/types.ts +3 -0
  350. package/src/tickers/search/upsert.ts +22 -7
  351. package/src/tickers/ticker-changes.ts +26 -9
  352. package/src/time-series/bar-spacing.ts +19 -0
  353. package/src/time-series/chart-data.ts +37 -13
  354. package/src/time-series/field-catalog.ts +5 -2
  355. package/src/time-series/forward-valuation.ts +10 -7
  356. package/src/time-series/fundamentals.ts +49 -11
  357. package/src/time-series/hooks.ts +2 -0
  358. package/src/time-series/live-bars.ts +3 -1
  359. package/src/time-series/parsed-history-cache.ts +2 -0
  360. package/src/time-series/price-units.ts +69 -0
  361. package/src/time-series/resolve.ts +71 -11
  362. package/src/time-series/session-bar.ts +138 -0
  363. package/src/time-series/spec.ts +20 -3
  364. package/src/time-series/studies.ts +211 -12
  365. package/src/time-series/transforms.ts +2 -0
  366. package/src/time-series/types.ts +11 -1
  367. package/src/time-series/valuation-price.ts +13 -0
  368. package/src/types/config.ts +24 -1
  369. package/src/types/financials.ts +2 -0
  370. package/src/types/headless.ts +27 -0
  371. package/src/types/plugin.ts +7 -0
  372. package/src/ui/host.tsx +5 -0
  373. package/src/updater/channel.ts +51 -0
  374. package/src/updater/github-releases.ts +85 -16
  375. package/src/updater/index.ts +128 -17
  376. package/src/utils/asset-category.ts +20 -0
  377. package/src/utils/datetime-format.ts +7 -1
  378. package/src/utils/decimal-comma.ts +83 -3
  379. package/src/utils/fundamentals.ts +7 -2
  380. package/src/utils/http-proxy-response.ts +66 -9
  381. package/src/utils/plugin-proxy-hosts.json +1 -5
  382. package/src/utils/semver.ts +47 -2
  383. package/src/utils/typo-distance.ts +22 -0
  384. package/src/plugins/builtin/shared/zip-text.ts +0 -46
@@ -1,11 +1,17 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { Box, ScrollBox, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
2
+ import { Box, ScrollBox, Span, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
3
3
  import { colors as themeColors, hoverBg } from "../../../theme/colors";
4
4
  import { CANONICAL_EXCHANGE_ALIASES } from "../../../utils/exchanges";
5
5
  import { displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
6
6
  import type { ResolvedSeries } from "../../../time-series/types";
7
7
  import { consumeChartMouseEvent, type ChartMouseEvent } from "../core/pointer";
8
- import { formatCompositeCarriedStamp, formatCompositePointDetails, formatCompositeSeriesValue, seriesPriceReference } from "./format";
8
+ import {
9
+ formatCompositeCarriedStamp,
10
+ formatCompositeDatasetDate,
11
+ formatCompositePointDetails,
12
+ formatCompositeSeriesValue,
13
+ seriesPriceReference,
14
+ } from "./format";
9
15
  import type { CompositeChartProps, CompositeChartScene } from "./types";
10
16
  import { seriesCueCssBackground, type SeriesLineCue } from "./series-cues";
11
17
 
@@ -45,15 +51,6 @@ function axisPriceReference(scene: CompositeChartScene | null, series: ResolvedS
45
51
  return own;
46
52
  }
47
53
 
48
- const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
49
-
50
- /** `Oct 4`, the axis's own wording, with the year when the chart ends in another. */
51
- function legendDateText(date: Date, endTime: number | undefined): string {
52
- const text = `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
53
- const endYear = endTime === undefined ? null : new Date(endTime).getUTCFullYear();
54
- return endYear === null || endYear === date.getUTCFullYear() ? text : `${text} ${date.getUTCFullYear()}`;
55
- }
56
-
57
54
  /**
58
55
  * A dataset's values arrive days late, so the legend dates them. Idle, one
59
56
  * date leads the legend when it is every value's; otherwise, and under the
@@ -72,7 +69,7 @@ function legendValueDates(
72
69
  valued += 1;
73
70
  if (entry.observationKind === "dataset" && cursorValue.point) dates.set(entry.id, cursorValue.point.date);
74
71
  }
75
- const text = new Map([...dates].map(([id, date]) => [id, legendDateText(date, scene?.endTime)] as const));
72
+ const text = new Map([...dates].map(([id, date]) => [id, formatCompositeDatasetDate(date, scene?.endTime)] as const));
76
73
  const cursorDate = scene?.cursorDate;
77
74
  if (cursorDate) {
78
75
  const cursorDay = cursorDate.toISOString().slice(0, 10);
@@ -191,6 +188,9 @@ export function CompositeLegend({
191
188
  const ownDate = valueDates.own.get(entry.id);
192
189
  // Carried from an earlier observation: another venue's closed session, a coarser series.
193
190
  const carried = !!scene?.cursorDate && !!cursorValue?.carried && cursorValue.value != null;
191
+ // What a futures price counts, beside its number as FUT prints it.
192
+ const statedUnit = entry.points.length > 0 && !formatValue && entry.statedUnit ? ` ${entry.statedUnit}` : "";
193
+ const valueTail = `${ownDate ? ` ${ownDate}` : ""}${changeText}`;
194
194
  const valueText = entry.points.length === 0
195
195
  ? entry.hidden ? "" : "no data"
196
196
  : `${formatLegendSeriesValue(
@@ -198,7 +198,7 @@ export function CompositeLegend({
198
198
  cursorValue?.value ?? null,
199
199
  formatValue,
200
200
  scene,
201
- )}${ownDate ? ` ${ownDate}` : ""}${changeText}`;
201
+ )}${statedUnit}${valueTail}`;
202
202
  const fullText = [entry.label, valueText].filter(Boolean).join(" ");
203
203
  const details = formatCompositePointDetails(cursorValue?.point);
204
204
  const exactTotal = entry.unitGroup.split(":")[0] === "currency-total"
@@ -220,6 +220,8 @@ export function CompositeLegend({
220
220
  width: Math.max(1, displayWidth(text)) + markerWidth,
221
221
  labelWidth,
222
222
  valueText,
223
+ statedUnit,
224
+ valueTail,
223
225
  toggleable,
224
226
  tooltip,
225
227
  carried,
@@ -361,8 +363,9 @@ export function CompositeLegend({
361
363
  data-gloom-role="composite-chart-legend-scroll"
362
364
  >
363
365
  <Box flexDirection="row" width={desiredSeriesWidth} height={1} gap={1}>
364
- {entries.map(({ entry, cue, marker, text, toggleable, tooltip, carried, width: entryWidth }, index) => {
366
+ {entries.map(({ entry, cue, marker, text, statedUnit, valueTail, toggleable, tooltip, carried, width: entryWidth }, index) => {
365
367
  const entryVisible = visibleSeriesIds.has(entry.id);
368
+ const unitAt = statedUnit ? text.length - statedUnit.length - valueTail.length : -1;
366
369
  return (
367
370
  <Box
368
371
  key={entry.id}
@@ -396,7 +399,15 @@ export function CompositeLegend({
396
399
  )}
397
400
  {/* The filled/hollow marker already says whether a series is
398
401
  shown; the word only repeated it in every legend slot. */}
399
- <Text fg={entryVisible && !carried ? themeColors.text : themeColors.textDim}>{text}</Text>
402
+ <Text fg={entryVisible && !carried ? themeColors.text : themeColors.textDim}>
403
+ {unitAt >= 0 ? (
404
+ <>
405
+ {text.slice(0, unitAt)}
406
+ <Span fg={themeColors.textDim}>{statedUnit}</Span>
407
+ {valueTail}
408
+ </>
409
+ ) : text}
410
+ </Text>
400
411
  </Box>
401
412
  );
402
413
  })}
@@ -39,6 +39,44 @@ export function resolvePanelCrosshair(
39
39
  };
40
40
  }
41
41
 
42
+ export interface SeriesAxisTag {
43
+ seriesId: string;
44
+ seriesLabel: string;
45
+ side: "left" | "right";
46
+ value: number;
47
+ label: string;
48
+ color: string;
49
+ yRatio: number;
50
+ carried: boolean;
51
+ }
52
+
53
+ /** Each observation belongs to its own axis, never to the pointer's height. */
54
+ export function resolveSeriesAxisTags(
55
+ panel: CompositePanelScene,
56
+ scene: Pick<CompositeChartScene, "cursorXRatio" | "cursorValues">,
57
+ format?: CompositeAxisValueFormatter,
58
+ ): SeriesAxisTag[] {
59
+ if (scene.cursorXRatio === null) return [];
60
+ const values = new Map(scene.cursorValues.map((entry) => [entry.seriesId, entry]));
61
+ return panel.series.flatMap(({ source }) => {
62
+ const cursor = values.get(source.id);
63
+ const domain = panel.axes[source.axis];
64
+ if (source.hidden || !domain || cursor?.value == null) return [];
65
+ const yRatio = projectCompositeValue(cursor.value, domain);
66
+ if (yRatio === null) return [];
67
+ return [{
68
+ seriesId: source.id,
69
+ seriesLabel: source.label,
70
+ side: source.axis,
71
+ value: cursor.value,
72
+ label: (format ?? formatCompositeCursorValue)(cursor.value, domain),
73
+ color: source.color,
74
+ yRatio,
75
+ carried: !!cursor.carried,
76
+ }];
77
+ });
78
+ }
79
+
42
80
  export function axisLabelRows(lines: string[]): ReadonlyMap<number, string> {
43
81
  return new Map(lines.flatMap((line, row) => {
44
82
  const label = line.trim();
@@ -28,6 +28,7 @@ function sameSeriesMeta(left: ResolvedSeries, right: ResolvedSeries): boolean {
28
28
  && left.color === right.color
29
29
  && left.unit === right.unit
30
30
  && left.unitGroup === right.unitGroup
31
+ && left.statedUnit === right.statedUnit
31
32
  && left.priceAssetCategory === right.priceAssetCategory
32
33
  && left.nativeFrequency === right.nativeFrequency
33
34
  && left.timestampMode === right.timestampMode
@@ -67,7 +67,7 @@ import {
67
67
  type PanGesture,
68
68
  type PendingWheel,
69
69
  } from "./panel-gestures";
70
- import { resolvePanelCrosshair, axisLabelRows, cursorAxisLabel, resolveSeriesCursorYRatio } from "./panel-labels";
70
+ import { resolvePanelCrosshair, axisLabelRows, cursorAxisLabel, resolveSeriesCursorYRatio, resolveSeriesAxisTags } from "./panel-labels";
71
71
  import type { KeyboardToolDrag } from "./use-composite-chart-keyboard";
72
72
 
73
73
  const webFrame = globalThis as typeof globalThis & {
@@ -101,6 +101,8 @@ interface CompositePanelSurfaceProps {
101
101
  onSelectDrawing: (id: string | null) => void;
102
102
  onActivate?: () => void;
103
103
  onCursorDateChange: (date: Date | null) => void;
104
+ /** The cursor shown is another chart's, so the pointer's level is not part of it. */
105
+ sharedCursor: boolean;
104
106
  /** Positive positions move toward older observations. A gesture supplies the frame and origin it started from. */
105
107
  onPanViewport: (
106
108
  deltaPositions: number,
@@ -147,6 +149,7 @@ export function CompositePanelSurface({
147
149
  onSelectDrawing,
148
150
  onActivate,
149
151
  onCursorDateChange,
152
+ sharedCursor,
150
153
  onPanViewport,
151
154
  onZoomViewport,
152
155
  onSetViewport,
@@ -171,7 +174,7 @@ export function CompositePanelSurface({
171
174
  const activeDrag = toolDrag ?? keyboardDrag?.drag ?? null;
172
175
  // A keyboard placement's end sits at the level the user put it, so it
173
176
  // draws a level line where otherwise only the pointer would.
174
- const heldCursorYRatio = keyboardDrag ? keyboardDrag.drag.endYRatio : cursorYRatio;
177
+ const heldCursorYRatio = keyboardDrag ? keyboardDrag.drag.endYRatio : sharedCursor ? null : cursorYRatio;
175
178
  const seriesCursorYRatio = useMemo(
176
179
  () => resolveSeriesCursorYRatio(panel, scene),
177
180
  [panel, scene],
@@ -207,7 +210,11 @@ export function CompositePanelSurface({
207
210
  // The level line follows the pointer only. A keyboard or shared cursor knows
208
211
  // its column, and the series markers and axis readout already say the value.
209
212
  // A tool placed from the keyboard is the exception: its end is a level.
210
- const pointerCursorYRatio = scene.cursorXRatio === null ? null : heldCursorYRatio;
213
+ // On the desktop and the web an ordinary hover reads each series on its own
214
+ // axis and draws only the vertical line; the level line, and the pointer's
215
+ // value on the axes, belong to an armed or placing tool.
216
+ const readsPointerLevel = !isDesktopWeb || armedTool !== null || activeDrag !== null;
217
+ const pointerCursorYRatio = scene.cursorXRatio === null || !readsPointerLevel ? null : heldCursorYRatio;
211
218
  const crosshair = useMemo(
212
219
  () => resolvePanelCrosshair(panel, bitmap, scene, pointerCursorYRatio, colors.crosshair),
213
220
  [bitmap, colors.crosshair, panel, pointerCursorYRatio, scene],
@@ -293,6 +300,10 @@ export function CompositePanelSurface({
293
300
  () => levels && levelDomain ? projectLevels(levels.items, levelDomain, levelDraft) : NO_LEVELS,
294
301
  [levelDomain, levelDraft, levels],
295
302
  );
303
+ // Drawings project through the time scale alone: the crosshair moving,
304
+ // here or on a linked chart, must not repaint the plot, which the terminal's
305
+ // kitty surface would upload whole again.
306
+ const drawingProjection = useMemo(() => ({ timeScale: scene.timeScale }), [scene.timeScale]);
296
307
  const bitmapLayers = useMemo(() => {
297
308
  if (!bitmap) return null;
298
309
  // The desktop composites overlays as vectors, so the plot raster stays put
@@ -309,7 +320,7 @@ export function CompositePanelSurface({
309
320
  draw: drawColor,
310
321
  },
311
322
  toolReadout?.direction ?? "up",
312
- { scene, panel, items: panelDrawings, selectedId: selectedDrawingId },
323
+ { scene: drawingProjection, panel, items: panelDrawings, selectedId: selectedDrawingId },
313
324
  );
314
325
  return [projectedLevels.length > 0 ? paintLevels(base, projectedLevels, selectedLevelId) : base];
315
326
  }, [
@@ -318,11 +329,11 @@ export function CompositePanelSurface({
318
329
  colors.crosshair,
319
330
  colors.negative,
320
331
  drawColor,
332
+ drawingProjection,
321
333
  isDesktopWeb,
322
334
  panel,
323
335
  panelDrawings,
324
336
  projectedLevels,
325
- scene,
326
337
  selectedDrawingId,
327
338
  selectedLevelId,
328
339
  toolReadout?.direction,
@@ -394,8 +405,11 @@ export function CompositePanelSurface({
394
405
  const cursorPixelY = activeCursorYRatio === null
395
406
  ? null
396
407
  : activeCursorYRatio * Math.max(panel.height * cellHeightPx - 1, 0);
397
- const leftCursorLabel = cursorAxisLabel(panel, "left", activeCursorYRatio, formatAxisValue);
398
- const rightCursorLabel = cursorAxisLabel(panel, "right", activeCursorYRatio, formatAxisValue);
408
+ // The pointer's value on the axes follows the level line it belongs to.
409
+ const axisCursorYRatio = isDesktopWeb ? pointerCursorYRatio : activeCursorYRatio;
410
+ const leftCursorLabel = cursorAxisLabel(panel, "left", axisCursorYRatio, formatAxisValue);
411
+ const rightCursorLabel = cursorAxisLabel(panel, "right", axisCursorYRatio, formatAxisValue);
412
+ const seriesAxisTags = useMemo(() => isDesktopWeb ? resolveSeriesAxisTags(panel, scene, formatAxisValue) : [], [formatAxisValue, isDesktopWeb, panel, scene]);
399
413
  const measureReadout = useMemo(() => {
400
414
  if (!activeDrag || !toolReadout?.summary) return null;
401
415
  const text = toolReadout.summary;
@@ -846,6 +860,8 @@ export function CompositePanelSurface({
846
860
  cursorBackgroundColor={colors.background}
847
861
  axisColor={colors.textDim}
848
862
  extraMarkers={leftAxisMarkers}
863
+ seriesTags={isDesktopWeb ? seriesAxisTags.filter((tag) => tag.side === "left") : undefined}
864
+ measuredPixelHeight={plotSize.height * cellHeightPx}
849
865
  />
850
866
  <Box width={axisGap} />
851
867
  </>
@@ -893,6 +909,8 @@ export function CompositePanelSurface({
893
909
  cursorBackgroundColor={colors.background}
894
910
  axisColor={colors.textDim}
895
911
  extraMarkers={rightAxisMarkers}
912
+ seriesTags={isDesktopWeb ? seriesAxisTags.filter((tag) => tag.side === "right") : undefined}
913
+ measuredPixelHeight={plotSize.height * cellHeightPx}
896
914
  />
897
915
  </>
898
916
  ) : null}
@@ -2,6 +2,8 @@ import { useMemo } from "react";
2
2
  import { Box, Text, useUiCapabilities } from "../../../ui";
3
3
  import { colors } from "../../../theme/colors";
4
4
  import { clamp } from "../../../utils/math";
5
+ import type { SeriesAxisTag } from "./panel-labels";
6
+ import { placeSeriesAxisTags, SeriesAxisTags } from "./series-axis-tags";
5
7
 
6
8
  function formatAxisCell(label: string | null, width: number): string {
7
9
  if (width <= 0) return "";
@@ -34,6 +36,9 @@ interface PriceAxisLabelsProps {
34
36
  axisColor?: string;
35
37
  /** Anchors that stay put while the cursor moves, such as a measure start. */
36
38
  extraMarkers?: readonly PriceAxisMarker[];
39
+ /** Desktop/web observations, separate from a drawing tool's endpoint. */
40
+ seriesTags?: readonly SeriesAxisTag[];
41
+ measuredPixelHeight?: number;
37
42
  }
38
43
 
39
44
  interface CursorPriceAxisOverlay {
@@ -134,6 +139,8 @@ export function PriceAxisLabels({
134
139
  cursorBackgroundColor = colors.bg,
135
140
  axisColor = colors.textDim,
136
141
  extraMarkers,
142
+ seriesTags,
143
+ measuredPixelHeight,
137
144
  }: PriceAxisLabelsProps) {
138
145
  const { cellHeightPx = 18, fractionalViewport = false } = useUiCapabilities();
139
146
  const overlay = useMemo(() => buildCursorPriceAxisOverlay({
@@ -147,7 +154,11 @@ export function PriceAxisLabels({
147
154
  const usePixelOverlay = fractionalViewport && overlay.labelText !== null && overlay.topPercent !== null;
148
155
  // Row-snapped labels drift up to a row off the gridlines they name.
149
156
  const usePixelTicks = fractionalViewport && axisTicks !== undefined;
150
- const pixelHeight = Math.max(height * cellHeightPx, 1);
157
+ const pixelHeight = Math.max(measuredPixelHeight ?? height * cellHeightPx, 1);
158
+ const placedSeriesTags = seriesTags ? placeSeriesAxisTags(seriesTags, pixelHeight, cellHeightPx) : [];
159
+ const lanes = Math.max(1, ...placedSeriesTags.map((tag) => tag.lane + 1));
160
+ // Fixed reference labels yield to observations, never the other way round.
161
+ const visibleMarkers = extraMarkers?.filter((marker) => placedSeriesTags.every((tag) => Math.abs(tag.centerY - marker.pixelY) >= cellHeightPx));
151
162
  const placedTicks = usePixelTicks
152
163
  ? placeAxisTicks({
153
164
  ticks: axisTicks,
@@ -155,7 +166,8 @@ export function PriceAxisLabels({
155
166
  cellHeightPx,
156
167
  badgeCentersPx: [
157
168
  ...(usePixelOverlay ? [(overlay.topPercent! / 100) * (pixelHeight - 1)] : []),
158
- ...(extraMarkers?.map((marker) => marker.pixelY) ?? []),
169
+ ...(visibleMarkers?.map((marker) => marker.pixelY) ?? []),
170
+ ...placedSeriesTags.map((tag) => tag.centerY),
159
171
  ],
160
172
  })
161
173
  : null;
@@ -233,7 +245,7 @@ export function PriceAxisLabels({
233
245
  </Box>
234
246
  );
235
247
  }) : null}
236
- {usePixelOverlay || usePixelTicks ? extraMarkers?.map((marker) => (
248
+ {usePixelOverlay || usePixelTicks ? visibleMarkers?.map((marker) => (
237
249
  <Box
238
250
  key={`${marker.label}:${marker.pixelY}`}
239
251
  width={axisSectionWidth}
@@ -255,6 +267,7 @@ export function PriceAxisLabels({
255
267
  {axisPaddingWidth > 0 ? <Box width={axisPaddingWidth} /> : null}
256
268
  </Box>
257
269
  )) : null}
270
+ {seriesTags ? <SeriesAxisTags placed={placedSeriesTags} pixelHeight={pixelHeight} lanes={lanes} background={cursorBackgroundColor} /> : null}
258
271
  {usePixelOverlay ? (
259
272
  <Box
260
273
  width={axisSectionWidth}
@@ -661,16 +661,73 @@ function buildCursorValues(
661
661
  }));
662
662
  }
663
663
 
664
+ /**
665
+ * The bar a time from another chart lands on: the last one at or before it,
666
+ * as a bar stands for the time from its open until the next. A time before
667
+ * the first bar in view lands on none, and so does one past the plot's right
668
+ * edge while the chart is panned back from its newest bar, rather than
669
+ * pinning the cursor to an edge the time is not at. A chart showing its
670
+ * newest bar lands any later time on it, so a monthly series still reads its
671
+ * latest month on a day after it.
672
+ */
673
+ function linkedCursorDate(scene: CompositeChartScene, requested: Date): Date | null {
674
+ const target = requested.getTime();
675
+ const { dates, timeScale } = scene;
676
+ if (!Number.isFinite(target) || dates.length === 0) return null;
677
+ const plotEnd = unprojectCompositeTimestamp(timeScale, 1);
678
+ const newest = timeScale.kind === "market" ? timeScale.anchors.at(-1)?.timestamp : scene.newestTime;
679
+ if (target > plotEnd && (newest === undefined || newest > plotEnd)) return null;
680
+ let low = 0;
681
+ let high = dates.length;
682
+ while (low < high) {
683
+ const middle = low + Math.floor((high - low) / 2);
684
+ if (dates[middle]!.getTime() <= target) low = middle + 1;
685
+ else high = middle;
686
+ }
687
+ return dates[low - 1] ?? null;
688
+ }
689
+
690
+ /** The bar another chart's cursor time lands on (see `linkedCursorDate`), as its timestamp. */
691
+ export function compositeLinkedCursorTime(scene: CompositeChartScene, time: number): number | null {
692
+ return linkedCursorDate(scene, new Date(time))?.getTime() ?? null;
693
+ }
694
+
695
+ /**
696
+ * The last instant of the bar a cursor is on, which is the time it is shared
697
+ * at: a bar stands until the next one or its own interval ends, whichever is
698
+ * first. Another chart then lands on its last bar at or before it, so a daily
699
+ * cursor shows the day's last intraday bar, whose close is the day's, and an
700
+ * intraday cursor shows its own day's bar rather than the next one's. On
701
+ * calendar time the newest bar, with no next one, stands as long as the one
702
+ * before it did, so a series' latest month lands a daily chart on that
703
+ * month's latest day rather than its first.
704
+ */
705
+ export function compositeCursorAsOf(scene: CompositeChartScene, date: Date): number {
706
+ const start = date.getTime();
707
+ const next = scene.dates.find((entry) => entry.getTime() > start)?.getTime() ?? Number.POSITIVE_INFINITY;
708
+ const previous = scene.dates.findLast((entry) => entry.getTime() < start)?.getTime();
709
+ const intervalEnd = scene.timeScale.kind === "market" ? start + scene.timeScale.cadenceMs
710
+ : next === Number.POSITIVE_INFINITY && previous !== undefined ? start + (start - previous)
711
+ : Number.POSITIVE_INFINITY;
712
+ const end = Math.min(next, intervalEnd);
713
+ return Number.isFinite(end) ? end - 1 : start;
714
+ }
715
+
664
716
  /**
665
717
  * Applies cursor-only state without rebuilding panel domains or projected
666
718
  * series. Keeping those references stable lets renderers reuse the expensive
667
- * base chart while the cursor moves.
719
+ * base chart while the cursor moves. `linked` places a time another chart's
720
+ * cursor is at (see `linkedCursorDate`); otherwise the cursor goes to the
721
+ * nearest bar.
668
722
  */
669
723
  export function applyCompositeChartCursor(
670
724
  scene: CompositeChartScene,
671
725
  requestedCursor: Date | null,
726
+ snap: "nearest" | "linked" = "nearest",
672
727
  ): CompositeChartScene {
673
- const cursorDate = requestedCursor ? nearestDate(scene.dates, requestedCursor) : null;
728
+ const cursorDate = !requestedCursor ? null
729
+ : snap === "linked" ? linkedCursorDate(scene, requestedCursor)
730
+ : nearestDate(scene.dates, requestedCursor);
674
731
  const currentTimestamp = scene.cursorDate?.getTime() ?? null;
675
732
  const nextTimestamp = cursorDate?.getTime() ?? null;
676
733
  if (currentTimestamp === nextTimestamp) return scene;
@@ -818,6 +875,7 @@ export function buildCompositeChartScene(
818
875
  cursorDate,
819
876
  cursorXRatio,
820
877
  cursorValues: buildCursorValues(panelScenes, cursorDate, { startTime, endTime, timeScale }),
878
+ newestTime: lastTime,
821
879
  ...(timeZone ? { timeZone } : {}),
822
880
  };
823
881
  }
@@ -0,0 +1,67 @@
1
+ import { Box, Text } from "../../../ui";
2
+ import { clamp } from "../../../utils/math";
3
+ import type { SeriesAxisTag } from "./panel-labels";
4
+
5
+ /**
6
+ * Keep every tag. Order is by value height, then authored series order for
7
+ * ties. A forward pass separates neighbours; a backward pass fits the bottom
8
+ * edge. Panels shorter than the readout use parallel lanes, not dropped tags
9
+ * or smaller type. The gutter reserves those lanes before the cursor moves.
10
+ */
11
+ export function placeSeriesAxisTags(tags: readonly SeriesAxisTag[], pixelHeight: number, labelHeight: number) {
12
+ const height = Math.max(pixelHeight, labelHeight);
13
+ const capacity = Math.max(1, Math.floor(height / labelHeight));
14
+ const sorted = tags.map((tag, order) => ({ ...tag, order, anchorY: clamp(tag.yRatio, 0, 1) * Math.max(pixelHeight - 1, 0) }))
15
+ .sort((a, b) => a.anchorY - b.anchorY || a.order - b.order);
16
+ const placed: Array<SeriesAxisTag & { anchorY: number; centerY: number; lane: number }> = [];
17
+ for (let start = 0; start < sorted.length; start += capacity) {
18
+ const lane = sorted.slice(start, start + capacity);
19
+ const centers = lane.map((tag) => clamp(tag.anchorY, labelHeight / 2, height - labelHeight / 2));
20
+ for (let i = 1; i < centers.length; i++) centers[i] = Math.max(centers[i]!, centers[i - 1]! + labelHeight);
21
+ if (centers.length) centers[centers.length - 1] = Math.min(centers.at(-1)!, height - labelHeight / 2);
22
+ for (let i = centers.length - 2; i >= 0; i--) centers[i] = Math.min(centers[i]!, centers[i + 1]! - labelHeight);
23
+ lane.forEach((tag, index) => placed.push({ ...tag, centerY: centers[index]!, lane: Math.floor(start / capacity) }));
24
+ }
25
+ return placed;
26
+ }
27
+
28
+ export function SeriesAxisTags({ placed, pixelHeight, lanes, background }: {
29
+ placed: ReturnType<typeof placeSeriesAxisTags>;
30
+ pixelHeight: number;
31
+ lanes: number;
32
+ background: string;
33
+ }) {
34
+ const percent = (y: number) => `${100 * y / Math.max(pixelHeight, 1)}%`;
35
+ return placed.map((tag) => {
36
+ const left = tag.side === "left";
37
+ const edge = left ? "right" : "left";
38
+ const inset = `${tag.lane * 100 / lanes}%`;
39
+ return <Box key={tag.seriesId} style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
40
+ {/* The tick stays at the observation, even when the badge must move. */}
41
+ <Box
42
+ data-gloom-role="chart-series-axis-anchor"
43
+ data-series-id={tag.seriesId}
44
+ style={{ position: "absolute", [edge]: 0, top: percent(tag.anchorY), width: `calc(${inset} + .5em)`, height: 0, borderTop: `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}` }}
45
+ />
46
+ <Box style={{ position: "absolute", [edge]: `calc(${inset} + .25em)`, top: percent(Math.min(tag.anchorY, tag.centerY)), height: percent(Math.abs(tag.centerY - tag.anchorY)), width: 0, borderLeft: `1px solid ${tag.color}` }} />
47
+ <Box
48
+ data-gloom-role="chart-series-axis-tag"
49
+ data-series-id={tag.seriesId}
50
+ data-side={tag.side}
51
+ data-value={tag.value}
52
+ data-carried={tag.carried ? "true" : undefined}
53
+ aria-label={`${tag.seriesLabel}: ${tag.label}${tag.carried ? " (carried)" : ""}`}
54
+ style={{
55
+ position: "absolute", [edge]: `calc(${inset} + .5em)`, top: percent(tag.centerY), transform: "translateY(-50%)",
56
+ width: `calc(${100 / lanes}% - .5em)`, height: "1.5em", lineHeight: "1.5em",
57
+ alignItems: left ? "flex-end" : "flex-start", justifyContent: "center",
58
+ // Every badge paints above every leader, including another lane's.
59
+ zIndex: 2, color: tag.color, backgroundColor: background, whiteSpace: "nowrap",
60
+ borderBlockEnd: tag.carried ? `1px dashed ${tag.color}` : undefined,
61
+ }}
62
+ >
63
+ <Text fg={tag.color} selectable={false}>{tag.label}</Text>
64
+ </Box>
65
+ </Box>;
66
+ });
67
+ }
@@ -153,7 +153,7 @@ export function resolveDrawingFromDrag(
153
153
  }
154
154
 
155
155
  function projectDrawing(
156
- scene: CompositeChartScene,
156
+ scene: Pick<CompositeChartScene, "timeScale">,
157
157
  panel: CompositePanelScene,
158
158
  drawing: ChartDrawing,
159
159
  ): Array<{ xRatio: number; yRatio: number }> | null {
@@ -516,7 +516,7 @@ export function drawChartToolOverlay(
516
516
  colors: ChartToolColors,
517
517
  direction: "up" | "down" = "up",
518
518
  drawings?: {
519
- scene: CompositeChartScene;
519
+ scene: Pick<CompositeChartScene, "timeScale">;
520
520
  panel: CompositePanelScene;
521
521
  items: readonly ChartDrawing[];
522
522
  selectedId?: string | null;
@@ -181,6 +181,8 @@ export interface CompositeChartScene {
181
181
  cursorPanels?: CompositePanelScene[];
182
182
  /** IANA zone the time axis and cursor read in: the venue's for intraday market bars, else UTC. */
183
183
  timeZone?: string;
184
+ /** The newest observation's time, in view or not. */
185
+ newestTime?: number;
184
186
  }
185
187
 
186
188
  export interface BuildCompositeChartSceneOptions {
@@ -222,6 +224,27 @@ export interface CompositeChartXAxis {
222
224
  formatCursor?: (xRatio: number) => string;
223
225
  }
224
226
 
227
+ /**
228
+ * A crosshair shared between charts, as the last instant of the bar it is
229
+ * on. The chart subscribes itself, so a move re-renders the charts and not
230
+ * the panes around them.
231
+ */
232
+ interface CompositeCursorLink {
233
+ subscribe(listener: () => void): () => void;
234
+ /**
235
+ * Another chart's cursor, shown in place of this chart's own: on the last
236
+ * bar at or before it, and not at all outside the view. Null while this
237
+ * chart's cursor is the shared one, or there is none.
238
+ */
239
+ time(): number | null;
240
+ /** Whether this chart's cursor is the shared one. */
241
+ held(): boolean;
242
+ /** Reports this chart's cursor, or null when it leaves. */
243
+ publish(time: number | null): void;
244
+ /** Clears the shared cursor wherever it is, as Esc on a chart showing it does. */
245
+ clear(): void;
246
+ }
247
+
225
248
  export interface CompositeChartProps {
226
249
  series: ResolvedSeries[];
227
250
  /** Optional legend model; can include hidden series that are not plotted. */
@@ -248,6 +271,11 @@ export interface CompositeChartProps {
248
271
  /** Permit panning into an older unloaded window so the owner can backfill it. */
249
272
  allowHistoricalBackfill?: boolean;
250
273
  cursorDate?: Date | null;
274
+ /**
275
+ * A crosshair shared with other charts. By default an interactive chart
276
+ * with a time axis in a pane shares its layout tab's; null keeps it out.
277
+ */
278
+ cursorLink?: CompositeCursorLink | null;
251
279
  viewport?: {
252
280
  start: Date;
253
281
  end: Date;
@@ -0,0 +1,46 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { useOptionalPaneInstanceId } from "../../../state/app/context";
3
+
4
+ /**
5
+ * A pane's range control and its charts share one request: choosing the range
6
+ * that is already selected sends the charts back to it, as `0` does on a chart
7
+ * whose view was panned or zoomed. A chart answers for its own view, and a pane
8
+ * that keeps a view of its own (a saved one, a loaded window) answers for that.
9
+ */
10
+ type ViewReset = () => void;
11
+
12
+ const listeners = new Map<string, Set<ViewReset>>();
13
+ /** Charts outside a pane (a dialog, a test) share one scope. */
14
+ const NO_PANE = "";
15
+
16
+ function runViewResets(scope: string): void {
17
+ for (const reset of [...listeners.get(scope) ?? []]) reset();
18
+ }
19
+
20
+ /**
21
+ * Registers what happens in this pane when a range is chosen again. Keep it
22
+ * idempotent and a no-op for a view that was never moved: the request also
23
+ * arrives when the user presses the range that was already showing.
24
+ */
25
+ export function useChartViewResetListener(reset: ViewReset, enabled = true): void {
26
+ const scope = useOptionalPaneInstanceId() ?? NO_PANE;
27
+ const latest = useRef(reset);
28
+ latest.current = reset;
29
+ useEffect(() => {
30
+ if (!enabled) return;
31
+ const run = () => latest.current();
32
+ let set = listeners.get(scope);
33
+ if (!set) listeners.set(scope, set = new Set());
34
+ set.add(run);
35
+ return () => {
36
+ set.delete(run);
37
+ if (set.size === 0 && listeners.get(scope) === set) listeners.delete(scope);
38
+ };
39
+ }, [enabled, scope]);
40
+ }
41
+
42
+ /** Sends this pane's charts back to the range they were given. */
43
+ export function useChartViewReset(): () => void {
44
+ const scope = useOptionalPaneInstanceId() ?? NO_PANE;
45
+ return useCallback(() => runViewResets(scope), [scope]);
46
+ }
@@ -4,7 +4,7 @@ import {
4
4
  addTickerToPortfolio,
5
5
  isManualPortfolio,
6
6
  removeTickerFromPortfolio,
7
- } from "../../../plugins/builtin/portfolio-list/mutations";
7
+ } from "../../../portfolio/mutations";
8
8
  import {
9
9
  applyCollectionMembershipChange,
10
10
  getCollectionTargetOptions,