gloomberb 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/data.ts +2 -1
  9. package/src/api-client/errors.ts +38 -0
  10. package/src/api-client/hiring.ts +3 -0
  11. package/src/api-client/index.ts +4 -0
  12. package/src/api-client/ipo.ts +24 -1
  13. package/src/api-client/mna.ts +3 -0
  14. package/src/api-client/paths.ts +9 -1
  15. package/src/api-client/roadmap.ts +85 -0
  16. package/src/api-client/types.ts +12 -0
  17. package/src/app/runtime/startup.ts +3 -1
  18. package/src/architecture/core-builtin-allowlist.json +2 -2
  19. package/src/capabilities/chart-series.ts +2 -1
  20. package/src/cli/commands/market.ts +44 -16
  21. package/src/cli/commands/news-search.ts +22 -0
  22. package/src/cli/commands/overview.ts +84 -19
  23. package/src/cli/commands/search.ts +13 -13
  24. package/src/cli/commands/ticker.ts +32 -2
  25. package/src/cli/context.ts +1 -0
  26. package/src/cli/errors.ts +17 -2
  27. package/src/cli/glossary.ts +8 -1
  28. package/src/cli/history-rows.ts +35 -7
  29. package/src/cli/index-roots.ts +0 -7
  30. package/src/cli/known-gaps.ts +11 -0
  31. package/src/cli/listing-arg.ts +74 -11
  32. package/src/cli/pane-functions/capabilities.ts +1 -0
  33. package/src/cli/pane-functions/discovery.ts +21 -1
  34. package/src/cli/pane-functions/freshness.ts +7 -1
  35. package/src/cli/pane-functions/function-help.ts +7 -0
  36. package/src/cli/pane-functions/index.ts +6 -3
  37. package/src/cli/quote-answer.ts +30 -7
  38. package/src/cli/saved-copy-persistence.ts +88 -0
  39. package/src/components/command-bar/list/model.ts +5 -0
  40. package/src/components/command-bar/routes/root/runtime.ts +29 -8
  41. package/src/components/command-bar/selection-runtime.ts +64 -32
  42. package/src/components/command-bar/surface/index.tsx +2 -0
  43. package/src/components/layout/detached-pane-shell.tsx +2 -2
  44. package/src/components/layout/header.tsx +2 -2
  45. package/src/components/layout/pane/capture-stamp.ts +2 -2
  46. package/src/components/layout/pane/footer/index.tsx +23 -6
  47. package/src/components/layout/pane/footer/model.ts +116 -16
  48. package/src/components/layout/pane/header.tsx +2 -2
  49. package/src/components/layout/pane/status-footer.ts +37 -3
  50. package/src/components/layout/status-widget-space.ts +9 -0
  51. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  52. package/src/components/onboarding/onboarding-wizard.tsx +6 -3
  53. package/src/components/ticker/list-table-view.tsx +4 -20
  54. package/src/components/ui/display.tsx +29 -3
  55. package/src/components/ui/icon.tsx +12 -3
  56. package/src/core/app-services.ts +11 -2
  57. package/src/data/app-persistence.ts +9 -7
  58. package/src/data/plugin-cache.ts +20 -3
  59. package/src/data/plugin-state-store.ts +81 -24
  60. package/src/data/resource-store.ts +130 -58
  61. package/src/data/saved-copies.ts +8 -0
  62. package/src/data/session-store.ts +31 -8
  63. package/src/data/sqlite/access.ts +313 -0
  64. package/src/data/sqlite/checkpointer.ts +198 -0
  65. package/src/data/sqlite/database.ts +47 -4
  66. package/src/data/sqlite/retry.ts +50 -9
  67. package/src/data/ticker-store.ts +57 -23
  68. package/src/docs/snapshot.json +32 -5
  69. package/src/i18n/es.ts +2 -4
  70. package/src/i18n/ja.ts +2 -4
  71. package/src/i18n/ko.ts +2 -4
  72. package/src/i18n/zh-cn.ts +2 -4
  73. package/src/i18n/zh-tw.ts +2 -4
  74. package/src/market-data/depositary-pair.ts +25 -1
  75. package/src/market-data/quotes/metadata.ts +7 -0
  76. package/src/market-data/realized-volatility.ts +4 -2
  77. package/src/news/aggregator.ts +7 -1
  78. package/src/news/types.ts +6 -0
  79. package/src/plugins/auto-update-node.ts +10 -6
  80. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  81. package/src/plugins/builtin/alerts/cli.ts +34 -11
  82. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  83. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  84. package/src/plugins/builtin/alerts/headless.ts +10 -6
  85. package/src/plugins/builtin/cds/client.ts +142 -30
  86. package/src/plugins/builtin/cds/headless.ts +35 -8
  87. package/src/plugins/builtin/cds/model.ts +145 -18
  88. package/src/plugins/builtin/cds/pane.tsx +16 -5
  89. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  90. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  91. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  92. package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
  93. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  94. package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
  95. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  96. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  97. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  98. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  99. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  100. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  101. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  102. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  103. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  104. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  105. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  106. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  107. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  108. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  109. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  110. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  111. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  112. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  113. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  114. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  115. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  116. package/src/plugins/builtin/composite-plugins.ts +2 -1
  117. package/src/plugins/builtin/cot/client.ts +28 -2
  118. package/src/plugins/builtin/cot/headless.ts +4 -1
  119. package/src/plugins/builtin/cot/index.tsx +4 -2
  120. package/src/plugins/builtin/cot/model.ts +58 -7
  121. package/src/plugins/builtin/cot/pane.tsx +6 -2
  122. package/src/plugins/builtin/cpi/client.ts +6 -5
  123. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  124. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  125. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  126. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  127. package/src/plugins/builtin/crypto-board/series.ts +4 -4
  128. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  129. package/src/plugins/builtin/doe/client.ts +6 -5
  130. package/src/plugins/builtin/doe/headless.ts +3 -2
  131. package/src/plugins/builtin/doe/pane.tsx +13 -3
  132. package/src/plugins/builtin/doe/prices.ts +2 -3
  133. package/src/plugins/builtin/doe/series.ts +4 -4
  134. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  135. package/src/plugins/builtin/earnings-calls/data.ts +37 -17
  136. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  137. package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
  138. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  139. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  140. package/src/plugins/builtin/econ/detail-view.tsx +18 -4
  141. package/src/plugins/builtin/econ/figures.ts +48 -0
  142. package/src/plugins/builtin/econ/index.tsx +14 -5
  143. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  144. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  145. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  146. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  147. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  148. package/src/plugins/builtin/executives/data.ts +6 -1
  149. package/src/plugins/builtin/executives/pane.tsx +9 -21
  150. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  151. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  152. package/src/plugins/builtin/gpu/client.ts +4 -1
  153. package/src/plugins/builtin/gpu/series.ts +2 -3
  154. package/src/plugins/builtin/help/index.tsx +61 -59
  155. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  156. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  157. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  158. package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
  159. package/src/plugins/builtin/holders/headless.ts +4 -2
  160. package/src/plugins/builtin/holders/pane.tsx +3 -0
  161. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  162. package/src/plugins/builtin/insider/headless.ts +2 -2
  163. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  164. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  165. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  166. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  167. package/src/plugins/builtin/jodi/client.ts +4 -1
  168. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  169. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  170. package/src/plugins/builtin/macro-day/model.ts +39 -5
  171. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  172. package/src/plugins/builtin/market-movers/client.ts +3 -0
  173. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  174. package/src/plugins/builtin/market-movers/index.tsx +10 -5
  175. package/src/plugins/builtin/market-movers/screener.ts +27 -6
  176. package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
  177. package/src/plugins/builtin/mna/headless.ts +2 -0
  178. package/src/plugins/builtin/mna/pane.tsx +3 -1
  179. package/src/plugins/builtin/money-markets/series.ts +3 -5
  180. package/src/plugins/builtin/news/headless.ts +3 -1
  181. package/src/plugins/builtin/news/index.tsx +3 -0
  182. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  183. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  184. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  185. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  186. package/src/plugins/builtin/power/gas-client.ts +2 -1
  187. package/src/plugins/builtin/power/prices-client.ts +3 -2
  188. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  189. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  190. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  191. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  192. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  193. package/src/plugins/builtin/research/event-model.ts +30 -4
  194. package/src/plugins/builtin/research/index.tsx +7 -3
  195. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  196. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  197. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  198. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  199. package/src/plugins/builtin/risk-factors/data.ts +12 -6
  200. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  201. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  202. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  203. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  204. package/src/plugins/builtin/roadmap/model.ts +67 -0
  205. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  206. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  207. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  208. package/src/plugins/builtin/sec/headless.ts +2 -1
  209. package/src/plugins/builtin/sec/index.tsx +3 -0
  210. package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
  211. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  212. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  213. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  214. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  215. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  216. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  217. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  218. package/src/plugins/builtin/short-volume/series.ts +2 -3
  219. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  220. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  221. package/src/plugins/builtin/thirteenf/api.ts +15 -9
  222. package/src/plugins/builtin/thirteenf/data.ts +39 -11
  223. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  224. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  225. package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
  226. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  227. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
  228. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  229. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  230. package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
  231. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  232. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  233. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  234. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  235. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  236. package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
  237. package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
  238. package/src/plugins/builtin/volatility/client.ts +43 -16
  239. package/src/plugins/builtin/volatility/headless.ts +5 -2
  240. package/src/plugins/builtin/volatility/model.ts +17 -6
  241. package/src/plugins/builtin/volatility/pane.tsx +11 -4
  242. package/src/plugins/builtin/volatility/series.ts +2 -1
  243. package/src/plugins/builtin/yield-curve/client.ts +59 -1
  244. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  245. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  246. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  247. package/src/plugins/catalog-browser.ts +2 -1
  248. package/src/plugins/host-link.ts +1 -1
  249. package/src/plugins/installer.ts +50 -39
  250. package/src/portfolio/column-values.ts +11 -7
  251. package/src/renderers/dom/desktop/icons.tsx +10 -2
  252. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  253. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  254. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  255. package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
  256. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  257. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  258. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  259. package/src/renderers/electrobun/bun/index.ts +26 -2
  260. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  261. package/src/renderers/electrobun/bun/window/create.ts +45 -4
  262. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  263. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  264. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  265. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  266. package/src/renderers/electrobun/launcher/preload.ts +13 -0
  267. package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
  268. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  269. package/src/renderers/electrobun/view/news-source.ts +2 -0
  270. package/src/renderers/electrobun/view/ui-host.tsx +16 -1
  271. package/src/series/resolve.ts +2 -1
  272. package/src/series/types.ts +5 -0
  273. package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
  274. package/src/sources/gloomberb-cloud/index.ts +42 -8
  275. package/src/telemetry/stall-reports-node.ts +13 -2
  276. package/src/telemetry/stall-reports.ts +189 -13
  277. package/src/test-support/outage.ts +8 -0
  278. package/src/tickers/company-name-listing.ts +110 -40
  279. package/src/tickers/company-name.ts +71 -2
  280. package/src/tickers/index-roots.ts +7 -0
  281. package/src/tickers/listing-fate.ts +2 -2
  282. package/src/tickers/search/home-listing.ts +93 -8
  283. package/src/tickers/search/index.ts +7 -3
  284. package/src/tickers/search/ranking.ts +5 -1
  285. package/src/tickers/search/upsert.ts +13 -0
  286. package/src/tickers/ticker-changes.ts +8 -4
  287. package/src/time-series/market.ts +5 -3
  288. package/src/time-series/price-comparison.ts +20 -2
  289. package/src/time-series/resolve.ts +20 -1
  290. package/src/time-series/types.ts +7 -0
  291. package/src/types/company-from.ts +40 -0
  292. package/src/types/data-provider.ts +7 -0
  293. package/src/types/financials.ts +45 -3
  294. package/src/types/headless.ts +6 -0
  295. package/src/ui/host.tsx +6 -1
  296. package/src/utils/crypto-pair.ts +121 -7
  297. package/src/utils/declared-dividends.ts +1 -1
  298. package/src/utils/financial-statements.ts +73 -16
  299. package/src/utils/fundamentals.ts +53 -10
  300. package/src/utils/lease-liabilities.ts +54 -0
  301. package/src/utils/main-thread-stall.ts +47 -2
  302. package/src/utils/perf-marks.ts +23 -11
  303. package/src/utils/price-history-integrity.ts +47 -1
  304. package/src/version.ts +2 -2
@@ -83,7 +83,7 @@ import { movedAwayChange } from "../../tickers/ticker-changes";
83
83
  import { companyMemberships, loadCompanyMemberships, type CompanyMemberships, type ListMembership } from "../issuer-membership";
84
84
  import { isNotATickerMessage } from "../not-a-ticker";
85
85
  import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
86
- import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
86
+ import { cedearRatioText, depositaryPairText, isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../market-data/depositary-pair";
87
87
  import { cliFreshnessFooter } from "../result";
88
88
  import { providerMissReason } from "../../sources/provider-errors";
89
89
  import { exportEntriesTable, reportFooterLines, type CliReportTables } from "../report-tables";
@@ -100,6 +100,7 @@ import { bondFigures, NOT_SEC_YIELD } from "../../market-data/bond-funds";
100
100
  import { readFundFacts } from "../../market-data/fund-facts";
101
101
  import { taxEquivalentLines, type TaxEquivalentAnalysis } from "../../market-data/tax-equivalent";
102
102
  import { isRoundTheClockCoin } from "../../utils/crypto-pair";
103
+ import { ipoDetailNotes, ipoMetricRows, loadTickerIpo, type TickerIpo } from "../../plugins/builtin/ipo-calendar/listing-ipo";
103
104
  import { coinNewsFallbackNotice } from "../../sources/gloomberb-cloud/news";
104
105
 
105
106
  const NEWS_ITEM_LIMIT = 5;
@@ -126,6 +127,8 @@ const METADATA_SEPARATOR = " · ";
126
127
  interface TickerCommandDependencies {
127
128
  /** `--exchange`: the listing, for a symbol that trades in several places. */
128
129
  exchange?: string;
130
+ /** A stock's IPO of the last year with its lock-up news; a failure leaves the IPO section out. */
131
+ listingIpo?: (listing: { symbol: string; exchange?: string | null }) => Promise<TickerIpo | null>;
129
132
  initMarketData?: () => Promise<MarketContext>;
130
133
  fail?: (message: string, details?: string) => never;
131
134
  printResult?: CliCommandContext["printResult"];
@@ -1081,8 +1084,11 @@ export async function buildTickerReport({
1081
1084
  fundFacts = null,
1082
1085
  memberships,
1083
1086
  offerings = [],
1087
+ ipo = null,
1084
1088
  }: {
1085
1089
  symbol: string;
1090
+ /** A stock's IPO of the last year, with its lock-up estimate and news (loadTickerIpo). */
1091
+ ipo?: TickerIpo | null;
1086
1092
  /** The lists holding the company, by any of its listings (loadCompanyMemberships); the saved listing's own lists when absent. */
1087
1093
  memberships?: CompanyMemberships;
1088
1094
  /** The filings behind the latest statements, and a newer quarter's results release. */
@@ -1165,6 +1171,9 @@ export async function buildTickerReport({
1165
1171
  if (depositaryPair) {
1166
1172
  lines.push(cliStyles.muted(`ADR ratio ${depositaryPairText(depositaryPair, isReceiptSide(financials))}. Implied FX and premium: gloom fn ADR ${quote?.symbol ?? symbol}`));
1167
1173
  }
1174
+ // A CEDEAR states its ratio in its company's shares, as a receipt does.
1175
+ const cedearRatio = listingCedearRatio(financials);
1176
+ if (cedearRatio) lines.push(cliStyles.muted(`CEDEAR ratio ${cedearRatioText(cedearRatio)}.`));
1168
1177
 
1169
1178
  // A list that holds the company as another of its listings says which: "Watchlists R (as RCI.B:TSX)".
1170
1179
  const lists = memberships ?? companyMemberships(config, tickerFile, [], null);
@@ -1250,7 +1259,15 @@ export async function buildTickerReport({
1250
1259
  ]);
1251
1260
  }
1252
1261
 
1253
- const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
1262
+ // A stock that listed in the last year: its IPO, the move since, and when its lock-up is estimated to end.
1263
+ if (ipo) {
1264
+ appendMetricSection(lines, "IPO", ipoMetricRows(ipo.deal, quote ? getRegularSessionDisplay(quote)?.price : null));
1265
+ for (const note of ipoDetailNotes(ipo.deal, ipo.news)) lines.push(cliStyles.muted(note));
1266
+ }
1267
+
1268
+ // A listing of the last year may have no statements yet: its IPO says how recent it is.
1269
+ const listing = { listedSince: ipo ? ipo.deal.listingDate ?? ipo.deal.firstDay?.session ?? null : null };
1270
+ const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials, listing),
1254
1271
  { text: enterpriseValueText }, distributions, bondFund, codes);
1255
1272
  const reportedCurrency = financials.financialCurrency?.trim();
1256
1273
  const statements = [...financials.annualStatements, ...financials.quarterlyStatements];
@@ -1349,9 +1366,11 @@ function buildTickerStructuredData({
1349
1366
  bondFund = null,
1350
1367
  memberships,
1351
1368
  offerings = [],
1369
+ ipo = null,
1352
1370
  }: {
1353
1371
  symbol: string;
1354
1372
  listing: CliListing;
1373
+ ipo?: TickerIpo | null;
1355
1374
  memberships: CompanyMemberships;
1356
1375
  statementFilings: StatementFilingsReport | null;
1357
1376
  offerings?: readonly OfferingFiling[];
@@ -1434,6 +1453,8 @@ function buildTickerStructuredData({
1434
1453
  // and the offering prospectuses filed since that the cash does not include.
1435
1454
  cashRunway: runway,
1436
1455
  offeringsAfterBalanceSheet: runway ? offeringsAfter(offerings, runway.periodEnd) : [],
1456
+ // A stock that listed in the last year: its IPO deal (offer price, first day, lock-up estimate) and lock-up news.
1457
+ ...ipo ? { ipo: { ...ipo.deal, lockupNews: ipo.news } } : {},
1437
1458
  // The form and filed date of the latest periods' filings, and a newer quarter's results released before its statements.
1438
1459
  statementFilings,
1439
1460
  notes,
@@ -1462,6 +1483,8 @@ function buildTickerStructuredData({
1462
1483
  }
1463
1484
 
1464
1485
  function depositaryPairJson(financials: TickerFinancials) {
1486
+ const cedear = listingCedearRatio(financials);
1487
+ if (cedear) return { cedearRatio: { ...cedear, text: cedearRatioText(cedear) } };
1465
1488
  const pair = listingDepositaryPair(financials);
1466
1489
  if (!pair) return {};
1467
1490
  const receipt = isReceiptSide(financials);
@@ -1573,6 +1596,11 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1573
1596
  });
1574
1597
  // An offering since the latest balance sheet, read only when the report has a runway to flag.
1575
1598
  const offerings = loadOfferingFilings(statementSources, { symbol: requestSymbol, exchange }, cashRunway(resolvedFinancials) != null);
1599
+ // A stock's IPO of the last year, read beside the rest; it never fails the report.
1600
+ const ipoLoad = quote && resolveTickerInstrumentKind(tickerFile, resolvedFinancials) === "equity"
1601
+ ? (dependencies.listingIpo ?? loadTickerIpo)({ symbol: requestSymbol, exchange: exchange || quote.listingExchangeName || quote.exchangeName })
1602
+ .catch(logFailure("cli.commands-ticker", "Read a listing's IPO failed", null))
1603
+ : Promise.resolve(null);
1576
1604
  const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
1577
1605
  notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
1578
1606
  dataProvider.getNews({
@@ -1640,6 +1668,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1640
1668
  bondFund: await bondFund,
1641
1669
  memberships,
1642
1670
  offerings: await offerings,
1671
+ ipo: await ipoLoad,
1643
1672
  }),
1644
1673
  });
1645
1674
  return;
@@ -1666,6 +1695,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1666
1695
  fundFacts: await fundFacts,
1667
1696
  memberships,
1668
1697
  offerings: await offerings,
1698
+ ipo: await ipoLoad,
1669
1699
  }));
1670
1700
  if (freshness) console.log(`\n${cliFreshnessFooter(freshness, quote ?? resolvedFinancials)}`);
1671
1701
  });
@@ -141,6 +141,7 @@ export async function initCliServices(options: CliServicesOptions = {}) {
141
141
  config,
142
142
  plugins: getLoadablePlugins(options.externalPlugins ?? []),
143
143
  externalPlugins: options.externalPlugins ?? [],
144
+ interactive: false,
144
145
  });
145
146
  services.providerRouter.setConfigAccessor(() => config);
146
147
  services.providerRouter.setBackgroundRevalidation(false);
package/src/cli/errors.ts CHANGED
@@ -13,7 +13,7 @@ export const CLI_ERROR_CODE = "cli_error";
13
13
  /** The code of a command that ran, and the data service has nothing for what it asked. */
14
14
  export const NO_DATA_CODE = "no_data";
15
15
  /** The code of a command whose requests never reached the data service. */
16
- export const SERVICE_UNREACHABLE_CODE = "service_unreachable";
16
+ const SERVICE_UNREACHABLE_CODE = "service_unreachable";
17
17
 
18
18
  /** What to check when nothing answered: the connection, and the proxy when one is set. */
19
19
  function unreachableHint(): string {
@@ -74,6 +74,21 @@ export function failRetryable(message: string, code: string): never {
74
74
  throw new CliFailure(message, undefined, code, true);
75
75
  }
76
76
 
77
+ /**
78
+ * What a command says when its requests never reached the data service:
79
+ * `message` names what it could not get (by default only that), and the
80
+ * details what to check. `--json` marks it retryable.
81
+ */
82
+ export function unreachableError(message = DATA_SERVICE_UNREACHABLE_MESSAGE): CliErrorObject {
83
+ return { code: SERVICE_UNREACHABLE_CODE, message, details: unreachableHint(), retryable: true };
84
+ }
85
+
86
+ /** Fails as a command whose requests never reached the data service (`unreachableError`). */
87
+ export function failUnreachable(message?: string): never {
88
+ const error = unreachableError(message);
89
+ throw new CliFailure(error.message, error.details, error.code, error.retryable);
90
+ }
91
+
77
92
  export function closeAndFail(persistence: AppPersistence, message: string, details?: string): never {
78
93
  persistence.close();
79
94
  fail(message, details);
@@ -104,7 +119,7 @@ function cliErrorObject(error: unknown): CliErrorObject {
104
119
  }
105
120
  // No answer came back at all: in Gloom's words, whichever fetch failed (Bun's "Unable to connect", a refused proxy).
106
121
  if (isNetworkFailure(error)) {
107
- return { code: SERVICE_UNREACHABLE_CODE, message: DATA_SERVICE_UNREACHABLE_MESSAGE, details: unreachableHint(), retryable: true };
122
+ return unreachableError();
108
123
  }
109
124
  // Gloom Cloud refused what was asked (an unknown series, a bad date): the input was wrong, not the program.
110
125
  if (error instanceof ApiRequestError && error.status === 400) {
@@ -404,7 +404,8 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
404
404
  },
405
405
  {
406
406
  term: "Net debt", aliases: ["net debt/ebitda", "net debt to ebitda", "net leverage"], functions: ["FA"], shown: ["Net Debt"],
407
- definition: "Total debt less cash and short-term investments. Negative means more cash than debt. "
407
+ definition: "Total debt less cash and short-term investments, with the balance sheet's lease liabilities under IFRS (\"incl. leases\"); "
408
+ + "a company reporting under US GAAP (a 10-K, or a 20-F such as Alibaba's) keeps its operating leases out (\"excl. op. leases\"). Negative means more cash than debt. "
408
409
  + "Net debt / EBITDA is how many years of EBITDA would repay it; FA divides by a year of EBITDA, the trailing four quarters in a quarterly column.",
409
410
  },
410
411
  {
@@ -437,6 +438,12 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
437
438
  definition: "How many ordinary shares one depositary receipt holds: 1 ADR = 10 ordinary shares means one GGAL in New York is ten GGAL.BA shares in Buenos Aires. "
438
439
  + "A receipt's price compares with its home price only once the home price is multiplied by the ratio.",
439
440
  },
441
+ {
442
+ term: "CEDEAR ratio", aliases: ["cedear", "cedears", "certificado de deposito argentino"],
443
+ functions: ["DES"], shown: ["CEDEAR ratio", "CEDEAR Ratio"],
444
+ definition: "How many Buenos Aires CEDEARs make the company's shares they stand for, from BYMA's published list: 20 AAPL:BYMA = 1 Apple Inc. share. "
445
+ + "A CEDEAR's market cap is its company's, in the currency of the listing it stands for (Apple's in dollars), not the certificate's peso price times a share count.",
446
+ },
440
447
  {
441
448
  term: "ADR premium", aliases: ["adr discount", "adr vs home", "receipt premium", "adr premium/discount"],
442
449
  functions: ["ADR"], shown: ["ADR vs home", "premium", "discount"],
@@ -14,7 +14,9 @@ import { isRoundTheClockCoin } from "../utils/crypto-pair";
14
14
  import { currencyPairCode } from "../utils/currency-pair";
15
15
  import { currencyUnitLabel, resolveCurrencyUnit } from "../utils/currency-units";
16
16
  import { getPricePointTimestamp } from "../utils/price-history";
17
- import { latestTradeIndex, missingVolumeNotice, pricePointIntegrityFlags, pricePointValues } from "../utils/price-history-integrity";
17
+ import {
18
+ isWickFlag, latestTradeIndex, missingVolumeNotice, pricePointIntegrityFlags, pricePointValues, pricePointWickFlags,
19
+ } from "../utils/price-history-integrity";
18
20
  import { formatUtcTime } from "../utils/utc-time";
19
21
  import { historyCoverageNotice } from "../sources/history-coverage";
20
22
  import { barLastSession, barPeriodToDate, type BarPeriodToDate } from "../market-data/market/bar-period";
@@ -253,10 +255,17 @@ function flagCodes(row: Pick<HistoryRow, "flag">): string[] {
253
255
  return row.flag ? row.flag.split(",") : [];
254
256
  }
255
257
 
258
+ /** The codes of a bar left blank for prices that contradict each other: not a break before it, nor a withheld wick. */
259
+ function blankingCodes(row: Pick<HistoryRow, "flag">): string[] {
260
+ return flagCodes(row).filter((code) => !BREAK_FLAGS.has(code) && !isWickFlag(code));
261
+ }
262
+
256
263
  /**
257
264
  * Intraday bars keep their UTC time; daily and longer bars are trading dates.
258
265
  * A bar whose prices contradict each other is blanked exactly as HP blanks it,
259
- * and says why in `flag`. A bar after a gap or a jump in the series keeps its
266
+ * and says why in `flag`. A low or high too far from the open and close to
267
+ * have traded is left blank, the rest of the bar kept, flagged `low<close/2`
268
+ * or `high>2*open`. A bar after a gap or a jump in the series keeps its
260
269
  * prices and is flagged `gap` or `jump`.
261
270
  */
262
271
  export function historyRows(
@@ -277,7 +286,7 @@ export function historyRows(
277
286
  const time = times[index]!;
278
287
  const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
279
288
  const values = pricePointValues(point);
280
- const flags = pricePointIntegrityFlags(point);
289
+ const flags = [...pricePointIntegrityFlags(point), ...pricePointWickFlags(point)];
281
290
  const price = (value: number | null) => value == null ? null : cleanBarPrice(value, priceDivisor);
282
291
  return {
283
292
  date: intraday ? iso.replace(".000Z", "Z") : iso.slice(0, 10),
@@ -448,7 +457,7 @@ export function historyMissingVolumeNote(
448
457
  if (!historyReportsVolume(rows)) return null;
449
458
  const latestTrade = latestTradeIndex(rows.map((row) => Date.parse(row.date)));
450
459
  const missing = rows.filter((row, index) => row.volume == null && row.venueVolume == null && !row.partial && !row.filled
451
- && !flagCodes(row).some((code) => !BREAK_FLAGS.has(code)) && index !== latestTrade).length;
460
+ && blankingCodes(row).length === 0 && index !== latestTrade).length;
452
461
  return missingVolumeNotice(missing, rows.length) ?? null;
453
462
  }
454
463
 
@@ -833,10 +842,9 @@ export function historyGapNote(
833
842
  * keep their prices and are not counted.
834
843
  */
835
844
  export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): string | null {
836
- const contradictions = (row: Pick<HistoryRow, "flag">) => flagCodes(row).filter((code) => !BREAK_FLAGS.has(code));
837
- const flagged = rows.filter((row) => contradictions(row).length > 0);
845
+ const flagged = rows.filter((row) => blankingCodes(row).length > 0);
838
846
  if (flagged.length === 0) return null;
839
- const codes = new Set(flagged.flatMap(contradictions));
847
+ const codes = new Set(flagged.flatMap(blankingCodes));
840
848
  const kinds = [
841
849
  codes.has("high<open") || codes.has("high<close") ? "high below open or close" : "",
842
850
  codes.has("low>open") || codes.has("low>close") ? "low above open or close" : "",
@@ -845,3 +853,23 @@ export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): stri
845
853
  const count = `${flagged.length} of ${rows.length} ${flagged.length === 1 && rows.length === 1 ? "bar" : "bars"}`;
846
854
  return `${count} ${flagged.length === 1 ? "has" : "have"} ${kinds.join(", or ")} in the source data; their prices are left blank`;
847
855
  }
856
+
857
+ /**
858
+ * "The low of 2021-11-29 is under half its open and close, too far to have
859
+ * traded; left blank and out of ranges", or null when no bar has such a wick
860
+ * (pricePointWickFlags). Three or more bars are counted.
861
+ */
862
+ export function historyWickNote(rows: readonly Pick<HistoryRow, "date" | "flag">[]): string | null {
863
+ const flagged = rows.filter((row) => flagCodes(row).some(isWickFlag));
864
+ if (flagged.length === 0) return null;
865
+ const sides = new Set(flagged.flatMap((row) => flagCodes(row).filter(isWickFlag).map((code) => code.slice(0, code.indexOf(code.startsWith("low") ? "<" : ">")))));
866
+ const reason = "too far to have traded; left blank and out of ranges";
867
+ const only = flagged[0]!;
868
+ if (flagged.length === 1 && sides.size === 1) {
869
+ return sides.has("low") ? `The low of ${only.date} is under half its open and close, ${reason}`
870
+ : `The high of ${only.date} is over twice its open and close, ${reason}`;
871
+ }
872
+ const kinds = [sides.has("low") ? "a low under half" : "", sides.has("high") ? "a high over twice" : ""].filter(Boolean).join(" or ");
873
+ const dates = flagged.length <= LISTED_GAPS ? ` (${listItems(flagged.map((row) => row.date))})` : "";
874
+ return `${flagged.length} of ${rows.length} bars have ${kinds} their open and close${dates}, ${reason}`;
875
+ }
@@ -35,10 +35,3 @@ export function indexRootTryLine(root: IndexRoot, command = "quote"): string {
35
35
  export function indexRootAlsoLine(root: IndexRoot, command = "quote"): string {
36
36
  return `also ${root.symbol} ${root.name} (gloom ${command} ${root.symbol})`;
37
37
  }
38
-
39
- /** Cboe's Treasury yield indices: 13-week bill, 5-, 10- and 30-year notes. Each level is a yield in percent. */
40
- const YIELD_INDEX_SYMBOLS: ReadonlySet<string> = new Set(["^IRX", "^FVX", "^TNX", "^TYX"]);
41
-
42
- export function isYieldIndexSymbol(symbol: string): boolean {
43
- return YIELD_INDEX_SYMBOLS.has(symbol.trim().toUpperCase());
44
- }
@@ -222,6 +222,17 @@ const TOPIC_POINTERS: ReadonlyArray<{
222
222
  closestLabel: "COMEX curves",
223
223
  closest: [["CTM", "gloom fn CTM HG for copper, gloom fn CTM ALI for aluminium"]],
224
224
  },
225
+ {
226
+ // No source serves a jet fuel (kerosene) price. Heating oil, NY Harbor ULSD, is what airlines hedge it with and
227
+ // analysts read it by; the EIA's weekly US stocks are a catalog series (checked 2026-10-29: quote HO=F,
228
+ // fn COT HO and fn G CAP:doe:jet all answer).
229
+ topic: "Jet fuel (kerosene)",
230
+ availability: "no price series; heating oil (NY Harbor ULSD) is the usual proxy, its price: gloom quote HO=F. "
231
+ + "US jet fuel stocks (EIA, weekly): gloom fn G CAP:doe:jet",
232
+ phrases: ["jet fuel", "jet fuels", "kerosene", "kerosine", "jet kerosene", "aviation fuel", "aviation fuels", "avtur"],
233
+ closestLabel: "positioning",
234
+ closest: [["COT", "heating oil, the usual jet fuel proxy: gloom fn COT HO"]],
235
+ },
225
236
  {
226
237
  // "exchange reserves" alone reads as crypto's; "foreign exchange reserves" is a central bank's, so only whole.
227
238
  topic: "Crypto exchange reserves and exchange flows",
@@ -15,9 +15,9 @@ import {
15
15
  type ListingVenue,
16
16
  } from "../tickers/listing-venues";
17
17
  import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
18
- import { findListingOfCompanyName, isCompanyNameInput, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
18
+ import { findListingOfCompanyName, isCompanyNameInput, marksAnotherListing, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
19
19
  import { movedAwayChange, startTickerChangeLookup, type MovedTickerChange } from "../tickers/ticker-changes";
20
- import { loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
20
+ import { lineMayHaveMoved, loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
21
21
  import { quotedListingName } from "../tickers/listing-name";
22
22
  import { cliStyles } from "../utils/cli-output";
23
23
  import { groupByCompany, namesDepositaryReceipt, sameCompany, sameListedName, shortCompanyName } from "../tickers/company-name";
@@ -36,7 +36,7 @@ import { withMarketData } from "./scoped-context";
36
36
  import { failWithFields } from "./errors";
37
37
  import { stripStrayEdges } from "./symbol-args";
38
38
  import { isNoProviderMessage, isNoSourceAnswer, isProviderBusy } from "../sources/provider-errors";
39
- import { dashedCoinPair, MAJOR_COIN_NAMES } from "../utils/crypto-pair";
39
+ import { dashedCoinPair, exchangeLineCoinListing, isSpotCommodityOrCurrencyPair, MAJOR_COIN_NAMES } from "../utils/crypto-pair";
40
40
  import { boardCoinFor, primeBoardCoins } from "./board-coins";
41
41
  import { knownGapMessage } from "./known-gaps";
42
42
  import { currencyPairSpelling } from "../utils/currency-pair";
@@ -107,6 +107,12 @@ export interface ListingArg {
107
107
  * `readAs` asks about it, and a command the listing fails asks too.
108
108
  */
109
109
  meant?: { symbol: string; name: string };
110
+ /**
111
+ * A bare symbol a company's name was read as still says what it was read
112
+ * as (`listingChoiceNotes`): GETYQ for "getty images", whose NYSE line
113
+ * became GETYQ. Other bare symbols say where the data service chose instead.
114
+ */
115
+ readFromName?: true;
110
116
  }
111
117
 
112
118
  /** "Did you mean JPM (JPMorgan Chase & Co.)?", for a name read as a fund or an index; empty for any other listing. */
@@ -176,8 +182,13 @@ function parseCleanListingArg(
176
182
  // `SAN:` asks which listing; with --exchange it has its answer.
177
183
  if (option && typed.endsWith(":")) typed = typed.slice(0, -1);
178
184
  const venueKey = parsePublicTickerKey(typed);
179
- // BTC/USD, as crypto venues write a pair, is the coin's own BTC-USD.
180
- const parsed = { ...venueKey, symbol: dashedCoinPair(venueKey.symbol) ?? venueKey.symbol };
185
+ // A crypto exchange's line of a pair (BTC/GBP:COINBASE PRO, as search offered it) is the crypto venue's BTC-GBP:CCC.
186
+ const line = venueKey.exchange || optionCode ? exchangeLineCoinListing(venueKey.symbol, venueKey.exchange ?? optionCode) : null;
187
+ if (line) return { symbol: line.symbol, exchange: line.exchange, key: `${line.symbol}:${line.exchange}` };
188
+ // BTC/USD, as crypto venues write a pair, is the coin's own BTC-USD; spot crude (WTI/USD) and gold by the gram
189
+ // (GAU/USD:COMMODITY) are not coins, though tokens share their letters.
190
+ const coinPair = isSpotCommodityOrCurrencyPair(venueKey.symbol, venueKey.exchange ?? optionCode) ? null : dashedCoinPair(venueKey.symbol);
191
+ const parsed = { ...venueKey, symbol: coinPair ?? venueKey.symbol };
181
192
  const ownCode = parsed.exchange ? typedExchangeCode(typed) : "";
182
193
  let exchange = parsed.exchange ?? option;
183
194
  let code = ownCode || optionCode;
@@ -452,7 +463,7 @@ export function coinListingNotes(
452
463
  * its warnings.
453
464
  */
454
465
  export async function listingChoiceNotes(
455
- listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "readAs">,
466
+ listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "readAs" | "readFromName">,
456
467
  deps: ListingDeps,
457
468
  quote: Pick<Quote, "name" | "listingExchangeName" | "exchangeName" | "instrumentType" | "stale" | "delisting"> | null | undefined,
458
469
  /** The venues `loadBareVenues` already started; looked up here when not given. */
@@ -460,6 +471,8 @@ export async function listingChoiceNotes(
460
471
  ): Promise<string[]> {
461
472
  const coin = coinListingNotes(listing, quote);
462
473
  if (!isBareListing(listing)) return [...listingReadNotes(listing), ...coin];
474
+ // A bare symbol a name was read as says so: "Read "getty images" as GETYQ (...). GETY became GETYQ on 2026-10-22."
475
+ if (listing.readFromName) return [...listingReadNotes(listing), ...coin];
463
476
  if (!quote || UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return coin;
464
477
  const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
465
478
  if (!exchange) return coin;
@@ -475,7 +488,7 @@ export async function listingChoiceNotes(
475
488
  * as another listing (BTC, the Grayscale trust) ends with the coin's pair.
476
489
  */
477
490
  export async function quotedListingChoiceNotes(
478
- listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "request" | "readAs">,
491
+ listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "request" | "readAs" | "readFromName">,
479
492
  deps: ListingDeps,
480
493
  ): Promise<string[]> {
481
494
  const venues = loadBareVenues(listing, deps);
@@ -1325,6 +1338,22 @@ export async function oneWordCompanyName(typed: string, deps: Pick<ListingDeps,
1325
1338
  return found ? companyListingArg(typed, found) : null;
1326
1339
  }
1327
1340
 
1341
+ /**
1342
+ * A word the service marks as a company's name (`searchAliases`), read as
1343
+ * that company's listing as `oneWordCompanyName` reads a word, for a bare
1344
+ * symbol no source quotes, before it is read on the symbol's listings abroad:
1345
+ * "siemens" is SIE on XETRA (Siemens AG), not SIEMENS on NSE (Siemens Ltd.).
1346
+ * Null when the service marks no listing with the word, or marks the word's
1347
+ * own symbol, and when search names no company or is too busy to say.
1348
+ */
1349
+ export async function serviceNamedCompany(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<ListingArg | null> {
1350
+ // Most symbols are no company's name: one search says so before any listing is weighed or quoted.
1351
+ if (!ONE_WORD_NAME.test(typed.trim()) || !await marksAnotherListing(deps.dataProvider, typed)) return null;
1352
+ const found = await findOneWordCompany(typed, deps);
1353
+ if (!found?.named || found.symbol.toUpperCase() === typed.trim().toUpperCase()) return null;
1354
+ return companyListingArg(typed, found);
1355
+ }
1356
+
1328
1357
  /** The company `oneWordCompanyName` reads a word as, before it is read as a listing. */
1329
1358
  async function findOneWordCompany(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<CompanyNameListing | null> {
1330
1359
  const word = typed.trim();
@@ -1346,7 +1375,36 @@ function isMajorCoinName(word: string): boolean {
1346
1375
  }
1347
1376
 
1348
1377
  function findCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">, options: { oneWord?: boolean } = {}): Promise<CompanyNameListing | null> {
1349
- return findListingOfCompanyName(deps.dataProvider, typed, { meant: true, quoted: true, ...options });
1378
+ return findListingOfCompanyName(deps.dataProvider, typed, { meant: true, quoted: true, movedLine: (listing) => movedLineOf(listing, deps), ...options });
1379
+ }
1380
+
1381
+ /**
1382
+ * Where the company of a US line that stopped trading, or has no quote, trades
1383
+ * now when it moved off the symbol (`loadListingFate`): GETY became GETYQ on
1384
+ * 2026-10-22, so "getty images" is GETYQ rather than its delisted NYSE line or
1385
+ * the Munich line still quoting the old shares. A line that trades is not
1386
+ * looked up.
1387
+ */
1388
+ async function movedLineOf(
1389
+ listing: { symbol: string; exchange: string; quote: Quote | null; name: string | null },
1390
+ deps: Pick<ListingDeps, "dataProvider">,
1391
+ ): Promise<{ symbol: string; note: string } | null> {
1392
+ const { quote, name, ...line } = listing;
1393
+ if (quote && !lineMayHaveMoved(quote)) return null;
1394
+ const fate = await loadListingFate(line, deps.dataProvider, { quote });
1395
+ const moved = fate.moved ?? (quote ? null : movedWithoutQuote(fate.change, name));
1396
+ return moved ? { symbol: moved.tradesAs, note: moved.note } : null;
1397
+ }
1398
+
1399
+ /**
1400
+ * The move of a line with no quote, when it is the move of the company search
1401
+ * names on the line. With no quote nothing tells the company's change from an
1402
+ * older one's: a reused symbol's change data is the company that held it
1403
+ * before, so it is followed only when that company is this one.
1404
+ */
1405
+ function movedWithoutQuote(change: ListingFate["change"], name: string | null): MovedTickerChange | null {
1406
+ const moved = movedAwayChange(change);
1407
+ return moved && sameCompany(name, moved.issuer) ? moved : null;
1350
1408
  }
1351
1409
 
1352
1410
  async function readCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">, options: { oneWord?: boolean } = {}): Promise<ListingArg | null> {
@@ -1355,11 +1413,16 @@ async function readCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvi
1355
1413
  }
1356
1414
 
1357
1415
  function companyListingArg(typed: string, found: CompanyNameListing): ListingArg {
1358
- const key = `${found.symbol}:${found.exchange}`;
1416
+ const key = found.exchange ? `${found.symbol}:${found.exchange}` : found.symbol;
1359
1417
  // A fund or an index carrying the name asks about the company's stock: "Did you mean JPM (JPMorgan Chase & Co.)?".
1360
1418
  const meant = found.meant ? { symbol: found.meant.symbol, name: found.meant.name } : null;
1361
1419
  const question = meant ? ` ${meantQuestion({ meant })}` : "";
1362
- return { ...parseListingArg(key), readAs: `Read "${typed.trim()}" as ${key} (${found.name}).${question}`, ...meant ? { meant } : {} };
1420
+ // A company that moved off the line search lists says so: "GETY became GETYQ on 2026-10-22."
1421
+ const moved = found.moved ? ` ${found.moved}.` : "";
1422
+ return {
1423
+ ...parseListingArg(key), readAs: `Read "${typed.trim()}" as ${key} (${found.name}).${moved}${question}`,
1424
+ ...meant ? { meant } : {}, ...found.exchange ? {} : { readFromName: true as const },
1425
+ };
1363
1426
  }
1364
1427
 
1365
1428
  /**
@@ -1377,7 +1440,7 @@ export async function companyNameSuggestion(typed: string, deps: Pick<ListingDep
1377
1440
  const found = isCompanyNameInput(words) ? await findCompanyName(words, deps).catch(busyAsMiss) : await findOneWordCompany(words, deps);
1378
1441
  const listing = found?.meant ?? found;
1379
1442
  if (!listing || listing.symbol.toUpperCase() === words.toUpperCase()) return null;
1380
- return { symbol: isUsListingExchange(listing.exchange) ? listing.symbol : `${listing.symbol}:${listing.exchange}`, name: listing.name };
1443
+ return { symbol: !listing.exchange || isUsListingExchange(listing.exchange) ? listing.symbol : `${listing.symbol}:${listing.exchange}`, name: listing.name };
1381
1444
  } catch (error) {
1382
1445
  logFailure("cli.listing-arg", "Search a company name for a suggestion failed")(error);
1383
1446
  return null;
@@ -48,6 +48,7 @@ const NON_DATA_PANE_IDS = new Set([
48
48
  "local-agent-workspace",
49
49
  "macro-tv",
50
50
  "plugin-marketplace",
51
+ "roadmap",
51
52
  "world-venue-map",
52
53
  ]);
53
54
 
@@ -12,7 +12,9 @@ import type {
12
12
  PluginPersistence,
13
13
  } from "../../types/plugin";
14
14
  import { createPluginTeamState } from "../../plugins/team-state";
15
+ import { bindPluginContextNamespaces } from "../../plugins/registry/plugin-state";
15
16
  import type { MarketContext } from "../types";
17
+ import { createCommandPluginPersistence } from "../saved-copy-persistence";
16
18
  import type { PaneFunctionCatalog } from "./catalog";
17
19
 
18
20
  export interface PaneDiscoveryOptions {
@@ -173,23 +175,41 @@ export async function createPaneCatalog(context: MarketContext, plugins: GloomPl
173
175
  brokers,
174
176
  });
175
177
 
178
+ // Each plugin's caches save what a command fetches where the app keeps them, so a failed refresh answers from them.
179
+ const resources = context.persistence?.resources;
180
+ const persistences: Array<ReturnType<typeof createCommandPluginPersistence>> = [];
181
+ const commandPersistence = (stateId: string) => {
182
+ const persistence = createCommandPluginPersistence(resources, `plugin:${stateId}`);
183
+ persistences.push(persistence);
184
+ return persistence;
185
+ };
186
+
176
187
  const destroy = () => {
177
188
  let failure: unknown;
178
189
  for (const plugin of setupPlugins.splice(0).reverse()) {
179
190
  try { plugin.dispose?.(); } catch (error) { failure ??= error; }
180
191
  }
192
+ // A plugin that keeps its persistence past dispose must not reach a store the command closes.
193
+ for (const persistence of persistences.splice(0)) persistence.release();
181
194
  brokers.clear();
182
195
  if (context.resolveBroker === resolveBroker) context.resolveBroker = previousResolver;
183
196
  if (failure) throw failure;
184
197
  };
185
198
 
199
+ const namespaced = (stateId: string) => ({
200
+ persistence: commandPersistence(stateId),
201
+ resume: discoveryContext.resume,
202
+ configState: discoveryContext.configState,
203
+ teamState: discoveryContext.teamState,
204
+ });
186
205
  try {
187
206
  for (const plugin of plugins) {
188
207
  for (const pane of plugin.panes ?? []) panes.set(pane.id, pane);
189
208
  for (const template of plugin.paneTemplates ?? []) paneTemplates.set(template.id, template);
190
209
  if (plugin.setup) {
191
210
  setupPlugins.push(plugin);
192
- await plugin.setup(discoveryContext);
211
+ const stateId = plugin.stateId ?? plugin.id;
212
+ await plugin.setup(bindPluginContextNamespaces({ ...discoveryContext, ...namespaced(stateId) }, namespaced));
193
213
  }
194
214
  }
195
215
  } catch (error) {
@@ -123,6 +123,8 @@ export interface ReportFreshness {
123
123
  ageOnly?: true;
124
124
  /** What the newest observation is, said before its time: `newest story Tue 29 Sep 2026 20:17 UTC`. */
125
125
  asOfLabel?: string;
126
+ /** The line says when the report was retrieved after the newest observation's date (`retrieved Wed 28 Oct 2026 19:07 UTC`). */
127
+ retrievedToo?: true;
126
128
  /** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
127
129
  note?: string;
128
130
  /** Shorter forms of `note`, in the order a capture's line too long for it tries them. */
@@ -610,6 +612,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
610
612
  ...(oldestAgeMinutes != null ? { oldestAgeMinutes } : {}),
611
613
  ...(status === "unreported" && declared.ageOnly ? { ageOnly: true as const } : {}),
612
614
  ...(asOfLabel ? { asOfLabel } : {}),
615
+ ...(asOf && declared.retrievedToo ? { retrievedToo: true as const } : {}),
613
616
  ...(note ? { note } : {}),
614
617
  ...(noteVariants.length > 0 ? { noteVariants } : {}),
615
618
  ...(schedule ? { schedule } : {}),
@@ -1061,8 +1064,11 @@ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail =
1061
1064
  const undated = detail.date === "none";
1062
1065
  return [
1063
1066
  ...(freshness.sources?.length ? sourcesAsOfParts(freshness, freshness.sources, undated)
1064
- : freshness.unreached && !freshness.asOf ? []
1067
+ // Undated, a saved copy is not "retrieved" now: its own part says when it was saved.
1068
+ : (freshness.unreached || freshness.savedAt) && !freshness.asOf ? []
1065
1069
  : [asOfText(freshness, detail.asOf === "short", local, undated)]),
1070
+ // A list dated by its newest filing says when it was read as well.
1071
+ ...(freshness.retrievedToo && freshness.asOf && !freshness.savedAt ? [`retrieved ${timeText(freshness.retrievedAt, local)}`] : []),
1066
1072
  ...(freshness.schedule && detail.schedule !== "none"
1067
1073
  ? [typeof detail.schedule === "number" ? freshness.scheduleVariants?.[detail.schedule] ?? freshness.schedule : freshness.schedule] : []),
1068
1074
  ...(freshness.oldestAgeMinutes != null ? [`oldest ${ageText(freshness.oldestAgeMinutes)}`] : []),
@@ -1426,6 +1426,13 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
1426
1426
  data: null,
1427
1427
  bloomberg: [],
1428
1428
  },
1429
+ ROADMAP: {
1430
+ summary: "What is being built, what is planned and what shipped, with each item's votes. Pro members vote on what is building or planned, once per item, and can take the vote back.",
1431
+ usage: ["ROADMAP"],
1432
+ keys: [OPEN, key("v", "ote")],
1433
+ data: null,
1434
+ bloomberg: [],
1435
+ },
1429
1436
  HELP: {
1430
1437
  summary: "Help and the function reference. HELP with a function opens its card, F1 opens the focused pane's, and HELP HELP reaches support.",
1431
1438
  usage: ["HELP", "HELP OMON", "HELP HELP"],
@@ -36,8 +36,8 @@ import {
36
36
  ListingArgError, listingFateLoader, listingReadNotes, loadForListing, loadListingQuoteAnswer, quotedListingChoiceNotes, refuseMovedListing,
37
37
  } from "../listing-arg";
38
38
  import { notATickerStop } from "../company-name-hint";
39
- import { CLI_ERROR_CODE, failWithError, isCliFailure, SERVICE_UNREACHABLE_CODE } from "../errors";
40
- import { DATA_SERVICE_UNREACHABLE_MESSAGE } from "../../utils/network-failure";
39
+ import { CLI_ERROR_CODE, failUnreachable, failWithError, isCliFailure, unreachableError } from "../errors";
40
+ import { DATA_SERVICE_UNREACHABLE_MESSAGE, isNetworkFailure } from "../../utils/network-failure";
41
41
  import { isTickerLinkPeer } from "../../layout/ticker-navigation";
42
42
  import { knownGapMessage, commandPointer, commandPointerLeads, topicPointer } from "../known-gaps";
43
43
  import { selectReportTables, type CliReportTables } from "../report-tables";
@@ -264,7 +264,7 @@ export function reportFailure(
264
264
  const reasons = [...data.errors ?? [], ...typeof data.reason === "string" ? [data.reason] : []];
265
265
  const unreached = reasons.filter((reason) => reason.includes(DATA_SERVICE_UNREACHABLE_MESSAGE));
266
266
  if (unreached.length > 0 && (data.empty || data.unavailableSymbols.length > 0)) {
267
- return { code: SERVICE_UNREACHABLE_CODE, message: `${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."), retryable: true };
267
+ return unreachableError(`${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."));
268
268
  }
269
269
  if (!data.empty && !(noListing && noValues) && !(failsWithoutData && foundNothing(data))) return null;
270
270
  const unavailable = data.unavailableSymbols.length > 0 ? `no data for ${data.unavailableSymbols.join(", ")}` : "no data";
@@ -618,6 +618,9 @@ async function runPaneCliCommand(ctx: CliCommandContext, run: () => Promise<void
618
618
  // A listing it cannot use says what to type instead; a name that opens nothing, what is close to it.
619
619
  if (error instanceof ListingArgError || error instanceof UnknownPaneFunctionError) ctx.fail(error.message, error.details || undefined);
620
620
  const message = error instanceof Error ? error.message : String(error);
621
+ // Nothing answered: said as every command says it, with what to check.
622
+ if (message.includes(DATA_SERVICE_UNREACHABLE_MESSAGE)) failUnreachable(message);
623
+ if (isNetworkFailure(error)) failUnreachable();
621
624
  // A failure the command already worded keeps its advice line.
622
625
  const stated = error instanceof Error ? (error as Error & { details?: unknown }).details : undefined;
623
626
  const details = typeof stated === "string" ? stated : undefined;