@gloom-sh/terminal 0.18.5 → 0.19.0

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 (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -109,7 +109,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
109
109
  const refreshedCurrent = refreshedIv?.owner === ivLoader && refreshedIv.at > (loadedIv.updatedAt ?? 0) ? refreshedIv.data : null;
110
110
  const iv = { ...loadedIv, data: refreshedCurrent ?? loadedIv.data };
111
111
  ivDataRef.current = iv.data;
112
- useLiveSessionRefresh(refreshIv, ATM_IV_REFRESH_MS, showIv && spotAvailable && !!iv.data?.reference && !loadedIv.loading,
112
+ const ivRefresh = useLiveSessionRefresh(refreshIv, ATM_IV_REFRESH_MS, showIv && spotAvailable && !!iv.data?.reference && !loadedIv.loading,
113
113
  loadedIv.updatedAt);
114
114
  const model = useMemo(() => history.data ? projectRealizedVolatility(history.data.history, {
115
115
  symbol: history.data.symbol, estimator, windows, lookbackYears: Number(lookback) === 2 ? 2 : 1,
@@ -117,7 +117,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
117
117
  const chartInput = useMemo(() => model ? { history: model.history, rolling: model.series, windows: model.windows,
118
118
  priceUnit: priceSeriesUnit(quote?.currency ?? "", quote?.instrumentType), iv: showIv ? iv.data?.reference ?? null : null } : null,
119
119
  [model, quote?.currency, quote?.instrumentType, showIv, iv.data]);
120
- const notices = [identityError, history.error, history.data?.error, iv.error, iv.data?.error,
120
+ const notices = [identityError, history.error, history.data?.error, iv.error, ivRefresh.error, iv.data?.error,
121
121
  ...(model?.warnings ?? []), ...(showIv ? iv.data?.warnings ?? [] : []),
122
122
  ...(showIv && symbol && !spotAvailable ? ["Current ATM IV unavailable: underlying quote is missing or stale"] : []),
123
123
  ].filter((value): value is string => !!value);
@@ -161,7 +161,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
161
161
  stale: history.data?.stale ?? false, source: history.data?.source ?? null,
162
162
  asOf: model?.asOf?.toISOString() ?? null,
163
163
  errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []),
164
- ...(showIv ? [iv.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
164
+ ...(showIv ? [iv.error, ivRefresh.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
165
165
  ].filter((value): value is string => !!value))],
166
166
  currentIv: showIv && iv.data?.reference ? { value: iv.data.reference.value * 100,
167
167
  date: iv.data.reference.date.toISOString(), label: iv.data.reference.label,
@@ -7,6 +7,7 @@ import type {
7
7
  } from "../../../api-client/types";
8
8
  import { createPluginCache } from "../../../data/plugin-cache";
9
9
  import { isAccessDenied } from "../../../api-client/errors";
10
+ import { quoteTradingExchange } from "../../../market-data/quotes/metadata";
10
11
  import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
11
12
  import {
12
13
  buildRotation,
@@ -75,6 +76,10 @@ export function validateRotationHistory(
75
76
  throw new Error("Invalid daily history.");
76
77
  return response.data;
77
78
  }
79
+ /** The venue a quote is for, as `quote` names it; empty when it names none. */
80
+ function listingVenue(quote: CloudQuotePayload | undefined): string {
81
+ return quote ? quoteTradingExchange(quote) : "";
82
+ }
78
83
  export async function fetchRotation(
79
84
  benchmark: RotationInstrument,
80
85
  instruments: RotationInstrument[],
@@ -106,6 +111,16 @@ export async function fetchRotation(
106
111
  if (isAccessDenied(error)) throw error;
107
112
  quoteError = "Listing currencies unavailable.";
108
113
  }
114
+ // A benchmark typed without its exchange (ACWI) is the listing its quote is for (ACWI:NASDAQ), as `quote`
115
+ // reads it: that venue's calendar picks the weekly closes.
116
+ const venue = benchmark.exchange
117
+ ? ""
118
+ : listingVenue(quotes.get(rotationId(benchmark)));
119
+ const reference = venue ? { ...benchmark, exchange: venue } : benchmark;
120
+ const requested = unique.map((row) => ({
121
+ id: rotationId(row),
122
+ instrument: rotationId(row) === rotationId(benchmark) ? reference : row,
123
+ }));
109
124
  const start = new Date(now);
110
125
  start.setUTCFullYear(start.getUTCFullYear() - 3);
111
126
  start.setUTCDate(1);
@@ -115,10 +130,9 @@ export async function fetchRotation(
115
130
  let next = 0;
116
131
  // Bound Cloud history requests; shared server caches batch upstream demand.
117
132
  await Promise.all(
118
- Array.from({ length: Math.min(4, unique.length) }, async () => {
119
- while (next < unique.length) {
120
- const instrument = unique[next++]!,
121
- id = rotationId(instrument),
133
+ Array.from({ length: Math.min(4, requested.length) }, async () => {
134
+ while (next < requested.length) {
135
+ const { id, instrument } = requested[next++]!,
122
136
  currency = quotes.get(id)?.currency ?? null;
123
137
  try {
124
138
  const response = await client.getCloudHistory(
@@ -1,6 +1,6 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { fetchRotation } from "./client";
3
- import { rotationInstruments, sectorRotationInstruments } from "./model";
3
+ import { DEFAULT_ROTATION_BENCHMARK, rotationInstruments, sectorRotationInstruments } from "./model";
4
4
 
5
5
  /** Same rounding as the pane: ratios to 2 decimals, percentiles whole. */
6
6
  const fixed = (value: unknown, digits: number) =>
@@ -21,7 +21,7 @@ export const rotationHeadless: HeadlessPaneDefinition<"bundle"> = {
21
21
  key: "benchmark",
22
22
  type: "string",
23
23
  description: "Benchmark listing.",
24
- defaultValue: "SPY:NYSEARCA",
24
+ defaultValue: DEFAULT_ROTATION_BENCHMARK,
25
25
  },
26
26
  {
27
27
  key: "trail",
@@ -44,7 +44,7 @@ export const rotationHeadless: HeadlessPaneDefinition<"bundle"> = {
44
44
  },
45
45
  async load(args, ctx) {
46
46
  const references = rotationInstruments(
47
- String(args.options.benchmark ?? "SPY:NYSEARCA"),
47
+ String(args.options.benchmark ?? DEFAULT_ROTATION_BENCHMARK),
48
48
  );
49
49
  if (references.length !== 1) throw new Error("Choose one benchmark.");
50
50
  const instruments = args.symbols.length
@@ -3,6 +3,7 @@ import type { PluginModule } from "../plugin-module";
3
3
  import { rotationCache } from "./client";
4
4
  import { rotationHeadless } from "./headless";
5
5
  import { RelativeRotationPane } from "./pane";
6
+ import { DEFAULT_ROTATION_BENCHMARK } from "./model";
6
7
 
7
8
  export const relativeRotationModule: PluginModule = {
8
9
  panes: [
@@ -41,7 +42,7 @@ export const relativeRotationModule: PluginModule = {
41
42
  key: "benchmark",
42
43
  label: "Benchmark",
43
44
  type: "text",
44
- placeholder: "SPY:NYSEARCA",
45
+ placeholder: DEFAULT_ROTATION_BENCHMARK,
45
46
  },
46
47
  {
47
48
  key: "trail",
@@ -2,6 +2,7 @@ import { publishedWeekClose } from "../analytics/sharpe-cadence";
2
2
  import { historyStatistics } from "../../../components/chart/curve/model";
3
3
  import type { CloudPricePointPayload } from "../../../api-client/types";
4
4
  import { getSectorCollection } from "../sectors/sector-data";
5
+ import { rotationQuadrant, rotationSeries, type RotationQuadrantName } from "../alerts/metric-model";
5
6
  import {
6
7
  canonicalExchange,
7
8
  normalizeSymbol,
@@ -11,6 +12,8 @@ import {
11
12
  const DAY = 86_400_000;
12
13
  const WEEK = 7 * DAY;
13
14
  export const ROTATION_LIMIT = 24;
15
+ /** RRG's benchmark when none is chosen; RRG alerts default to it too. */
16
+ export const DEFAULT_ROTATION_BENCHMARK = "SPY:NYSEARCA";
14
17
  export interface RotationInstrument {
15
18
  symbol: string;
16
19
  exchange: string;
@@ -46,8 +49,7 @@ export interface RotationRow {
46
49
  asOf: string | null;
47
50
  strength: number | null;
48
51
  momentum: number | null;
49
- quadrant:
50
- "Leading" | "Weakening" | "Lagging" | "Improving" | "Neutral" | null;
52
+ quadrant: RotationQuadrantName | null;
51
53
  strengthRank: RotationRank;
52
54
  momentumRank: RotationRank;
53
55
  relativeReturn13w: number | null;
@@ -149,21 +151,8 @@ function rotationRank(
149
151
  lastDate: stats.endDate,
150
152
  };
151
153
  }
152
- export function rotationQuadrant(
153
- strength: number | null,
154
- momentum: number | null,
155
- ): RotationRow["quadrant"] {
156
- if (strength == null || momentum == null) return null;
157
- if (Math.abs(strength - 100) < 1e-9 || Math.abs(momentum - 100) < 1e-9)
158
- return "Neutral";
159
- return strength > 100
160
- ? momentum > 100
161
- ? "Leading"
162
- : "Weakening"
163
- : momentum > 100
164
- ? "Improving"
165
- : "Lagging";
166
- }
154
+ /** The weekly strength, momentum and quadrant math is the RRG alert's, shared with Gloom Cloud's delivery. */
155
+ export { rotationQuadrant };
167
156
  export function buildRotation(
168
157
  benchmark: RotationHistory,
169
158
  instruments: RotationHistory[],
@@ -219,10 +208,12 @@ export function buildRotation(
219
208
  const benchmarkUsable = !!benchmark.currency && !!asOf;
220
209
  const rows = instruments.map((source): RotationRow => {
221
210
  const closes = rotationCloses(source.points, through);
211
+ // A benchmark with no usable weeks says so once in `gaps`; its rows are not to blame for it.
222
212
  const gaps = [
223
213
  source.error,
224
214
  source.stale ? "History is stale." : null,
225
- source.currency !== benchmark.currency || !source.currency
215
+ benchmarkUsable &&
216
+ (source.currency !== benchmark.currency || !source.currency)
226
217
  ? "Price currency does not match the benchmark."
227
218
  : null,
228
219
  ].filter((x): x is string => !!x);
@@ -240,34 +231,16 @@ export function buildRotation(
240
231
  ? ratio
241
232
  : null;
242
233
  });
243
- const strengths = ratios.map((ratio, index) => {
244
- const window = ratios.slice(Math.max(0, index - 12), index + 1);
245
- if (
246
- ratio == null ||
247
- window.length !== 13 ||
248
- window.some((value) => value == null)
249
- )
250
- return null;
251
- const mean =
252
- (window as number[]).reduce((sum, value) => sum + value, 0) / 13;
253
- return (100 * ratio) / mean;
254
- });
234
+ const series = rotationSeries(ratios);
255
235
  const history = weeks.map((week, index): RotationPoint => {
256
- const strength = strengths[index] ?? null,
257
- previous = index >= 4 ? (strengths[index - 4] ?? null) : null;
258
- const contiguous =
259
- index >= 4 &&
260
- strengths.slice(index - 4, index + 1).every((value) => value != null);
236
+ const { strength, momentum } = series[index]!;
261
237
  const priorRatio = index >= 13 ? (ratios[index - 13] ?? null) : null,
262
238
  ratio = ratios[index] ?? null;
263
239
  return {
264
240
  date: weekly.get(week)?.date ?? week,
265
241
  week,
266
242
  strength,
267
- momentum:
268
- strength != null && previous != null && contiguous
269
- ? (100 * strength) / previous
270
- : null,
243
+ momentum,
271
244
  relativeReturn13w:
272
245
  ratio != null &&
273
246
  priorRatio != null &&
@@ -284,7 +257,10 @@ export function buildRotation(
284
257
  Number.isFinite(latest.strength) &&
285
258
  latest.momentum != null &&
286
259
  Number.isFinite(latest.momentum);
287
- if (!latest || latest.strength == null || latest.momentum == null)
260
+ if (
261
+ benchmarkUsable &&
262
+ (!latest || latest.strength == null || latest.momentum == null)
263
+ )
288
264
  gaps.push(
289
265
  "At least 17 contiguous matched weekly closes are required; a missing week breaks the trail.",
290
266
  );
@@ -36,6 +36,7 @@ import {
36
36
  import { useResearchCloudSession } from "../shared/research-cloud-session";
37
37
  import { cachedRotation, loadRotation } from "./client";
38
38
  import {
39
+ DEFAULT_ROTATION_BENCHMARK,
39
40
  ROTATION_LIMIT,
40
41
  rotationId,
41
42
  rotationTrailWeeks,
@@ -140,13 +141,13 @@ function RotationDetail({
140
141
  export function RelativeRotationPane(props: PaneProps) {
141
142
  const [scope] = usePaneSettingValue("scope", "sectors");
142
143
  const [symbols] = usePaneSettingValue("symbols", "");
143
- const [benchmarkText] = usePaneSettingValue("benchmark", "SPY:NYSEARCA");
144
+ const [benchmarkText] = usePaneSettingValue("benchmark", DEFAULT_ROTATION_BENCHMARK);
144
145
  const [trailText] = usePaneSettingValue("trail", "6");
145
146
  const { collectionId } = usePaneCollection();
146
147
  const tickers = useTickers();
147
148
  const context = useMemo(() => {
148
149
  try {
149
- const references = rotationInstruments(benchmarkText || "SPY:NYSEARCA");
150
+ const references = rotationInstruments(benchmarkText || DEFAULT_ROTATION_BENCHMARK);
150
151
  if (references.length !== 1)
151
152
  throw new Error("Choose one benchmark in pane settings.");
152
153
  const instruments =
@@ -446,6 +446,7 @@ export function CorporateActionsView({
446
446
  const financialCurrency = tickerFinancials?.financialCurrency;
447
447
  const trailingEps = tickerFinancials?.fundamentals?.eps;
448
448
  const trailingEpsCurrency = tickerFinancials?.fundamentals?.epsCurrency;
449
+ const trailingEpsSource = tickerFinancials?.fundamentals?.trailingEpsSource;
449
450
  const fundamentalsCurrency = tickerFinancials?.fundamentals?.financialCurrency;
450
451
  const marketCapCurrency = tickerFinancials?.fundamentals?.marketCapCurrency;
451
452
  const quoteCurrency = tickerFinancials?.quote?.currency;
@@ -453,10 +454,10 @@ export function CorporateActionsView({
453
454
  () => (quarterlyStatements || financialCurrency || trailingEps != null ? {
454
455
  quarterlyStatements: quarterlyStatements ?? [],
455
456
  financialCurrency,
456
- fundamentals: { eps: trailingEps, epsCurrency: trailingEpsCurrency, financialCurrency: fundamentalsCurrency, marketCapCurrency },
457
+ fundamentals: { eps: trailingEps, epsCurrency: trailingEpsCurrency, financialCurrency: fundamentalsCurrency, marketCapCurrency, trailingEpsSource },
457
458
  quote: quoteCurrency ? { currency: quoteCurrency } : null,
458
459
  } : null),
459
- [financialCurrency, fundamentalsCurrency, marketCapCurrency, quarterlyStatements, quoteCurrency, trailingEps, trailingEpsCurrency],
460
+ [financialCurrency, fundamentalsCurrency, marketCapCurrency, quarterlyStatements, quoteCurrency, trailingEps, trailingEpsCurrency, trailingEpsSource],
460
461
  );
461
462
  const actionsLoader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
462
463
  if (!dataProvider?.getCorporateActions) throw new Error("Corporate actions source unavailable");
@@ -15,7 +15,7 @@ import {
15
15
  } from "../../../utils/format";
16
16
  import { computeTTM } from "../ticker-detail/financials/aggregation";
17
17
  import { declaredDividends } from "../../../utils/declared-dividends";
18
- import { fundamentalsEpsCurrency } from "../../../utils/fundamentals";
18
+ import { fundamentalsEpsCurrency, SERVED_TRAILING_EPS } from "../../../utils/fundamentals";
19
19
  import {
20
20
  earningsDateStatus,
21
21
  earningsDetail,
@@ -175,11 +175,11 @@ function estimateValue(pair: EstimatePair): string {
175
175
  }
176
176
 
177
177
  /**
178
- * What the TTM row reads: the statements for its four quarters and revenue, and the trailing EPS Gloom Cloud serves
179
- * with the fundamentals, the figure `ticker` prints as EPS (TTM).
178
+ * What the TTM row reads: the statements for its four quarters and revenue, and the trailing EPS of the fundamentals,
179
+ * the figure `ticker` prints as EPS (TTM) (the filed quarters' when they give one, see time-series/filed-trailing-eps).
180
180
  */
181
181
  export type EventFinancials = Pick<TickerFinancials, "quarterlyStatements" | "financialCurrency"> & {
182
- fundamentals?: Pick<NonNullable<TickerFinancials["fundamentals"]>, "eps" | "epsCurrency" | "financialCurrency" | "marketCapCurrency"> | null;
182
+ fundamentals?: Pick<NonNullable<TickerFinancials["fundamentals"]>, "eps" | "epsCurrency" | "financialCurrency" | "marketCapCurrency" | "trailingEpsSource"> | null;
183
183
  /** The listing's currency, which says whether an unlabelled EPS can be in the statements' (fundamentalsCurrency). */
184
184
  quote?: Pick<NonNullable<TickerFinancials["quote"]>, "currency"> | null;
185
185
  };
@@ -204,9 +204,10 @@ function adjustedQuarterSum(
204
204
  }
205
205
 
206
206
  /**
207
- * The TTM row's EPS is the GAAP diluted trailing figure, the one `ticker` prints as EPS (TTM). Without it, the four
208
- * statement quarters' diluted EPS summed. The reported rows above it are often adjusted, so they are never added to
209
- * it: only when all four are adjusted does the detail also give their sum.
207
+ * The TTM row's EPS is the GAAP diluted trailing figure `ticker` prints as EPS (TTM): the last four filed quarters',
208
+ * or Gloom Cloud's trailing figure, named so, when they give none. Without either, the four statement quarters'
209
+ * diluted EPS summed. The reported rows above it are often adjusted, so they are never added to it: only when all
210
+ * four are adjusted does the detail also give their sum.
210
211
  */
211
212
  function ttmRow(
212
213
  quarterlyStatements: readonly FinancialStatement[],
@@ -226,7 +227,8 @@ function ttmRow(
226
227
  date: latest?.date ?? "",
227
228
  status: "TTM",
228
229
  period: "4 qtrs",
229
- detail: [eps == null ? "statement sum" : served ? "GAAP diluted" : "GAAP diluted (statements)",
230
+ detail: [eps == null ? "statement sum" : !served ? "GAAP diluted (statements)"
231
+ : financials?.fundamentals?.trailingEpsSource === "served" ? `GAAP diluted (${SERVED_TRAILING_EPS})` : "GAAP diluted",
230
232
  adjusted == null ? null : `adj sum ${formatNumber(adjusted, 2)}`].filter(Boolean).join(", "),
231
233
  epsCurrency: served ? fundamentalsEpsCurrency(financials) : ttm.currency ?? financials?.financialCurrency,
232
234
  revenueCurrency: ttm.currency ?? financials?.financialCurrency,
@@ -83,7 +83,7 @@ function projectEarningsEstimatesHeadless(
83
83
  returned: rows.length,
84
84
  truncated: rows.length < matching.length,
85
85
  coverageNote: notice?.text,
86
- epsBasis: `${EPS_BASIS_NOTE} The TTM row's EPS is the GAAP diluted trailing figure ticker shows as EPS (TTM), or the four statement quarters' diluted EPS when none is served; reported quarters of different bases are never summed, and the row adds an adjusted sum only when all four quarters are adjusted.`,
86
+ epsBasis: `${EPS_BASIS_NOTE} The TTM row's EPS is the GAAP diluted trailing figure ticker shows as EPS (TTM): the last four filed quarters', or Gloom Cloud's TTM (named so) when they give none, or the four statement quarters' diluted EPS when neither is there; reported quarters of different bases are never summed, and the row adds an adjusted sum only when all four quarters are adjusted.`,
87
87
  dateNote: "Fiscal period dates are not announcement dates. Revenue is attached only to an identified statement period.",
88
88
  unmatchedReportedPeriods,
89
89
  },
@@ -96,7 +96,10 @@ const VALUATION_COLUMNS: HeadlessPaneColumn[] = [
96
96
  ...([["fcfYield", "FCF Yield"], ["revenueGrowth", REVENUE_GROWTH_HEADER], ["operatingMargin", "TTM Op Margin"]] as const).map(([key, header]) => ({
97
97
  key, header, align: "right" as const,
98
98
  format: (value: unknown, row: Record<string, unknown>) => value == null ? gapText(row, key)
99
- : key === "revenueGrowth" ? formatPercent(Number(value)) : formatLevelPercent(Number(value)),
99
+ : key === "revenueGrowth" ? formatPercent(Number(value))
100
+ // A margin of the latest fiscal year, where no trailing quarters were served, says which year.
101
+ : key === "operatingMargin" && typeof row.operatingMarginPeriod === "string"
102
+ ? `${formatLevelPercent(Number(value))} ${row.operatingMarginPeriod}` : formatLevelPercent(Number(value)),
100
103
  })),
101
104
  ];
102
105
 
@@ -226,6 +229,7 @@ export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
226
229
  ...rows.flatMap((row) => row.evSalesUnavailable ? [`${row.symbol}: ${row.evSalesUnavailable}`] : []),
227
230
  ...rows.flatMap((row) => row.evSalesNote ? [`${row.symbol}: ${row.evSalesNote}`] : []),
228
231
  ...rows.flatMap((row) => row.marketCapNote ? [`${row.symbol}: ${row.marketCapNote}`] : []),
232
+ ...rows.flatMap((row) => row.marketCapWithheld ? [`${row.symbol}: ${row.marketCapWithheld}`] : []),
229
233
  ],
230
234
  multipleBasis: "Comparable P/E fields require a positive finite multiple over positive EPS, and evSales an enterprise value above zero; notMeaningful says why one is null, and reportedMultiples preserves the finite provider P/E values.",
231
235
  marketCapBasis: "Market caps retain their own currency and source. Fundamentals retrieval time is not a valuation date; quote timestamps do not date fallback caps.",
@@ -1,9 +1,10 @@
1
1
  import { formatCurrency } from "../../../utils/format";
2
2
  import { comparablePriceEarnings, notMeaningfulMultiples } from "../../../utils/price-earnings";
3
- import { marketCapBasisNote, viewMarketCapitalization } from "../../../utils/market-capitalization";
3
+ import { marketCapBasisNote, viewMarketCapitalization, withheldMarketCap } from "../../../utils/market-capitalization";
4
4
  import { enterpriseValueNotMeaningful, enterpriseValueReconciliationNote, enterpriseValueWithdrawn, reportedEnterpriseValue } from "../../../utils/fundamentals";
5
5
  import { getExtendedSessionDisplay, getQuoteSessionFields } from "../../../market-data/market/status";
6
6
  import { atRegularSessionPrice, movePriceDerivedFigures, regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
7
+ import { fiscalPeriodLabel, fiscalYearEndMonth } from "../../../utils/fiscal-period";
7
8
  import { latestFinancialPeriod, latestReportedPeriod } from "../../../utils/latest-financial-period";
8
9
  import type { Quote, TickerFinancials } from "../../../types/financials";
9
10
 
@@ -50,6 +51,25 @@ function fcfYieldGap(fundamentals: TickerFinancials["fundamentals"], capCurrency
50
51
  }
51
52
  export const RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE = "Fundamentals stale: retained values may be out of date";
52
53
 
54
+ /**
55
+ * The operating margin of the latest fiscal year, for a row whose trailing
56
+ * twelve months are not whole (a quarter the source has not carried yet):
57
+ * that year's operating income over its revenue, named for the year ("FY25")
58
+ * so a cell never passes it off as trailing. Null when the latest year lacks
59
+ * either figure; an older year is never stood in for it.
60
+ */
61
+ function fiscalYearOperatingMargin(annualStatements: TickerFinancials["annualStatements"] | undefined) {
62
+ const latest = latestFinancialPeriod(annualStatements, (row) => row.date);
63
+ if (!latest || !finiteNumber(latest.operatingIncome) || !finiteNumber(latest.totalRevenue) || latest.totalRevenue <= 0) return null;
64
+ const year = /^FY\d{2}(\d{2})$/.exec(fiscalPeriodLabel(latest.date, "annual", fiscalYearEndMonth(annualStatements ?? [])))?.[1];
65
+ return {
66
+ value: latest.operatingIncome / latest.totalRevenue,
67
+ // The fiscal year's own name; the calendar year of its end where the year cannot be named.
68
+ label: `FY${year ?? latest.date.slice(2, 4)}`,
69
+ periodEnd: latest.date.slice(0, 10),
70
+ };
71
+ }
72
+
53
73
  /**
54
74
  * A peer's row. Last, its change and the extended print are the quote's
55
75
  * sessions as `quote` reads them (getQuoteSessionFields): after the close,
@@ -97,14 +117,17 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
97
117
  && fundamentals.freeCashFlow != null && Number.isFinite(fundamentals.freeCashFlow) && capitalization.value > 0
98
118
  ? fundamentals.freeCashFlow / capitalization.value : null;
99
119
  const revenueGrowth = fundamentals?.revenueGrowth ?? fundamentals?.lastQuarterGrowth ?? null;
100
- const operatingMargin = fundamentals?.operatingMargin ?? null;
120
+ const trailingOperatingMargin = fundamentals?.operatingMargin ?? null;
121
+ const fiscalYearMargin = trailingOperatingMargin == null ? fiscalYearOperatingMargin(financials?.annualStatements) : null;
122
+ const operatingMargin = trailingOperatingMargin ?? fiscalYearMargin?.value ?? null;
101
123
  const trailingPE = trailingNotMeaningful ? null : comparablePriceEarnings(reportedMultiples.trailingPE);
102
124
  const forwardPE = forwardNotMeaningful ? null : comparablePriceEarnings(reportedMultiples.forwardPE);
103
125
  // Every figure a row lacks says why, so an export's blank cell is never silent: N/M multiples say so themselves.
104
126
  const gaps: Partial<Record<RelativeValuationGapKey, string>> = {};
105
127
  if (session.price == null) gaps.price = quoteStale ? QUOTE_STALE : NOT_REPORTED;
106
128
  if (session.changePercent == null) gaps.changePercent = quoteStale ? QUOTE_STALE : NOT_REPORTED;
107
- if (capitalization == null) gaps.marketCap = quoteStale && !fundamentals?.marketCap ? QUOTE_STALE : NOT_REPORTED;
129
+ const withheld = withheldMarketCap(fundamentals);
130
+ if (capitalization == null) gaps.marketCap = withheld?.gap ?? (quoteStale && !fundamentals?.marketCap ? QUOTE_STALE : NOT_REPORTED);
108
131
  if (trailingPE == null && !trailingNotMeaningful && reportedMultiples.trailingPE == null) gaps.trailingPE = NOT_REPORTED;
109
132
  if (forwardPE == null && !forwardNotMeaningful && reportedMultiples.forwardPE == null) gaps.forwardPE = NOT_REPORTED;
110
133
  if (evSales == null && !evSalesNotMeaningful) gaps.evSales = evSalesGap(fundamentals, quote?.currency, evSalesWithdrawn, enterpriseValueGap);
@@ -136,6 +159,8 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
136
159
  marketCapProvenance: capitalization?.provenance ?? null,
137
160
  /** valuation's note when the cap is the share count times the price: the provider's figure it replaced. */
138
161
  marketCapNote: marketCapBasisNote(capitalization) ?? null,
162
+ /** Why Gloom Cloud withheld the cap (a receipt priced as ordinary shares), as `valuation` and EQS say it; null otherwise. */
163
+ marketCapWithheld: withheld?.note ?? null,
139
164
  trailingPE,
140
165
  forwardPE,
141
166
  reportedMultiples,
@@ -151,6 +176,9 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
151
176
  fcfYield,
152
177
  revenueGrowth,
153
178
  operatingMargin,
179
+ /** The fiscal year the operating margin is of ("FY25") when no trailing figure was served; null when it is trailing twelve months. */
180
+ operatingMarginPeriod: fiscalYearMargin?.label ?? null,
181
+ operatingMarginPeriodEnd: fiscalYearMargin?.periodEnd ?? null,
154
182
  /** Why each missing figure is missing, in a few words the row draws in its place. */
155
183
  gaps,
156
184
  };
@@ -97,7 +97,7 @@ function extendedMove(row: RelativeRow, columnId: ExtendedColumnId): number | nu
97
97
  * AFTER-HRS or OVERNIGHT column, named as BI and MOST name it, follows CHG%
98
98
  * for each session in `extendedSessions`.
99
99
  */
100
- function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[]): RelativeColumn[] {
100
+ function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly ExtendedSession[], marginNamesYear: boolean): RelativeColumn[] {
101
101
  const symbolWidth = 8;
102
102
  const priceWidth = 10;
103
103
  const pctWidth = 8;
@@ -117,7 +117,8 @@ function buildRelativeColumns(baseCurrency: string, extendedSessions: readonly E
117
117
  { id: "fcfYield", label: "FCF%", width: metricWidth, align: "right" },
118
118
  // Latest quarter against the year before, and a trailing-twelve-month margin.
119
119
  { id: "revenueGrowth", label: REVENUE_GROWTH_LABEL, width: metricWidth, align: "right" },
120
- { id: "operatingMargin", label: "TTM OP%", width: metricWidth, align: "right" },
120
+ // Wider while a peer's margin is of its latest fiscal year and names it ("16.83% FY25").
121
+ { id: "operatingMargin", label: "TTM OP%", width: marginNamesYear ? 12 : metricWidth, align: "right" },
121
122
  ];
122
123
  }
123
124
 
@@ -217,9 +218,10 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
217
218
  const fxRates = useFxRatesMap([baseCurrency, ...rows.map((row) => row.marketCapCurrency)]);
218
219
  // A pre-market or after-hours print gets its own column when a peer has one.
219
220
  const extendedSessionsKey = extendedSessionsOf(rows).join(",");
221
+ const marginNamesYear = rows.some((row) => row.operatingMarginPeriod != null);
220
222
  const columns = useMemo(
221
- () => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
222
- [baseCurrency, extendedSessionsKey],
223
+ () => buildRelativeColumns(baseCurrency, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], marginNamesYear),
224
+ [baseCurrency, extendedSessionsKey, marginNamesYear],
223
225
  );
224
226
 
225
227
  const comparableRows = useMemo(() => rows.map((row) => ({
@@ -319,7 +321,7 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
319
321
  case "revenueGrowth":
320
322
  return { text: formatPercent(row.revenueGrowth ?? undefined), color: shownPriceColor((row.revenueGrowth ?? 0) * 100, 2) };
321
323
  case "operatingMargin":
322
- return { text: formatLevelPercent(row.operatingMargin ?? undefined), color: colors.text };
324
+ return { text: `${formatLevelPercent(row.operatingMargin ?? undefined)}${row.operatingMargin != null && row.operatingMarginPeriod ? ` ${row.operatingMarginPeriod}` : ""}`, color: colors.text };
323
325
  }
324
326
  }, []);
325
327
 
@@ -4,7 +4,8 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
4
4
  import { formatCompactCurrency } from "../../../utils/format";
5
5
  import { resolveHeadlessInstrument } from "../shared/headless-market-data";
6
6
  import { loadReverseDcfInputs } from "./client";
7
- import { describeFcfMargin, DISCOUNT_RATES, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS, type ImpliedGrowth } from "./model";
7
+ import { describeFcfMargin, DISCOUNT_RATES, pastGrowthBasis, projectReverseDcf, TERMINAL_GROWTH, TERMINAL_GROWTHS, ttmBaseNote,
8
+ type ImpliedGrowth } from "./model";
8
9
 
9
10
  const percent = (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--";
10
11
  /** An assumption as it is set: 9%, 2.5%. */
@@ -35,6 +36,7 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
35
36
  const quote = inputs.financials?.quote;
36
37
  // The enterprise value is measured at the regular session price, so the report is dated by it: after the close, the close.
37
38
  const valuedAt = regularSessionValuationTime(quote);
39
+ const notes = [model.enterpriseValueNote, ttmBaseNote(model)].filter((note): note is string => !!note);
38
40
  return {
39
41
  sections: [
40
42
  { title: "Reverse DCF", columns: [{ key: "label", header: "" }, { key: "value", header: "Value" }], rows: [
@@ -42,7 +44,7 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
42
44
  : `${typeof growth === "number" ? percent(growth) : growth} at ${rate(discountRate)} discount, ${rate(TERMINAL_GROWTH)} terminal` },
43
45
  { label: "Implied revenue, year 10", value: model.impliedRevenue == null ? "--"
44
46
  : `${formatCompactCurrency(model.impliedRevenue, currency)} at ${describeFcfMargin(fcfMargin, fcfMarginBasis)}` },
45
- { label: "Past FCF growth", value: model.pastGrowth ? `${percent(model.pastGrowth.rate)} over ${model.pastGrowth.years}y` : "--" },
47
+ { label: "Past FCF growth", value: model.pastGrowth ? `${percent(model.pastGrowth.rate)} ${pastGrowthBasis(model.pastGrowth)}` : "--" },
46
48
  { label: "FCF yield", value: percent(model.fcfYield) },
47
49
  { label: "Enterprise value", value: `${formatCompactCurrency(model.enterpriseValue ?? undefined, currency)}${model.enterpriseValueNote ? " (provider)" : ""}` },
48
50
  { label: "Free cash flow, TTM", value: formatCompactCurrency(model.freeCashFlow ?? undefined, currency) },
@@ -60,7 +62,7 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
60
62
  },
61
63
  unavailableSymbols: model.implied ? [] : [instrument.symbol],
62
64
  errors: [inputs.error, model.error].filter((value): value is string => !!value),
63
- ...(model.enterpriseValueNote ? { notes: [model.enterpriseValueNote] } : {}),
65
+ ...notes.length > 0 ? { notes } : {},
64
66
  metadata: { currency: model.currency, discountRate, terminalGrowth: TERMINAL_GROWTH, discountRates: DISCOUNT_RATES, fcfMargin,
65
67
  fcfMarginSource: fcfMarginBasis.source, companyFcfMargin: fcfMarginBasis.company, fcfMarginGap: fcfMarginBasis.gap,
66
68
  impliedRevenue: model.impliedRevenue, stale: inputs.stale, fetchedAt: inputs.fetchedAt, methodology: "docs/research-data.md#reverse-dcf" },
@@ -1,6 +1,8 @@
1
1
  import { unavailableText } from "../../../components/ui/status";
2
2
  import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
3
3
  import type { Fundamentals, TickerFinancials } from "../../../types/financials";
4
+ import { fiscalPeriodLabel, fiscalYearEndMonth } from "../../../utils/fiscal-period";
5
+ import { formatCompactCurrency } from "../../../utils/format";
4
6
  import { enterpriseValueCheck, enterpriseValueLabel, enterpriseValueNotMeaningful, enterpriseValueReconciliationNote,
5
7
  enterpriseValueWithdrawn, reportedEnterpriseValue } from "../../../utils/fundamentals";
6
8
 
@@ -50,8 +52,10 @@ export interface ReverseDcfModel {
50
52
  fcfMarginBasis: FcfMarginBasis;
51
53
  /** Year-ten revenue at `fcfMargin`: trailing free cash flow grown at the implied rate for ten years, over the margin. */
52
54
  impliedRevenue: number | null;
53
- /** Annual FCF growth between the latest annual statement and one up to five years earlier, when both are positive. */
54
- pastGrowth: { rate: number; years: number } | null;
55
+ /** Annual FCF growth between the latest annual statement and one up to five years earlier, when both are positive. `period` names the two years: FY2021–FY2025. */
56
+ pastGrowth: { rate: number; years: number; period: string } | null;
57
+ /** The latest annual statement's free cash flow, which the past growth ends at, and the year it is of (FY2025). */
58
+ fiscalYearFcf: { value: number; label: string } | null;
55
59
  /** Implied growth for each discount rate (rows) at each terminal growth (columns). */
56
60
  sensitivity: { discountRate: number; implied: (ImpliedGrowth | null)[] }[];
57
61
  error: string | null;
@@ -82,15 +86,65 @@ export function solveImpliedGrowth(enterpriseValue: number, fcf: number, discoun
82
86
  return { kind: "rate", value: (low + high) / 2 };
83
87
  }
84
88
 
85
- function pastFcfGrowth(financials: TickerFinancials): ReverseDcfModel["pastGrowth"] {
86
- const annual = financials.annualStatements
89
+ /** The latest annual statements with a free cash flow, oldest first, as many as the past growth looks back over. */
90
+ function annualFreeCashFlows(financials: TickerFinancials) {
91
+ return financials.annualStatements
87
92
  .filter((row) => typeof row.freeCashFlow === "number" && Number.isFinite(row.freeCashFlow))
88
93
  .sort((a, b) => a.date.localeCompare(b.date))
89
94
  .slice(-6);
95
+ }
96
+
97
+ /** The two years a past growth spans, as issuers name them: `FY2021–FY2025`; period end dates where a year is off the fiscal calendar. */
98
+ function fiscalSpan(financials: TickerFinancials, from: string, to: string): string {
99
+ const month = fiscalYearEndMonth(financials.annualStatements);
100
+ const [start, end] = [from, to].map((date) => fiscalPeriodLabel(date, "annual", month)) as [string, string];
101
+ return start.startsWith("FY") && end.startsWith("FY") ? `${start}–${end}` : `${from.slice(0, 10)} to ${to.slice(0, 10)}`;
102
+ }
103
+
104
+ function pastFcfGrowth(financials: TickerFinancials): ReverseDcfModel["pastGrowth"] {
105
+ const annual = annualFreeCashFlows(financials);
90
106
  const first = annual[0], last = annual.at(-1);
91
107
  if (!first || !last || first === last || !(first.freeCashFlow! > 0) || !(last.freeCashFlow! > 0)) return null;
92
108
  const years = Math.round((Date.parse(last.date) - Date.parse(first.date)) / (365.25 * 86_400_000));
93
- return years > 0 ? { rate: (last.freeCashFlow! / first.freeCashFlow!) ** (1 / years) - 1, years } : null;
109
+ return years > 0
110
+ ? { rate: (last.freeCashFlow! / first.freeCashFlow!) ** (1 / years) - 1, years, period: fiscalSpan(financials, first.date, last.date) }
111
+ : null;
112
+ }
113
+
114
+ function latestFiscalYearFcf(financials: TickerFinancials): ReverseDcfModel["fiscalYearFcf"] {
115
+ const last = annualFreeCashFlows(financials).at(-1);
116
+ return last ? { value: last.freeCashFlow!, label: fiscalPeriodLabel(last.date, "annual", fiscalYearEndMonth(financials.annualStatements)) } : null;
117
+ }
118
+
119
+ /** How far above the latest fiscal year's free cash flow the trailing twelve months' must be to say the two bases differ. */
120
+ const TTM_BASE_RATIO = 1.5;
121
+
122
+ /**
123
+ * How a trailing free cash flow more than 1.5 times the latest fiscal year's
124
+ * differs from it (`2.7x` FY2025's `5.3B USD`): the growth priced in starts
125
+ * from the trailing figure, the past growth ends at the year's. Null when they
126
+ * are near each other, the year's is not positive, or the model has no growth
127
+ * to say.
128
+ */
129
+ export function ttmBaseGap(model: Pick<ReverseDcfModel, "freeCashFlow" | "fiscalYearFcf" | "currency" | "error">): { multiple: string; year: string; fcf: string } | null {
130
+ const { freeCashFlow, fiscalYearFcf } = model;
131
+ if (model.error || freeCashFlow == null || !fiscalYearFcf || !(fiscalYearFcf.value > 0) || !(freeCashFlow > fiscalYearFcf.value * TTM_BASE_RATIO)) return null;
132
+ return {
133
+ multiple: `${(freeCashFlow / fiscalYearFcf.value).toFixed(1)}x`,
134
+ year: fiscalYearFcf.label,
135
+ fcf: formatCompactCurrency(fiscalYearFcf.value, model.currency ?? undefined),
136
+ };
137
+ }
138
+
139
+ /** "The TTM base differs from FY2025: TTM FCF is 2.7x that year's 5.3B USD.", the one line a report adds when `ttmBaseGap` finds a gap. */
140
+ export function ttmBaseNote(model: Parameters<typeof ttmBaseGap>[0]): string | null {
141
+ const gap = ttmBaseGap(model);
142
+ return gap ? `The TTM base differs from ${gap.year}: TTM FCF is ${gap.multiple} that year's ${gap.fcf}.` : null;
143
+ }
144
+
145
+ /** What the past growth rate is over, after the rate: `a year, FY2021–FY2025`. */
146
+ export function pastGrowthBasis(growth: NonNullable<ReverseDcfModel["pastGrowth"]>): string {
147
+ return `a year, ${growth.period}`;
94
148
  }
95
149
 
96
150
  /**
@@ -145,7 +199,8 @@ export function projectReverseDcf(
145
199
  const empty: ReverseDcfModel = { symbol: options.symbol, currency, discountRate: options.discountRate, terminalGrowth: TERMINAL_GROWTH,
146
200
  enterpriseValue, computedEnterpriseValue: check?.computed ?? null, enterpriseValueNote: check ? enterpriseValueReconciliationNote(fundamentals) ?? null : null,
147
201
  freeCashFlow, fcfYield: null, implied: null, fcfMargin, fcfMarginBasis, impliedRevenue: null,
148
- pastGrowth: financials ? pastFcfGrowth(financials) : null, sensitivity: [], error: null };
202
+ pastGrowth: financials ? pastFcfGrowth(financials) : null, fiscalYearFcf: financials ? latestFiscalYearFcf(financials) : null,
203
+ sensitivity: [], error: null };
149
204
  const valueCurrency = fundamentals?.marketCapCurrency;
150
205
  // ADRs and cross-listings can report cash flows in one currency and trade in another.
151
206
  if (currency && valueCurrency && currency !== valueCurrency) {