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
@@ -1,31 +1,88 @@
1
- import type { FinancialStatement, StatementFill, StatementFillField } from "../types/financials";
1
+ import type { FinancialStatement, StatementFill, StatementFillField, TickerFinancials } from "../types/financials";
2
+ import { settleWithin } from "./async-deadline";
3
+ import { logFailure } from "./log-failure";
2
4
  import { copyIncomeField, incomeFieldOwner, INCOME_STATEMENT_FIELDS, isIncomeStatementField } from "./income-statement";
3
5
  import { mergeStatementGaps } from "./statement-gaps";
4
6
 
5
7
  export const FINANCIAL_VINTAGE_NOTICE = "Latest available statements may include restatements. Historical as-of values are not reconstructed.";
6
8
 
9
+ /** The oldest dated period of a report's columns (a TTM column has no date), or null when none has one. */
10
+ function oldestPeriod(dates: readonly string[]): string | null {
11
+ return dates.filter((date) => /^\d{4}-\d{2}-\d{2}/.test(date)).map((date) => date.slice(0, 10)).sort()[0] ?? null;
12
+ }
13
+
7
14
  /**
8
15
  * The closing lines of a statements report that say how far back it goes, from
9
- * the oldest period it served: the report holds no older ones, and `GF` reads
10
- * one metric over more periods, as far as its own history reaches. Empty when
11
- * no period has a date, so a report with nothing in it says nothing.
16
+ * the oldest period it served: the report holds no older ones. `older` says
17
+ * whether `GF`, which reads one metric from the listing's extended history
18
+ * (`hasOlderStatements`), goes further back: it is pointed to only then, and
19
+ * when it does not, the listing has no older ones at all (IAG on LSE: its
20
+ * statements start in 2022, in GF too). Null when that could not be told in
21
+ * time: the report then says nothing of older periods. Empty too when no
22
+ * period has a date, so a report with nothing in it says nothing.
12
23
  */
13
24
  export function statementHistoryNotes(
14
25
  period: "annual" | "quarterly",
15
26
  dates: readonly string[],
16
27
  symbol: string,
28
+ older: boolean | null,
17
29
  ): string[] {
18
- const oldest = dates.filter((date) => /^\d{4}-\d{2}-\d{2}/.test(date)).map((date) => date.slice(0, 10)).sort()[0];
19
- if (!oldest) return [];
20
- return period === "annual"
21
- ? [
22
- `annual history from ${oldest.slice(0, 4)}; older years not served`,
23
- `earlier years, one metric at a time: gloom fn GF ${symbol} --period annual --periods 40`,
24
- ]
25
- : [
26
- `quarterly history from ${oldest}; older quarters not served`,
27
- `earlier quarters, one metric at a time: gloom fn GF ${symbol} --period quarterly --periods 40`,
28
- ];
30
+ const oldest = oldestPeriod(dates);
31
+ if (!oldest || older === null) return [];
32
+ const [from, unit] = period === "annual" ? [oldest.slice(0, 4), "years"] : [oldest, "quarters"];
33
+ if (older === false) return [`${period} history from ${from}; older ${unit} not available for this listing`];
34
+ return [
35
+ `${period} history from ${from}; older ${unit} not served`,
36
+ ...older ? [`earlier ${unit}, one metric at a time: gloom fn GF ${symbol} --period ${period} --periods 40`] : [],
37
+ ];
38
+ }
39
+
40
+ /**
41
+ * How long a statements report waits on the extended history for its
42
+ * older-periods note, as a lock note waits on its routes' checks: a read still
43
+ * out then leaves the report on time, without the note. The read goes on and
44
+ * warms the cache for the next report.
45
+ */
46
+ const OLDER_STATEMENTS_WAIT_MS = 3_000;
47
+
48
+ /**
49
+ * Whether the listing's extended statement history, which `GF` reads, has a
50
+ * period older than the oldest of `dates`, a report's columns: a US filer's
51
+ * filings reach years back; a listing abroad has nothing past what the report
52
+ * served. Null when the history could not be read within `waitMs`.
53
+ */
54
+ export async function hasOlderStatements(
55
+ loadExtended: () => Promise<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>,
56
+ period: "annual" | "quarterly",
57
+ dates: readonly string[],
58
+ waitMs = OLDER_STATEMENTS_WAIT_MS,
59
+ ): Promise<boolean | null> {
60
+ const oldest = oldestPeriod(dates);
61
+ if (!oldest) return null;
62
+ const read = Promise.resolve().then(loadExtended).catch((error: unknown) => {
63
+ logFailure("utils.financial-statements", "Reading the extended statement history failed")(error);
64
+ throw error;
65
+ });
66
+ const extended = await settleWithin(read, waitMs);
67
+ if (!extended) return null;
68
+ const rows = period === "annual" ? extended.annualStatements : extended.quarterlyStatements;
69
+ return rows.some((row) => /^\d{4}-\d{2}-\d{2}/.test(row.date) && row.date.slice(0, 10) < oldest);
70
+ }
71
+
72
+ /** Venues whose companies report half-yearly and yearly, with no quarterly statements: Australia's (ASX, and Cboe Australia, which trades ASX companies) and New Zealand's. */
73
+ const HALF_YEARLY_VENUES = new Set(["ASX", "CXA", "NZX"]);
74
+ const DAY_MS = 86_400_000;
75
+
76
+ /**
77
+ * Whether a listing's company reports half-yearly rather than quarterly: it
78
+ * lists on a venue whose companies do (Qantas on ASX), or the periods of its
79
+ * "quarterly" statements are half a year apart.
80
+ */
81
+ export function reportsHalfYearly(exchange: string | null | undefined, quarterly: readonly Pick<FinancialStatement, "date">[]): boolean {
82
+ if (exchange && HALF_YEARLY_VENUES.has(exchange.toUpperCase())) return true;
83
+ const times = quarterly.map(({ date }) => Date.parse(`${date.slice(0, 10)}T00:00:00Z`)).filter(Number.isFinite).sort((a, b) => a - b);
84
+ const gaps = times.slice(1).map((time, index) => (time - times[index]!) / DAY_MS);
85
+ return gaps.length > 0 && gaps.every((days) => days > 150 && days < 215);
29
86
  }
30
87
 
31
88
  /** How old a statement's latest period may be before a report says how old it is. */
@@ -106,7 +163,7 @@ function retainedFill(merged: FinancialStatement, rows: Array<FinancialStatement
106
163
 
107
164
  export const SEC_EPS_BASIS_NOTICE = "SEC EPS uses corroborated split-adjusted share bases. Unverified bases are unavailable.";
108
165
 
109
- const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
166
+ const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "accountingStandard", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
110
167
  const NEARBY_PERIOD_END_MS = 7 * 24 * 60 * 60 * 1_000;
111
168
 
112
169
  /** An explicit field map is authoritative: omitted fields have unknown availability. */
@@ -2,6 +2,7 @@ import { formatFxRate } from "../cli/fx-pair";
2
2
  import type { EnterpriseValueCheck, Fundamentals, Quote, TickerFinancials } from "../types/financials";
3
3
  import { quotedFxPair } from "./currency-pair";
4
4
  import { resolveCurrencyUnit } from "./currency-units";
5
+ import { isUsListingExchange } from "./exchanges";
5
6
  import { formatCompact } from "./format";
6
7
  import { formatUtcTime } from "./utc-time";
7
8
 
@@ -44,7 +45,10 @@ export function enterpriseValueCheck(
44
45
  // Not meaningful needs no figure of its own: a source EV below zero may come with no cap or currency.
45
46
  if (check.status === "not-meaningful") return withdrawn ? check : undefined;
46
47
  if (!check.currency) return undefined;
47
- if (check.status === "withdrawn") return withdrawn && finiteNumber(check.computed) && finiteNumber(check.source) ? check : undefined;
48
+ // A withdrawn EV comes with the figure it was checked against, or with why no balance sheet checked it.
49
+ if (check.status === "withdrawn") {
50
+ return withdrawn && ((finiteNumber(check.computed) && finiteNumber(check.source)) || !!check.reason?.trim()) ? check : undefined;
51
+ }
48
52
  if (check.status === "computed") {
49
53
  return finiteNumber(check.computed) && fundamentals?.enterpriseValue === check.computed ? check : undefined;
50
54
  }
@@ -86,8 +90,9 @@ export function enterpriseValueLabel(fundamentals: Parameters<typeof enterpriseV
86
90
  /**
87
91
  * The reconciliation line under a labelled, withdrawn or not meaningful EV: "EV 61.75B USD (provider) vs 63.75B
88
92
  * computed from market cap + total debt - cash (balance sheet 2026-06-30).", "EV N/M: cash and short-term investments
89
- * exceed market cap + total debt (balance sheet 2026-06-30)." For an EV withdrawn without a check, that the source
90
- * failed validation. Undefined when the EV matches, is unchecked or computed, or has no check. Read it from the
93
+ * exceed market cap + total debt (balance sheet 2026-06-30)." A balance sheet in another currency names the rate it
94
+ * was converted at ("balance sheet 2026-06-30, EUR at 0.8614 GBP"). A withdrawn EV no balance sheet checked gives
95
+ * Gloom Cloud's reason; one withdrawn without a check, that the source failed validation. Undefined when the EV matches, is unchecked or computed, or has no check. Read it from the
91
96
  * fundamentals the rows show (atRegularSessionPrice moves the check with the EV and the market cap), so its figures
92
97
  * are theirs.
93
98
  */
@@ -96,14 +101,22 @@ export function enterpriseValueReconciliationNote(fundamentals: Parameters<typeo
96
101
  if (notMeaningful) return `EV N/M: ${notMeaningful.charAt(0).toLowerCase()}${notMeaningful.slice(1)}.`;
97
102
  const check = enterpriseValueCheck(fundamentals);
98
103
  if (!check && enterpriseValueWithdrawn(fundamentals)) return "EV unavailable: the provider's figure failed validation.";
104
+ const reason = check?.reason?.trim().replace(/\.$/, "");
105
+ if (check?.status === "withdrawn" && !finiteNumber(check.computed) && reason) {
106
+ return `EV unavailable: ${reason.charAt(0).toLowerCase()}${reason.slice(1)}.`;
107
+ }
99
108
  if (!check || !finiteNumber(check.computed) || !finiteNumber(check.source)
100
109
  || (check.status !== "differs" && check.status !== "withdrawn")) return undefined;
101
110
  const source = `${formatCompact(check.source)} ${check.currency}`;
102
111
  const computed = formatCompact(check.computed);
103
- const basis = `market cap + total debt - cash${check.balanceSheetDate ? ` (balance sheet ${check.balanceSheetDate})` : ""}`;
112
+ const leases = finiteNumber(check.leases) && check.leases > 0 ? " + leases" : "";
113
+ const fx = check.fx && finiteNumber(check.fx.rate) ? `, ${check.fx.from} at ${check.fx.rate.toPrecision(4)} ${check.currency}` : "";
114
+ const sheet = check.balanceSheetDate ? ` (balance sheet ${check.balanceSheetDate}${fx})` : "";
115
+ const basis = `market cap + total debt${leases} - cash${sheet}`;
104
116
  if (check.status === "differs") return `EV ${source} (provider) vs ${computed} computed from ${basis}.`;
105
117
  const ratio = check.computed > 0 ? ` is ${(check.source / check.computed).toFixed(1)}x` : " is far from";
106
- return `EV unavailable: the provider's ${source}${ratio} the ${computed} computed from ${basis}.`;
118
+ const twice = check.convertedTwice && check.convertedFrom ? `; it converts ${check.convertedFrom} to ${check.currency} twice` : "";
119
+ return `EV unavailable: the provider's ${source}${ratio} the ${computed} computed from ${basis}${twice}.`;
107
120
  }
108
121
 
109
122
  /**
@@ -119,24 +132,53 @@ export function computedEnterpriseValueNote(fundamentals: Parameters<typeof ente
119
132
  }
120
133
 
121
134
  /** What names the currency of the trailing figures: the fundamentals' own labels, else the statements' on a listing that trades in theirs. */
122
- type FundamentalsCurrencies = Pick<TickerFinancials, "financialCurrency"> & {
135
+ type FundamentalsCurrencies = Pick<TickerFinancials, "financialCurrency" | "statementHistory"> & {
123
136
  fundamentals?: Pick<Fundamentals, "financialCurrency" | "epsCurrency" | "marketCapCurrency"> | null;
124
- quote?: Pick<Quote, "currency"> | null;
137
+ quote?: Pick<Quote, "currency" | "exchangeName" | "listingExchangeName" | "isDepositaryReceipt"> | null;
125
138
  };
126
139
 
140
+ /** What a caller knows of the listing beyond its quote: the day it listed (its IPO's), when known. */
141
+ interface ListingAge {
142
+ listedSince?: string | null;
143
+ now?: Date;
144
+ }
145
+
146
+ /** A listing that started within about a year: its statements may not be served yet. */
147
+ const RECENT_LISTING_DAYS = 372;
148
+
149
+ /**
150
+ * The listing's own currency, standing in for figures with no statements to tell their currency by: only for a
151
+ * listing of the last year or so (a recent IPO such as HAWK) or a US listing in dollars that is no receipt, and
152
+ * never after a statements read that failed. A minor unit (GBp) does not say whether figures are pence or pounds.
153
+ */
154
+ function listingCurrencyFallback(financials: FundamentalsCurrencies | null | undefined, listing: ListingAge): string | undefined {
155
+ const listed = financials?.fundamentals?.marketCapCurrency?.trim() || financials?.quote?.currency?.trim();
156
+ if (!listed || resolveCurrencyUnit(listed).divisor !== 1) return undefined;
157
+ if (financials?.statementHistory?.status === "retryable-failure") return undefined;
158
+ const currency = resolveCurrencyUnit(listed).currency;
159
+ const since = listing.listedSince ? Date.parse(`${listing.listedSince.slice(0, 10)}T00:00:00Z`) : NaN;
160
+ const recent = Number.isFinite(since) && (listing.now ?? new Date()).getTime() - since <= RECENT_LISTING_DAYS * 86_400_000;
161
+ const quote = financials?.quote;
162
+ const home = currency === "USD" && quote?.isDepositaryReceipt !== true
163
+ && isUsListingExchange(quote?.listingExchangeName || quote?.exchangeName);
164
+ return recent || home ? currency : undefined;
165
+ }
166
+
127
167
  /**
128
168
  * The currency of the trailing EPS, revenue, income and cash flows: the one the fundamentals declare for themselves.
129
169
  * A block that declares none is in the statements' currency only when the listing trades in it; on a listing quoted
130
170
  * in another (a receipt in USD beside TWD books, YPFD in ARS beside USD statements) its currency is unknown, since
131
- * Gloom Cloud labels every block whose currency it can tell. Never the quote's.
171
+ * Gloom Cloud labels every block whose currency it can tell. With no statements to tell by, the listing's own
172
+ * currency where that is safe (listingCurrencyFallback).
132
173
  */
133
174
  export function fundamentalsCurrency(
134
175
  financials: FundamentalsCurrencies | null | undefined,
176
+ listing: ListingAge = {},
135
177
  ): string | undefined {
136
178
  const declared = financials?.fundamentals?.financialCurrency?.trim();
137
179
  if (declared) return declared;
138
180
  const statements = financials?.financialCurrency?.trim();
139
- if (!statements) return undefined;
181
+ if (!statements) return listingCurrencyFallback(financials, listing);
140
182
  const listed = financials?.fundamentals?.marketCapCurrency?.trim() || financials?.quote?.currency?.trim();
141
183
  return !listed || resolveCurrencyUnit(listed).currency === resolveCurrencyUnit(statements).currency ? statements : undefined;
142
184
  }
@@ -147,8 +189,9 @@ export function fundamentalsCurrency(
147
189
  */
148
190
  export function fundamentalsEpsCurrency(
149
191
  financials: FundamentalsCurrencies | null | undefined,
192
+ listing: ListingAge = {},
150
193
  ): string | undefined {
151
- return financials?.fundamentals?.epsCurrency?.trim() || fundamentalsCurrency(financials);
194
+ return financials?.fundamentals?.epsCurrency?.trim() || fundamentalsCurrency(financials, listing);
152
195
  }
153
196
 
154
197
  const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
@@ -0,0 +1,54 @@
1
+ import type { FinancialStatement } from "../types/financials";
2
+
3
+ /**
4
+ * How net debt treats the lease liabilities a listing's balance sheets report (docs/financial-ratios.md#leases):
5
+ *
6
+ * - `included`: added to total debt. IFRS 16 puts every lessee lease on the balance sheet and reports EBITDA before
7
+ * lease costs, so leverage with the leases in is like for like (IAG's June 2026 balance sheet: 6.96bn of
8
+ * borrowings, 6.91bn of leases, 9.18bn of cash).
9
+ * - `excluded`: a company reporting under US GAAP, whatever form it files (a 10-K, or a 20-F as BABA, PDD and JD
10
+ * do). Its finance leases are already in total debt and its operating lease cost is in EBITDA, so its operating
11
+ * leases stay out.
12
+ *
13
+ * The standard is the one Gloom Cloud reads from the issuer's latest annual report to the SEC
14
+ * (`accountingStandard`), the signal its enterprise value check bridges by. Where no row states it, a row dated by
15
+ * a 10-K or 10-Q (`dateSource: "sec"`) is US GAAP, and a company that files none with the SEC is IFRS.
16
+ */
17
+ export type LeaseBasis = "included" | "excluded";
18
+
19
+ type LeaseLines = Pick<FinancialStatement, "capitalLeaseObligations" | "dateSource" | "accountingStandard">;
20
+
21
+ const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
22
+
23
+ /** The listing's lease basis, or undefined when no balance sheet on file reports lease liabilities. */
24
+ export function leaseBasis(statements: Iterable<LeaseLines>): LeaseBasis | undefined {
25
+ let leases = false;
26
+ let standard: FinancialStatement["accountingStandard"];
27
+ let secDated = false;
28
+ for (const statement of statements) {
29
+ if (finite(statement.capitalLeaseObligations) && statement.capitalLeaseObligations > 0) leases = true;
30
+ standard ??= statement.accountingStandard;
31
+ if (statement.dateSource === "sec") secDated = true;
32
+ }
33
+ if (!leases) return undefined;
34
+ return (standard ? standard === "us-gaap" : secDated) ? "excluded" : "included";
35
+ }
36
+
37
+ /** Amounts are whole units; some providers round a leg to the thousand. */
38
+ const close = (a: number, b: number) => Math.abs(a - b) <= Math.max(1_000, Math.abs(a) * 1e-6);
39
+
40
+ /**
41
+ * A balance sheet's total debt and lease liabilities. A total that already holds the lease line (long-term and
42
+ * current debt plus leases) is read as the borrowings beside it, so the leases count once. Undefined without both
43
+ * lines: a missing lease line is never zero.
44
+ */
45
+ export function debtAndLeases(
46
+ statement: Pick<FinancialStatement, "totalDebt" | "capitalLeaseObligations" | "longTermDebt" | "currentDebt">,
47
+ ): { totalDebt: number; leases: number } | undefined {
48
+ const total = statement.totalDebt;
49
+ const leases = statement.capitalLeaseObligations;
50
+ if (!finite(total) || !finite(leases) || leases < 0) return undefined;
51
+ const { longTermDebt: long, currentDebt: current } = statement;
52
+ const holds = leases > 0 && (finite(long) || finite(current)) && close(total, (long ?? 0) + (current ?? 0) + leases);
53
+ return { totalDebt: holds ? total - leases : total, leases };
54
+ }
@@ -9,15 +9,31 @@ const SAMPLE_INTERVAL_MS = 250;
9
9
  */
10
10
  const STALL_THRESHOLD_MS = 400;
11
11
 
12
+ /** The facts a `sqlite.blocked` step was measured with (data/sqlite/retry.ts). */
13
+ interface StallSqliteDetail {
14
+ /** The statement's fixed name, such as "touch cached resource". */
15
+ operation: string;
16
+ attempts: number;
17
+ /** Whether it waited on another connection's lock, rather than being slow on its own. */
18
+ lockWait: boolean;
19
+ }
20
+
12
21
  export interface StallEvent {
13
22
  blockedMs: number;
14
23
  /** The last measured section before the stall (perf-marks), if any. */
15
24
  lastMeasured?: string;
16
25
  /** How long before the stall was noticed that section ended. */
17
26
  lastMeasuredEndedMsAgo?: number;
27
+ /** When that section was a SQLite statement that held the thread. */
28
+ sqlite?: StallSqliteDetail;
29
+ /** How long the process had been running when the stall was noticed. */
30
+ uptimeMs?: number;
31
+ /** Whether the process had finished starting (`markStartupComplete`). */
32
+ phase?: "startup" | "steady";
18
33
  }
19
34
 
20
35
  let stallSink: ((event: StallEvent) => void) | null = null;
36
+ let startupComplete = false;
21
37
 
22
38
  /**
23
39
  * Where this process's stalls go besides the Debug Log: the stall log and its
@@ -31,6 +47,20 @@ export function setStallSink(next: ((event: StallEvent) => void) | null): () =>
31
47
  };
32
48
  }
33
49
 
50
+ /**
51
+ * The process is up: its first screen has its data, or (the desktop app's
52
+ * Bun process) the main window has what it asked for. Stalls before this are
53
+ * reported as startup ones.
54
+ */
55
+ export function markStartupComplete(): void {
56
+ startupComplete = true;
57
+ }
58
+
59
+ /** Test seam. */
60
+ export function resetStartupCompleteForTests(): void {
61
+ startupComplete = false;
62
+ }
63
+
34
64
  function noteStall(event: StallEvent): void {
35
65
  try {
36
66
  stallSink?.(event);
@@ -39,6 +69,14 @@ function noteStall(event: StallEvent): void {
39
69
  }
40
70
  }
41
71
 
72
+ /** The facts of a `sqlite.blocked` sample, when they have the shape retry.ts gives them. */
73
+ function sqliteDetail(metadata: Record<string, unknown> | undefined): StallSqliteDetail | undefined {
74
+ if (!metadata) return undefined;
75
+ const { operation, attempts, lockWait } = metadata;
76
+ if (typeof operation !== "string" || typeof attempts !== "number") return undefined;
77
+ return { operation, attempts, lockWait: lockWait === true };
78
+ }
79
+
42
80
  export interface MainThreadStallMonitorOptions {
43
81
  intervalMs?: number;
44
82
  thresholdMs?: number;
@@ -60,7 +98,9 @@ function defaultNow(): number {
60
98
  * comes back late, the app was unresponsive for exactly that long. The report
61
99
  * carries the last measured section and how long before the stall it ended,
62
100
  * which is usually enough to tell "the cache read did it" from "the render
63
- * did it" in a report that only says the app hung for a few seconds.
101
+ * did it" in a report that only says the app hung for a few seconds, and how
102
+ * long the process had been up, so a slow start reads apart from a hang
103
+ * hours in.
64
104
  */
65
105
  export function startMainThreadStallMonitor(
66
106
  options: MainThreadStallMonitorOptions = {},
@@ -72,7 +112,8 @@ export function startMainThreadStallMonitor(
72
112
  const cancel = options.cancel ?? ((handle) => clearInterval(handle as ReturnType<typeof setInterval>));
73
113
  const report = options.report
74
114
  ?? ((blockedMs, context) => {
75
- recordPerfSample("runtime.stall", blockedMs, context);
115
+ // Logged, but not a step: the next stall still names the work before it.
116
+ recordPerfSample("runtime.stall", blockedMs, context, { lastStep: false });
76
117
  noteStall({ blockedMs, ...context });
77
118
  });
78
119
 
@@ -83,10 +124,14 @@ export function startMainThreadStallMonitor(
83
124
  previous = current;
84
125
  if (blockedMs < thresholdMs) return;
85
126
  const sample = lastPerfSample();
127
+ const sqlite = sample?.name === "sqlite.blocked" ? sqliteDetail(sample.metadata) : undefined;
86
128
  report(blockedMs, {
87
129
  ...(sample
88
130
  ? { lastMeasured: sample.name, lastMeasuredEndedMsAgo: Math.round(current - sample.endedAt) }
89
131
  : {}),
132
+ ...(sqlite ? { sqlite } : {}),
133
+ uptimeMs: Math.round(current),
134
+ phase: startupComplete ? "steady" : "startup",
90
135
  });
91
136
  }, intervalMs);
92
137
  if (handle && typeof (handle as { unref?: () => void }).unref === "function") {
@@ -20,27 +20,34 @@ function now(): number {
20
20
  : Date.now();
21
21
  }
22
22
 
23
- // Two scalars rather than an object: every measured section writes them, and
24
- // only the stall monitor ever reads them.
23
+ // Scalars rather than an object: every measured section writes them, and
24
+ // only the stall monitor ever reads them. The metadata is the caller's own
25
+ // object, kept by reference.
25
26
  let lastSampleName: string | null = null;
26
27
  let lastSampleEndedAt = 0;
28
+ let lastSampleMetadata: Record<string, unknown> | undefined;
27
29
 
28
30
  /**
29
- * The most recent measured section and when it ended. The stall monitor reads
30
- * it so a report of "the app froze" carries the last thing the app is known to
31
- * have been doing.
31
+ * The most recent measured section, when it ended and what it was measured
32
+ * with. The stall monitor reads it so a report of "the app froze" carries the
33
+ * last thing the app is known to have been doing.
32
34
  */
33
- export function lastPerfSample(): { name: string; endedAt: number } | null {
34
- return lastSampleName == null ? null : { name: lastSampleName, endedAt: lastSampleEndedAt };
35
+ export function lastPerfSample(): { name: string; endedAt: number; metadata?: Record<string, unknown> } | null {
36
+ if (lastSampleName == null) return null;
37
+ return { name: lastSampleName, endedAt: lastSampleEndedAt, ...(lastSampleMetadata ? { metadata: lastSampleMetadata } : {}) };
35
38
  }
36
39
 
37
40
  function logSlowPerfSample(
38
41
  name: string,
39
42
  durationMs: number,
40
43
  metadata?: Record<string, unknown>,
44
+ lastStep = true,
41
45
  ): void {
42
- lastSampleName = name;
43
- lastSampleEndedAt = now();
46
+ if (lastStep) {
47
+ lastSampleName = name;
48
+ lastSampleEndedAt = now();
49
+ lastSampleMetadata = metadata;
50
+ }
44
51
  const payload = {
45
52
  durationMs: Math.round(durationMs * 10) / 10,
46
53
  ...(metadata ?? {}),
@@ -60,13 +67,18 @@ export function isPerfTraceEnabled(): boolean {
60
67
  return traceToStderr;
61
68
  }
62
69
 
63
- /** A sample measured elsewhere (a React Profiler callback, for instance). */
70
+ /**
71
+ * A sample measured elsewhere (a React Profiler callback, for instance).
72
+ * `lastStep: false` logs it without making it the step a later stall names:
73
+ * the stall monitor's own samples, which measure no work.
74
+ */
64
75
  export function recordPerfSample(
65
76
  name: string,
66
77
  durationMs: number,
67
78
  metadata?: Record<string, unknown>,
79
+ options?: { lastStep?: boolean },
68
80
  ): void {
69
- logSlowPerfSample(name, durationMs, metadata);
81
+ logSlowPerfSample(name, durationMs, metadata, options?.lastStep !== false);
70
82
  }
71
83
 
72
84
  export function measurePerf<T>(
@@ -46,6 +46,50 @@ export function pricePointIntegrityFlags(point: PricePoint): string[] {
46
46
  return flags;
47
47
  }
48
48
 
49
+ /**
50
+ * A wick too far from its bar's body to have traded: a low under half the lower
51
+ * of open and close, or a high over twice the higher, as `low<close/2` or
52
+ * `high>2*open` (IAG on LSE, 2021-11-29: a low of 0.0155 under a close of
53
+ * 1.316). Only that wick is withheld: the open and close stand, so closes,
54
+ * returns and drawdowns read the bar while lows, highs and ranges do not.
55
+ * Empty when the wicks are plausible, or when the bar's prices contradict
56
+ * each other, which blanks it whole (pricePointIntegrityFlags).
57
+ */
58
+ export function pricePointWickFlags(point: PricePoint): string[] {
59
+ const { open, high, low, close } = point;
60
+ if (pricePointIntegrityFlags(point).length > 0) return [];
61
+ const body = [["open", open], ["close", close]].filter((entry): entry is [string, number] => isFiniteNumber(entry[1]));
62
+ if (body.length === 0) return [];
63
+ // On a tie the close names the side, as the price the bar is read by.
64
+ const [bottomName, bottom] = body.reduce((least, entry) => entry[1] <= least[1] ? entry : least);
65
+ const [topName, top] = body.reduce((most, entry) => entry[1] >= most[1] ? entry : most);
66
+ const flags: string[] = [];
67
+ if (isFiniteNumber(low) && bottom > 0 && low < bottom / 2) flags.push(`low<${bottomName}/2`);
68
+ if (isFiniteNumber(high) && top > 0 && high > top * 2) flags.push(`high>2*${topName}`);
69
+ return flags;
70
+ }
71
+
72
+ /** Whether a flag code is a withheld wick (pricePointWickFlags), whose bar keeps its open and close. */
73
+ export function isWickFlag(code: string): boolean {
74
+ return /^(low<(open|close)\/2|high>2\*(open|close))$/.test(code);
75
+ }
76
+
77
+ /** A bar's high and low with a wick too far from its body to have traded (pricePointWickFlags) left out. */
78
+ export function pricePointWicks(point: PricePoint): { high: number | undefined; low: number | undefined } {
79
+ const flags = pricePointWickFlags(point);
80
+ return {
81
+ high: flags.some((flag) => flag.startsWith("high")) ? undefined : point.high,
82
+ low: flags.some((flag) => flag.startsWith("low")) ? undefined : point.low,
83
+ };
84
+ }
85
+
86
+ /** "1 bar has a low or high too far from its open and close to have traded; left out of ranges.", or undefined for none. */
87
+ export function priceHistoryWickNotice(count: number): string | undefined {
88
+ return count > 0
89
+ ? `${count === 1 ? "1 bar has a low or high" : `${count} bars have lows or highs`} too far from ${count === 1 ? "its" : "their"} open and close to have traded; left out of ranges.`
90
+ : undefined;
91
+ }
92
+
49
93
  export function pricePointIntegrity(point: PricePoint): PriceHistoryIntegrity | undefined {
50
94
  if (pricePointIntegrityFlags(point).length === 0) return undefined;
51
95
  const date = new Date(point.date);
@@ -58,8 +102,10 @@ export function pricePointIntegrity(point: PricePoint): PriceHistoryIntegrity |
58
102
  /** Calculation/display fields deliberately exclude the contradictory source row. */
59
103
  export function pricePointValues(point: PricePoint, integrity = pricePointIntegrity(point)) {
60
104
  const value = (number: number | undefined) => !integrity && isFiniteNumber(number) ? number : null;
105
+ // A wick too far from the body to have traded is left out; the rest of the bar stands.
106
+ const wicks = pricePointWicks(point);
61
107
  return {
62
- open: value(point.open), high: value(point.high), low: value(point.low),
108
+ open: value(point.open), high: value(wicks.high), low: value(wicks.low),
63
109
  close: value(point.close), volume: value(point.volume),
64
110
  ...(integrity ? { integrity } : {}),
65
111
  };
package/src/version.ts CHANGED
@@ -3,7 +3,7 @@ import packageJson from "../package.json" with { type: "json" };
3
3
  export const VERSION = packageJson.version;
4
4
 
5
5
  /**
6
- * The npm package this copy came from: `gloomberb`, or `@gloom-sh/terminal`
7
- * once releases are also published under that name.
6
+ * The npm package this copy came from: `@gloom-sh/terminal` or `gloomberb`,
7
+ * which every release publishes with the same build.
8
8
  */
9
9
  export const PACKAGE_NAME = packageJson.name;