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
@@ -3,6 +3,7 @@ import type { CliCommandDef } from "../../types/plugin";
3
3
  import { TIME_RANGES, type TimeRange } from "../../time-series/range";
4
4
  import { yearToDateStartIndex } from "../../market-data/market/year-to-date";
5
5
  import type { EarningsEvent, QuoteBatchResult, QuoteSubscriptionTarget, SecFilingItem } from "../../types/data-provider";
6
+ import { companyDataNotes, type CompanyFrom } from "../../types/company-from";
6
7
  import type { NewsArticle, NewsFeed, NewsQuery } from "../../news/types";
7
8
  import type {
8
9
  AnalystResearchData,
@@ -39,7 +40,7 @@ import {
39
40
  quoteChangeText,
40
41
  } from "../helpers";
41
42
  import { cliArrow, cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
42
- import { filledPeriodsNote, stalePeriodNote, statementHistoryNotes } from "../../utils/financial-statements";
43
+ import { filledPeriodsNote, hasOlderStatements, stalePeriodNote, statementHistoryNotes } from "../../utils/financial-statements";
43
44
  import { formatPerShareNumber } from "../../utils/reported-money";
44
45
  import { isCurrencyPairListing } from "../../utils/fx-market-hours";
45
46
  import {
@@ -130,6 +131,7 @@ import {
130
131
  historyCoverageWarning,
131
132
  historyFacts,
132
133
  historyFlagNote,
134
+ historyWickNote,
133
135
  historyGapNote,
134
136
  historyJumpNote,
135
137
  historyUnbrokenFrom,
@@ -165,6 +167,7 @@ import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures
165
167
  import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
166
168
  import { newsEmptyMessage } from "./news-hints";
167
169
  import {
170
+ closestStoriesReason,
168
171
  commodityNewsRoute,
169
172
  commonWordCandidate,
170
173
  METALS_NEWS_TOPIC,
@@ -179,6 +182,7 @@ import {
179
182
  noMetalStoriesMessage,
180
183
  noStoriesMessage,
181
184
  quotedSearchCommand,
185
+ searchClosestNewsStories,
182
186
  searchNewsStories,
183
187
  wordMeansMore,
184
188
  } from "./news-search";
@@ -189,7 +193,7 @@ import { exportedFundamentals } from "../../utils/price-earnings";
189
193
  import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
190
194
  import { fxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
191
195
  import { quoteBarNotes } from "../../market-data/quotes/intraday-bar";
192
- import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
196
+ import { cedearRatioText, depositaryPairText, isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../market-data/depositary-pair";
193
197
  import { isPesoDollarPair, loadPesoDollarRates, pesoDollarRateLines, pesoDollarRatesJson } from "../peso-dollar-rates";
194
198
  import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
195
199
  import { viewMarketCapitalization } from "../../utils/market-capitalization";
@@ -199,6 +203,7 @@ import {
199
203
  cloudNewsFreshness,
200
204
  cloudRealtimeAccess,
201
205
  REPORTED_DATA,
206
+ SEC_FILING_ROWS,
202
207
  SEC_FILINGS,
203
208
  } from "../../plugins/builtin/shared/report-freshness";
204
209
  import {
@@ -242,7 +247,7 @@ import {
242
247
  type ListingIdentity,
243
248
  } from "../listing-arg";
244
249
  import { bareListingNote, listingFateNotes, noLongerTradingNote, tradingListing } from "../../tickers/listing-fate";
245
- import { isYieldIndexSymbol } from "../index-roots";
250
+ import { isYieldIndexSymbol } from "../../tickers/index-roots";
246
251
  import {
247
252
  analyzeTaxEquivalent, distributionBasis, loadTreasuryTenors, parseTaxRates, TAX_RATE_HELP_OPTIONS, taxEquivalentJson, type TaxRates,
248
253
  } from "../../market-data/tax-equivalent";
@@ -913,7 +918,7 @@ function corporateActionRows(data: CorporateActionsData) {
913
918
  ...declaredDividends(data).map((event) => ({
914
919
  type: "dividend",
915
920
  date: event.exDate,
916
- detail: `declared ${currency ? formatDistributionAmount(event.amount, currency) : cleanDecimal(event.amount)}`
921
+ detail: `declared ${event.special ? "special " : ""}${currency ? formatDistributionAmount(event.amount, currency) : cleanDecimal(event.amount)}`
917
922
  + (event.payDate ? `, payable ${event.payDate}` : ""),
918
923
  })),
919
924
  ...data.splits.map((event) => ({
@@ -1105,8 +1110,10 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
1105
1110
  const quiet = ended ? null : noPrintsTodayText(row.quote, now);
1106
1111
  return [ended ?? (quiet ? `${label}: ${quiet}` : []), quoteBarNotes(label, row.quote, now)].flat();
1107
1112
  }),
1108
- // A receipt and its home line each say the ratio, as `ticker` does.
1113
+ // A receipt and its home line each say the ratio, as `ticker` does; a CEDEAR names itself in its own.
1109
1114
  ...data.flatMap((row) => {
1115
+ const cedear = listingCedearRatio({ quote: row.quote });
1116
+ if (cedear) return [`${cedearRatioText(cedear)}.`];
1110
1117
  const pair = listingDepositaryPair({ quote: row.quote });
1111
1118
  return pair ? [`${row.target.symbol}: ${depositaryPairText(pair, isReceiptSide({ quote: row.quote }))}.`] : [];
1112
1119
  }),
@@ -1458,7 +1465,7 @@ async function loadListingHistory(
1458
1465
  ...window ? [...windowHistoryNotes(window, data), coverage ?? []]
1459
1466
  : range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date, Date.now(), coverage != null), coverage ?? []],
1460
1467
  // The status line says how far a bar still forming runs; the note is for the bars it does not name.
1461
- gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1468
+ gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyWickNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1462
1469
  historyVenueVolumeNote(data) ?? [],
1463
1470
  historyBarStatusNote(forming ? served : data, { tradingDay: venueTradingDay(venue, Date.now()) }) ?? [],
1464
1471
  ].flat();
@@ -1875,7 +1882,11 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
1875
1882
  // Years filled from the listing's exchange, for the revenue and net income columns.
1876
1883
  const filled = filledPeriodsNote(financials.annualStatements.filter((statement) => rows.some((row) => row.date === statement.date)),
1877
1884
  new Set(["totalRevenue", "netIncome"]));
1878
- const historyNotes = [...stale ? [stale] : [], ...filled ? [filled] : [], ...statementHistoryNotes("annual", rows.map((row) => row.date), listing.key)];
1885
+ // GF is pointed to only when the listing's extended history goes further back than these years.
1886
+ const dates = rows.map((row) => row.date);
1887
+ const older = await hasOlderStatements(() => market.dataProvider.getTickerFinancials(listing.request.symbol, listing.request.exchange,
1888
+ { statementHistory: "extended" }), "annual", dates);
1889
+ const historyNotes = [...stale ? [stale] : [], ...filled ? [filled] : [], ...statementHistoryNotes("annual", dates, listing.key, older)];
1879
1890
  // NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
1880
1891
  const tables = tabular
1881
1892
  ? selectTables({
@@ -1932,26 +1943,29 @@ const NEWS_COLUMNS = [
1932
1943
  async function printNewsSearch(
1933
1944
  text: string,
1934
1945
  articles: NewsArticle[],
1935
- { reason, empty, commodity = true, failure, ticker = null }: {
1946
+ { reason, empty, commodity = true, failure, ticker = null, closest = false }: {
1936
1947
  reason?: string;
1937
1948
  empty?: string;
1938
1949
  commodity?: boolean;
1939
1950
  failure?: string;
1940
1951
  /** The listing a coin pair's search stands in for. */
1941
1952
  ticker?: string | null;
1953
+ /** No story has every word: these are the ones with the most. */
1954
+ closest?: boolean;
1942
1955
  },
1943
1956
  ctx: Parameters<CliCommandDef["execute"]>[1],
1944
1957
  ) {
1945
1958
  const searched = failure === undefined;
1946
1959
  const named = searched && commodity && articles.length === 0 && commodityNamedIn(text) !== null;
1947
1960
  const route = named ? await commodityNewsRoute(text) : null;
1961
+ const closestReason = closest && articles.length > 0 ? closestStoriesReason(text) : null;
1948
1962
  ctx.printResult({
1949
1963
  data: articles,
1950
1964
  ...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
1951
- metadata: { ticker, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
1965
+ metadata: { ticker, search: text, feed: null, ...(closestReason ? { match: "closest" } : {}), ...(route ? { futuresNews: route.command } : {}) },
1952
1966
  freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
1953
1967
  }, {
1954
- heading: newsSearchHeading(text, reason),
1968
+ heading: newsSearchHeading(text, [reason, closestReason].filter(Boolean).join(" · ") || undefined),
1955
1969
  rows: newsRows,
1956
1970
  columns: NEWS_COLUMNS,
1957
1971
  empty: named ? noStoriesMessage(text, route) : empty ?? (searched ? noStoriesMessage(text, null) : "No stories to show."),
@@ -2013,7 +2027,8 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
2013
2027
  if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloom news <query>");
2014
2028
  const problem = newsSearchTextProblem(text);
2015
2029
  if (problem) ctx.fail(problem);
2016
- await printNewsSearch(text, await searchNewsStories(text, limit), {}, ctx);
2030
+ const answer = await searchClosestNewsStories(text, limit);
2031
+ await printNewsSearch(text, answer.found, { closest: answer.closest }, ctx);
2017
2032
  return;
2018
2033
  }
2019
2034
  // A metal's future or name is its market's news, not a company's: GOLD in capitals stays Gold.com's.
@@ -2193,7 +2208,7 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2193
2208
  ...listingMetadata(resolved), ...(form ? { form } : {}),
2194
2209
  ...(page ? { announcementsPage: page.url } : {}),
2195
2210
  },
2196
- freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
2211
+ freshness: rowsFreshness(filingRows(filings), SEC_FILING_ROWS),
2197
2212
  ...(choice.length > 0 ? { warnings: choice } : {}),
2198
2213
  }, {
2199
2214
  heading: listingHeading(resolved),
@@ -2205,7 +2220,7 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2205
2220
  empty: [form ? `No ${form} filings found for ${listingTitle(resolved)}.`
2206
2221
  : identity ? noSecReportsNotice(resolved)
2207
2222
  : `No SEC filings found for ${listingTitle(resolved)}.`, pageLine].filter(Boolean).join("\n"),
2208
- ...(pageLine ? { footnote: pageLine, exportNotes: [pageLine] } : {}),
2223
+ ...filingsFootnote(pageLine, filings[0]?.companyFrom),
2209
2224
  });
2210
2225
  } catch (error) {
2211
2226
  if (!(error instanceof SecRegistrantMismatchError)) throw error;
@@ -2223,6 +2238,16 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2223
2238
  });
2224
2239
  }
2225
2240
 
2241
+ /**
2242
+ * The lines `filings` prints under its rows and CSV writes as notes: an ASX
2243
+ * listing's announcements page, and whose filings a foreign line shows when
2244
+ * they are its company's home line's.
2245
+ */
2246
+ function filingsFootnote(pageLine: string | null | undefined, companyFrom: CompanyFrom | undefined): { footnote?: string; exportNotes?: string[] } {
2247
+ const lines = [...(pageLine ? [pageLine] : []), ...companyDataNotes(companyFrom)];
2248
+ return lines.length ? { footnote: lines.join("\n"), exportNotes: lines } : {};
2249
+ }
2250
+
2226
2251
  /**
2227
2252
  * `filings` for a company on ASX that files nothing with the SEC: its latest
2228
2253
  * ASX announcements from Gloom Cloud, newest first, under the line saying it
@@ -2393,7 +2418,8 @@ async function printBeneficialOwners(
2393
2418
  heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
2394
2419
  // Text draws each column as the HDS report does; CSV and JSON keep the raw values.
2395
2420
  textColumns: BENEFICIAL_REPORT_COLUMNS.map((column) => ({ ...column, format: column.textFormat ?? column.format })),
2396
- ...(legend ? { footnote: legend } : {}),
2421
+ // A foreign line's owners read from its company's home line say whose.
2422
+ ...(legend || payload.companyFrom ? { footnote: [legend, ...companyDataNotes(payload.companyFrom)].filter(Boolean).join("\n") } : {}),
2397
2423
  columns: [
2398
2424
  ...BENEFICIAL_REPORT_COLUMNS.map(({ key, header, align }) => ({ key, header, ...(align ? { align } : {}) })),
2399
2425
  { key: "otherPersons", header: "Other persons" },
@@ -2494,7 +2520,7 @@ async function runHolders(
2494
2520
  const columns = holderReportColumns(shownRows, valueCurrency);
2495
2521
  // Dated as HDS dates it: by the list's as-of; an insider report of Form 4 filings alone as INS dates it, by the newest.
2496
2522
  const freshness = form4Only
2497
- ? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" })
2523
+ ? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILING_ROWS, basis: "Form 4 filings" })
2498
2524
  : rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
2499
2525
  // Named as fn HDS takes it, so the closing notes point to the same listing.
2500
2526
  const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
@@ -2564,6 +2590,8 @@ async function runHolders(
2564
2590
  insiderCommand ? insiderSummary(data, listing, identity, form4) : "",
2565
2591
  ].filter(Boolean).join("\n"),
2566
2592
  empty: `No holders reported for ${listingTitle(identity)}.`,
2593
+ // Holders read from the company's home line say whose; the SEC index's are the listing's own.
2594
+ ...(!indexed && data.companyFrom ? { footnote: companyDataNotes(data.companyFrom).join("\n") } : {}),
2567
2595
  });
2568
2596
  });
2569
2597
  }
@@ -3257,7 +3285,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
3257
3285
  title: "Bars",
3258
3286
  lines: [
3259
3287
  `Each range comes in one bar size: ${historyIntervalsByRange()}. The line under the heading, and the --json metadata, give the currency, the bar size actually served, the first and last bar and the bar count. A warning says when the bars are coarser, or start later, than the range asked for, and for a coin, which trades every day, names each day it has no bar for.`,
3260
- "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. --json rows carry currency, unit, interval and flag.",
3288
+ "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. A low under half the open and close, or a high over twice them, is left blank alone and flagged low<close/2 or high>2*open. --json rows carry currency, unit, interval and flag.",
3261
3289
  "A break in the series flags the bar after it, which keeps its prices: gap after more than three months with no bar (weeks for weekly, daily and intraday bars), jump when the close is over 100 times the one before or under a hundredth of it. A warning names each break, and the heading says the date the series runs unbroken from.",
3262
3290
  "The heading's last line is the max drawdown of the bars shown: the largest fall from a close to a later close, with the peak and the trough, their dates, and the date a close got back to the peak, if one did (maxDrawdown in --json).",
3263
3291
  ],
@@ -82,6 +82,28 @@ export async function searchNewsStories(text: string, limit: number, topics?: re
82
82
  return response.items.map((item) => mapCloudNewsArticle(item));
83
83
  }
84
84
 
85
+ /** A search's stories, and whether they are its closest ones because no story has every word. */
86
+ export interface NewsSearchAnswer {
87
+ found: NewsArticle[];
88
+ closest: boolean;
89
+ }
90
+
91
+ /**
92
+ * The stories matching the text; when no story has every word of a search of
93
+ * several, the ones with the most of them, most words first. Spellings of
94
+ * one word are one word to the service: lockup, lock-up and lock up;
95
+ * spinoff and spin-off; expiry and expiration.
96
+ */
97
+ export async function searchClosestNewsStories(text: string, limit: number): Promise<NewsSearchAnswer> {
98
+ const response = await apiClient.searchCloudNews(text, limit, undefined, { closest: true });
99
+ return { found: response.items.map((item) => mapCloudNewsArticle(item)), closest: response.match === "closest" };
100
+ }
101
+
102
+ /** What the heading says of a search no story has every word of. */
103
+ export function closestStoriesReason(text: string): string {
104
+ return `no story has every word of "${text}"; the closest, most words first`;
105
+ }
106
+
85
107
  /** The NI code of the metal markets: prices, futures, bullion and the trusts that hold it, not a miner's releases. */
86
108
  export const METALS_NEWS_TOPIC = "metals";
87
109
 
@@ -2,7 +2,7 @@ import { logFailure } from "../../utils/log-failure";
2
2
  import { apiClient } from "../../api-client";
3
3
  import { ApiRequestError } from "../../api-client/errors";
4
4
  import type { CliCommandDef } from "../../types/plugin";
5
- import { withCliServices, withMarketData } from "../context";
5
+ import { withCliServices, withConfigData, withMarketData } from "../context";
6
6
  import { CLI_ERROR_CODE, failWithFields } from "../errors";
7
7
  import type { CliResultColumn } from "../result";
8
8
  import { formatCompact } from "../../utils/format";
@@ -25,17 +25,27 @@ import {
25
25
  import { MOVER_CANDIDATES } from "../../plugins/builtin/market-movers/client";
26
26
  import { hasPartialFloat, meetsMinFloat, minFloatShares, normalizeMinFloat, PARTIAL_FLOAT_NOTE } from "../../plugins/builtin/market-movers/min-float";
27
27
  import { fetchEconCalendarFrom } from "../../plugins/builtin/econ/calendar-source";
28
- import { computedFredSeriesNote, seriesNotOnFred } from "../../sources/gloomberb-cloud/fred-series";
28
+ import {
29
+ attachFredSeriesPersistence, computedFredSeriesNote, loadCachedFredSeries, resetFredSeriesPersistence, seriesNotOnFred,
30
+ } from "../../sources/gloomberb-cloud/fred-series";
31
+ import type { CloudFredSeriesPayload } from "../../api-client";
32
+ import { savedCopyMark } from "../../data/saved-copies";
33
+ import { ratesMacroPluginMeta } from "../../plugins/builtin/builtin-plugin-meta";
34
+ import { createCommandPluginPersistence } from "../saved-copy-persistence";
29
35
  import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../market-data/market/status";
30
36
  import {
31
37
  COUNTRY_CYCLE,
32
38
  COUNTRY_FILTER_HINT,
39
+ awaitsConsensus,
33
40
  countriesWithoutEvents,
41
+ econEventName,
34
42
  FILTER_CYCLE,
35
43
  loadCalendar,
36
44
  eventCountry,
37
45
  matchesCountry,
46
+ matchesEventSearch,
38
47
  matchesImpact,
48
+ NO_CONSENSUS_NOTE,
39
49
  noScheduledReleasesMessage,
40
50
  parseCountryFilter,
41
51
  shortCalendarEnd,
@@ -303,7 +313,7 @@ function utcDateTimePart(value: unknown, part: "date" | "time", readerClock = tr
303
313
  return part === "date" ? iso.slice(0, 10) : `${iso.slice(11, 16)} UTC${readerClock ? localTimeSuffix(date.getTime()) : ""}`;
304
314
  }
305
315
 
306
- const ECON_USAGE = "econ [--country <regions or countries>] [--impact <level>] [--from <date>] [--tz <zone>]";
316
+ const ECON_USAGE = "econ [--country <regions or countries>] [--impact <level>] [--search <words>] [--from <date>] [--tz <zone>]";
307
317
  const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
308
318
  /** "Sat" for 2026-10-31. */
309
319
  const weekdayOf = (date: string) => WEEKDAYS[new Date(`${date}T00:00:00Z`).getUTCDay()]!;
@@ -470,13 +480,15 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
470
480
  const rawArgs = [...args];
471
481
  const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
472
482
  const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
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.");
473
485
  const fromOption = parseDateOption(takeOption(rawArgs, "--from"), "--from", ctx);
474
486
  const tzOption = takeOption(rawArgs, "--tz");
475
487
  const viewerZone = tzOption == null ? null : canonicalTimeZone(tzOption);
476
488
  if (tzOption != null && !viewerZone) {
477
489
  ctx.fail(`Unknown time zone "${tzOption}".`, "Use an IANA zone name, such as America/New_York, Europe/London or Asia/Tokyo.");
478
490
  }
479
- rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country and --impact.");
491
+ rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country, --impact and --search.");
480
492
  await withCliServices(ctx, async () => {
481
493
  const now = Date.now();
482
494
  const todayStart = Math.floor(now / DAY_MS) * DAY_MS;
@@ -487,7 +499,9 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
487
499
  : null;
488
500
  const past = stored?.events ? stored : null;
489
501
  const unread = stored && !stored.events && stored.failure ? `The stored calendar could not be read (${stored.failure}); listing the current calendar` : null;
490
- const { data: events, fetchedAt, stale } = past ? { data: past.events, fetchedAt: Date.now(), stale: false } : await loadCalendar(ctx.cliOptions.refresh);
502
+ const { data: events, fetchedAt, stale, source } = past
503
+ ? { data: past.events, fetchedAt: Date.now(), stale: false, source: "network" as const }
504
+ : await loadCalendar(ctx.cliOptions.refresh);
491
505
  // The calendar lists from today (UTC). --from moves the first day; --tail
492
506
  // alone keeps the newest events of the whole calendar, the past week's too.
493
507
  const from = fromOption ?? (ctx.cliOptions.tail != null ? null : new Date(todayStart).toISOString().slice(0, 10));
@@ -506,9 +520,10 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
506
520
  // Countries asked for that nothing in the window is for: a quiet week there, not an unknown country.
507
521
  const inWindow = events.filter((event) => event.date.getTime() >= fromTime);
508
522
  const unscheduled = countriesWithoutEvents(country, inWindow);
509
- const rows = inWindow
510
- .filter((event) => matchesCountry(event, country) && matchesImpact(event, impact))
511
- .sort((left, right) => left.date.getTime() - right.date.getTime())
523
+ const listed = inWindow
524
+ .filter((event) => matchesCountry(event, country) && matchesImpact(event, impact) && (search == null || matchesEventSearch(event, search)))
525
+ .sort((left, right) => left.date.getTime() - right.date.getTime());
526
+ const rows = listed
512
527
  .map((event) => {
513
528
  // The viewer's clock (--tz) for a row with a time: its date and time there.
514
529
  const clock = viewerZone && !hasNoReleaseTime(event) ? zonedClock(event.date.getTime(), viewerZone) : null;
@@ -522,7 +537,8 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
522
537
  ...(viewerZone ? { viewerDate: clock?.date ?? null, viewerTime: clock?.time ?? null } : {}),
523
538
  country: eventCountry(event),
524
539
  impact: event.impact,
525
- event: event.event,
540
+ // As the calendar names it: the Fed's decision is the upper bound of its range.
541
+ event: econEventName(event),
526
542
  actual: figures.actual,
527
543
  forecast: figures.forecast,
528
544
  prior: figures.prior,
@@ -553,15 +569,20 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
553
569
  // A filter of countries only, none of which has anything in the window, says so as its empty list.
554
570
  const allUnscheduled = unscheduled.length > 0 && unscheduled.length === country.split(",").length;
555
571
  const notes = [listedBefore, listedAfter, ...(rows.length > 0 ? blankTimeNotes(rows) : []),
556
- rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null]
572
+ rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null,
573
+ // A dash under Forecast on a release still to come is a consensus not in yet, not a missing figure.
574
+ listed.some(awaitsConsensus) ? NO_CONSENSUS_NOTE : null]
557
575
  .filter((note): note is string => !!note);
558
576
  const footnote = notes.join("\n");
577
+ // The calendar as read, not its rows: those are scheduled, mostly ahead. One that could not be
578
+ // refreshed is the copy an earlier read saved, and says when that was.
579
+ const read = rowsFreshness([], { status: "not-a-feed", basis: "calendar", asOf: fetchedAt }, { stale });
580
+ const calendarFreshness = read && source === "stale-fallback" ? { ...read, savedAt: new Date(fetchedAt).toISOString() } : read;
559
581
  ctx.printResult({
560
582
  data: rows,
561
583
  ...(unread ? { warnings: [unread] } : {}),
562
- metadata: { country, impact, from, listedThrough, storedFrom: past?.storedFrom ?? null, ...(viewerZone ? { timeZone: viewerZone } : {}) },
563
- // The calendar as read, not its rows: those are scheduled, mostly ahead.
564
- freshness: rowsFreshness([], { status: "not-a-feed", basis: "calendar", asOf: fetchedAt }, { stale }),
584
+ metadata: { country, impact, ...(search != null ? { search } : {}), from, listedThrough, storedFrom: past?.storedFrom ?? null, ...(viewerZone ? { timeZone: viewerZone } : {}) },
585
+ freshness: calendarFreshness,
565
586
  }, {
566
587
  dateKey: "date",
567
588
  defaultLimit: 50,
@@ -607,6 +628,35 @@ function isUnsupportedSeries(error: unknown): boolean {
607
628
  return error.details?.code === "unsupported_series" || /^Unsupported FRED series\b/i.test(message);
608
629
  }
609
630
 
631
+ /**
632
+ * Runs `run` with the FRED series cache saving what it fetches in the profile,
633
+ * where the app's rates and macro panes keep it: a refresh that fails then
634
+ * answers from the copy an earlier read saved, and the report says so.
635
+ */
636
+ function withSavedFredSeries<T>(ctx: Parameters<CliCommandDef["execute"]>[1], run: () => Promise<T>): Promise<T> {
637
+ return withConfigData(ctx, async ({ persistence }) => {
638
+ attachFredSeriesPersistence(createCommandPluginPersistence(persistence.resources, `plugin:${ratesMacroPluginMeta.stateId}`));
639
+ try {
640
+ return await run();
641
+ } finally {
642
+ resetFredSeriesPersistence();
643
+ }
644
+ });
645
+ }
646
+
647
+ /**
648
+ * A FRED series from Gloom Cloud, or the copy an earlier read saved when it
649
+ * cannot be refreshed (marked as such). A refusal (a series Gloom Cloud no
650
+ * longer serves, access denied) is said, not covered: `loadCachedFredSeries`
651
+ * throws it.
652
+ */
653
+ async function loadFredSeries(
654
+ seriesId: string,
655
+ params: { startDate?: string; limit?: number; sortOrder: "asc" | "desc" },
656
+ ): Promise<CloudFredSeriesPayload> {
657
+ return (await loadCachedFredSeries({ seriesId, ...params }, () => apiClient.getCloudFredSeries(seriesId, params))).data;
658
+ }
659
+
610
660
  async function runFredList(filter: string, ctx: Parameters<CliCommandDef["execute"]>[1]) {
611
661
  let catalog;
612
662
  try {
@@ -652,9 +702,19 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
652
702
  ctx.fail(change ? `Unknown change "${change}".` : "--change needs 1D or 1W.", "Use 1D (from the previous observation) or 1W (from a week before).");
653
703
  }
654
704
  const seriesId = requireOneArg(args, FRED_USAGE, "series", ctx).toUpperCase();
705
+ await withSavedFredSeries(ctx, () => printFredSeries(ctx, { seriesId, start, startDate, sortOrder, horizon }));
706
+ }
707
+
708
+ async function printFredSeries(ctx: Parameters<CliCommandDef["execute"]>[1], { seriesId, start, startDate, sortOrder, horizon }: {
709
+ seriesId: string;
710
+ start: string | undefined;
711
+ startDate: string;
712
+ sortOrder: "asc" | "desc";
713
+ horizon: ReturnType<typeof parseFredChange>;
714
+ }) {
655
715
  // A change needs the observation it is measured from: the fetch reaches back before the first row shown.
656
716
  const fetchStart = horizon ? fredChangeFetchStart(startDate) : startDate;
657
- const data = await apiClient.getCloudFredSeries(seriesId, { startDate: fetchStart, sortOrder }).catch((error: unknown) => {
717
+ const data = await loadFredSeries(seriesId, { startDate: fetchStart, sortOrder }).catch((error: unknown) => {
658
718
  if (isUnsupportedSeries(error)) ctx.fail(`Unsupported FRED series ${seriesId}. List supported series with: gloom fred --list`);
659
719
  throw error;
660
720
  });
@@ -668,7 +728,9 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
668
728
  const releasedOf = (observation: { publisher?: string; publishedAt?: string; releasedOn?: string }) =>
669
729
  observation.releasedOn ?? (observation.publisher?.trim() ? observation.publishedAt : undefined);
670
730
  // A value FRED does not list yet carries its publisher and when it published, in every export.
731
+ // A row from the copy an earlier read saved, the refresh having failed, carries its mark: the source line says so.
671
732
  const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string; releasedOn?: string }) => ({
733
+ ...savedCopyMark(data),
672
734
  date: observation.date,
673
735
  value: fredExportValue(seriesId, observation.value),
674
736
  ...(changes ? { change: changes.get(observation.date)?.change ?? null, changeFrom: changes.get(observation.date)?.changeFrom ?? null } : {}),
@@ -687,7 +749,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
687
749
  computedFredSeriesNote(seriesId),
688
750
  ].filter((note): note is string => note != null);
689
751
  // Nothing on or after the start: name where the series ends, so an empty answer is not a dead end.
690
- const latest = rows.length > 0 ? null : await apiClient.getCloudFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
752
+ const latest = rows.length > 0 ? null : await loadFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
691
753
  .then((payload) => payload.observations[0] ?? null).catch(logFailure("cli.commands-overview", "Read latest economic series observation failed", null));
692
754
  // CSV and NDJSON are rows alone: an empty one would read as a series with no data, so it fails and says where the series ends.
693
755
  if (rows.length === 0 && (ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson")) {
@@ -759,10 +821,12 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
759
821
  const startDate = parseStartDate(takeOption(args, "--start"), ctx);
760
822
  rejectArgs(args, "yield-curve [--start <date>]", ctx);
761
823
  const series = Object.keys(YIELD_TENORS);
762
- const results = await Promise.all(series.map(async (seriesId) => {
763
- const data = await apiClient.getCloudFredSeries(seriesId, { startDate, sortOrder: "desc" });
824
+ const results = await withSavedFredSeries(ctx, () => Promise.all(series.map(async (seriesId) => {
825
+ const data = await loadFredSeries(seriesId, { startDate, sortOrder: "desc" });
764
826
  const [latest, previous] = data.observations;
765
827
  return {
828
+ // A tenor from the copy an earlier read saved, the refresh having failed, says so in the source line.
829
+ ...savedCopyMark(data),
766
830
  seriesId,
767
831
  date: latest?.date ?? "",
768
832
  // Yields in percent, which the exports' `currency_or_unit` column says (`pct`), as `fred` does.
@@ -775,7 +839,7 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
775
839
  latest,
776
840
  publishedAt: latest?.publisher ? null : fredPublicationTime(data.info, latest?.date),
777
841
  };
778
- }));
842
+ })));
779
843
  // Tenors whose latest yield FRED does not list yet name the Treasury and when it published them.
780
844
  const ahead = fredProvenance(results.flatMap((row) => row.latest ? [row.latest] : []));
781
845
  // The curve shown was published with its last tenor: the newest of their FRED updates, when each has one.
@@ -907,10 +971,11 @@ export const overviewCliCommands: CliCommandDef[] = [
907
971
  options: [
908
972
  { 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" },
909
973
  { 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" },
910
975
  { 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` },
911
976
  { 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" },
912
977
  ],
913
- examples: ["econ", "econ --country US --impact high", "econ --country MX,BR,ZA,IN,KR", "econ --from 2026-10-12 --country US",
978
+ examples: ["econ", "econ --country US --impact high", "econ --search nonfarm", "econ --country MX,BR,ZA,IN,KR", "econ --from 2026-10-12 --country US",
914
979
  "econ --country BR --tz America/New_York", "econ --tail 20"],
915
980
  },
916
981
  execute: runEcon,
@@ -16,6 +16,7 @@ import {
16
16
  candidateExchange,
17
17
  LISTING_SEARCH_LIMIT,
18
18
  listingDistance,
19
+ primaryCompanyLine,
19
20
  searchTickerCandidates,
20
21
  type TickerSearchCandidate,
21
22
  } from "../../tickers/search";
@@ -34,8 +35,7 @@ import { readBrokerIndexCode } from "../listing-arg";
34
35
  import { knownGapMessage, commandPointer } from "../known-gaps";
35
36
  import { instrumentTypeLabel } from "../../tickers/instrument-kind";
36
37
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
37
- import { companyNameExtraWords, groupByCompany, sameCompany } from "../../tickers/company-name";
38
- import { namedByService } from "../../tickers/company-name-listing";
38
+ import { companyLines, companyNameExtraWords, groupByCompany, namedByService, sameCompany } from "../../tickers/company-name";
39
39
  import { findCatalogSeries, renderSeriesMatches, type SeriesMatch } from "../series-search";
40
40
  import { centralBankAskedAbout, centralBankCiting, type CentralBank } from "../../utils/central-banks";
41
41
  import { createPaneCatalog } from "../pane-functions/discovery";
@@ -283,17 +283,17 @@ function withHomeListingPrimary(query: string, candidates: TickerSearchCandidate
283
283
  || primary.symbol.toUpperCase() === query.toUpperCase()) return candidates;
284
284
  const company = resolveSearchName(primary);
285
285
  const marked = !!primary.result && namedByService(query, primary.result);
286
- const lines = candidates.filter((candidate) => candidate.kind === "search" && candidate.category !== "Funds & Derivatives"
287
- && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other") && sameCompany(resolveSearchName(candidate), company)
288
- && (!marked || companyNameExtraWords(resolveSearchName(candidate), company) != null));
289
- const score = (candidate: TickerSearchCandidate) => candidate.result?.popularity ?? candidate.popularity ?? null;
290
- const [home] = [...lines].sort((a, b) => listingDistance(a) - listingDistance(b) || (score(b) ?? -1) - (score(a) ?? -1) || candidates.indexOf(a) - candidates.indexOf(b));
291
- if (!home || home === primary) return candidates;
292
- const closer = listingDistance(home) < listingDistance(primary);
293
- const [homeScore, primaryScore] = [score(home), score(primary)];
294
- const moreSearched = !marked && listingDistance(home) === listingDistance(primary) && homeScore != null
295
- && (primaryScore == null || homeScore >= primaryScore * 2);
296
- if (!closer && !moreSearched) return candidates;
286
+ // Never a line of another business its name is in (`companyLines`): Siemens Energy AG is not Siemens AG's.
287
+ const lines = companyLines(primary, candidates.filter((candidate) => candidate === primary || (candidate.kind === "search"
288
+ && candidate.category !== "Funds & Derivatives" && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other"))),
289
+ (candidate) => ({
290
+ name: resolveSearchName(candidate), exchange: candidateExchange(candidate),
291
+ receipt: listingDistance(candidate) === 2, home: candidate.result?.homeListing === true,
292
+ })).filter((candidate) => candidate === primary || !marked || companyNameExtraWords(resolveSearchName(candidate), company) != null);
293
+ const home = primaryCompanyLine(primary, lines, (candidate) => ({
294
+ distance: listingDistance(candidate), score: candidate.result?.popularity ?? candidate.popularity ?? null,
295
+ }), { named: marked });
296
+ if (home === primary) return candidates;
297
297
  const rest = candidates.filter((candidate) => candidate !== home);
298
298
  const at = rest.indexOf(primary);
299
299
  return [...rest.slice(0, at), { ...home, category: "Primary Listing" as const }, { ...primary, category: "Other Listings" as const }, ...rest.slice(at + 1)];