@gloom-sh/terminal 0.19.1 → 0.19.3

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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -0,0 +1,211 @@
1
+ import { hasRegularCloseHour, isAlwaysOpenExchange, regularSessionCloseOn, sessionCalendarTimeZone } from "../market-data/market/freshness";
2
+ import { fetchHistoryResult } from "../sources/history-result";
3
+ import type { DataProvider } from "../types/data-provider";
4
+ import { isRoundTheClockCoin } from "../utils/crypto-pair";
5
+ import { canonicalExchange, isUsListingExchange } from "../utils/exchanges";
6
+ import { logFailure } from "../utils/log-failure";
7
+ import { zonedDateTimeParts, zonedWallClockToUtcMs } from "../utils/zoned-date-time";
8
+ import type { PriceComparison } from "./price-comparison";
9
+ import type { ResolvedSeries } from "./types";
10
+
11
+ const HOUR_MS = 60 * 60_000;
12
+ const DAY_MS = 24 * HOUR_MS;
13
+ /** How far back hourly bars are read for a comparison: Gloom Cloud keeps a coin's for a year. A longer span keeps UTC closes. */
14
+ const HOURLY_REACH_MS = 370 * DAY_MS;
15
+
16
+ /** The venue a compared leg trades on, as its sessions are read. */
17
+ function legVenue(series: ResolvedSeries | undefined): string | undefined {
18
+ return series?.timeBasis?.exchange || series?.listing?.exchange || undefined;
19
+ }
20
+
21
+ /** A coin that trades round the clock, its daily bar the UTC day: BTC-USD on the crypto venue. */
22
+ function isRoundTheClockLeg(series: ResolvedSeries | undefined): boolean {
23
+ if (!series) return false;
24
+ const venue = legVenue(series);
25
+ return (!!venue && isAlwaysOpenExchange(venue)) || isRoundTheClockCoin(series.listing?.symbol, series.listing?.exchange);
26
+ }
27
+
28
+ /**
29
+ * A leg whose venue closes each day at a known hour: a US fund, a London share. A venue whose close hour is not
30
+ * known has no close to measure a coin at: the coin keeps its UTC closes, and the notice names no close.
31
+ */
32
+ function sessionVenueOf(series: ResolvedSeries | undefined): string | null {
33
+ const venue = legVenue(series);
34
+ return venue && !isRoundTheClockLeg(series) && !isAlwaysOpenExchange(venue) && sessionCalendarTimeZone(venue) && hasRegularCloseHour(venue)
35
+ ? venue : null;
36
+ }
37
+
38
+ /** `US` for a US listing's venue, else the venue's code, as the close a coin is measured against is named. */
39
+ function sessionMarketName(venue: string): string {
40
+ return isUsListingExchange(venue) ? "US" : canonicalExchange(venue);
41
+ }
42
+
43
+ type HourlyRead = Awaited<ReturnType<typeof fetchHistoryResult>>;
44
+ type HourlyProvider = Pick<DataProvider, "getDetailedPriceHistory" | "getDetailedPriceHistoryWithMetadata">;
45
+ interface HourlyReads {
46
+ /** Recent reads by listing and span: a chart resolves again on every live quote, and asks for the same hours. */
47
+ spans: Map<string, { at: number; read: Promise<HourlyRead> }>;
48
+ /** Listings whose read failed, until when they are not asked again. */
49
+ failedUntil: Map<string, number>;
50
+ }
51
+ const hourlyReadsBySource = new WeakMap<HourlyProvider, HourlyReads>();
52
+ const HOURLY_READ_LIMIT = 64;
53
+ /** A span reaching the last hour is read again after this long; an older one is kept until it falls out. */
54
+ const RECENT_READ_TTL_MS = 60_000;
55
+ /** A listing whose hourly bars failed to load is not asked again for this long, so the failure is logged once in it. */
56
+ const FAILED_READ_TTL_MS = 2 * 60_000;
57
+ const logHourlyFailure = logFailure("time-series.coin-session-close", "Read a coin's hourly bars for a comparison failed", null);
58
+
59
+ function hourlyBars(
60
+ provider: HourlyProvider,
61
+ symbol: string,
62
+ exchange: string,
63
+ start: number,
64
+ end: number,
65
+ now: number,
66
+ ): Promise<HourlyRead> {
67
+ let reads = hourlyReadsBySource.get(provider);
68
+ if (!reads) hourlyReadsBySource.set(provider, reads = { spans: new Map(), failedUntil: new Map() });
69
+ const listing = `${symbol}:${exchange}`;
70
+ if ((reads.failedUntil.get(listing) ?? 0) > now) return Promise.resolve(null);
71
+ // A span ending now is keyed by its minute, so a chart redrawn within it reads it once.
72
+ end = Math.ceil(end / 60_000) * 60_000;
73
+ const key = `${listing}:${start}:${end}`;
74
+ const cached = reads.spans.get(key);
75
+ if (cached && (end < now - HOUR_MS || now - cached.at < RECENT_READ_TTL_MS)) return cached.read;
76
+ const read = fetchHistoryResult(provider as DataProvider, symbol, exchange, { kind: "detail", start: new Date(start), end: new Date(end), interval: "1h" })
77
+ .then((result) => result ?? Promise.reject(new Error("no hourly bars")))
78
+ .catch((error: unknown) => {
79
+ // A failed read is asked again only after FAILED_READ_TTL_MS, and logged once for it.
80
+ reads!.failedUntil.set(listing, now + FAILED_READ_TTL_MS);
81
+ if (reads!.spans.get(key)?.read === read) reads!.spans.delete(key);
82
+ return logHourlyFailure(error);
83
+ });
84
+ reads.spans.delete(key);
85
+ reads.spans.set(key, { at: now, read });
86
+ if (reads.spans.size > HOURLY_READ_LIMIT) reads.spans.delete(reads.spans.keys().next().value!);
87
+ return read;
88
+ }
89
+
90
+ export interface SessionCloseTiming {
91
+ /** The venue whose close the coins are compared against: the first compared leg that trades in sessions. */
92
+ venue: string;
93
+ /** Coin legs measured at that venue's close on each of their days, from hourly bars. */
94
+ atSessionClose: ReadonlySet<string>;
95
+ /** Of those, the legs whose newest day's session has closed: their end is that close, not a price still moving. */
96
+ closedEnds: ReadonlySet<string>;
97
+ }
98
+
99
+ /** The venue's wall clock of `time` in its zone, to measure a day without a session at its usual close. */
100
+ function wallClock(time: number, zone: string): { hour: number; minute: number } {
101
+ const { hour, minute } = zonedDateTimeParts(time, zone);
102
+ return { hour, minute };
103
+ }
104
+
105
+ /**
106
+ * A daily comparison of a coin with a listing that trades in sessions (IBIT
107
+ * against BTC-USD) compares the fund's close at 20:00 UTC with the coin's at
108
+ * 00:00 UTC, four hours later, which on a falling night is most of the gap.
109
+ * When the coin's hourly bars cover its whole span, every one of its daily
110
+ * points from the base to its end is replaced by its price at the session
111
+ * venue's close (the close of the hourly bar ending then; a day the venue
112
+ * does not trade, at the clock time of its last close), so both legs and
113
+ * the line between are measured at the same instants. A coin whose hourly
114
+ * bars miss any of those hours (they go back a year) keeps its UTC closes
115
+ * throughout, and the comparison names each close time instead
116
+ * (priceComparisonCloseTimes): the line and its total never mix the two.
117
+ *
118
+ * Null when the comparison has no such pair of legs; otherwise the venue and
119
+ * the series, with the coin legs that could be measured at its close.
120
+ */
121
+ export async function coinLegsAtSessionClose(
122
+ comparison: PriceComparison | null,
123
+ series: readonly ResolvedSeries[],
124
+ provider: HourlyProvider,
125
+ now: number,
126
+ ): Promise<{ timing: SessionCloseTiming; series: ResolvedSeries[] } | null> {
127
+ if (comparison?.alignment !== "session-date" || !comparison.legs) return null;
128
+ const byId = new Map(series.map((entry) => [entry.id, entry]));
129
+ const compared = Object.keys(comparison.legs);
130
+ const venue = compared.map((id) => sessionVenueOf(byId.get(id))).find((entry): entry is string => !!entry);
131
+ const coins = compared.filter((id) => isRoundTheClockLeg(byId.get(id)) && byId.get(id)!.historyResolution === "1d");
132
+ const zone = venue ? sessionCalendarTimeZone(venue) : null;
133
+ if (!venue || !zone || coins.length === 0) return null;
134
+ const atSessionClose = new Set<string>();
135
+ const closedEnds = new Set<string>();
136
+ const next = [...series];
137
+ await Promise.all(coins.map(async (id) => {
138
+ const entry = byId.get(id)!;
139
+ const leg = comparison.legs![id]!;
140
+ const symbol = entry.listing?.symbol;
141
+ const baseClose = regularSessionCloseOn(venue, leg.baseDay);
142
+ // A coin's newest day still running is its latest price; measured at the close only once the venue has closed.
143
+ const endClose = regularSessionCloseOn(venue, leg.endDay);
144
+ const endTime = endClose !== null && endClose <= now ? endClose : leg.intraday ? null : endClose;
145
+ if (!symbol || baseClose === null || (endTime === null && !leg.intraday)) return;
146
+ // The instant each of the coin's days is measured at: the venue's close that day, or on a day it does not trade,
147
+ // the clock time of its last close before.
148
+ const instants = new Map<number, number>();
149
+ let clock = wallClock(baseClose, zone);
150
+ entry.points.forEach((point, index) => {
151
+ const day = point.date.toISOString().slice(0, 10);
152
+ if (day < leg.baseDay || day > leg.endDay) return;
153
+ if (day === leg.endDay && endTime === null) return;
154
+ const close = regularSessionCloseOn(venue, day);
155
+ if (close !== null) clock = wallClock(close, zone);
156
+ instants.set(index, close ?? zonedWallClockToUtcMs(zone, Number(day.slice(0, 4)), Number(day.slice(5, 7)), Number(day.slice(8, 10)),
157
+ clock.hour, clock.minute, 0));
158
+ });
159
+ const times = [...instants.values()];
160
+ const last = Math.max(baseClose, ...times);
161
+ if (last > now || last - baseClose > HOURLY_REACH_MS) return;
162
+ const read = await hourlyBars(provider, symbol, entry.listing?.exchange ?? "", baseClose - 2 * HOUR_MS, Math.min(now, last + HOUR_MS), now);
163
+ if (!read || read.resolution !== "1h") return;
164
+ // Each finished hourly bar's close by the time it closed; a bar still forming has no close yet.
165
+ const closes = new Map<number, number>();
166
+ for (const bar of read.points) {
167
+ const open = new Date(bar.date).getTime();
168
+ if (!bar.partial && Number.isFinite(open) && Number.isFinite(bar.close) && bar.close > 0) closes.set(open + HOUR_MS, bar.close);
169
+ }
170
+ // Every day or none: a line of closes at the venue's close with a few UTC closes between would be neither.
171
+ if (!closes.has(baseClose) || times.some((time) => !closes.has(time))) return;
172
+ const points = entry.points.map((point, index) => {
173
+ const price = instants.has(index) ? closes.get(instants.get(index)!)! : undefined;
174
+ return price === undefined ? point : { ...point, value: price, close: price, rawValue: price };
175
+ });
176
+ next[next.indexOf(entry)] = { ...entry, points };
177
+ atSessionClose.add(id);
178
+ if (endTime !== null) closedEnds.add(id);
179
+ }));
180
+ return { timing: { venue, atSessionClose, closedEnds }, series: next };
181
+ }
182
+
183
+ /**
184
+ * What a daily comparison of coins with listings that trade in sessions says
185
+ * of when each is measured: `BTC-USD at the US close` when its hourly bars
186
+ * put it there, else `BTC-USD to 00:00 UTC, 4 h after the US close`. Null
187
+ * when no coin is compared with such a listing.
188
+ */
189
+ export function priceComparisonCloseTimes(
190
+ comparison: Pick<PriceComparison, "legs">,
191
+ series: readonly ResolvedSeries[],
192
+ labels: ReadonlyMap<string, string>,
193
+ atSessionClose: ReadonlySet<string> = new Set(),
194
+ ): string | null {
195
+ const byId = new Map(series.map((entry) => [entry.id, entry]));
196
+ const compared = Object.keys(comparison.legs ?? {});
197
+ const venue = compared.map((id) => sessionVenueOf(byId.get(id))).find((entry): entry is string => !!entry);
198
+ const coins = compared.filter((id) => isRoundTheClockLeg(byId.get(id)));
199
+ if (!venue || coins.length === 0) return null;
200
+ const market = sessionMarketName(venue);
201
+ const parts = coins.map((id) => {
202
+ const label = labels.get(id) ?? id;
203
+ if (atSessionClose.has(id)) return `${label} at the ${market} close`;
204
+ const leg = comparison.legs![id]!;
205
+ const sessionClose = leg.intraday ? null : regularSessionCloseOn(venue, leg.endDay);
206
+ const coinClose = Date.parse(`${leg.endDay}T00:00:00Z`) + DAY_MS;
207
+ const hours = sessionClose === null ? null : Math.round((coinClose - sessionClose) / HOUR_MS);
208
+ return hours !== null && hours > 0 ? `${label} to 00:00 UTC, ${hours} h after the ${market} close` : `${label} to 00:00 UTC`;
209
+ });
210
+ return parts.join("; ");
211
+ }
@@ -54,6 +54,8 @@ interface HistoryAnchor {
54
54
  /** Identity of the loaded version of the history's newest bar. */
55
55
  key: string;
56
56
  time: number;
57
+ /** The session's open, from a quote whose session is the whole bar (quoteSessionRange). */
58
+ open: number | null;
57
59
  /** Live extremes and close observed while this bar is still forming. */
58
60
  high: number | null;
59
61
  low: number | null;
@@ -211,6 +213,7 @@ export class LiveBarAccumulator {
211
213
  this.anchor = {
212
214
  key,
213
215
  time: latestTime,
216
+ open: forming ? previous?.open ?? null : null,
214
217
  high: carried?.high ?? null,
215
218
  low: carried?.low ?? null,
216
219
  close: carried?.close ?? null,
@@ -316,6 +319,7 @@ export class LiveBarAccumulator {
316
319
  return;
317
320
  }
318
321
  if (!forming && this.inBar(time, anchor.time, options, exchange)) {
322
+ if (range?.open !== undefined) anchor.open = range.open;
319
323
  anchor.high = Math.max(anchor.high ?? price, price, range?.high ?? price);
320
324
  anchor.low = Math.min(anchor.low ?? price, price, range?.low ?? price);
321
325
  anchor.close = price;
@@ -364,8 +368,15 @@ export class LiveBarAccumulator {
364
368
  const anchor = this.anchor!;
365
369
  if (anchor.point?.source === latest && anchor.point.version === this.version) return anchor.point.value;
366
370
  let value = latest;
367
- // A later quote cannot establish which reported OHLC field was wrong.
368
- if (!pricePointIntegrity(latest)) {
371
+ if (pricePointIntegrity(latest)) {
372
+ // A later quote cannot establish which reported OHLC field was wrong, unless it names the bar's whole session:
373
+ // its open, range, price and count are the bar (mergeQuoteIntoLatestBar).
374
+ if (anchor.open !== null && anchor.close !== null) {
375
+ const close = anchor.close;
376
+ value = { ...latest, open: anchor.open, high: Math.max(anchor.high ?? close, anchor.open, close),
377
+ low: Math.min(anchor.low ?? close, anchor.open, close), close, ...this.cumulative !== null ? { volume: this.cumulative } : {} };
378
+ }
379
+ } else {
369
380
  let volume = latest.volume;
370
381
  const end = anchor.volumeEnd ?? this.cumulative;
371
382
  if (isFiniteNumber(volume) && end !== null && anchor.volumeBase !== null) {
@@ -3,7 +3,7 @@ import { canonicalTimeSeriesFieldId, isMarketFieldId } from "./field-catalog";
3
3
  import type { QuoteSubscriptionTarget } from "../types/data-provider";
4
4
  import type { Quote } from "../types/financials";
5
5
  import type { ChartSeriesSpec, ChartSpec, SecuritySeriesSource } from "./types";
6
- import { activeStudyInputSeriesIds } from "./studies";
6
+ import { activeStudyInputSeriesIds } from "./studies/resolve";
7
7
  import { valuationSeriesUsesLiveQuote } from "./fundamentals";
8
8
  import { hasValidQuoteObservationTime } from "../market-data/quotes/freshness";
9
9
  import type { InstrumentRef } from "../market-data/request-types";
@@ -1,5 +1,5 @@
1
1
  import type { PricePoint, TickerFinancials } from "../types/financials";
2
- import { pricePointIntegrity, pricePointValues, priceHistoryIntegrityNotice, mergePriceHistoryIntegrity, type PriceHistoryIntegrity } from "../utils/price-history-integrity";
2
+ import { pricePointIntegrity, pricePointValues, pricePointWicks, priceHistoryIntegrityNotice, mergePriceHistoryIntegrity, type PriceHistoryIntegrity } from "../utils/price-history-integrity";
3
3
  import { canonicalTimeSeriesFieldId, isFundamentalFieldId, isMarketFieldId } from "./field-catalog";
4
4
  import { extractFundamentalSeries } from "./fundamentals";
5
5
  import type { ChartSeriesPriceHistoryIntegrity, ResolvedSeries, SecuritySeriesSource, SeriesPeriod, TimeSeriesPoint } from "./types";
@@ -73,8 +73,10 @@ function aggregatePriceHistory(
73
73
  for (const point of sorted) {
74
74
  const key = periodKey(point.date, period);
75
75
  const open = isFiniteNumber(point.open) ? point.open : point.close;
76
- const high = isFiniteNumber(point.high) ? point.high : point.close;
77
- const low = isFiniteNumber(point.low) ? point.low : point.close;
76
+ // A wick too far from its bar's body to have traded does not set the week's or month's range.
77
+ const wicks = pricePointWicks(point);
78
+ const high = isFiniteNumber(wicks.high) ? wicks.high : isFiniteNumber(point.high) ? Math.max(open, point.close) : point.close;
79
+ const low = isFiniteNumber(wicks.low) ? wicks.low : isFiniteNumber(point.low) ? Math.min(open, point.close) : point.close;
78
80
  const current = buckets.get(key);
79
81
  if (!current) {
80
82
  buckets.set(key, {
@@ -0,0 +1,159 @@
1
+ import type { PricePoint, Quote } from "../types/financials";
2
+ import { latestRegularSessionClose, regularSessionCloseOn, sessionCalendarTimeZone } from "../market-data/market/freshness";
3
+ import { quoteTradingDay } from "../market-data/quotes/day-range";
4
+ import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
5
+ import { canonicalExchange, isUsListingExchange } from "../utils/exchanges";
6
+ import { zonedDateKey } from "../utils/zoned-date-time";
7
+
8
+ /** What the footer of an intraday total says while a session has closed but its closing print is not in yet. */
9
+ export const CLOSING_PRINT_PENDING_TEXT = "closing print not in yet";
10
+
11
+ /** A close this far from the session's last trade is another listing's or another unit's, never its auction print. */
12
+ const MAX_CLOSE_MOVE = 0.25;
13
+
14
+ function barTime(point: Pick<PricePoint, "date">): number {
15
+ return new Date(point.date as Date | string | number).getTime();
16
+ }
17
+
18
+ const positive = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value > 0;
19
+
20
+ /** Whether `close` can be the close of a session whose last trade was `lastTrade`. */
21
+ function nearLastTrade(close: number, lastTrade: number | null): boolean {
22
+ return lastTrade === null || !positive(lastTrade) || Math.abs(close / lastTrade - 1) <= MAX_CLOSE_MOVE;
23
+ }
24
+
25
+ /** The newest regular session that has closed, when the bars hold it, and its last trade there. */
26
+ export interface ClosedSession {
27
+ /** The session's local date. */
28
+ date: string;
29
+ /** The close of its last regular bar: the last trade before the closing auction. */
30
+ lastTrade: number;
31
+ }
32
+
33
+ /** When each local date's session closed, read once per date. */
34
+ function sessionCloses(venue: string | undefined) {
35
+ const closes = new Map<string, number | null>();
36
+ return (date: string) => {
37
+ if (!closes.has(date)) closes.set(date, regularSessionCloseOn(venue, date));
38
+ return closes.get(date)!;
39
+ };
40
+ }
41
+
42
+ /** Each closed session's last regular bar, by the session's local date (a later bar is after-hours). */
43
+ function lastRegularBars(points: readonly PricePoint[], venue: string | undefined, now: number): Map<string, number> {
44
+ const zone = sessionCalendarTimeZone(venue);
45
+ const latest = latestRegularSessionClose(venue, now);
46
+ const last = new Map<string, number>();
47
+ if (!zone || !latest) return last;
48
+ const closeOn = sessionCloses(venue);
49
+ points.forEach((point, index) => {
50
+ const time = barTime(point);
51
+ if (!Number.isFinite(time)) return;
52
+ const date = zonedDateKey(time, zone);
53
+ if (date > latest.date) return;
54
+ const close = closeOn(date);
55
+ if (close === null || time >= close) return;
56
+ last.set(date, index);
57
+ });
58
+ return last;
59
+ }
60
+
61
+ /**
62
+ * The newest regular session that has closed by `now` (the venue's calendar),
63
+ * when the bars hold a regular bar of it: the session whose official close a
64
+ * quote is read for. Null while the venue trades past its last session's
65
+ * bars, for venues without sessions, and when the bars end earlier.
66
+ */
67
+ export function newestClosedSession(points: readonly PricePoint[], options: { venue: string | undefined; now: number }): ClosedSession | null {
68
+ const latest = latestRegularSessionClose(options.venue, options.now);
69
+ if (!latest) return null;
70
+ const index = lastRegularBars(points, options.venue, options.now).get(latest.date);
71
+ return index === undefined ? null : { date: latest.date, lastTrade: points[index]!.close };
72
+ }
73
+
74
+ /** What the newest closed session's last bar ends on: a close, the note that its print is to come, or nothing to change. */
75
+ export type NewestSessionClose = { close: number } | { pending: true } | null;
76
+
77
+ /**
78
+ * The close the newest closed session ends on, for its last regular bar
79
+ * (`session`, from newestClosedSession). In order:
80
+ *
81
+ * 1. The quote's official close of that session (`regularClose`, dated by
82
+ * `regularCloseSessionDate` or the quote's trading day).
83
+ * 2. Right after a US close (POST) without it, the note that the print is to
84
+ * come: the quote's price then is the last trade before the auction.
85
+ * 3. Otherwise (the venue closed, the next pre-market, a listing that
86
+ * publishes no closing print) the session's finished daily bar, then the
87
+ * quote's own close: its price once closed, its previous close in the
88
+ * next pre-market.
89
+ *
90
+ * Every figure from the quote must be the bars' listing (same venue, same
91
+ * currency where both are known), in the bars' units, and within 25% of the
92
+ * session's last trade; one that is not is not used. A quote of the session
93
+ * still trading, or none, leaves the daily bar's close or nothing.
94
+ */
95
+ export function newestSessionClose(
96
+ quote: Quote | null | undefined,
97
+ session: ClosedSession & {
98
+ venue: string | undefined;
99
+ /** The listing's currency, where known. */
100
+ currency?: string | null;
101
+ /** The session's finished daily bar's close, where one was read. */
102
+ dailyClose?: number | null;
103
+ },
104
+ ): NewestSessionClose {
105
+ const daily = positive(session.dailyClose) && nearLastTrade(session.dailyClose, session.lastTrade) ? { close: session.dailyClose } : null;
106
+ if (!quote || quote.marketState === "REGULAR") return daily;
107
+ const quoteVenue = quote.listingExchangeName || quote.exchangeName;
108
+ if (quoteVenue && session.venue && canonicalExchange(quoteVenue) !== canonicalExchange(session.venue)) return daily;
109
+ if (session.currency && quote.currency && session.currency !== quote.currency) return daily;
110
+ const usable = (value: number | undefined): value is number => positive(value)
111
+ && !hasLikelyQuoteUnitMismatch({ currency: quote.currency, price: session.lastTrade }, { currency: quote.currency, price: value })
112
+ && nearLastTrade(value, session.lastTrade);
113
+ const official = positive(quote.regularClose) && (quote.regularCloseSessionDate ?? quoteTradingDay(quote)) === session.date
114
+ ? quote.regularClose : undefined;
115
+ if (official !== undefined) return usable(official) ? { close: official } : daily;
116
+ if (quote.marketState === "POST" && isUsListingExchange(canonicalExchange(session.venue))) return { pending: true };
117
+ if (daily) return daily;
118
+ if (quote.marketState === "PRE" || quote.marketState === "PREPRE") {
119
+ const previous = quote.previousClose;
120
+ return (!quote.previousCloseDate || quote.previousCloseDate === session.date) && usable(previous) ? { close: previous } : null;
121
+ }
122
+ return quoteTradingDay(quote) === session.date && usable(quote.price) ? { close: quote.price } : null;
123
+ }
124
+
125
+ /**
126
+ * Intraday bars of a listing that trades in sessions, each closed session's
127
+ * last regular bar closing on that session's official close (`closes`, by
128
+ * the venue's local date) instead of the last trade before the closing
129
+ * auction: an hourly 19:30 UTC bar of a thin ETF ends on a 19:38 trade at
130
+ * 12.31 while the 20:00 closing print is 12.19, which every total measured
131
+ * from the bars then misses. The bar's high and low widen to take the close.
132
+ * A close more than 25% off the bar's own is another unit's, and not taken.
133
+ *
134
+ * A session still trading, bars after its close (after-hours) and venues
135
+ * without sessions are left as served; the points array is the same one when
136
+ * nothing changed.
137
+ */
138
+ export function withOfficialCloses(
139
+ points: PricePoint[],
140
+ closes: ReadonlyMap<string, number>,
141
+ options: { venue: string | undefined; now: number },
142
+ ): PricePoint[] {
143
+ if (points.length === 0 || closes.size === 0) return points;
144
+ let changed = false;
145
+ const next = points.slice();
146
+ for (const [date, index] of lastRegularBars(points, options.venue, options.now)) {
147
+ const close = closes.get(date);
148
+ const bar = next[index]!;
149
+ if (!positive(close) || bar.close === close || !nearLastTrade(close, bar.close)) continue;
150
+ next[index] = {
151
+ ...bar,
152
+ close,
153
+ ...(positive(bar.high) ? { high: Math.max(bar.high, close) } : {}),
154
+ ...(positive(bar.low) ? { low: Math.min(bar.low, close) } : {}),
155
+ };
156
+ changed = true;
157
+ }
158
+ return changed ? next : points;
159
+ }
@@ -1,9 +1,10 @@
1
1
  import { scalarPointValue } from "./alignment";
2
2
  import { getTimeSeriesField } from "./field-catalog";
3
3
  import type { ChartSpec, ResolvedSeries, TimeSeriesPoint } from "./types";
4
- import { chartBarSizeSource, type ManualChartResolution } from "./resolution";
4
+ import { CHART_RESOLUTION_STEP_MS, chartBarSizeSource, getChartResolutionLabel, isIntradayResolution, type ManualChartResolution } from "./resolution";
5
5
  import type { TimeRange } from "./range";
6
6
  import { zonedDateTimeParts } from "../utils/zoned-date-time";
7
+ import { priceComparisonCloseTimes } from "./coin-session-close";
7
8
  import { barLastSession } from "../market-data/market/bar-period";
8
9
  import {
9
10
  barCloseDay, isIntradayDailyBar, isReturnPeriod, LATEST_INTRADAY_TEXT, periodBaseDay, periodBaseIndex, periodDayText,
@@ -52,6 +53,10 @@ export interface PriceComparisonOptions {
52
53
  */
53
54
  preset?: TimeRange | null;
54
55
  now?: number;
56
+ /** Coin legs whose daily closes were taken at the session venue's close (coinLegsAtSessionClose). */
57
+ atSessionClose?: ReadonlySet<string>;
58
+ /** Of those, the legs whose newest day ends at a session close that has passed: not a price still moving. */
59
+ closedEnds?: ReadonlySet<string>;
55
60
  }
56
61
 
57
62
  export function observationDate(time: number, series: Pick<ResolvedSeries, "timeBasis">): string {
@@ -63,7 +68,24 @@ export function observationDate(time: number, series: Pick<ResolvedSeries, "time
63
68
  return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
64
69
  }
65
70
 
66
- /** Date-only research windows select local sessions; runtime timestamps remain exact. */
71
+ /**
72
+ * The day the weekly or monthly bar holding `date` is dated by: its week's
73
+ * Monday or its month's 1st, as `history --from` keeps the week or month its
74
+ * first date falls in. A daily bar is its own day.
75
+ */
76
+ function windowBarStart(date: string, resolution: "1d" | "1wk" | "1mo"): string {
77
+ if (resolution === "1mo") return `${date.slice(0, 7)}-01`;
78
+ if (resolution === "1d") return date;
79
+ const day = Date.parse(`${date}T00:00:00Z`);
80
+ return new Date(day - ((new Date(day).getUTCDay() + 6) % 7) * 86_400_000).toISOString().slice(0, 10);
81
+ }
82
+
83
+ /**
84
+ * Date-only research windows select local sessions; runtime timestamps remain
85
+ * exact. Weekly and monthly bars keep the bar the window starts in: a window
86
+ * from 2019-12-31 in monthly bars starts with December 2019's, whose close is
87
+ * that day's, so a comparison is measured from it.
88
+ */
67
89
  export function priceObservationWindowFilter(
68
90
  series: ResolvedSeries,
69
91
  bounds: { start: number | null; end: number | null },
@@ -74,11 +96,12 @@ export function priceObservationWindowFilter(
74
96
  const calendarWindow = calendarBars && dateWindow
75
97
  && /^\d{4}-\d{2}-\d{2}$/.test(dateWindow.start)
76
98
  && /^\d{4}-\d{2}-\d{2}$/.test(dateWindow.end) ? dateWindow : null;
99
+ const start = calendarWindow ? windowBarStart(calendarWindow.start, resolution as "1d" | "1wk" | "1mo") : null;
77
100
  return (time) => {
78
101
  if (!Number.isFinite(time)) return false;
79
102
  const key = calendarWindow ? observationDate(time, series) : null;
80
103
  return key !== null
81
- ? key >= calendarWindow!.start && key <= calendarWindow!.end
104
+ ? key >= start! && key <= calendarWindow!.end
82
105
  : (bounds.start === null || time >= bounds.start) && (bounds.end === null || time <= bounds.end);
83
106
  };
84
107
  }
@@ -138,6 +161,28 @@ export function totalReturnLevelsNotice(spec: ChartSpec, series: readonly Resolv
138
161
  priceOnly.length ? ` Price only for ${priceOnly.join(", ")}: distribution history unavailable.` : ""}`;
139
162
  }
140
163
 
164
+ /**
165
+ * The bar size intraday legs are matched in: the chart's, or when it has none
166
+ * the coarsest the legs were served in. Null when neither is intraday.
167
+ */
168
+ function intradayBucketMs(resolution: ManualChartResolution | "auto", series: ReadonlyArray<ResolvedSeries | undefined>): number | null {
169
+ const sizes = [resolution, ...series.map((entry) => entry?.historyResolution)]
170
+ .filter((size): size is ManualChartResolution => !!size && size !== "auto" && size in CHART_RESOLUTION_STEP_MS && isIntradayResolution(size as ManualChartResolution));
171
+ if (resolution !== "auto" && sizes[0] === resolution) return CHART_RESOLUTION_STEP_MS[resolution];
172
+ return sizes.length ? Math.max(...sizes.map((size) => CHART_RESOLUTION_STEP_MS[size])) : null;
173
+ }
174
+
175
+ /** `1H` for an hour's bucket, as the chart names its bar size. */
176
+ function barLabel(bucketMs: number): string {
177
+ const size = (Object.keys(CHART_RESOLUTION_STEP_MS) as ManualChartResolution[]).find((key) => CHART_RESOLUTION_STEP_MS[key] === bucketMs);
178
+ return size ? getChartResolutionLabel(size) : `${Math.round(bucketMs / 60_000)}m`;
179
+ }
180
+
181
+ /** `IBIT and BTC-USD`, `A, B and C`. */
182
+ function listLabels(labels: readonly string[]): string {
183
+ return labels.length <= 1 ? labels.join("") : `${labels.slice(0, -1).join(", ")} and ${labels.at(-1)}`;
184
+ }
185
+
141
186
  interface ComparedLeg {
142
187
  id: string;
143
188
  label: string;
@@ -185,6 +230,9 @@ export function resolvePriceComparison(
185
230
  const dropped: NonNullable<PriceComparison["dropped"]> = [];
186
231
  const reported = new Set<string>();
187
232
  const legs: ComparedLeg[] = [];
233
+ // Intraday bars compare by the bar of the chart's size they open in: a fund's 13:30 bar and a coin's 13:00 bar are
234
+ // one hourly bar, so the fund's last bar of the day (19:30, closing at 20:00) meets the coin's bar closing then.
235
+ const bucketMs = calendarBars ? null : intradayBucketMs(resolution, seriesIds.map((id) => byId.get(id)!));
188
236
  for (const id of seriesIds) {
189
237
  const entry = byId.get(id)!;
190
238
  const inWindow = priceObservationWindowFilter(entry, bounds, resolution, dateWindow);
@@ -193,7 +241,7 @@ export function resolvePriceComparison(
193
241
  for (const point of entry.points) {
194
242
  const time = point.date.getTime();
195
243
  if (!Number.isFinite(time) || scalarPointValue(point) === null) continue;
196
- const key = calendarBars ? observationDate(time, entry) : String(time);
244
+ const key = calendarBars ? observationDate(time, entry) : String(bucketMs ? Math.floor(time / bucketMs) * bucketMs : time);
197
245
  points.set(key, point);
198
246
  if (inWindow(time)) window.add(key);
199
247
  }
@@ -219,7 +267,16 @@ export function resolvePriceComparison(
219
267
  return legs.length ? keys(legs[0]!).filter((key) => sets.every((set) => set.has(key))) : [];
220
268
  };
221
269
  const sharedWindow = shared((leg) => leg.window);
222
- const sharedStart = sharedWindow.find((key) => legs.every((leg) => nonzero(leg, key)));
270
+ // Authored dates (`--from 2026-10-19`) measure from the close before the first date, as RETURN does: the last
271
+ // date the legs share before it. Weekly and monthly bars keep the bar the window starts in.
272
+ const authoredFrom = !preset && dateWindow && /^\d{4}-\d{2}-\d{2}$/.test(dateWindow.start) && (resolution === "1d" || !calendarBars)
273
+ ? dateWindow.start : null;
274
+ const firstInWindow = authoredFrom ? legs.map((leg) => leg.window[0]!).sort(order)[0] : undefined;
275
+ const authoredBase = authoredFrom && firstInWindow !== undefined
276
+ ? shared((leg) => leg.keys).filter((key) => (calendarBars ? key < authoredFrom : order(key, firstInWindow) < 0)
277
+ && legs.every((leg) => nonzero(leg, key))).at(-1)
278
+ : undefined;
279
+ const sharedStart = authoredBase ?? sharedWindow.find((key) => legs.every((leg) => nonzero(leg, key)));
223
280
  // The period is measured back from the chart's newest observation, by its session day.
224
281
  const anchor = period && legs.length
225
282
  ? legs.map((leg) => calendarBars ? sessionDay(leg.window.at(-1)!) : dayOf(leg, leg.window.at(-1)!)).sort().at(-1)!
@@ -256,7 +313,14 @@ export function resolvePriceComparison(
256
313
  const droppedText = dropped.length
257
314
  ? ` Left out: ${dropped.map((entry) => reported.has(entry.seriesId) ? entry.label : `${entry.label} (${entry.reason})`).join("; ")}.`
258
315
  : "";
259
- if (!compared.length) return { seriesIds, start: null, end: null, notice: unavailable, ...(dropped.length ? { dropped } : {}) };
316
+ if (!compared.length) {
317
+ // Intraday legs that never trade in the same bar: said, with the bar size that compares them.
318
+ const apart = !calendarBars && legs.length >= 2 && sharedWindow.length === 0 && legs.every((leg) => leg.window.length >= 2);
319
+ const notice = apart
320
+ ? `${basis} Comparison unavailable: ${listLabels(legs.map((leg) => leg.label))} share no ${bucketMs ? `${barLabel(bucketMs)} ` : ""}bar times in this range; daily bars (1D) compare them.`
321
+ : unavailable;
322
+ return { seriesIds, start: null, end: null, notice, ...(dropped.length ? { dropped } : {}) };
323
+ }
260
324
  const sourceBounds = Object.fromEntries(compared.map(({ leg, start, end }) => [leg.id, {
261
325
  start: leg.points.get(start)!.date.getTime(),
262
326
  end: leg.points.get(end)!.date.getTime(),
@@ -266,7 +330,7 @@ export function resolvePriceComparison(
266
330
  const endDay = calendarBars ? sessionDay(end) : dayOf(leg, end);
267
331
  // A daily bar of the session still trading, or a week or month running through it.
268
332
  return [leg.id, { baseDay: calendarBars ? sessionDay(start) : dayOf(leg, start), endDay,
269
- intraday: calendarBars && isIntradayDailyBar(endDay, legVenue(leg.entry), now) }];
333
+ intraday: calendarBars && !options.closedEnds?.has(leg.id) && isIntradayDailyBar(endDay, legVenue(leg.entry), now) }];
270
334
  }));
271
335
  const span = calendarBars
272
336
  ? (() => {
@@ -274,7 +338,10 @@ export function resolvePriceComparison(
274
338
  const to = legsText(compared.map(({ leg }) => ({ label: leg.label, text: legDays[leg.id]!.intraday
275
339
  ? `${periodDayText(legDays[leg.id]!.endDay, currentYear)}, ${LATEST_INTRADAY_TEXT}`
276
340
  : `${periodDayText(legDays[leg.id]!.endDay, currentYear)} close` })));
277
- return `From ${from} to ${to}; market session times may differ.`;
341
+ // A coin beside a listing that closes each day says when it is measured: at that close, or at 00:00 UTC.
342
+ const timing = priceComparisonCloseTimes({ legs: legDays }, compared.map(({ leg }) => leg.entry),
343
+ new Map(compared.map(({ leg }) => [leg.id, leg.label])), options.atSessionClose);
344
+ return `From ${from} to ${to}; ${timing ?? "market session times may differ"}.`;
278
345
  })()
279
346
  : baseDay && sharedBase !== undefined
280
347
  ? `From ${periodDayText(legDays[compared[0]!.leg.id]!.baseDay, currentYear)} close to ${displayDate(Math.max(...Object.values(sourceBounds).map((range) => range.end)))}.`