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
@@ -24,7 +24,8 @@ import {
24
24
  barPriceDecimals, formatBarPrice, historyAllStart, historyBarsAfterHole, historyChangeBaseNote, historyMissingPeriodsNote, quotedPriceDivisor,
25
25
  } from "../../../../cli/history-rows";
26
26
  import {
27
- MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, priceHistoryIntegrityNotice,
27
+ MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, pricePointWickFlags, priceHistoryIntegrityNotice,
28
+ priceHistoryWickNotice,
28
29
  } from "../../../../utils/price-history-integrity";
29
30
  import {
30
31
  useAssetData,
@@ -257,6 +258,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
257
258
  [points, venue],
258
259
  );
259
260
  const integrityNotice = priceHistoryIntegrityNotice(rows.filter((row) => row.point.integrity).length);
261
+ const wickNotice = useMemo(() => priceHistoryWickNotice(points.filter((point) => pricePointWickFlags(point).length > 0).length), [points]);
260
262
  const volumeMissing = useMemo(() => missingVolumeCount(rows.map((row) => ({
261
263
  ...row.point, date: row.time, partial: row.partial, filled: row.filled,
262
264
  }))), [rows]);
@@ -356,7 +358,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
356
358
 
357
359
  usePaneNoticeFooter({
358
360
  registrationId: "historical-prices:integrity",
359
- notices: [integrityNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
361
+ notices: [integrityNotice, wickNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
360
362
  focused,
361
363
  title: "Price history data",
362
364
  });
@@ -404,6 +406,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
404
406
  : loading ? (rows.length ? "Refreshing" : "Loading") : rows.length ? "Available" : "Empty"],
405
407
  ...(allStart ? [["Source starts", allStart]] : []),
406
408
  ...(integrityNotice ? [["Warning", integrityNotice]] : []),
409
+ ...(wickNotice ? [["Warning", wickNotice]] : []),
407
410
  ...gapNotices.map((notice) => ["Gap note", notice]),
408
411
  ...(volumeNotice ? [["Volume note", volumeNotice]] : []),
409
412
  ...(error ? [["Error", error]] : []),
@@ -505,13 +505,46 @@ function coversLatestYear(ttm: FinancialTableStatement, latestYear: FinancialSta
505
505
  return days <= 7;
506
506
  }
507
507
 
508
+ /** Counts of the shares a company has issued or has outstanding, which are never zero; treasury shares can be. */
509
+ const OUTSTANDING_COUNT_FIELDS = ["basicShares", "dilutedShares", "shareIssued", "ordinarySharesNumber"] as const;
510
+
511
+ /**
512
+ * Share counts that cannot be the company's: zero, or under a thousandth of its
513
+ * typical count (IAG's 4,642 for Q3 2025, millions read as shares, which shows
514
+ * as 0.00 bn and a fall of 100%). They are missing, not a value to grow from.
515
+ */
516
+ function withoutImpossibleShareCounts(
517
+ annualStatements: FinancialStatement[],
518
+ quarterlyStatements: FinancialStatement[],
519
+ ): { annualStatements: FinancialStatement[]; quarterlyStatements: FinancialStatement[] } {
520
+ const all = [...annualStatements, ...quarterlyStatements];
521
+ const floors = new Map<string, number>();
522
+ for (const field of OUTSTANDING_COUNT_FIELDS) {
523
+ const counts = all.map((row) => row[field]).filter((value): value is number => typeof value === "number" && value > 0).sort((a, b) => a - b);
524
+ floors.set(field, counts.length ? counts[Math.floor(counts.length / 2)]! / 1000 : 0);
525
+ }
526
+ const impossible = (row: FinancialStatement, field: typeof OUTSTANDING_COUNT_FIELDS[number]) => {
527
+ const value = row[field];
528
+ return typeof value === "number" && (value <= 0 || value < floors.get(field)!);
529
+ };
530
+ const clean = (rows: FinancialStatement[]) => rows.map((row) => {
531
+ const drop = OUTSTANDING_COUNT_FIELDS.filter((field) => impossible(row, field));
532
+ if (!drop.length) return row;
533
+ const next = { ...row };
534
+ for (const field of drop) delete next[field];
535
+ return next;
536
+ });
537
+ return { annualStatements: clean(annualStatements), quarterlyStatements: clean(quarterlyStatements) };
538
+ }
539
+
508
540
  export function selectFinancialStatements(
509
541
  period: FinancialPeriod,
510
542
  statement: string,
511
- annualStatements: FinancialStatement[],
512
- quarterlyStatements: FinancialStatement[],
543
+ servedAnnual: FinancialStatement[],
544
+ servedQuarterly: FinancialStatement[],
513
545
  limit = period === "annual" ? 5 : 6,
514
546
  ) {
547
+ const { annualStatements, quarterlyStatements } = withoutImpossibleShareCounts(servedAnnual, servedQuarterly);
515
548
  // A period with nothing on this statement (a fiscal year-end balance point on
516
549
  // the income statement) would render as an all-dash column.
517
550
  const tabRows = FINANCIAL_SUB_TABS.find((tab) => tab.key === statement)?.rows;
@@ -4,6 +4,7 @@ import { formatNumber, pickUnit } from "../../../../utils/format";
4
4
  import { pricePointIntegrity } from "../../../../utils/price-history-integrity";
5
5
  import { formatPerShareNumber } from "../../../../utils/reported-money";
6
6
  import { hasStatementGap } from "../../../../utils/statement-gaps";
7
+ import { debtAndLeases, leaseBasis, type LeaseBasis } from "../../../../utils/lease-liabilities";
7
8
  import { cashAndShortTermInvestments, excludesCashRunway, formatRunwayMonths, SHORT_RUNWAY_HISTORY, monthlyOperatingBurn, QUARTER_MONTHS, reportsShortTermTotal, runwayMonths, YEAR_MONTHS } from "../../../../utils/cash-runway";
8
9
  import {
9
10
  buildPreviousStatementMap,
@@ -58,6 +59,8 @@ export interface RatioPeriod {
58
59
  price: RatioAmount;
59
60
  /** The filer reports cash and short-term investments as a total in some period on file (utils/cash-runway). */
60
61
  shortTermTotalOnFile?: boolean;
62
+ /** How net debt treats the lease liabilities the listing's balance sheets report; absent when none do. */
63
+ leaseBasis?: LeaseBasis;
61
64
  }
62
65
 
63
66
  interface RatioInputDef {
@@ -75,6 +78,13 @@ interface RatioInputDef {
75
78
  trailing?: boolean;
76
79
  /** A shorter name for quarterly columns, so the label, TTM and its unit fit the label column. */
77
80
  trailingLabel?: string;
81
+ /** Debt net of cash: where the listing reports lease liabilities, a row naming them follows it (withLeaseRow). */
82
+ netDebt?: boolean;
83
+ /**
84
+ * Not an operand: a line the input before it includes or leaves out, shown under it. Neither its value nor
85
+ * its gap enters the ratio.
86
+ */
87
+ memo?: boolean;
78
88
  value: (period: RatioPeriod) => RatioAmount;
79
89
  }
80
90
 
@@ -245,12 +255,41 @@ function marketCap(period: RatioPeriod): RatioAmount {
245
255
  return period.price * shares;
246
256
  }
247
257
 
258
+ /**
259
+ * Total debt less cash and short-term investments, with the balance sheet's lease liabilities where the listing's
260
+ * are included (utils/lease-liabilities). A balance sheet without its lease line then has no net debt.
261
+ */
262
+ function netDebt({ statement, leaseBasis: basis }: RatioPeriod): RatioAmount {
263
+ const cash = cashAndShortTerm(statement);
264
+ if (basis === "included") {
265
+ const lines = debtAndLeases(statement);
266
+ return lines === undefined || cash === undefined ? "not-reported" : lines.totalDebt + lines.leases - cash;
267
+ }
268
+ const debt = finite(statement.totalDebt);
269
+ return debt === undefined || cash === undefined ? "not-reported" : debt - cash;
270
+ }
271
+
248
272
  function enterpriseValue(period: RatioPeriod): RatioAmount {
249
- const debt = finite(period.statement.totalDebt);
250
- const cash = cashAndShortTerm(period.statement);
251
- if (debt === undefined || cash === undefined) return "not-reported";
273
+ const debt = netDebt(period);
274
+ if (isGap(debt)) return debt;
252
275
  const cap = marketCap(period);
253
- return isGap(cap) ? cap : cap + debt - cash;
276
+ return isGap(cap) ? cap : cap + debt;
277
+ }
278
+
279
+ /** The lease liabilities net debt includes, or a US GAAP filer's operating leases it leaves out. */
280
+ const LEASE_ROW_LABEL: Record<LeaseBasis, string> = { included: "incl. leases", excluded: "excl. op. leases" };
281
+
282
+ /** The ratio with a row naming the listing's lease liabilities under its net debt, when it reports any. */
283
+ function withLeaseRow(def: RatioDef, basis: LeaseBasis | undefined): RatioDef {
284
+ const at = def.inputs.findIndex((input) => input.netDebt);
285
+ if (!basis || at < 0) return def;
286
+ const row: RatioInputDef = {
287
+ label: LEASE_ROW_LABEL[basis],
288
+ format: "money",
289
+ memo: true,
290
+ value: ({ statement }) => reported(finite(statement.capitalLeaseObligations)),
291
+ };
292
+ return { ...def, inputs: [...def.inputs.slice(0, at + 1), row, ...def.inputs.slice(at + 1)] };
254
293
  }
255
294
 
256
295
  const DSO: RatioDef = {
@@ -378,15 +417,7 @@ export const RATIO_TABS: RatioTabDef[] = [
378
417
  label: "Net Debt / EBITDA",
379
418
  format: "multiple",
380
419
  inputs: [
381
- {
382
- label: "Net Debt",
383
- format: "money",
384
- value: ({ statement }) => {
385
- const debt = finite(statement.totalDebt);
386
- const cash = cashAndShortTerm(statement);
387
- return debt === undefined || cash === undefined ? "not-reported" : debt - cash;
388
- },
389
- },
420
+ { label: "Net Debt", format: "money", netDebt: true, value: netDebt },
390
421
  trailingFlow("EBITDA", line("ebitda"), "÷"),
391
422
  ],
392
423
  compute: ([debt, ebitda]) => over(debt!, ebitda!),
@@ -517,7 +548,7 @@ export const RATIO_TABS: RatioTabDef[] = [
517
548
  label: "EV / EBITDA",
518
549
  format: "multiple",
519
550
  inputs: [
520
- { label: "Enterprise Value", format: "money", value: enterpriseValue },
551
+ { label: "Enterprise Value", format: "money", netDebt: true, value: enterpriseValue },
521
552
  trailingFlow("EBITDA", line("ebitda"), "÷"),
522
553
  ],
523
554
  // A negative enterprise value makes the multiple meaningless too.
@@ -568,10 +599,11 @@ function inputAmount(input: RatioInputDef, period: RatioPeriod): RatioAmount {
568
599
 
569
600
  export function evaluateRatio(def: RatioDef, period: RatioPeriod): RatioCell {
570
601
  const inputs = def.inputs.map((input) => inputAmount(input, period));
571
- const gap = GAP_ORDER.find((candidate) => inputs.includes(candidate));
602
+ const operands = inputs.filter((_, index) => !def.inputs[index]!.memo);
603
+ const gap = GAP_ORDER.find((candidate) => operands.includes(candidate));
572
604
  if (gap) return { value: gap, inputs };
573
- const values = inputs.map((amount, index) => (
574
- def.inputs[index]!.annualized && period.quarterly ? (amount as number) * QUARTERS_PER_YEAR : amount as number
605
+ const values = def.inputs.flatMap((input, index) => (
606
+ input.memo ? [] : [input.annualized && period.quarterly ? (inputs[index] as number) * QUARTERS_PER_YEAR : inputs[index] as number]
575
607
  ));
576
608
  const value = def.compute(values, period);
577
609
  return { value: isGap(value) || Number.isFinite(value) ? value : "not-meaningful", inputs };
@@ -702,6 +734,7 @@ export function ratioPeriods(
702
734
  onFile: { annual: readonly FinancialStatement[]; quarterly: readonly FinancialStatement[] } = { annual: [], quarterly: [] },
703
735
  ): RatioPeriod[] {
704
736
  const shortTermTotalOnFile = reportsShortTermTotal([...onFile.annual, ...onFile.quarterly, ...statements]);
737
+ const leases = leaseBasis([...onFile.annual, ...onFile.quarterly, ...statements]);
705
738
  return statements.map((statement) => {
706
739
  const quarterly = period === "quarterly" && statement.date !== "TTM";
707
740
  return {
@@ -711,6 +744,7 @@ export function ratioPeriods(
711
744
  ...(quarterly ? { trailing: trailingYear(statement, onFile.annual, onFile.quarterly) } : {}),
712
745
  price: price(statement),
713
746
  shortTermTotalOnFile,
747
+ ...(leases ? { leaseBasis: leases } : {}),
714
748
  };
715
749
  });
716
750
  }
@@ -758,11 +792,12 @@ export function buildRatioTableModel(
758
792
  ): RatioTableModel {
759
793
  const quarterly = periods.some((period) => period.quarterly);
760
794
  const totalOnFile = periods.some((period) => period.shortTermTotalOnFile);
795
+ const leases = periods.find((period) => period.leaseBasis)?.leaseBasis;
761
796
  const ratios = visibleRatios(tab, quarterly)
762
797
  .filter((def) => def.format !== "months" || !excludesCashRunway(profile))
763
798
  .map((def) => def.format === "months" && !totalOnFile
764
799
  ? { ...def, inputs: def.inputs.map((input, index) => index === 0 ? { ...input, label: "Cash" } : input) }
765
- : def);
800
+ : withLeaseRow(def, leases));
766
801
  const cells = new Map(ratios.map((def) => [def.id, periods.map((period) => evaluateRatio(def, period))]));
767
802
  const inputs = ratios.flatMap((def) => def.inputs.map((input, index) => {
768
803
  const values = cells.get(def.id)!.map((cell) => cell.inputs[index]).filter((value) => typeof value === "number");
@@ -1,4 +1,7 @@
1
- import { FINANCIAL_VINTAGE_NOTICE, filledPeriodsNote, stalePeriodNote, statementHistoryNotes } from "../../../utils/financial-statements";
1
+ import {
2
+ FINANCIAL_VINTAGE_NOTICE, filledPeriodsNote, hasOlderStatements, reportsHalfYearly, stalePeriodNote, statementHistoryNotes,
3
+ } from "../../../utils/financial-statements";
4
+ import { canonicalExchange } from "../../../utils/exchanges";
2
5
  import { hasValidQuoteObservationTime } from "../../../market-data/quotes/freshness";
3
6
  import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, quoteHeadlineTime, type ExtendedSession } from "../../../market-data/market/status";
4
7
  import type { HeadlessPaneColumn, HeadlessPaneDefinition, HeadlessPaneFreshness, HeadlessPaneOptionValues } from "../../../types/headless";
@@ -6,7 +9,8 @@ import type { TimeRange } from "../../../time-series/range";
6
9
  import { formatNumber, formatPercentRaw } from "../../../utils/format";
7
10
  import { formatMarketPrice, formatMarketPriceWithCurrency, formatPriceObservation, penceFigure, quoteFormatOptions, withCurrencyMinorDigits, type MarketFormatOptions } from "../../../market-data/market/format";
8
11
  import {
9
- MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, priceHistoryIntegrityNotice,
12
+ MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, pricePointWickFlags, priceHistoryIntegrityNotice,
13
+ priceHistoryWickNotice,
10
14
  } from "../../../utils/price-history-integrity";
11
15
  import {
12
16
  buildFinancialTableModel, financialStatementCurrency, financialStatementDateNotice, financialStatementLimitations,
@@ -151,18 +155,23 @@ export const financialStatementsHeadless: HeadlessPaneDefinition<"rows"> = {
151
155
  const stale = table ? stalePeriodNote(financialStatementPeriods(table.statements, table.subTab)) : null;
152
156
  // Years filled from the listing's exchange, with the lines this table shows.
153
157
  const filled = table ? filledPeriodsNote(table.statements, financialSubTabFields(table.subTab)) : null;
158
+ const periodDates = dates.map(({ date }) => date);
159
+ const missing = rows.length ? null : await missingPeriodsError(ctx, symbol, financials, requestedPeriod, hasRequestedStatements, options.statement);
154
160
  return {
155
161
  rows, columns, unavailableSymbols: rows.length ? [] : [symbol],
156
- ...(!hasRequestedStatements ? { errors: [`${symbol}: No ${requestedPeriod} financial statement coverage.`] } : {}),
162
+ ...(missing ? { errors: [missing] } : {}),
157
163
  // The report closes with how old its latest period is, how far back its periods go, and how to read further.
158
- notes: [...stale ? [stale] : [], ...filled ? [filled] : [], ...offering ? [offering] : [], ...statementHistoryNotes(requestedPeriod, dates.map(({ date }) => date), symbol)],
164
+ notes: [...stale ? [stale] : [], ...filled ? [filled] : [], ...offering ? [offering] : [],
165
+ ...statementHistoryNotes(requestedPeriod, periodDates, symbol, await reportOlderStatements(ctx, symbol, requestedPeriod, periodDates))],
159
166
  metadata: {
160
167
  symbol, name: financials.quote?.name ?? symbol, currency: statementCurrency ?? null, unit: table?.unit ?? null, quoteCurrency: financials.quote?.currency ?? null,
161
168
  statement: table?.subTab.key ?? options.statement, statementLabel: table?.subTab.name ?? null,
162
169
  period: requestedPeriod, growthBasis: requestedPeriod === "quarterly" ? "QoQ" : "YoY", columns: dates,
163
170
  shareBasis: shareBases.size === 1 ? [...shareBases][0] ?? null : null,
164
171
  ...(receiptPeriods.length > 0 ? { adrRatio: financials.fundamentals?.adrRatio ?? financials.quote?.adrRatio ?? null } : {}),
165
- notices: rows.length ? [FINANCIAL_VINTAGE_NOTICE, ...(shareBasisNotice ? [shareBasisNotice] : [])] : [],
172
+ // A quarter's change is against the quarter before it, which a reader of a table of quarters must be told.
173
+ notices: rows.length ? [FINANCIAL_VINTAGE_NOTICE, ...requestedPeriod === "quarterly" ? [QUARTERLY_GROWTH_NOTICE] : [],
174
+ ...(shareBasisNotice ? [shareBasisNotice] : [])] : [],
166
175
  limitations: financialStatementLimitations(financials),
167
176
  dateProvenance: financialStatementDateNotice(table?.statements ?? []),
168
177
  },
@@ -172,6 +181,44 @@ export const financialStatementsHeadless: HeadlessPaneDefinition<"rows"> = {
172
181
 
173
182
  const reportAmount = (amount: RatioAmount) => (typeof amount === "number" ? amount : null);
174
183
 
184
+ /** What a table of quarters says its growth is measured against. */
185
+ const QUARTERLY_GROWTH_NOTICE = "Growth is vs prior quarter.";
186
+
187
+ type HeadlessContext = Parameters<typeof financialStatementsHeadless.load>[1];
188
+
189
+ /** Whether GF, reading the listing's extended statement history, goes back further than the report (statementHistoryNotes). */
190
+ function reportOlderStatements(ctx: HeadlessContext, symbol: string, period: "annual" | "quarterly", dates: readonly string[]) {
191
+ return hasOlderStatements(async () => {
192
+ const { symbol: bare, exchange } = await resolveHeadlessInstrument(ctx, symbol);
193
+ return ctx.marketData.getTickerFinancials(bare, exchange ?? "", { statementHistory: "extended" });
194
+ }, period, dates);
195
+ }
196
+
197
+ /**
198
+ * Why a statements report has no rows, and what to read instead: a listing
199
+ * with years but no quarters names its years, and says so of a company that
200
+ * reports half-yearly (Qantas on ASX). Null when it has years and quarters,
201
+ * whatever this table lacks.
202
+ */
203
+ async function missingPeriodsError(
204
+ ctx: HeadlessContext,
205
+ symbol: string,
206
+ financials: Awaited<ReturnType<typeof loadHeadlessFinancials>>,
207
+ period: "annual" | "quarterly",
208
+ hasRequestedStatements: boolean,
209
+ statement: unknown,
210
+ ): Promise<string | null> {
211
+ const hasYears = financials.annualStatements.length > 0;
212
+ if (period === "annual" || !hasYears) return hasRequestedStatements ? null : `${symbol}: No ${period} financial statement coverage.`;
213
+ const exchange = await resolveHeadlessInstrument(ctx, symbol).then(({ exchange }) => canonicalExchange(exchange ?? ""))
214
+ .catch(logFailure("ticker-detail.headless", "Resolving the listing of a statements report failed", ""));
215
+ const halfYearly = reportsHalfYearly(exchange, financials.quarterlyStatements);
216
+ if (hasRequestedStatements && !halfYearly) return null;
217
+ const tab = typeof statement === "string" && statement && statement !== "income" ? ` --statement ${statement}` : "";
218
+ const years = `For its years: gloom fn FA ${symbol} --period annual${tab}`;
219
+ return halfYearly ? `${symbol} reports half-yearly, not quarterly. ${years}` : `${symbol}: No quarterly financial statement coverage. ${years}`;
220
+ }
221
+
175
222
  /** The note that an offering filed after the latest balance sheet is not in its cash yet; a failed read leaves it out. */
176
223
  async function reportOfferingNote(
177
224
  ctx: Parameters<typeof financialStatementsHeadless.load>[1],
@@ -240,16 +287,18 @@ async function loadRatioReport(
240
287
  const runwayShown = liquidity && table.ratios.some((def) => def.format === "months"
241
288
  && table.cells.get(def.id)!.some((cell) => typeof cell.value === "number"));
242
289
  const offering = liquidity ? await reportOfferingNote(ctx, symbol, financials) : null;
290
+ const periodDates = dates.map(({ date }) => date);
291
+ const missing = rows.length ? null : await missingPeriodsError(ctx, symbol, financials, period, hasRequestedStatements, tab.key);
243
292
  return {
244
293
  rows, columns, unavailableSymbols: rows.length ? [] : [symbol],
245
- ...(!hasRequestedStatements ? { errors: [`${symbol}: No ${period} financial statement coverage.`] } : {}),
294
+ ...(missing ? { errors: [missing] } : {}),
246
295
  ...(tab.key === "valuation" && hasRequestedStatements && !history?.length ? { complete: false } : {}),
247
296
  notes: [
248
297
  ...[stalePeriodNote(dates.map(({ periodEnd }) => ({ date: periodEnd, lead: true })))].filter((note): note is string => !!note),
249
298
  ...[filledPeriodsNote(table?.periods.map(({ statement }) => statement) ?? [])].filter((note): note is string => !!note),
250
299
  ...offering ? [offering] : [],
251
300
  ...runwayShown ? [runwayMethod(reportsShortTermTotal([...financials.quarterlyStatements, ...financials.annualStatements]))] : [],
252
- ...statementHistoryNotes(period, dates.map(({ date }) => date), symbol),
301
+ ...statementHistoryNotes(period, periodDates, symbol, await reportOlderStatements(ctx, symbol, period, periodDates)),
253
302
  ],
254
303
  metadata: {
255
304
  symbol, name: financials.quote?.name ?? symbol, currency: statementCurrency ?? null,
@@ -453,7 +502,9 @@ export const historicalPricesHeadless: HeadlessPaneDefinition<"rows"> = {
453
502
  const drawdown = maxDrawdown(shown);
454
503
  const drawdownLine = drawdown ? maxDrawdownText(drawdown, { basis: `${historyBarSizeWord(size)} closes`, price: (value) => formatBarPrice(value, decimals),
455
504
  date: (value) => isDailyOrLongerBar(size) ? value.slice(0, 10) : formatUtcTime(value) }) : null;
456
- const reportNotices = [drawdownLine, notice].filter((line): line is string => line != null);
505
+ // A wick left out of the range is said, though the bar's close still counts.
506
+ const wickNotice = priceHistoryWickNotice(points.filter((point) => pricePointWickFlags(point).length > 0).length);
507
+ const reportNotices = [drawdownLine, notice, wickNotice].filter((line): line is string => line != null);
457
508
  return {
458
509
  rows: shown,
459
510
  columns: historyColumns(size, marked, false, false, decimals),
@@ -1,9 +1,9 @@
1
1
  import { formatPriceEarnings, NOT_MEANINGFUL } from "../../../../utils/price-earnings";
2
2
  import { convertMarketCapitalization } from "../../../../utils/market-capitalization";
3
3
  import { priceColor } from "../../../../theme/colors";
4
- import type { CompanyProfile, DepositaryPair, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
4
+ import type { CedearRatio, CompanyProfile, DepositaryPair, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
5
5
  import { adrRatioText } from "../../../../utils/depositary-receipt";
6
- import { ratioCount } from "../../../../market-data/depositary-pair";
6
+ import { cedearRatioText, ratioCount } from "../../../../market-data/depositary-pair";
7
7
  import { ADR_PAIR_TAB_ID, ADR_PAIR_TEMPLATE_ID } from "../adr-pair/model";
8
8
  import type { Portfolio, TickerPosition, TickerRecord } from "../../../../types/ticker";
9
9
  import type { BrokerInstanceConfig } from "../../../../types/config";
@@ -151,6 +151,7 @@ export function buildOverviewStats({
151
151
  nextEarnings,
152
152
  depositaryReceipt = false,
153
153
  depositaryPair = null,
154
+ cedearRatio = null,
154
155
  today,
155
156
  dividend,
156
157
  bond = null,
@@ -169,6 +170,8 @@ export function buildOverviewStats({
169
170
  depositaryReceipt?: boolean;
170
171
  /** The receipt pair the listing is a side of, as Gloom Cloud serves it; `receiptSide` when the listing is the receipt. */
171
172
  depositaryPair?: { pair: DepositaryPair; receiptSide: boolean } | null;
173
+ /** A Buenos Aires CEDEAR's ratio in its company's shares, as Gloom Cloud serves it. */
174
+ cedearRatio?: CedearRatio | null;
172
175
  /** The listing's calendar day, YYYY-MM-DD; dates before it are left out. */
173
176
  today?: string;
174
177
  /** The history the dividend yield reads (useOverviewDividend): a trailing one is measured from its cash, a forward one stops with it; without it the yield stands. */
@@ -223,6 +226,11 @@ export function buildOverviewStats({
223
226
  ? { label: "ADR Ratio", value: `1:${count}`, detail: [adrRatioText(pair.ratio)!, `${count} sh per ADR`], link: ADR_PAIR }
224
227
  : { label: "ADR", value: pair.receipt, detail: [`1 ADR = ${count} of these shares`, `1:${count}`], link: ADR_PAIR });
225
228
  }
229
+ if (cedearRatio) {
230
+ const receipts = ratioCount(cedearRatio.receipts);
231
+ const shares = ratioCount(cedearRatio.shares);
232
+ stats.push({ label: "CEDEAR Ratio", value: `${receipts}:${shares}`, detail: [cedearRatioText(cedearRatio), `${receipts} per ${shares} sh`] });
233
+ }
226
234
  const trailingPE = liveTrailingPE(quote, fundamentals);
227
235
  if (trailingPE != null) {
228
236
  stats.push({ label: "P/E (TTM)", value: formatPriceEarnings(trailingPE) });
@@ -42,7 +42,7 @@ import { appTtmRate, enterpriseValueReconciliationNote, ttmAtAppRate, ttmConvers
42
42
  import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
43
43
  import { zonedDateKey } from "../../../utils/zoned-date-time";
44
44
  import { sharesOutstandingInReceipts } from "../../../utils/depositary-receipt";
45
- import { isReceiptSide, listingDepositaryPair } from "../../../market-data/depositary-pair";
45
+ import { isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../../market-data/depositary-pair";
46
46
  import {
47
47
  CompactRangeBar,
48
48
  FundamentalsGrid,
@@ -335,6 +335,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
335
335
  nextEarnings: financials?.nextEarnings,
336
336
  depositaryReceipt: sharesOutstandingInReceipts(quote, fundamentals, description),
337
337
  depositaryPair: depositaryPair ? { pair: depositaryPair, receiptSide: isReceiptSide(financials) } : null,
338
+ cedearRatio: listingCedearRatio(financials),
338
339
  // Report and ex-dividend dates are the listing's calendar days.
339
340
  today: zonedDateKey(Date.now(), chartTimeZone ?? "America/New_York"),
340
341
  dividend,
@@ -326,16 +326,21 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
326
326
  selectResearchTab(tabId);
327
327
  return true;
328
328
  }, [selectResearchTab, visibleTabIds]);
329
+ // Without a ticker the pane draws only its empty state, and the tab list has
330
+ // no Overview, so the fallback above lands on the first tab that needs no
331
+ // ticker. Nobody opened it, and keeping it mounted would load its data on
332
+ // every ticker switch.
333
+ const shownTabId = ticker && visibleTabIds.has(resolvedTabId) ? resolvedTabId : null;
329
334
  const renderedTabIds = useMemo(() => {
330
335
  const next = new Set<string>();
331
336
  for (const tabId of mountedTabIds) {
332
337
  if (visibleTabIds.has(tabId)) next.add(tabId);
333
338
  }
334
- if (visibleTabIds.has(resolvedTabId)) {
335
- next.add(resolvedTabId);
339
+ if (shownTabId) {
340
+ next.add(shownTabId);
336
341
  }
337
342
  return next;
338
- }, [mountedTabIds, resolvedTabId, visibleTabIds]);
343
+ }, [mountedTabIds, shownTabId, visibleTabIds]);
339
344
 
340
345
  const handlePluginCapture = useCallback((capturing: boolean) => {
341
346
  setPluginCaptured(capturing);
@@ -354,12 +359,12 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
354
359
  for (const tabId of current) {
355
360
  if (visibleTabIds.has(tabId)) next.add(tabId);
356
361
  }
357
- if (visibleTabIds.has(resolvedTabId)) {
358
- next.add(resolvedTabId);
362
+ if (shownTabId) {
363
+ next.add(shownTabId);
359
364
  }
360
365
  return sameStringSet(current, next) ? current : next;
361
366
  });
362
- }, [resolvedTabId, visibleTabIds]);
367
+ }, [shownTabId, visibleTabIds]);
363
368
 
364
369
  if (!ticker) {
365
370
  const isEmptyFollowCollection = paneInstance?.binding?.kind === "follow" && !!collectionId && collectionTickerCount === 0;
@@ -1,4 +1,5 @@
1
- import { createPluginCache } from "../../../data/plugin-cache";
1
+ import { createPluginCache, savedCopyForTransientFailure } from "../../../data/plugin-cache";
2
+ import { servedFromSavedCopy } from "../../../data/saved-copies";
2
3
  import type { ConnectionHealthRegistry } from "../../../core/connection-health";
3
4
  import type { PluginPersistence } from "../../../types/plugin";
4
5
  import {
@@ -37,6 +38,8 @@ export interface TreasuryAuctionsResult {
37
38
  fetchedAt: number;
38
39
  /** True when the network failed and expired cache was served instead. */
39
40
  stale: boolean;
41
+ /** The refresh failed and the copy saved at this time (ms) answered instead. */
42
+ savedAt?: number;
40
43
  refreshError?: string;
41
44
  }
42
45
 
@@ -84,11 +87,18 @@ export async function loadTreasuryAuctions(
84
87
  window: AuctionHistoryWindow = AUCTION_HISTORY_DAYS,
85
88
  ): Promise<TreasuryAuctionsResult> {
86
89
  if (window === AUCTION_HISTORY_ALL) return loadAllTreasuryAuctions(force, loader);
87
- const result = await cache.load(`recent:${window}`, walk(loader, () => auctionWindowQuery(window)), { force });
88
- return {
90
+ // A saved board answers only for a failure asking again may get past; a refusal or an invalid page is the error.
91
+ const result = savedCopyForTransientFailure(await cache.load(`recent:${window}`, walk(loader, () => auctionWindowQuery(window)), { force }));
92
+ return answered({
89
93
  auctions: result.data, fetchedAt: result.fetchedAt, stale: result.stale,
94
+ ...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}),
90
95
  ...(result.refreshError ? { refreshError: result.refreshError } : {}),
91
- };
96
+ });
97
+ }
98
+
99
+ /** A board a saved copy answered: marked as one, so a command ends by saying when it was saved, not with its failure. */
100
+ function answered(result: TreasuryAuctionsResult): TreasuryAuctionsResult {
101
+ return result.savedAt != null ? servedFromSavedCopy(result, result.savedAt) : result;
92
102
  }
93
103
 
94
104
  /**
@@ -100,16 +110,18 @@ export async function loadTreasuryAuctions(
100
110
  async function loadAllTreasuryAuctions(force: boolean, loader: AuctionLoader): Promise<TreasuryAuctionsResult> {
101
111
  const boundary = auctionArchiveBoundary();
102
112
  const [recent, archive] = await Promise.all([
103
- cache.load(`since:${boundary}`, walk(loader, () => ({ from: boundary })), { force }),
113
+ cache.load(`since:${boundary}`, walk(loader, () => ({ from: boundary })), { force }).then(savedCopyForTransientFailure),
104
114
  archiveCache.load(`archive:${boundary}`, walk(loader, () => ({
105
115
  from: AUCTION_HISTORY_START, before: boundary, pageSize: AUCTION_ARCHIVE_PAGE_SIZE,
106
- }))),
116
+ }))).then(savedCopyForTransientFailure),
107
117
  ]);
108
118
  const seen = new Set<string>();
109
119
  const auctions = [...recent.data, ...archive.data].filter((auction) => !seen.has(auction.id) && Boolean(seen.add(auction.id)));
110
120
  const refreshError = recent.refreshError ?? archive.refreshError;
111
- return {
121
+ const savedAt = Math.min(...[recent, archive].map((part) => part.source === "stale-fallback" ? part.fetchedAt : Infinity));
122
+ return answered({
112
123
  auctions, fetchedAt: recent.fetchedAt, stale: recent.stale || archive.stale,
124
+ ...(Number.isFinite(savedAt) ? { savedAt } : {}),
113
125
  ...(refreshError ? { refreshError } : {}),
114
- };
126
+ });
115
127
  }
@@ -1,3 +1,4 @@
1
+ import { ApiRequestError } from "../../../api-client/errors";
1
2
  import { createThrottledFetch } from "../../../utils/throttled-fetch";
2
3
  import type { TreasuryAuction, TreasuryAuctionRaw } from "./types";
3
4
 
@@ -222,9 +223,8 @@ export async function fetchTreasuryAuctions(
222
223
  ): Promise<TreasuryAuction[]> {
223
224
  return fetchAuctionPages(async (page) => {
224
225
  const response = await TREASURY_FETCH.fetch(buildAuctionsUrl(query, page));
225
- if (!response.ok) {
226
- throw new Error(`Treasury Fiscal Data request failed (${response.status})`);
227
- }
226
+ // The status stays: an overloaded server is a failure a saved board may answer for, a refusal is not.
227
+ if (!response.ok) throw new ApiRequestError(`Treasury Fiscal Data request failed (${response.status})`, response.status);
228
228
  return response.json();
229
229
  });
230
230
  }
@@ -5,6 +5,7 @@ import type {
5
5
  import { formatCompact } from "../../../utils/format";
6
6
  import { AUCTION_HISTORY_ALL, AUCTION_HISTORY_START, fetchTreasuryAuctions, type AuctionHistoryWindow } from "./client";
7
7
  import { loadTreasuryAuctions, type TreasuryAuctionsResult } from "./cache";
8
+ import { unansweredRefreshError } from "../../../data/saved-copies";
8
9
  import type { TreasuryAuction } from "./types";
9
10
  import { FIXED_INCOME_VIEW_STATE } from "../fixed-income/model";
10
11
  import {
@@ -28,27 +29,34 @@ import {
28
29
  } from "./model";
29
30
 
30
31
  /**
31
- * A completed auction's missing bid-to-cover or bidder split is a blank cell:
32
- * the source has bid-to-cover regularly from late 1999 and the split only from
33
- * 2008-04-07. "-" stays for an announced auction whose results are not out yet.
32
+ * A missing bid-to-cover or bidder split is a blank cell: the source has
33
+ * bid-to-cover regularly from late 1999 and the split only from 2008-04-07,
34
+ * and an announced auction has no results to fill them; its Rate says so.
34
35
  */
35
- const missing = (row: Record<string, unknown>) => isPendingAuction(row as unknown as TreasuryAuction) ? "-" : "";
36
- const formatPercent = (value: unknown, row: Record<string, unknown>) => value == null ? missing(row) : `${Number(value).toFixed(1)}%`;
36
+ const formatPercent = (value: unknown) => value == null ? "" : `${Number(value).toFixed(1)}%`;
37
+
38
+ /** What an announced auction's Rate reads until the source publishes its results. */
39
+ const RESULTS_NOT_PUBLISHED = "results not published";
40
+ const pending = (row: Record<string, unknown>) => isPendingAuction(row as unknown as TreasuryAuction);
37
41
 
38
42
  /** Why the stop-out column is not a tail; the docs page says the same at length. */
39
43
  const STOP_OUT_NOTE = "Stop-out vs avg is the high yield minus the average/median yield, in bp (bills: discount rates). "
40
44
  + "A true tail, the stop-out against the 1 pm when-issued yield, needs the when-issued yield, which the source does not carry.";
41
45
 
42
46
  const MISSING_METRICS_NOTE = "A blank B/C or bidder share is one the source does not have for that auction: "
43
- + "bid-to-cover is regular from late 1999, the indirect, direct and dealer split from 2008-04-07. - is a result not out yet.";
47
+ + "bid-to-cover is regular from late 1999, the indirect, direct and dealer split from 2008-04-07.";
44
48
 
45
49
  /** A completed auction the source gives no bid-to-cover or no bidder split for. */
46
50
  const lacksMetrics = (auction: TreasuryAuction) => !isPendingAuction(auction)
47
51
  && (auction.bidToCoverRatio == null || indirectPct(auction) == null || directPct(auction) == null || dealerPct(auction) == null);
48
52
 
49
- /** The rate as the table prints it; a TIPS or FRN row says what its number is (`2.653% real`, `4.0bp spread`). */
53
+ /**
54
+ * The rate as the table prints it; a TIPS or FRN row says what its number is
55
+ * (`2.653% real`, `4.0bp spread`), and an announced auction that its results
56
+ * are not published yet.
57
+ */
50
58
  const rateCell = (value: unknown, row: Record<string, unknown>) => {
51
- const text = formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), "-");
59
+ const text = formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), pending(row) ? RESULTS_NOT_PUBLISHED : "-");
52
60
  const qualifier = value == null ? null : rateQualifier(row as unknown as TreasuryAuction);
53
61
  return qualifier ? `${text} ${qualifier}` : text;
54
62
  };
@@ -61,14 +69,16 @@ const COLUMNS = [
61
69
  key: "rate",
62
70
  header: "Rate",
63
71
  align: "right" as const,
64
- format: (value: unknown, row: Record<string, unknown>) => formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), "-"),
72
+ format: (value: unknown, row: Record<string, unknown>) => formatAuctionRate(
73
+ row as unknown as TreasuryAuction, value == null ? null : Number(value), pending(row) ? RESULTS_NOT_PUBLISHED : "-",
74
+ ),
65
75
  textFormat: rateCell,
66
76
  },
67
- { key: "stopOutVsAvgBp", header: "Stop-out vs avg (bp)", align: "right" as const, format: (value: unknown) => value == null ? "-" : Number(value).toFixed(1) },
68
77
  {
69
- key: "bidToCoverRatio", header: "B/C", align: "right" as const,
70
- format: (value: unknown, row: Record<string, unknown>) => value == null ? missing(row) : Number(value).toFixed(2),
78
+ key: "stopOutVsAvgBp", header: "Stop-out vs avg (bp)", align: "right" as const,
79
+ format: (value: unknown, row: Record<string, unknown>) => value == null ? pending(row) ? "" : "-" : Number(value).toFixed(1),
71
80
  },
81
+ { key: "bidToCoverRatio", header: "B/C", align: "right" as const, format: (value: unknown) => value == null ? "" : Number(value).toFixed(2) },
72
82
  { key: "indirectPercent", header: "Indirect", align: "right" as const, format: formatPercent },
73
83
  { key: "directPercent", header: "Direct", align: "right" as const, format: formatPercent },
74
84
  { key: "dealerPercent", header: "Dealer", align: "right" as const, format: formatPercent },
@@ -147,7 +157,8 @@ function createTreasuryAuctionsHeadless(
147
157
  ];
148
158
  return {
149
159
  complete: !result.refreshError,
150
- errors: result.refreshError ? [result.refreshError] : [],
160
+ // A saved copy answering the failed refresh ends the report by saying when it was saved instead.
161
+ errors: [unansweredRefreshError(result)].filter((error): error is string => !!error),
151
162
  notes,
152
163
  rows: auctions.map((auction) => ({
153
164
  ...auction,