gloomberb 0.19.2 → 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 (472) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -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 +20 -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.tsx +23 -1
  20. package/src/architecture/core-builtin-allowlist.json +2 -1
  21. package/src/capabilities/factories.ts +1 -0
  22. package/src/cli/ascii-output.ts +42 -0
  23. package/src/cli/base-converter.ts +10 -2
  24. package/src/cli/command-options.ts +16 -1
  25. package/src/cli/commands/crypto-hints.ts +23 -4
  26. package/src/cli/commands/market.ts +92 -23
  27. package/src/cli/commands/news-search.ts +24 -5
  28. package/src/cli/commands/overview.ts +9 -4
  29. package/src/cli/commands/search.ts +71 -12
  30. package/src/cli/commands/system.ts +22 -30
  31. package/src/cli/commands/ticker.ts +49 -9
  32. package/src/cli/commands/version.ts +33 -0
  33. package/src/cli/context.ts +5 -9
  34. package/src/cli/data-dir.ts +7 -0
  35. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  36. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  37. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  38. package/src/cli/desktop-pane-shot.ts +196 -256
  39. package/src/cli/early-command.ts +42 -0
  40. package/src/cli/earnings-calendar.ts +9 -0
  41. package/src/cli/entry.ts +19 -13
  42. package/src/cli/errors.ts +12 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/history-export.ts +22 -2
  45. package/src/cli/history-rows.ts +154 -4
  46. package/src/cli/index.ts +18 -1
  47. package/src/cli/known-gaps.ts +40 -5
  48. package/src/cli/listing-arg.ts +46 -9
  49. package/src/cli/options.ts +1 -1
  50. package/src/cli/output-file.ts +60 -13
  51. package/src/cli/pane-functions/catalog.ts +14 -3
  52. package/src/cli/pane-functions/data.ts +1 -1
  53. package/src/cli/pane-functions/dom.ts +45 -0
  54. package/src/cli/pane-functions/freshness.ts +46 -9
  55. package/src/cli/pane-functions/function-help.ts +17 -9
  56. package/src/cli/pane-functions/headless.ts +22 -14
  57. package/src/cli/pane-functions/index.ts +6 -2
  58. package/src/cli/pane-functions/options.ts +6 -2
  59. package/src/cli/pane-functions/resolver.ts +27 -5
  60. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  61. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  62. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  63. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  64. package/src/cli/quote-answer.ts +20 -0
  65. package/src/cli/registry.ts +41 -13
  66. package/src/cli/result.ts +12 -6
  67. package/src/cli/ticker-resolution.ts +2 -1
  68. package/src/cli/types.ts +2 -0
  69. package/src/components/chart/composite/format.ts +3 -0
  70. package/src/components/chart-table/header.tsx +1 -1
  71. package/src/components/chart-table/layout.ts +28 -11
  72. package/src/components/command-bar/action-runtime.ts +4 -0
  73. package/src/components/command-bar/commands/direct/index.ts +18 -33
  74. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  75. package/src/components/command-bar/commands/registry.ts +10 -1
  76. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  77. package/src/components/command-bar/routes/root/runtime.ts +24 -1
  78. package/src/components/command-bar/surface/index.tsx +9 -3
  79. package/src/components/company-logo.tsx +2 -1
  80. package/src/components/layout/header.tsx +8 -3
  81. package/src/components/layout/market-summary.ts +25 -7
  82. package/src/components/layout/pane/footer/index.tsx +7 -2
  83. package/src/components/layout/pane/footer/model.ts +40 -5
  84. package/src/components/market-board/index.tsx +3 -2
  85. package/src/components/onboarding/desks-step.tsx +12 -4
  86. package/src/components/onboarding/first-run-workspace.ts +81 -4
  87. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  88. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  89. package/src/components/onboarding/onboarding-wizard.tsx +54 -23
  90. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  91. package/src/components/onboarding/run-setup-again.ts +24 -0
  92. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  93. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  94. package/src/components/onboarding/wizard-model.ts +57 -3
  95. package/src/components/onboarding/wizard-positions.ts +1 -1
  96. package/src/components/ui/delayed-mark.tsx +56 -0
  97. package/src/components/ui/external-link.tsx +3 -6
  98. package/src/components/ui/icon.tsx +8 -1
  99. package/src/components/ui/logo-mark.tsx +8 -5
  100. package/src/components/ui/stat-grid.tsx +263 -19
  101. package/src/data/config/store/node.ts +11 -5
  102. package/src/data/config/store/normalize.ts +16 -3
  103. package/src/data/profile-permissions.ts +47 -0
  104. package/src/data/resource-store.ts +34 -31
  105. package/src/data/sqlite/access.ts +19 -8
  106. package/src/data/sqlite/checkpointer.ts +95 -8
  107. package/src/data/sqlite/database.ts +40 -7
  108. package/src/docs/snapshot.json +191 -78
  109. package/src/i18n/es.ts +18 -2
  110. package/src/i18n/ja.ts +18 -2
  111. package/src/i18n/ko.ts +18 -2
  112. package/src/i18n/zh-cn.ts +18 -2
  113. package/src/i18n/zh-tw.ts +18 -2
  114. package/src/layout/desk-keys.ts +4 -1
  115. package/src/layout/desks.ts +38 -5
  116. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  117. package/src/market-data/batched-reads.ts +16 -0
  118. package/src/market-data/coordinator/auxiliary.ts +4 -2
  119. package/src/market-data/coordinator/index.ts +11 -2
  120. package/src/market-data/futures-board.ts +1 -1
  121. package/src/market-data/futures-contract.ts +3 -1
  122. package/src/market-data/market/index-level.ts +6 -4
  123. package/src/market-data/published-apac-sessions.ts +6 -0
  124. package/src/news/aggregator.ts +7 -4
  125. package/src/news/news-model.ts +11 -0
  126. package/src/news/story-id.ts +22 -0
  127. package/src/news/ticker-symbols.ts +17 -1
  128. package/src/notifications/app-notifier.ts +35 -14
  129. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  130. package/src/plugins/builtin/account-management/index.tsx +2 -3
  131. package/src/plugins/builtin/account-management/model.ts +1 -1
  132. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  133. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  134. package/src/plugins/builtin/alerts/events.ts +10 -2
  135. package/src/plugins/builtin/alerts/index.tsx +5 -5
  136. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  137. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  138. package/src/plugins/builtin/alerts/targets.ts +4 -1
  139. package/src/plugins/builtin/attention/headless.ts +1 -1
  140. package/src/plugins/builtin/attention/index.tsx +2 -4
  141. package/src/plugins/builtin/attention/listing.ts +1 -1
  142. package/src/plugins/builtin/attention/series.ts +1 -1
  143. package/src/plugins/builtin/awards/index.tsx +1 -2
  144. package/src/plugins/builtin/backtest/client.ts +1 -0
  145. package/src/plugins/builtin/backtest/index.tsx +1 -2
  146. package/src/plugins/builtin/backtest/rules.ts +3 -1
  147. package/src/plugins/builtin/backtest/window.ts +8 -7
  148. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  149. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  150. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  151. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  152. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  153. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  154. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  155. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  156. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  157. package/src/plugins/builtin/buildout/format.ts +9 -0
  158. package/src/plugins/builtin/buildout/index.tsx +51 -5
  159. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  160. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  161. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  162. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  163. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  164. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  165. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  166. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  167. package/src/plugins/builtin/cds/headless.ts +10 -2
  168. package/src/plugins/builtin/cds/model.ts +7 -3
  169. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  170. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  171. package/src/plugins/builtin/changelog/index.tsx +3 -2
  172. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
  173. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  174. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  175. package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
  176. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  177. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  178. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  179. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  180. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  181. package/src/plugins/builtin/chat/index.tsx +5 -5
  182. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  183. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  184. package/src/plugins/builtin/cloud/index.tsx +1 -2
  185. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  186. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  187. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  188. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  189. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  190. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  191. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  192. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  193. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  194. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  195. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  196. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  197. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  198. package/src/plugins/builtin/connections/index.ts +2 -3
  199. package/src/plugins/builtin/cot/index.tsx +1 -2
  200. package/src/plugins/builtin/cpi/index.tsx +1 -2
  201. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  202. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  203. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  204. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  205. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  206. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  207. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  208. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  209. package/src/plugins/builtin/crypto-board/series.ts +2 -0
  210. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  211. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  212. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  213. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  214. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  215. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  216. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  217. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  218. package/src/plugins/builtin/debug/index.tsx +1 -1
  219. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  220. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  221. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  222. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  223. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  224. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  225. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  226. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  227. package/src/plugins/builtin/docs/index.tsx +2 -3
  228. package/src/plugins/builtin/doe/headless.ts +16 -3
  229. package/src/plugins/builtin/doe/index.tsx +5 -4
  230. package/src/plugins/builtin/earnings/board.tsx +2 -1
  231. package/src/plugins/builtin/earnings/history.tsx +2 -1
  232. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  233. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  234. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  235. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  236. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  237. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  238. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  239. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  240. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  241. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  242. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  243. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  244. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  245. package/src/plugins/builtin/exposure/index.tsx +1 -2
  246. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  247. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  248. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  249. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  250. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  251. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  252. package/src/plugins/builtin/futures/index.tsx +15 -0
  253. package/src/plugins/builtin/futures/model.ts +14 -0
  254. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  255. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  256. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  257. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  258. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  259. package/src/plugins/builtin/gpu/board.tsx +51 -12
  260. package/src/plugins/builtin/gpu/client.ts +15 -2
  261. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  262. package/src/plugins/builtin/gpu/headless.ts +14 -2
  263. package/src/plugins/builtin/gpu/index.tsx +1 -2
  264. package/src/plugins/builtin/gpu/model.ts +20 -2
  265. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  266. package/src/plugins/builtin/help/index.tsx +1 -1
  267. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  268. package/src/plugins/builtin/holders/index.tsx +2 -3
  269. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  270. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  271. package/src/plugins/builtin/jodi/index.tsx +1 -2
  272. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  273. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  274. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  275. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  276. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  277. package/src/plugins/builtin/market-movers/index.tsx +7 -1
  278. package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
  279. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  280. package/src/plugins/builtin/members/index.tsx +1 -2
  281. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  282. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  283. package/src/plugins/builtin/news/headless.ts +7 -0
  284. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  285. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  286. package/src/plugins/builtin/notes/index.tsx +3 -6
  287. package/src/plugins/builtin/openfda/index.tsx +1 -2
  288. package/src/plugins/builtin/options/view.tsx +6 -5
  289. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  290. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  291. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  292. package/src/plugins/builtin/perps/index.tsx +1 -2
  293. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  294. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  295. package/src/plugins/builtin/plugin-module.ts +16 -2
  296. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  297. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  298. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  299. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  300. package/src/plugins/builtin/power/index.tsx +1 -2
  301. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  302. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  303. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  304. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  305. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  306. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  307. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  308. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  309. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  310. package/src/plugins/builtin/research/index.tsx +5 -8
  311. package/src/plugins/builtin/research-search/index.tsx +1 -2
  312. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  313. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  314. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  315. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  316. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  317. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  318. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  319. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  320. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  321. package/src/plugins/builtin/sec/headless.ts +5 -1
  322. package/src/plugins/builtin/sec/index.tsx +4 -2
  323. package/src/plugins/builtin/sec/model.ts +2 -0
  324. package/src/plugins/builtin/sectors/index.tsx +11 -5
  325. package/src/plugins/builtin/series-inventory.ts +2 -0
  326. package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
  327. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  328. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  329. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  330. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  331. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  332. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  333. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  334. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  335. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  336. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  337. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  338. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  339. package/src/plugins/builtin/thirteenf/api.ts +116 -13
  340. package/src/plugins/builtin/thirteenf/data.ts +132 -30
  341. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  342. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  343. package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
  344. package/src/plugins/builtin/thirteenf/types.ts +12 -0
  345. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
  346. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  347. package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
  348. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  349. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  350. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  351. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  352. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  353. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  354. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  355. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  357. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  358. package/src/plugins/builtin/volatility/index.tsx +1 -2
  359. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  360. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  361. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  362. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  363. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  364. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  365. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  366. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  367. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  368. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  369. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  370. package/src/plugins/bundle.ts +52 -2
  371. package/src/plugins/catalog-browser.ts +2 -2
  372. package/src/plugins/catalog-cli.ts +20 -0
  373. package/src/plugins/host-link.ts +13 -1
  374. package/src/plugins/installer.ts +15 -4
  375. package/src/plugins/loader.ts +43 -11
  376. package/src/plugins/peer-imports.ts +201 -0
  377. package/src/plugins/peer-retry.ts +26 -29
  378. package/src/plugins/plugin-process.ts +128 -0
  379. package/src/plugins/redirection-guard.ts +45 -0
  380. package/src/remote/endpoint.ts +16 -3
  381. package/src/renderers/browser/main.tsx +3 -2
  382. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  383. package/src/renderers/dom/desktop/icons.tsx +2 -0
  384. package/src/renderers/dom/host/image-surface.tsx +46 -21
  385. package/src/renderers/dom/styles.css +7 -0
  386. package/src/renderers/dom/toast-host.tsx +2 -0
  387. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  388. package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
  389. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  390. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  391. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  392. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  393. package/src/renderers/electrobun/bun/window/create.ts +4 -2
  394. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  395. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  396. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  397. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  398. package/src/renderers/electrobun/view/main.tsx +7 -2
  399. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  400. package/src/renderers/opentui/browser-open.ts +10 -4
  401. package/src/renderers/opentui/image/surface.tsx +1 -1
  402. package/src/renderers/opentui/start.tsx +12 -6
  403. package/src/renderers/opentui/toast-host.tsx +2 -1
  404. package/src/series/registry.ts +18 -0
  405. package/src/series/standalone.ts +10 -1
  406. package/src/series/types.ts +7 -0
  407. package/src/series/validate.ts +11 -2
  408. package/src/sources/gloomberb-cloud/index.ts +28 -5
  409. package/src/sources/gloomberb-cloud/news.ts +24 -3
  410. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  411. package/src/sources/provider-errors.ts +26 -3
  412. package/src/sources/provider-router/history.ts +5 -0
  413. package/src/sources/provider-router/index.ts +13 -0
  414. package/src/sync/core-contributors.ts +32 -0
  415. package/src/telemetry/attention-counts.ts +64 -6
  416. package/src/telemetry/attention-setting.ts +33 -0
  417. package/src/telemetry/crash-reports-node.ts +3 -1
  418. package/src/telemetry/stall-clocks.ts +93 -0
  419. package/src/telemetry/stall-reports-node.ts +12 -7
  420. package/src/telemetry/stall-reports.ts +34 -5
  421. package/src/test-support/preload.ts +9 -0
  422. package/src/tickers/company-name.ts +19 -0
  423. package/src/tickers/listing-fate.ts +14 -3
  424. package/src/tickers/saved-instrument.ts +17 -0
  425. package/src/tickers/search/home-listing.ts +13 -2
  426. package/src/tickers/search/index.ts +4 -1
  427. package/src/tickers/search/types.ts +6 -1
  428. package/src/time-series/bar-aggregation.ts +7 -1
  429. package/src/time-series/base-series.ts +3 -3
  430. package/src/time-series/chart-bounds.ts +1 -1
  431. package/src/time-series/chart-data.ts +5 -2
  432. package/src/time-series/chart-seed.ts +1 -1
  433. package/src/time-series/coin-session-close.ts +211 -0
  434. package/src/time-series/live-bars.ts +13 -2
  435. package/src/time-series/live-quotes.ts +1 -1
  436. package/src/time-series/official-close.ts +159 -0
  437. package/src/time-series/price-comparison.ts +55 -6
  438. package/src/time-series/resolve.ts +12 -3
  439. package/src/time-series/series-presentation.ts +1 -1
  440. package/src/time-series/studies/formula.ts +205 -0
  441. package/src/time-series/studies/indicators.ts +299 -0
  442. package/src/time-series/studies/leverage.ts +75 -0
  443. package/src/time-series/studies/pair.ts +138 -0
  444. package/src/time-series/studies/resolve.ts +333 -0
  445. package/src/time-series/studies/shared.ts +141 -0
  446. package/src/time-series/studies/units.ts +243 -0
  447. package/src/time-series/studies/vwap.ts +274 -0
  448. package/src/types/config.ts +18 -2
  449. package/src/types/data-provider.ts +23 -0
  450. package/src/types/financials.ts +22 -0
  451. package/src/types/headless.ts +42 -3
  452. package/src/types/plugin.ts +32 -3
  453. package/src/ui/host.tsx +11 -0
  454. package/src/ui/toast.tsx +26 -1
  455. package/src/utils/ascii-text.ts +140 -0
  456. package/src/utils/cli-output.ts +91 -18
  457. package/src/utils/concurrency.ts +24 -0
  458. package/src/utils/exchanges.ts +6 -0
  459. package/src/utils/external-url.ts +20 -7
  460. package/src/utils/futures-generic.ts +4 -4
  461. package/src/utils/http-proxy-upstream.ts +124 -0
  462. package/src/utils/main-thread-stall.ts +75 -3
  463. package/src/utils/perf-marks.ts +36 -2
  464. package/src/utils/private-files.ts +141 -0
  465. package/src/utils/public-address.ts +99 -0
  466. package/src/account/upgrade-personalization.ts +0 -57
  467. package/src/account/upgrade-tickers.ts +0 -124
  468. package/src/api-client/upgrade-experiment.ts +0 -91
  469. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  470. package/src/renderers/electrobun/launcher/preload.ts +0 -13
  471. package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
  472. package/src/time-series/studies.ts +0 -1628
@@ -28,8 +28,12 @@ import { reportCrash } from "./crash-reports";
28
28
  * for the desktop app's own process, whether the app was in front and had a
29
29
  * window on screen: a hidden app's slowed timers read like a freeze. A stall
30
30
  * after a SQLite statement names the statement (a fixed name), its tries and
31
- * whether it waited on another program's lock. All of it is buckets and
32
- * flags.
31
+ * whether it waited on another program's lock. Where the platform's clocks
32
+ * can be read, it says how much of the stall the thread spent on the CPU
33
+ * and how busy the process's other threads were, which tells script or a
34
+ * collection (busy) from a wait on the disk, a lock or another thread
35
+ * (idle); the time the computer slept never counts as a stall (see
36
+ * utils/main-thread-stall.ts). All of it is buckets and flags.
33
37
  *
34
38
  * One report per process, bucket, section and app version a day; stalls of
35
39
  * the same kind after that are counted and carried in the next report, and
@@ -66,6 +70,10 @@ export interface StallReport {
66
70
  attempts?: number;
67
71
  /** After a SQLite statement: whether it waited on another connection's lock. */
68
72
  lockWait?: boolean;
73
+ /** Where the platform says: the share of the stall the thread was on the CPU, in percent, by tens. */
74
+ cpuPercent?: number;
75
+ /** Where the platform says: the CPU the process's other threads used meanwhile, in percent of one core, by tens. */
76
+ otherCpuPercent?: number;
69
77
  }
70
78
 
71
79
  type StallBucket = "1-2s" | "2-5s" | "5-15s" | "15s+";
@@ -115,6 +123,8 @@ const MAX_KINDS_KEPT = 200;
115
123
  const MAX_RECENT_KEPT = 500;
116
124
  /** What error tracking keeps of the details line; fields that do not fit are left out. */
117
125
  const MAX_DETAILS_LENGTH = 120;
126
+ /** Other threads' CPU past this many percent of a core reads as this. */
127
+ const MAX_OTHER_CPU_PERCENT = 990;
118
128
 
119
129
  let host: StallReporterHost | null = null;
120
130
  let attentionProbe: (() => StallAttention) | null = null;
@@ -198,20 +208,32 @@ function flag(value: boolean | null | undefined): string {
198
208
  return value == null ? "?" : value ? "1" : "0";
199
209
  }
200
210
 
211
+ /** A share or a core count in percent, by tens. */
212
+ function tens(value: number | undefined, max: number): number | undefined {
213
+ if (value === undefined || !Number.isFinite(value)) return undefined;
214
+ return Math.min(max, Math.max(0, Math.round(value * 10) * 10));
215
+ }
216
+
201
217
  /** The context a report carries, also written at the end of the log line. */
202
218
  interface StallContext {
203
219
  phase: StallReport["phase"];
204
220
  uptime: StallReport["uptime"];
205
221
  gap: GapBucket;
206
222
  attention: StallAttention | null;
223
+ cpuPercent?: number;
224
+ otherCpuPercent?: number;
207
225
  }
208
226
 
209
227
  function stallContext(event: StallEvent, attention: StallAttention | null): StallContext {
228
+ const cpuPercent = tens(event.threadCpu, 100);
229
+ const otherCpuPercent = tens(event.otherCpu, MAX_OTHER_CPU_PERCENT);
210
230
  return {
211
231
  phase: event.phase ?? "unknown",
212
232
  uptime: uptimeBucket(event.uptimeMs),
213
233
  gap: gapBucket(event),
214
234
  attention,
235
+ ...(cpuPercent === undefined ? {} : { cpuPercent }),
236
+ ...(otherCpuPercent === undefined ? {} : { otherCpuPercent }),
215
237
  };
216
238
  }
217
239
 
@@ -229,6 +251,8 @@ function formatStallLogLine(process: StallProcess, event: StallEvent, context: S
229
251
  `up=${context.uptime}`,
230
252
  `gap=${context.gap}`,
231
253
  ...(context.attention ? [`fg=${flag(context.attention.active)}`, `vis=${flag(context.attention.visible)}`] : []),
254
+ ...(context.cpuPercent === undefined ? [] : [`cpu=${context.cpuPercent}`]),
255
+ ...(context.otherCpuPercent === undefined ? [] : [`oth=${context.otherCpuPercent}`]),
232
256
  ].join(" ");
233
257
  return `${time} ${process.padEnd(8)} froze ${formatSeconds(event.blockedMs).padStart(6)} ${last}${sqlite} [${facts}]`;
234
258
  }
@@ -245,23 +269,26 @@ function reportArch(arch: string): StallReport["arch"] {
245
269
  }
246
270
 
247
271
  /**
248
- * The details line error tracking keeps, most telling first: the original
249
- * four fields, then the new ones. One that would not fit is left out.
272
+ * The details line error tracking keeps, most telling first. The platform
273
+ * comes last: error tracking has the system and its architecture already.
274
+ * One that would not fit is left out.
250
275
  */
251
276
  export function formatStallReportDetails(report: StallReport): string {
252
277
  const fields = [
253
278
  `process=${report.process}`,
254
279
  `count=${report.count}`,
255
280
  `longest=${report.longestMs}ms`,
256
- `platform=${report.platform}-${report.arch}`,
257
281
  `phase=${report.phase}`,
258
282
  `up=${report.uptime}`,
259
283
  `gap=${report.gap}`,
260
284
  ...(report.active === undefined ? [] : [`fg=${flag(report.active)}`]),
261
285
  ...(report.visible === undefined ? [] : [`vis=${flag(report.visible)}`]),
286
+ ...(report.cpuPercent === undefined ? [] : [`cpu=${report.cpuPercent}`]),
287
+ ...(report.otherCpuPercent === undefined ? [] : [`oth=${report.otherCpuPercent}`]),
262
288
  ...(report.lockWait === undefined ? [] : [`lock=${flag(report.lockWait)}`]),
263
289
  ...(report.attempts === undefined ? [] : [`try=${report.attempts}`]),
264
290
  `day=${report.day}`,
291
+ `platform=${report.platform}-${report.arch}`,
265
292
  ];
266
293
  let details = "";
267
294
  for (const field of fields) {
@@ -357,6 +384,8 @@ export function recordStall(process: StallProcess, event: StallEvent): void {
357
384
  uptime: context.uptime,
358
385
  gap: context.gap,
359
386
  ...(context.attention ? { active: context.attention.active, visible: context.attention.visible } : {}),
387
+ ...(context.cpuPercent === undefined ? {} : { cpuPercent: context.cpuPercent }),
388
+ ...(context.otherCpuPercent === undefined ? {} : { otherCpuPercent: context.otherCpuPercent }),
360
389
  ...(event.sqlite && section.startsWith("sqlite.blocked")
361
390
  ? { attempts: Math.max(1, Math.min(99, Math.round(event.sqlite.attempts))), lockWait: event.sqlite.lockWait }
362
391
  : {}),
@@ -1,5 +1,6 @@
1
1
  import { beforeEach } from "bun:test";
2
2
  import { resetCrashReporterForTests } from "../telemetry/crash-reports";
3
+ import { setCliAsciiLocaleOverride, setCliColorEnabledOverride, setCliPlainOverride, setCliWidthOverride } from "../utils/cli-output";
3
4
 
4
5
  /**
5
6
  * `bun test` runs every file in one process, so module state outlives the file
@@ -8,7 +9,15 @@ import { resetCrashReporterForTests } from "../telemetry/crash-reports";
8
9
  * queue and go out with the next reporter another file installs, and its key
9
10
  * would keep a later identical report from being queued at all. Every test
10
11
  * starts with no reporter, an empty queue and nothing seen.
12
+ *
13
+ * Commands also keep output overrides for the process: a parity test that
14
+ * runs --plain would otherwise make a later serializer test keep empty
15
+ * columns. Start each test with the default output modes and terminal width.
11
16
  */
12
17
  beforeEach(() => {
13
18
  resetCrashReporterForTests();
19
+ setCliAsciiLocaleOverride(null);
20
+ setCliColorEnabledOverride(null);
21
+ setCliPlainOverride(null);
22
+ setCliWidthOverride(null);
14
23
  });
@@ -204,6 +204,25 @@ export function groupByCompany<T>(
204
204
  return groups;
205
205
  }
206
206
 
207
+ /**
208
+ * A one-letter code a venue pads onto the end of a name after a run of
209
+ * spaces: the `R` of "Americold Realty Trust Inc. R". A longer word there
210
+ * (`AG`, `ASA`) is the name's own.
211
+ */
212
+ const STRAY_NAME_TAIL = /\s{2,}[A-Za-z0-9]$/;
213
+
214
+ /**
215
+ * A listing's name as a venue's file writes it, without the stray fragment a
216
+ * fixed-width file leaves after a run of spaces ("Americold Realty Trust
217
+ * Inc. R" on Hanover is "Americold Realty Trust Inc."), and with its spaces
218
+ * single. A name that is only the fragment keeps it.
219
+ */
220
+ export function cleanListingName(name: string): string {
221
+ const trimmed = name.trim();
222
+ const cut = trimmed.replace(STRAY_NAME_TAIL, "");
223
+ return (cut || trimmed).replace(/\s+/g, " ");
224
+ }
225
+
207
226
  /** A company's name without its legal form, for sentences: "Avis Budget Group, Inc." is "Avis Budget Group". */
208
227
  export function shortCompanyName(name: string): string {
209
228
  const words = name.trim().split(/\s+/).filter(Boolean);
@@ -132,6 +132,12 @@ export function listingChangeLine(fate: Pick<ListingFate, "change" | "formerIssu
132
132
  export const NO_FATE: ListingFate = { endedOn: null, change: null, note: null, moved: null, formerIssuer: null };
133
133
  /** How long a fate waits for a change lookup it started late, or for a stopped line's. */
134
134
  export const LATE_CHANGE_WAIT_MS = 2_000;
135
+ /**
136
+ * How long a command that says what became of a symbol's company (GOGL's merger) waits for the lookup it started
137
+ * beside its own request, once that is in: the lookup takes about a third of a second, and a cached request answers
138
+ * at once. A lookup that hangs costs a symbol that trades no more than this.
139
+ */
140
+ export const SUCCESSOR_CHANGE_WAIT_MS = 750;
135
141
 
136
142
  // The zones of the US exchanges; a venue in another is abroad.
137
143
  const US_TIME_ZONES = new Set(["America/New_York", "America/Chicago"]);
@@ -195,7 +201,11 @@ export function lineMayHaveMoved(quote: Quote | null, now = Date.now()): boolean
195
201
  export async function loadListingFate(
196
202
  listing: { symbol: string; exchange?: string | null; request?: { symbol: string; exchange: string } },
197
203
  dataProvider: Pick<DataProvider, "getQuote" | "getTickerChanges">,
198
- options: { quote?: Quote | null | Promise<Quote | null>; changes?: TickerChangeLookup } = {},
204
+ /**
205
+ * `graceMs`: how long a lookup the command started waits once the quote is in, for a command that says what
206
+ * became of the company beside what it serves (GOGL's merger), not only whether it moved. A short grace by default.
207
+ */
208
+ options: { quote?: Quote | null | Promise<Quote | null>; changes?: TickerChangeLookup; graceMs?: number } = {},
199
209
  ): Promise<ListingFate> {
200
210
  const request = listing.request ?? { symbol: listing.symbol, exchange: listing.exchange ?? "" };
201
211
  const lookup = options.changes ?? startTickerChangeLookup(dataProvider, listing.symbol, listing.exchange);
@@ -206,7 +216,7 @@ export async function loadListingFate(
206
216
  // A lookup started beside the command's request gets a short grace once that is ready, so a slow
207
217
  // one never holds a symbol that trades. A line that stopped is waited for: only its company can
208
218
  // have moved, and then the answer decides whether the command serves anything at all.
209
- const changes = await lookup.settle(!options.changes || lineMayHaveMoved(quote) ? LATE_CHANGE_WAIT_MS : undefined);
219
+ const changes = await lookup.settle(!options.changes || lineMayHaveMoved(quote) ? LATE_CHANGE_WAIT_MS : options.graceMs);
210
220
  return describeListingFate(listing, quote, changes);
211
221
  } catch (error) {
212
222
  logFailure("tickers-listing-fate", "Could not resolve listing fate")(error);
@@ -230,7 +240,8 @@ export function listingFateNotes(fate: ListingFate, served: { served: boolean; f
230
240
  return [
231
241
  // What it served ends on the listing's last session, and says so.
232
242
  fate.delisted ?? null,
233
- tickerChangeLine(change),
243
+ // A company that left the symbol, else one that merged away from it (GOGL), as `quote` says it.
244
+ listingChangeLine(fate),
234
245
  earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
235
246
  ].filter((line): line is string => !!line);
236
247
  }
@@ -0,0 +1,17 @@
1
+ import type { TickerRecord } from "../types/ticker";
2
+ import { parsePublicTickerKey } from "../utils/exchanges";
3
+
4
+ /**
5
+ * The listing a typed key stands for in a report (`resolveInstrument`): the
6
+ * venue it names (`SBK:JSE`), else the venue of the ticker saved under the key
7
+ * or under its bare symbol, so a saved `S32` held on the JSE is read there.
8
+ * `fn`, `shot` and the panes that read as they do resolve keys this way.
9
+ */
10
+ export async function resolveSavedInstrument(
11
+ key: string,
12
+ loadTicker: (key: string) => Promise<TickerRecord | null | undefined> | TickerRecord | null | undefined,
13
+ ): Promise<{ symbol: string; exchange?: string }> {
14
+ const parsed = parsePublicTickerKey(key);
15
+ const ticker = await loadTicker(key) ?? (key !== parsed.symbol ? await loadTicker(parsed.symbol) : null);
16
+ return { symbol: parsed.symbol, exchange: parsed.exchange ?? ticker?.metadata.exchange };
17
+ }
@@ -122,8 +122,11 @@ function isListedEquity(candidate: TickerSearchCandidate): boolean {
122
122
  * Siemens AG). The row the service marks with the word as typed is the
123
123
  * company the word means, ahead of a line abroad whose symbol is the word:
124
124
  * "siemens" is SIE on XETRA, not SIEMENS on NSE (Siemens Ltd.); a US line
125
- * keeps its ticker. A query that names a venue (LVMH:TSX, 7203.T) asked for
126
- * that line, and a saved row keeps the lead.
125
+ * keeps its ticker. A symbol typed whose first row is another company's
126
+ * regional line abroad leads with the line the service marks as a home under
127
+ * that symbol (YAR is Yara on OSL, not Americold on Hanover). A query that
128
+ * names a venue (LVMH:TSX, 7203.T) asked for that line, and a saved row keeps
129
+ * the lead.
127
130
  */
128
131
  export function withHomeListingLead<T extends TickerSearchCandidate>(items: T[], query: string): T[] {
129
132
  if (isQualifiedTickerQuery(query)) return items;
@@ -137,6 +140,14 @@ export function withHomeListingLead<T extends TickerSearchCandidate>(items: T[],
137
140
  && item.symbol.toUpperCase() !== lead.symbol.toUpperCase() && namedByService(query, item.result))
138
141
  : undefined;
139
142
  if (meant) return moveAhead(items, meant, lead);
143
+ // A line abroad that only quotes shares listed elsewhere (a German regional venue) gives way to the line the
144
+ // service marks as a company's home under the same symbol: YAR is Yara on Oslo, not Americold on Hanover.
145
+ const homeOfSymbol = typedSymbol && lead.result?.homeListing !== true && listingDistance(lead) === 1
146
+ && !isUsListingExchange(candidateExchange(lead))
147
+ ? items.find((item) => item !== lead && isListedEquity(item) && item.result?.homeListing === true
148
+ && item.symbol.toUpperCase() === lead.symbol.toUpperCase())
149
+ : undefined;
150
+ if (homeOfSymbol) return moveAhead(items, homeOfSymbol, lead);
140
151
  const name = candidateName(lead);
141
152
  const lines = companyRows(lead, items).filter((item) => item !== lead);
142
153
  const nearest = (candidates: T[]) => [...candidates].sort((a, b) => nameGap(name, candidateName(a)) - nameGap(name, candidateName(b))
@@ -507,7 +507,10 @@ async function resolveTickerSymbol(
507
507
  throw new AmbiguousContractError(symbol, matches.map((item) => tickerInstrumentLabel(item.symbol, item.result?.brokerContract)));
508
508
  }
509
509
  const listings = new Set(matches.map((item) => publicTickerKey(item.symbol, listingExchange(item.result!))));
510
- const ambiguity = (query: string) => new AmbiguousTickerError(query, [...listings], Object.fromEntries(matches.map((item) => [
510
+ // A line the service marks as a company's home leads the choices: YAR is Yara on Oslo before Americold's German lines.
511
+ const homeFirst = [...matches.filter((item) => item.result?.homeListing === true), ...matches.filter((item) => item.result?.homeListing !== true)];
512
+ const choices = new Set(homeFirst.map((item) => publicTickerKey(item.symbol, listingExchange(item.result!))));
513
+ const ambiguity = (query: string) => new AmbiguousTickerError(query, [...choices], Object.fromEntries(matches.map((item) => [
511
514
  publicTickerKey(item.symbol, listingExchange(item.result!)), listingChoiceName(item.result!),
512
515
  ])));
513
516
  if (pickerQuery && listings.size > 1) throw ambiguity(pickerQuery);
@@ -3,7 +3,12 @@ import type { Quote } from "../../types/financials";
3
3
  import type { TickerRecord } from "../../types/ticker";
4
4
 
5
5
  export type TickerSearchInstrumentClass = "equity" | "fund" | "derivative" | "other";
6
- type TickerSearchCategory = "Saved" | "Primary Listing" | "Other Listings" | "Funds & Derivatives" | "Related";
6
+ /**
7
+ * `Other Matches`: rows `gloom search` finds for the query that are not a listing
8
+ * of the primary listing's company (another company's DK, a line search names
9
+ * no company for), kept apart from its Other Listings.
10
+ */
11
+ type TickerSearchCategory = "Saved" | "Primary Listing" | "Other Listings" | "Other Matches" | "Funds & Derivatives" | "Related";
7
12
 
8
13
  export interface TickerSearchRankableItem {
9
14
  id: string;
@@ -182,6 +182,8 @@ function buildBar(bucket: Bucket, intraday: boolean): AggregatedPricePoint {
182
182
  let low = Number.POSITIVE_INFINITY;
183
183
  let volume: number | undefined;
184
184
  let venueVolume: PricePoint["venueVolume"] | null | undefined;
185
+ // What the volume counts carries over while every bar counts the same way.
186
+ let volumeBasis: string | null | undefined;
185
187
  let partial = false;
186
188
  let filled = false;
187
189
  const integrity: PriceHistoryIntegrity[] = [];
@@ -189,7 +191,10 @@ function buildBar(bucket: Bucket, intraday: boolean): AggregatedPricePoint {
189
191
  const close = bar.close;
190
192
  high = Math.max(high, isFiniteNumber(bar.high) ? bar.high : close);
191
193
  low = Math.min(low, isFiniteNumber(bar.low) ? bar.low : close);
192
- if (isFiniteNumber(bar.volume)) volume = (volume ?? 0) + bar.volume;
194
+ if (isFiniteNumber(bar.volume)) {
195
+ volume = (volume ?? 0) + bar.volume;
196
+ volumeBasis = volumeBasis === undefined || volumeBasis === bar.volumeBasis ? bar.volumeBasis ?? null : null;
197
+ }
193
198
  // One venue's trades add up only while every bar counts the same venue.
194
199
  if (bar.venueVolume && venueVolume !== null) {
195
200
  venueVolume = venueVolume === undefined || venueVolume.venue === bar.venueVolume.venue
@@ -207,6 +212,7 @@ function buildBar(bucket: Bucket, intraday: boolean): AggregatedPricePoint {
207
212
  low,
208
213
  close: last.close,
209
214
  ...(volume !== undefined ? { volume } : {}),
215
+ ...(volume !== undefined && volumeBasis ? { volumeBasis } : {}),
210
216
  ...(volume === undefined && venueVolume ? { venueVolume } : {}),
211
217
  ...(partial ? { partial: true } : {}),
212
218
  ...(filled ? { filled: true } : {}),
@@ -15,7 +15,7 @@ import {
15
15
  valuationPriceIssues,
16
16
  } from "./fundamentals";
17
17
  import { extractSecuritySeries } from "./market";
18
- import { activeStudyInputSeriesIds } from "./studies";
18
+ import { activeStudyInputSeriesIds } from "./studies/resolve";
19
19
  import { reportingCurrencySeries } from "./reporting-currency";
20
20
  import { compareChartQuoteRecency } from "./live-quotes";
21
21
  import { marketSeriesFrequency } from "./market";
@@ -138,8 +138,8 @@ export function baseSecuritySeries(
138
138
  : field.unit;
139
139
  const generic = field.unitGroup === "price"
140
140
  ? futuresGenericListing(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
141
- // A generic future on an index, VIX or Treasuries reads in points or 32nds, and an index
142
- // future's continuous symbol or contract in points, not dollars, as its quote does.
141
+ // Index and Treasury futures read in points or 32nds across generics,
142
+ // continuous symbols and listed contracts, as their quotes do.
143
143
  const genericBasis = field.unitGroup === "price" && isMarketFieldId(field.id)
144
144
  ? futuresListingPriceBasis(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
145
145
  // An index's level is points too, never the currency its members trade in.
@@ -15,7 +15,7 @@ import {
15
15
  type ManualChartResolution,
16
16
  } from "./resolution";
17
17
  import { TIME_RANGES, type TimeRange } from "./range";
18
- import { maxStudyWarmupPoints } from "./studies";
18
+ import { maxStudyWarmupPoints } from "./studies/resolve";
19
19
  import type { PriceComparison } from "./price-comparison";
20
20
  import { resolutionForExplicitMarketPeriods } from "./market-resolution";
21
21
  import { latestSessionStart, latestTradingSessionOpen, priorSessionClose } from "../market-data/market/trading-sessions";
@@ -103,8 +103,11 @@ function finiteOrFallback(value: number | undefined, fallback: number): number {
103
103
  }
104
104
 
105
105
  function mergeQuoteIntoLatestBar(latest: PricePoint, quotePrice: number, range?: QuoteSessionRange | null): PricePoint {
106
- // A later quote cannot establish which reported OHLC field was wrong.
107
- if (pricePointIntegrity(latest)) return latest;
106
+ if (pricePointIntegrity(latest)) {
107
+ // A later quote cannot establish which reported OHLC field was wrong, unless it names the bar's whole session: its
108
+ // open, range and price are the bar (GOGL's 2026-10-30 daily bar, served with a high under its open).
109
+ return range?.open === undefined ? latest : { ...latest, open: range.open, high: range.high, low: range.low, close: quotePrice };
110
+ }
108
111
  const open = finiteOrFallback(latest.open, latest.close);
109
112
  const high = finiteOrFallback(latest.high, Math.max(open, latest.close));
110
113
  const low = finiteOrFallback(latest.low, Math.min(open, latest.close));
@@ -10,7 +10,7 @@ import { aggregatePriceBars, futuresOpenMinuteOf, withBuiltBarSize } from "./bar
10
10
  import type { TickerFinancials } from "../types/financials";
11
11
  import { isMarketFieldId } from "./field-catalog";
12
12
  import { chartPriceHistoryIntegrityNotices } from "./market";
13
- import { resolveStudies } from "./studies";
13
+ import { resolveStudies } from "./studies/resolve";
14
14
  import { clipSeriesToWindow } from "./alignment";
15
15
  import { bufferPriceComparison, clipPriceComparison, priceComparisonBoundsForSeries, resolvePriceComparison } from "./price-comparison";
16
16
  import { readSeedListingFacts } from "./parsed-history-cache";
@@ -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";