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
@@ -1,10 +1,13 @@
1
1
  import { logFailure } from "../../utils/log-failure";
2
2
  import type { CliCommandDef } from "../../types/plugin";
3
3
  import { TIME_RANGES, type TimeRange } from "../../time-series/range";
4
+ import { CHART_RESOLUTION_STEP_MS } from "../../time-series/resolution";
4
5
  import { yearToDateStartIndex } from "../../market-data/market/year-to-date";
5
6
  import type { EarningsEvent, QuoteBatchResult, QuoteSubscriptionTarget, SecFilingItem } from "../../types/data-provider";
6
7
  import { companyDataNotes, type CompanyFrom } from "../../types/company-from";
7
8
  import type { NewsArticle, NewsFeed, NewsQuery } from "../../news/types";
9
+ import { newsSummaryLine } from "../../news/news-model";
10
+ import { shortStoryId } from "../../news/story-id";
8
11
  import type {
9
12
  AnalystResearchData,
10
13
  CorporateActionsData,
@@ -52,6 +55,7 @@ import {
52
55
  analystReferenceMetadata,
53
56
  analystUpside,
54
57
  formatAnalystReference,
58
+ ratingReason,
55
59
  } from "../../plugins/builtin/research/analyst-model";
56
60
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
57
61
  import { isQuoteStaleForCurrentSession, noPrintsTodayText } from "../../market-data/quotes/freshness";
@@ -103,6 +107,7 @@ import {
103
107
  } from "../../utils/option-expiry";
104
108
  import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
105
109
  import { apiClient } from "../../api-client";
110
+ import { CLOUD_QUOTE_DELAY_MINUTES } from "../../api-client/plan-rules";
106
111
  import type { ExchangeAnnouncementsPayload } from "../../api-client/exchange-announcements";
107
112
  import { exchangeAnnouncementsPage, listsExchangeAnnouncements, type ExchangeAnnouncementsPage } from "../../utils/exchange-announcements";
108
113
  import { dailyFxLeg, fxCloseOnDate, type FxCloseReading } from "../../market-data/fx-closes";
@@ -120,7 +125,7 @@ import {
120
125
  type CliReportTables,
121
126
  } from "../report-tables";
122
127
  import { historyCoverageNotice } from "../../sources/history-coverage";
123
- import { maxDrawdown, maxDrawdownText } from "../../utils/max-drawdown";
128
+ import { maxDrawdownText } from "../../utils/max-drawdown";
124
129
  import { isPastPriceWindow, loadPriceWindowHistory, PRICE_WINDOW_FROM_HELP, PRICE_WINDOW_TO_HELP, priceWindowText, readPriceWindow, type PriceWindow } from "../../utils/price-window";
125
130
  import {
126
131
  BAR_INTERVAL_LABELS,
@@ -132,6 +137,9 @@ import {
132
137
  historyFacts,
133
138
  historyFlagNote,
134
139
  historyWickNote,
140
+ historyContractDrawdown,
141
+ historyRollNote,
142
+ historyThinNote,
135
143
  historyGapNote,
136
144
  historyJumpNote,
137
145
  historyUnbrokenFrom,
@@ -146,13 +154,14 @@ import {
146
154
  historyUnit,
147
155
  quotedPriceDivisor,
148
156
  historyVolume,
157
+ historyVolumeBasis,
149
158
  historyVolumeText,
150
159
  type HistoryRow,
151
160
  type HistoryUnit,
152
161
  type HistoryVolume,
153
162
  } from "../history-rows";
154
163
  import {
155
- HISTORY_EXPORT_COLUMNS,
164
+ historyExportColumns,
156
165
  historyExportStatus,
157
166
  historyFailureRow,
158
167
  oneLineFailure,
@@ -191,7 +200,7 @@ import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
191
200
  import { isFiniteNumber } from "../../utils/guards";
192
201
  import { exportedFundamentals } from "../../utils/price-earnings";
193
202
  import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
194
- import { fxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
203
+ import { fxRateNotes, refreshedFxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
195
204
  import { quoteBarNotes } from "../../market-data/quotes/intraday-bar";
196
205
  import { cedearRatioText, depositaryPairText, isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../market-data/depositary-pair";
197
206
  import { isPesoDollarPair, loadPesoDollarRates, pesoDollarRateLines, pesoDollarRatesJson } from "../peso-dollar-rates";
@@ -246,13 +255,13 @@ import {
246
255
  type CliListing,
247
256
  type ListingIdentity,
248
257
  } from "../listing-arg";
249
- import { bareListingNote, listingFateNotes, noLongerTradingNote, tradingListing } from "../../tickers/listing-fate";
258
+ import { bareListingNote, listingFateNotes, noLongerTradingNote, SUCCESSOR_CHANGE_WAIT_MS, tradingListing } from "../../tickers/listing-fate";
250
259
  import { isYieldIndexSymbol } from "../../tickers/index-roots";
251
260
  import {
252
261
  analyzeTaxEquivalent, distributionBasis, loadTreasuryTenors, parseTaxRates, TAX_RATE_HELP_OPTIONS, taxEquivalentJson, type TaxRates,
253
262
  } from "../../market-data/tax-equivalent";
254
263
  import { CBOE_YIELD_INDEX_CLOSE } from "../../plugins/builtin/shared/treasury-cutoffs";
255
- import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
264
+ import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote, undatedListingsNote } from "../earnings-calendar";
256
265
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
257
266
  import { providerMissReason } from "../../sources/provider-errors";
258
267
  import { isNetworkFailure } from "../../utils/network-failure";
@@ -805,7 +814,10 @@ function financialStatementRows(financials: FinancialsCliData) {
805
814
 
806
815
  function newsRows(articles: NewsArticle[]) {
807
816
  return articles.map((article) => ({
817
+ id: article.id,
808
818
  title: article.title,
819
+ // Whole, as exports write it; the text table's line under the row (newsRowNote) runs it together.
820
+ summary: article.summary ?? null,
809
821
  source: article.source,
810
822
  publishedAt: isoDate(article.publishedAt),
811
823
  topic: article.topic,
@@ -823,6 +835,8 @@ function filingRows(filings: SecFilingItem[]) {
823
835
  companyName: filing.companyName ?? "",
824
836
  accessionNumber: filing.accessionNumber,
825
837
  url: filing.primaryDocumentUrl ?? filing.filingUrl,
838
+ // A current report's press release exhibit, where its index names one: the figures, not the cover page.
839
+ pressReleaseUrl: filing.pressReleaseUrl ?? "",
826
840
  items: secFilingItemCodes(filing.items) ?? "",
827
841
  // A current report's items by number and name: the results 8-K reads apart from another filed that day.
828
842
  itemsLabel: currentReportItemsLabel(filing.items) ?? "",
@@ -867,9 +881,16 @@ function analystRows(data: AnalystResearchData) {
867
881
  current: rating.current ?? "",
868
882
  prior: rating.prior ?? "",
869
883
  target: rating.currentPriceTarget ?? null,
884
+ ...(ratingReason(rating) ? { reason: rating.reason } : {}),
870
885
  }));
871
886
  }
872
887
 
888
+ /** The reason coverage of an action gave, under its row with the article. */
889
+ function analystRowNote(row: { reason?: AnalystResearchData["ratings"][number]["reason"] }): string | null {
890
+ const reason = row.reason;
891
+ return reason ? `${reason.text} · ${reason.url}` : null;
892
+ }
893
+
873
894
  /** Provider values carry binary floating-point noise such as 0.26940000000000003. */
874
895
  function cleanDecimal(value: number | undefined): string {
875
896
  return value == null || !Number.isFinite(value) ? "" : String(Number(value.toFixed(6)));
@@ -1044,7 +1065,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
1044
1065
  const listing = asked[index]!;
1045
1066
  // A bare symbol read on a listing abroad (SXR8 -> XETRA) is not the US symbol the changes are of.
1046
1067
  if (result.quote && bareListingNote(listing, result.quote)) return { change: null, formerIssuer: null };
1047
- const payload = await changes.get(listing)?.settle() ?? null;
1068
+ const payload = await changes.get(listing)?.settle(SUCCESSOR_CHANGE_WAIT_MS) ?? null;
1048
1069
  const current = tradingListing(result.quote, now);
1049
1070
  return { change: describeTickerChange(listing.symbol, payload, current), formerIssuer: describeFormerIssuer(listing.symbol, payload, current) };
1050
1071
  }));
@@ -1391,7 +1412,7 @@ async function loadListingHistory(
1391
1412
  : market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
1392
1413
  const quoteAnswer = loadListingQuoteAnswer(market.dataProvider, listing);
1393
1414
  const quoted = quoteAnswer.then((answer) => answer.quote);
1394
- const fate = lookups ? listingFateLoader(listing, market, quoted) : null;
1415
+ const fate = lookups ? listingFateLoader(listing, market, quoted, { successor: true }) : null;
1395
1416
  const request = { command: historyCommand(ask), noun: "history", ...(fate ? { fate } : {}) };
1396
1417
  const [loaded, quote] = await Promise.all([
1397
1418
  loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, request)
@@ -1435,21 +1456,28 @@ async function loadListingHistory(
1435
1456
  // A bar history: dated by its last bar, stale once bars of its size stop arriving.
1436
1457
  // The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
1437
1458
  const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
1459
+ // Signed out or free, the bars of a session in progress run as late as the delayed quote they end on, and say how late.
1460
+ const sessionTrading = quote?.marketState === "REGULAR" || quote?.marketState === "PRE" || quote?.marketState === "POST";
1461
+ const delayedTail = quote?.dataSource === "delayed" && !quote.delisting && (dayToDate || (intraday && sessionTrading));
1438
1462
  // The source that served the bars, a broker or another source when Gloom Cloud could not.
1439
1463
  const source = loaded.sourceName ? { source: loaded.sourceName } : {};
1440
1464
  // A window that closed before today is the history asked for: no later bar makes it stale.
1441
1465
  const freshness = withSavedCopy(window && isPastPriceWindow(window) ? rowsFreshness(data, {
1442
1466
  ...source,
1443
1467
  status: "not-a-feed", basis: `${interval ? BAR_INTERVAL_LABELS[interval] : "price"} bars, ${priceWindowText(window)}`, observedKey: "date", oldest: null,
1468
+ ...(volume ? { note: historyVolumeText(volume, interval, historyVolumeBasis(data)) } : {}),
1444
1469
  }) : rowsFreshness(data, {
1445
1470
  ...source,
1446
1471
  ...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
1447
1472
  observedKey: "date",
1448
1473
  oldest: null,
1449
1474
  cadenceExchange: venue,
1475
+ // An intraday bar is dated by its open, so the line names its span: `last bar 15:00–16:00 UTC`.
1476
+ ...(intraday && interval && interval in CHART_RESOLUTION_STEP_MS ? { barMinutes: CHART_RESOLUTION_STEP_MS[interval] / 60_000 } : {}),
1450
1477
  ...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
1451
1478
  ...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
1452
- ...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
1479
+ ...(delayedTail ? { status: "delayed" as const, delayMinutes: CLOUD_QUOTE_DELAY_MINUTES } : {}),
1480
+ ...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval, historyVolumeBasis(data)) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
1453
1481
  // The rows are drawn from the bars served, which say whether they are the saved copy.
1454
1482
  }), loaded);
1455
1483
  const gaps = historyGapNote(data, traded);
@@ -1467,6 +1495,8 @@ async function loadListingHistory(
1467
1495
  // The status line says how far a bar still forming runs; the note is for the bars it does not name.
1468
1496
  gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyWickNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1469
1497
  historyVenueVolumeNote(data) ?? [],
1498
+ // NG=F's rows are November's to the 23rd and December's from the 26th: each roll named, and thin bars said.
1499
+ historyRollNote(listing.symbol, data) ?? [], historyThinNote(data) ?? [],
1470
1500
  historyBarStatusNote(forming ? served : data, { tradingDay: venueTradingDay(venue, Date.now()) }) ?? [],
1471
1501
  ].flat();
1472
1502
  return {
@@ -1475,6 +1505,19 @@ async function loadListingHistory(
1475
1505
  };
1476
1506
  }
1477
1507
 
1508
+ /**
1509
+ * A continuous future's history is the front month's, not the contract its
1510
+ * quote names now: NG=F's rows from before the roll are November's under a
1511
+ * quote of "Natural Gas Dec 2026". The heading says "Natural Gas front month"
1512
+ * once the bars name their contracts.
1513
+ */
1514
+ function frontMonthIdentity(identity: ListingIdentity, symbol: string, rows: readonly HistoryRow[]): ListingIdentity {
1515
+ if (!/=F$/i.test(symbol) || !rows.some((row) => row.contract)) return identity;
1516
+ const board = futuresBoardContract(symbol);
1517
+ const name = board?.name ?? identity.name?.replace(/\s+[A-Z][a-z]{2}\s+\d{4}$/, "");
1518
+ return name ? { ...identity, name: `${name} front month` } : identity;
1519
+ }
1520
+
1478
1521
  /** What a listing with no bar for what was asked says, in a warning and in a list's status column. */
1479
1522
  function emptyHistoryNote(symbol: string, { range, window }: HistoryAsk): string {
1480
1523
  return window ? `No bars for ${symbol} from ${priceWindowText(window)}.` : `No bars for ${symbol} in ${range}.`;
@@ -1520,6 +1563,8 @@ const HISTORY_FLAG_TEXT_COLUMN: CliResultColumn = {
1520
1563
  row.partial ? cliStyles.muted("partial") : "",
1521
1564
  row.filled ? cliStyles.muted("filled") : "",
1522
1565
  row.unsettled ? cliStyles.muted("unsettled") : "",
1566
+ row.roll && typeof row.contract === "string" ? cliStyles.muted(`roll to ${row.contract}`) : "",
1567
+ row.thin ? cliStyles.muted("thin") : "",
1523
1568
  typeof row.status === "string" ? cliStyles.warning(row.status) : "",
1524
1569
  ].filter(Boolean).join(" "),
1525
1570
  };
@@ -1573,8 +1618,10 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1573
1618
  const missingVolume = historyReportsVolume(data) ? MISSING_VOLUME_TEXT : "";
1574
1619
  const textColumns = historyTextColumns(intraday, price).map((column) => column.key !== "volume" ? column
1575
1620
  : { ...column, format: historyVolumeCell(() => missingVolume) });
1576
- // The largest fall from a close to a later one in the bars shown, said under the heading.
1577
- const drawdown = maxDrawdown(data);
1621
+ // The largest fall from a close to a later one in the bars shown, said under the heading: within one contract
1622
+ // of a continuous future, never from one month's close to another's across a roll.
1623
+ const contractDrawdown = historyContractDrawdown(data);
1624
+ const drawdown = contractDrawdown?.drawdown ?? null;
1578
1625
  ctx.printResult({
1579
1626
  data,
1580
1627
  metadata: {
@@ -1591,20 +1638,21 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1591
1638
  // The first bar after the last gap or jump; the first bar when there is none.
1592
1639
  unbrokenFrom: historyUnbrokenFrom(data) ?? data[0]?.date ?? null,
1593
1640
  bars: data.length,
1594
- maxDrawdown: drawdown,
1641
+ maxDrawdown: drawdown && contractDrawdown?.contract ? { ...drawdown, contract: contractDrawdown.contract } : drawdown,
1595
1642
  asOf: freshness?.asOf ?? null,
1596
1643
  ...(volume ? { volumeUnit: volume.unit, volumePeriod: interval === "1d" ? "UTC day" : interval } : {}),
1597
1644
  },
1598
1645
  ...(notes.length > 0 ? { warnings: notes } : {}),
1599
1646
  freshness,
1600
1647
  }, {
1601
- heading: [listingHeading(identity), cliStyles.muted(historyFacts(unit, data).join(" · ")),
1602
- ...drawdown && interval ? [cliStyles.muted(maxDrawdownText(drawdown, { basis: `${BAR_INTERVAL_LABELS[interval]} closes`, price,
1648
+ heading: [listingHeading(frontMonthIdentity(identity, history.listing.symbol, data)), cliStyles.muted(historyFacts(unit, data).join(" · ")),
1649
+ ...drawdown && interval ? [cliStyles.muted(maxDrawdownText(drawdown, {
1650
+ basis: `${BAR_INTERVAL_LABELS[interval]} closes${contractDrawdown?.contract ? `, ${contractDrawdown.contract}` : ""}`, price,
1603
1651
  date: (text) => intraday ? formatUtcTime(text) : text.slice(0, 10) }))] : []].join("\n"),
1604
1652
  dateKey: "date",
1605
1653
  textColumns: [...textColumns, HISTORY_FLAG_TEXT_COLUMN],
1606
- // One header whatever the market is doing, for one symbol as for a list (HISTORY_EXPORT_COLUMNS).
1607
- columns: HISTORY_EXPORT_COLUMNS,
1654
+ // One header whatever the market is doing, for one symbol as for a list (historyExportColumns).
1655
+ columns: historyExportColumns(data),
1608
1656
  exportStatus: historyExportStatus,
1609
1657
  // Every exported row says what it is a bar of, the currency a price is in and its bar size, so it survives head and concatenation.
1610
1658
  exportIdentity: { symbolOrSeries: history.listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
@@ -1710,7 +1758,7 @@ async function runHistoryList(
1710
1758
  dateKey: "date",
1711
1759
  textColumns: [{ key: "symbol", header: "Symbol", shrink: false }, ...textColumns,
1712
1760
  { key: "currency", header: "Cur", shrink: false, format: currencyCell }, HISTORY_FLAG_TEXT_COLUMN],
1713
- columns: HISTORY_EXPORT_COLUMNS,
1761
+ columns: historyExportColumns(rows),
1714
1762
  exportStatus: historyExportStatus,
1715
1763
  // Every row names its symbol, the currency its prices are in, its bar size and the as-of of its own symbol's bars.
1716
1764
  exportIdentity: (row) => {
@@ -1929,9 +1977,18 @@ const NEWS_COLUMNS = [
1929
1977
  { key: "source", header: "Source" },
1930
1978
  { key: "title", header: "Title" },
1931
1979
  { key: "tickers", header: "Tickers" },
1980
+ // The short id `fn news-story-pane` opens the story by; exports keep the whole id.
1981
+ { key: "id", header: "ID", format: (value: unknown) => typeof value === "string" ? shortStoryId(value) : "" },
1932
1982
  { key: "url", header: "URL", optional: true },
1933
1983
  ];
1934
1984
 
1985
+ /**
1986
+ * A story's summary under its row, on one line, cut to the width (`rowNote`): the figure a headline leaves out.
1987
+ * None for a summary that only repeats the headline; exports keep the summary whole.
1988
+ */
1989
+ const newsRowNote = (row: Record<string, unknown>) => typeof row.summary === "string"
1990
+ ? newsSummaryLine({ title: String(row.title ?? ""), summary: row.summary }) : null;
1991
+
1935
1992
  /**
1936
1993
  * Stories matching text, printed as a symbol's news is, under a line saying
1937
1994
  * what they match. A text naming a commodity that matched nothing says so and
@@ -1968,6 +2025,7 @@ async function printNewsSearch(
1968
2025
  heading: newsSearchHeading(text, [reason, closestReason].filter(Boolean).join(" · ") || undefined),
1969
2026
  rows: newsRows,
1970
2027
  columns: NEWS_COLUMNS,
2028
+ rowNote: newsRowNote,
1971
2029
  empty: named ? noStoriesMessage(text, route) : empty ?? (searched ? noStoriesMessage(text, null) : "No stories to show."),
1972
2030
  });
1973
2031
  }
@@ -2011,6 +2069,7 @@ async function printMetalNews(route: MetalNewsRoute, limit: number, ctx: Paramet
2011
2069
  heading: metalNewsHeading(route),
2012
2070
  rows: newsRows,
2013
2071
  columns: NEWS_COLUMNS,
2072
+ rowNote: newsRowNote,
2014
2073
  empty: failure === undefined ? noMetalStoriesMessage(route) : "No stories to show.",
2015
2074
  ...(ticker ? { footnote: `${word} ${cliArrow("→")} ${route.metal}'s market news. For ${ticker.name ?? ticker.symbol} (ticker ${ticker.symbol}), try: ${companyNews}` } : {}),
2016
2075
  });
@@ -2113,6 +2172,7 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
2113
2172
  ...(identity ? { heading: listingHeading(identity) } : {}),
2114
2173
  rows: newsRows,
2115
2174
  columns: NEWS_COLUMNS,
2175
+ rowNote: newsRowNote,
2116
2176
  ...(empty ? { empty } : {}),
2117
2177
  ...(wordHint ? { footnote: wordHint } : {}),
2118
2178
  });
@@ -2127,15 +2187,18 @@ const FILING_COLUMNS = [
2127
2187
  { key: "url", header: "URL", optional: true },
2128
2188
  { key: "items", header: "Items" },
2129
2189
  { key: "itemsLabel", header: "Item names" },
2190
+ { key: "pressReleaseUrl", header: "Press release URL" },
2130
2191
  ];
2131
2192
 
2132
- // The text table names the items before the URL, which a narrow terminal drops first.
2193
+ // The text table names the items before the URL, which a narrow terminal drops first. A current report's
2194
+ // URL is its press release where it has one, the page its figures are on; exports keep both links.
2133
2195
  const FILING_TEXT_COLUMNS = [
2134
2196
  { key: "filingDate", header: "Date" },
2135
2197
  { key: "form", header: "Form" },
2136
2198
  { key: "companyName", header: "Company", maxWidth: 24 },
2137
2199
  { key: "itemsLabel", header: "Items" },
2138
- { key: "url", header: "URL", optional: true },
2200
+ { key: "url", header: "URL", optional: true,
2201
+ format: (value: unknown, row: Record<string, unknown>) => String(row.pressReleaseUrl || (value ?? "")) },
2139
2202
  ];
2140
2203
 
2141
2204
  /** The US listing the SEC files a symbol under, from the symbol's venues: NYSE for SAN's Banco Santander. */
@@ -2631,6 +2694,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2631
2694
  heading: listingHeading(identity),
2632
2695
  rows: analystRows,
2633
2696
  summary: (research) => analystSummary(research, quote),
2697
+ rowNote: analystRowNote,
2634
2698
  columns: [
2635
2699
  { key: "date", header: "Date" },
2636
2700
  { key: "firm", header: "Firm" },
@@ -2722,7 +2786,7 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2722
2786
  const refresh = ctx.cliOptions.refresh;
2723
2787
  const venues = loadBareVenues(listing, market);
2724
2788
  const quotePromise = loadListingQuote(market.dataProvider, listing);
2725
- const fate = listingFateLoader(listing, market, quotePromise);
2789
+ const fate = listingFateLoader(listing, market, quotePromise, { successor: true });
2726
2790
  const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
2727
2791
  const now = Date.now();
2728
2792
  // Expiries close on the listing's clock: a coin's options (Deribit) settle at 08:00 UTC on any day.
@@ -3078,7 +3142,9 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
3078
3142
  }));
3079
3143
  const freshness = fxPairFreshness(loaded);
3080
3144
  // A rate that is its pair's bar says which bar, and a stale one why it stands.
3081
- const rateNotes = fxRateNotes(new Map([...loaded].map(([code, leg]) => [code, leg.timing])));
3145
+ const timings = new Map([...loaded].map(([code, leg]) => [code, leg.timing]));
3146
+ // --refresh read every leg again: one that is still stale says the data service has no newer rate.
3147
+ const rateNotes = [...fxRateNotes(timings), ...ctx.cliOptions.refresh ? refreshedFxRateNotes(timings) : []];
3082
3148
  if (tenor && shortRates && !("error" in tenor)) {
3083
3149
  const read = await shortRates;
3084
3150
  if ("error" in read) throw read.error;
@@ -3214,12 +3280,14 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
3214
3280
  return listed ? earningsChoiceNote(listing, eventOf(listing), await listed) : null;
3215
3281
  })),
3216
3282
  ]);
3217
- const noCalendar = abroadNotes.join("\n");
3283
+ // A listing at home the calendar has no date for is named as well: a list never drops a symbol unsaid.
3284
+ const undated = undatedListingsNote(listings.filter((listing) => !isListingAbroad(listing) && !eventOf(listing)));
3285
+ const noCalendar = [...abroadNotes, ...undated ? [undated] : []].join("\n");
3218
3286
  // Text prints the listings without a calendar under the table; other formats carry them as warnings.
3219
3287
  const warnings = [
3220
3288
  ...listings.flatMap(listingReadNotes),
3221
3289
  ...choiceNotes.filter((note): note is string => !!note),
3222
- ...ctx.cliOptions.format === "text" ? [] : abroadNotes,
3290
+ ...ctx.cliOptions.format === "text" ? [] : [...abroadNotes, ...undated ? [undated] : []],
3223
3291
  ];
3224
3292
  ctx.printResult({
3225
3293
  data: events,
@@ -3292,7 +3360,8 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
3292
3360
  }, {
3293
3361
  title: "CSV",
3294
3362
  lines: [
3295
- "Every --csv has one header, whatever the symbol, range or market state: Date, Open, High, Low, Close, Volume, Flag, Partial, Filled, Unsettled, Volume unit, Volume note, Venue volume, Volume venue, status, then symbol_or_series, currency_or_unit, interval, source and as_of. Prices are plain numbers in the currency currency_or_unit names. Partial, Filled and Unsettled are true or false, and status says why a row has no prices.",
3363
+ "Every --csv has one header, whatever the symbol, range or market state: Date, Open, High, Low, Close, Volume, Flag, Partial, Filled, Unsettled, Volume unit, Volume note, Venue volume, Volume venue, Volume basis, status, then symbol_or_series, currency_or_unit, interval, source and as_of. Prices are plain numbers in the currency currency_or_unit names. Partial, Filled and Unsettled are true or false, and status says why a row has no prices.",
3364
+ "A continuous future whose bars name their contracts (NG=F) adds Contract, Roll and Thin before status: the contract per bar, and true or false for the bar it rolled at and a bar that traded far below those around it.",
3296
3365
  ],
3297
3366
  }, {
3298
3367
  title: "Several symbols",
@@ -72,14 +72,30 @@ export function newsFallbackText(symbol: string): string | null {
72
72
  }
73
73
 
74
74
  /**
75
- * The stories matching the text, in the service's order: stories with every
76
- * word in the headline first, then the ones with some only in the summary,
77
- * newest first within each. The service holds stories back from an account
75
+ * Stories newest first, as `news <words>` lists them. The service picks the
76
+ * best matches (every word in the headline before words only in the summary);
77
+ * the list is then read by date, which its help promises. A story with no
78
+ * date goes last; equal dates keep the service's order.
79
+ */
80
+ function newestFirst(articles: readonly NewsArticle[]): NewsArticle[] {
81
+ const time = (article: NewsArticle) => {
82
+ const at = new Date(article.publishedAt as Date | string | number).getTime();
83
+ return Number.isFinite(at) ? at : -Infinity;
84
+ };
85
+ return articles.map((article, index) => ({ article, index, at: time(article) }))
86
+ .sort((left, right) => right.at - left.at || left.index - right.index)
87
+ .map(({ article }) => article);
88
+ }
89
+
90
+ /**
91
+ * The stories matching the text, newest first (`newestFirst`): the service
92
+ * chooses them, stories with every word in the headline ahead of the ones with
93
+ * some only in the summary. The service holds stories back from an account
78
94
  * without real-time access, as every other news request.
79
95
  */
80
96
  export async function searchNewsStories(text: string, limit: number, topics?: readonly string[]): Promise<NewsArticle[]> {
81
97
  const response = await apiClient.searchCloudNews(text, limit, topics);
82
- return response.items.map((item) => mapCloudNewsArticle(item));
98
+ return newestFirst(response.items.map((item) => mapCloudNewsArticle(item)));
83
99
  }
84
100
 
85
101
  /** A search's stories, and whether they are its closest ones because no story has every word. */
@@ -96,7 +112,10 @@ export interface NewsSearchAnswer {
96
112
  */
97
113
  export async function searchClosestNewsStories(text: string, limit: number): Promise<NewsSearchAnswer> {
98
114
  const response = await apiClient.searchCloudNews(text, limit, undefined, { closest: true });
99
- return { found: response.items.map((item) => mapCloudNewsArticle(item)), closest: response.match === "closest" };
115
+ const found = response.items.map((item) => mapCloudNewsArticle(item));
116
+ // The closest stories stay most words first, as their heading says; a match of every word is read by date.
117
+ const closest = response.match === "closest";
118
+ return { found: closest ? found : newestFirst(found), closest };
100
119
  }
101
120
 
102
121
  /** What the heading says of a search no story has every word of. */
@@ -59,7 +59,7 @@ import { buildCorrelationSeries } from "../../plugins/builtin/correlation/matrix
59
59
  import { alignDailyCloses, correlateDailyCloses } from "../../plugins/builtin/correlation/compute";
60
60
  import { mixedSessionCloseNote } from "../../market-data/market/session-close-note";
61
61
  import { CORRELATION_RETURN_BASIS, loadCorrelationHistory } from "../../plugins/builtin/correlation/history";
62
- import { EXCHANGE_OPTION, listingIdentity, listingReadNotes, loadForListing, loadListingQuote, requireCliListing, type CliListing } from "../listing-arg";
62
+ import { coinListingNotes, EXCHANGE_OPTION, listingIdentity, listingReadNotes, loadForListing, loadListingQuote, requireCliListing, type CliListing } from "../listing-arg";
63
63
  import { CLI_COMMAND_GROUPS } from "../help";
64
64
  import { formatChangePercentCell, formatCompactCell } from "../helpers";
65
65
  import { WORLD_INDICES } from "../../plugins/builtin/world-indices/indices";
@@ -481,7 +481,7 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
481
481
  const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
482
482
  const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
483
483
  const search = takeOption(rawArgs, "--search")?.trim() ?? null;
484
- if (search === "") ctx.fail("--search needs the words of an event's name.", "Such as econ --search nonfarm.");
484
+ if (search === "") ctx.fail("--search needs the words of an event's name or its publisher.", "Such as econ --search nonfarm.");
485
485
  const fromOption = parseDateOption(takeOption(rawArgs, "--from"), "--from", ctx);
486
486
  const tzOption = takeOption(rawArgs, "--tz");
487
487
  const viewerZone = tzOption == null ? null : canonicalTimeZone(tzOption);
@@ -909,7 +909,12 @@ async function runCorrelation(rawArgs: string[], ctx: Parameters<CliCommandDef["
909
909
  asOf: newestReportTime(lastBars),
910
910
  oldest: oldestReportTime(lastBars),
911
911
  });
912
- const warnings = [...listingReadNotes(leftListing!), ...listingReadNotes(rightListing!), ...sessionNote ? [sessionNote] : []];
912
+ // A coin's code read as a listing (BTC, the Grayscale fund) says so, as `quote BTC` does.
913
+ const warnings = [
914
+ ...listingReadNotes(leftListing!), ...listingReadNotes(rightListing!),
915
+ ...coinListingNotes(leftListing!, leftQuote), ...coinListingNotes(rightListing!, rightQuote),
916
+ ...sessionNote ? [sessionNote] : [],
917
+ ];
913
918
  ctx.printResult({
914
919
  data: [{ left, right, samples: sampleSize, correlation }],
915
920
  ...(warnings.length > 0 ? { warnings } : {}),
@@ -971,7 +976,7 @@ export const overviewCliCommands: CliCommandDef[] = [
971
976
  options: [
972
977
  { flags: "--country <regions or countries>", description: "US, G7, EU, all (default), or countries by their two-letter codes, comma separated, such as MX or MX,BR,ZA" },
973
978
  { flags: "--impact <level>", description: "high, medium, low, or all (default all)" },
974
- { flags: "--search <words>", description: "Events whose name has these words, case, spaces and hyphens aside, such as nonfarm or fed funds" },
979
+ { flags: "--search <words>", description: "Events whose name or publisher has these words, case, spaces and hyphens aside, such as nonfarm, fed funds or USDA" },
975
980
  { flags: "--from <date>", description: `First day listed, as ${DATE_OPTION_EXAMPLES} (default today, UTC); an earlier day lists past events from the stored calendar and says where it starts` },
976
981
  { flags: "--tz <zone>", description: "Add a column with each release's time on that clock, an IANA zone such as America/New_York; with the weekday when it falls on another day than the UTC date" },
977
982
  ],
@@ -24,7 +24,7 @@ import { initMarketData, withMarketData } from "../context";
24
24
  import { listingChangeLine, loadListingFate } from "../../tickers/listing-fate";
25
25
  import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
26
26
  import { formerIssuerLine, startTickerChangeLookup, type TickerChangeLookup } from "../../tickers/ticker-changes";
27
- import { fail, failRetryable } from "../errors";
27
+ import { fail, failRetryable, failWithError, isCliFailure, unreachableError } from "../errors";
28
28
  import { isProviderBusy, SEARCH_BUSY_CODE, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
29
29
  import { isNetworkFailure } from "../../utils/network-failure";
30
30
  import type { MarketContext } from "../types";
@@ -36,6 +36,7 @@ import { knownGapMessage, commandPointer } from "../known-gaps";
36
36
  import { instrumentTypeLabel } from "../../tickers/instrument-kind";
37
37
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
38
38
  import { companyLines, companyNameExtraWords, groupByCompany, namedByService, sameCompany } from "../../tickers/company-name";
39
+ import { isLowQualityTickerName } from "../../tickers/search/result";
39
40
  import { findCatalogSeries, renderSeriesMatches, type SeriesMatch } from "../series-search";
40
41
  import { centralBankAskedAbout, centralBankCiting, type CentralBank } from "../../utils/central-banks";
41
42
  import { createPaneCatalog } from "../pane-functions/discovery";
@@ -133,8 +134,8 @@ export async function searchCandidatesForCli({
133
134
  const quote = BARE_SYMBOL.test(symbol) && candidates.some((candidate) => candidate.symbol.toUpperCase() === symbol)
134
135
  ? await (quoted ?? loadSearchedQuote(symbol, savedExchange, dataProvider))
135
136
  : null;
136
- return withQuotedNames(symbol, withUsLineOfPrimary(withHomeListingPrimary(trimmedQuery, withPrimaryListingFirst(symbol, candidates, quote))), quote)
137
- .slice(0, totalLimit);
137
+ const ordered = withQuotedNames(symbol, withUsLineOfPrimary(withHomeListingPrimary(trimmedQuery, withPrimaryListingFirst(symbol, candidates, quote))), quote);
138
+ return withOtherCompaniesApart(ordered).slice(0, totalLimit);
138
139
  } catch (error) {
139
140
  // Not "No matches found": a script or a reader would take that for a name nothing lists.
140
141
  if (isProviderBusy(error)) failRetryable(SEARCH_BUSY_MESSAGE, SEARCH_BUSY_CODE);
@@ -346,6 +347,35 @@ function withQuotedNames(symbol: string, candidates: TickerSearchCandidate[], qu
346
347
  });
347
348
  }
348
349
 
350
+ /** A symbol without a venue's suffix or code: DK for DK.BA or DK:BYMA. */
351
+ const baseSymbol = (symbol: string) => symbol.toUpperCase().replace(/[:.][A-Z]{1,4}$/, "");
352
+
353
+ /**
354
+ * Other Listings are the primary listing's company's own lines alone: by its
355
+ * name, a name it had (Cassava Sciences for Filana's FLNA), or, for a line
356
+ * search names no company for, its symbol on another venue. Another company's
357
+ * line, or a nameless line under another symbol (DKC1O on BYMA under
358
+ * `search DK`), is an Other Match, listed after them.
359
+ */
360
+ function withOtherCompaniesApart(candidates: TickerSearchCandidate[]): TickerSearchCandidate[] {
361
+ // A line search names no company for: no name, or only its symbol (DKC1O).
362
+ const nameless = (candidate: TickerSearchCandidate) =>
363
+ isLowQualityTickerName(candidate.result ? candidate.result.name ?? "" : resolveSearchName(candidate), candidate.symbol);
364
+ const primary = candidates.find((candidate) => candidate.category === "Primary Listing");
365
+ // A primary line search names no company for gives nothing to tell its company's lines by.
366
+ if (!primary || nameless(primary)) return candidates;
367
+ const names = [resolveSearchName(primary), primary.result?.formerName, ...primary.nameAliases ?? []];
368
+ const ownLine = (candidate: TickerSearchCandidate) => nameless(candidate)
369
+ ? baseSymbol(candidate.symbol) === baseSymbol(primary.symbol)
370
+ : names.some((company) => sameCompany(resolveSearchName(candidate), company));
371
+ const others = candidates.filter((candidate) => candidate.category === "Other Listings" && !ownLine(candidate));
372
+ if (others.length === 0) return candidates;
373
+ const kept = candidates.filter((candidate) => !others.includes(candidate));
374
+ const at = kept.findLastIndex((candidate) => candidate === primary || candidate.category === "Other Listings") + 1;
375
+ const apart = others.map((candidate) => ({ ...candidate, category: "Other Matches" as const }));
376
+ return [...kept.slice(0, at), ...apart, ...kept.slice(at)];
377
+ }
378
+
349
379
  /** "Saved", or the broker a row came from; never the data service's internal id. */
350
380
  function searchSourceLabel(candidate: TickerSearchCandidate): string {
351
381
  return candidate.kind === "ticker" ? "Saved" : candidate.result?.brokerLabel || "";
@@ -498,18 +528,19 @@ export function buildSearchReport({
498
528
  /** What the query was searched as: "Read VIX.X as ^VIX (Cboe Volatility Index)." */
499
529
  readAs?: string;
500
530
  }): string {
531
+ // Every note is prose: it wraps to a narrow --width.
501
532
  const lines = [renderSection(`Search: ${query}`)];
502
- if (readAs) lines.push("", readAs);
533
+ if (readAs) lines.push("", wrapToTerminal(readAs));
503
534
  const notes = [...fate.notes, ...formerIssuerNotes(candidates, fate)];
504
- if (notes.length > 0) lines.push("", ...notes);
535
+ if (notes.length > 0) lines.push("", ...notes.map((note) => wrapToTerminal(note)));
505
536
  const gap = knownGapMessage(query);
506
537
  const indexNote = gap ? null : indexRootNote(query, candidates);
507
- if (indexNote) lines.push("", indexNote);
508
- if (gap) lines.push("", gap);
538
+ if (indexNote) lines.push("", wrapToTerminal(indexNote));
539
+ if (gap) lines.push("", wrapToTerminal(gap));
509
540
  const bank = searchedCentralBank(query);
510
541
  if (bank) lines.push("", wrapToTerminal(centralBankLine(bank)));
511
542
  const setting = commandPointer(query);
512
- if (setting) lines.push("", setting);
543
+ if (setting) lines.push("", wrapToTerminal(setting));
513
544
  const charted = renderSeriesMatches(query, series, SEARCH_SERIES_LIMIT, cliTerminalWidth() ?? 100, "catalog");
514
545
  // A query that names the data ("carbon", "day-ahead") lists its series first; words that happen to
515
546
  // be in a series name ("apple") list them after the tickers.
@@ -523,9 +554,9 @@ export function buildSearchReport({
523
554
  if (functions.length > 0) lines.push("", renderFunctionMatches(functions));
524
555
  else if (!gap && !bank && !setting && !charted) {
525
556
  return [...lines, ...hasNonLatinLetters(query) ? [cliStyles.muted(OTHER_SCRIPT_NO_MATCH)]
526
- : [cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query))]].join("\n");
557
+ : [cliStyles.muted("No matches found."), cliStyles.muted(wrapToTerminal(catalogHint(query)))]].join("\n");
527
558
  }
528
- if (pointToCatalog) lines.push("", cliStyles.muted(catalogHint(query)));
559
+ if (pointToCatalog) lines.push("", cliStyles.muted(wrapToTerminal(catalogHint(query))));
529
560
  return lines.join("\n");
530
561
  }
531
562
 
@@ -558,11 +589,39 @@ export function buildSearchReport({
558
589
  ));
559
590
  }
560
591
  if (charted && !seriesFirst) lines.push("", charted);
561
- if (pointToCatalog) lines.push("", cliStyles.muted(catalogHint(query)));
592
+ if (pointToCatalog) lines.push("", cliStyles.muted(wrapToTerminal(catalogHint(query))));
562
593
 
563
594
  return lines.join("\n");
564
595
  }
565
596
 
597
+ /**
598
+ * What a search says from its own text, whatever the ticker lookup does: the
599
+ * data no source carries and its proxies ("baltic dry"), the central bank the
600
+ * query names, the setting or report it names.
601
+ */
602
+ function localSearchNotes(query: string): string[] {
603
+ const bank = searchedCentralBank(query);
604
+ return [knownGapMessage(query), bank ? centralBankLine(bank) : null, commandPointer(query)].filter((note): note is string => !!note);
605
+ }
606
+
607
+ /**
608
+ * A ticker lookup that was busy or never reached the data service fails only
609
+ * the ticker part of a search: the notes from local text print first (text) or
610
+ * ride in the failure's details (JSON), then the failure says that no tickers
611
+ * were looked up. A search with no such note fails as before.
612
+ */
613
+ function failTickerLookup(error: unknown, query: string, printNotes: boolean): never {
614
+ const notes = localSearchNotes(query);
615
+ const busy = isCliFailure(error) && error.code === SEARCH_BUSY_CODE;
616
+ if (notes.length === 0 || !(busy || isNetworkFailure(error))) throw error;
617
+ if (printNotes) console.log([renderSection(`Search: ${query}`), "", ...notes.map((note) => wrapToTerminal(note))].join("\n"));
618
+ const failure = busy ? { code: SEARCH_BUSY_CODE, message: TICKER_SEARCH_BUSY_MESSAGE, retryable: true } : unreachableError();
619
+ return failWithError(printNotes ? failure : { ...failure, details: [failure.details, ...notes].filter(Boolean).join("\n") });
620
+ }
621
+
622
+ /** The busy line after a search's own notes printed: only the ticker lookup is missing. */
623
+ const TICKER_SEARCH_BUSY_MESSAGE = "Ticker search is busy, try again in a moment.";
624
+
566
625
  /** The functions a search lists when it found no ticker, unless a known gap or a central bank already says where to look. */
567
626
  function searchedFunctions(
568
627
  query: string,
@@ -613,7 +672,7 @@ export async function search(query: string, dependencies: SearchCommandDependenc
613
672
  tickers,
614
673
  dataProvider,
615
674
  ...quote ? { quote } : {},
616
- });
675
+ }).catch((error: unknown) => failTickerLookup(error, trimmedQuery, !dependencies.printResult));
617
676
  const fate = quote && changes ? await searchFate(symbol, candidates, await quote, dataProvider, changes) : NO_SEARCH_FATE;
618
677
 
619
678
  const series = findCatalogSeries(dependencies.plugins ?? [], trimmedQuery, "names");