gloomberb 0.19.1 → 0.19.3

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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,8 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import type {
3
- CloudEarningsCallPayload,
4
- CloudEarningsTranscriptPayload,
5
- } from "../../../api-client";
2
+ import type { CloudEarningsCallPayload } from "../../../api-client";
6
3
  import { recordResearchActivity } from "../../../api-client/research-activity";
7
4
  import {
8
5
  DataTableStackView,
@@ -49,7 +46,11 @@ import {
49
46
  loadEarningsCalls,
50
47
  loadTranscript,
51
48
  statusOf,
49
+ type EarningsTranscriptResult,
52
50
  } from "./data";
51
+ import { isPermanentClientError } from "../../../api-client/errors";
52
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
53
+ import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
53
54
  import { callPeriod, callTitle, eventLabelOf, formatCallDate, formatDuration, formatSentiment } from "./format";
54
55
  import { findCallForQuarter, findInLines, MIN_FIND_LENGTH, turnParagraphs } from "./model";
55
56
  import { READER_TABS, TranscriptView, type ReaderTab, type TranscriptPlayback } from "./transcript-view";
@@ -148,6 +149,8 @@ interface TickerLookup {
148
149
  calls: CloudEarningsCallPayload[];
149
150
  fetchedAt?: number;
150
151
  stale?: boolean;
152
+ /** Its refresh failed and the copy saved at this time (ms) answered instead. */
153
+ savedAt?: number;
151
154
  refreshError?: string;
152
155
  refreshRequest?: number;
153
156
  }
@@ -280,6 +283,8 @@ function CallsListView({
280
283
  const [stale, setStale] = useState(initialShelf?.stale === true);
281
284
  // The company's home line the shelf is read from, for a receipt or a foreign line.
282
285
  const [companyFrom, setCompanyFrom] = useState(initialShelf?.companyFrom ?? null);
286
+ // The list's refresh failed and a copy saved at this time (ms) answered instead.
287
+ const [listSavedAt, setListSavedAt] = useState<number | null>(null);
283
288
  const [listFetchedAt, setListFetchedAt] = useState(initialShelf?.fetchedAt ?? 0);
284
289
  const listRequestVersion = useRef(0);
285
290
  const [listCompletedRequest, setListCompletedRequest] = useState(0);
@@ -292,7 +297,7 @@ function CallsListView({
292
297
 
293
298
  const [selectedId, setSelectedId] = usePluginPaneState<string | null>(`${stateKey}selectedId`, null);
294
299
  const [detailOpen, setDetailOpen] = usePluginPaneState<boolean>(`${stateKey}detailOpen`, false);
295
- const [transcript, setTranscript] = useState<CloudEarningsTranscriptPayload | null>(null);
300
+ const [transcript, setTranscript] = useState<EarningsTranscriptResult | null>(null);
296
301
  useEffect(() => {
297
302
  if (focused && transcript && detailOpen) recordResearchActivity("pro_feature_used", "transcripts");
298
303
  }, [focused, transcript, detailOpen]);
@@ -344,10 +349,12 @@ function CallsListView({
344
349
  setHasMore(result.sourceLimitReached === true);
345
350
  setStale(result.stale);
346
351
  setCompanyFrom(result.companyFrom ?? null);
352
+ setListSavedAt(result.savedAt ?? null);
347
353
  setListFetchedAt(result.fetchedAt);
348
354
  setListPending(result.pending === true && result.calls.length === 0);
355
+ // A saved copy says when it was saved, in the footer, rather than why the refresh failed.
349
356
  setListError(
350
- result.refreshError ? { message: result.refreshError, status: result.errorStatus } : null,
357
+ result.refreshError && result.savedAt == null ? { message: result.refreshError, status: result.errorStatus } : null,
351
358
  );
352
359
  setListStatus("loaded");
353
360
  setListCompletedRequest(request);
@@ -357,6 +364,7 @@ function CallsListView({
357
364
  setListPending(false);
358
365
  setHasMore(false);
359
366
  setNextOffset(0);
367
+ setListSavedAt(null);
360
368
  setListError({
361
369
  message: error instanceof Error ? error.message : String(error),
362
370
  status: statusOf(error),
@@ -415,6 +423,7 @@ function CallsListView({
415
423
  setListPending(held?.pending === true && held.calls.length === 0);
416
424
  setStale(held?.stale === true);
417
425
  setCompanyFrom(held?.companyFrom ?? null);
426
+ setListSavedAt(null);
418
427
  setListFetchedAt(held?.fetchedAt ?? 0);
419
428
  setListError(null);
420
429
  setLookup(null);
@@ -482,19 +491,22 @@ function CallsListView({
482
491
  calls: result.calls,
483
492
  fetchedAt: result.fetchedAt,
484
493
  stale: result.stale,
485
- refreshError: result.refreshError,
494
+ savedAt: result.savedAt,
495
+ refreshError: result.savedAt == null ? result.refreshError : undefined,
486
496
  refreshRequest,
487
497
  });
488
498
  })
489
499
  .catch((error: unknown) => {
490
500
  if (cancelled) return;
491
- const retainRows = ![401, 402, 403].includes(statusOf(error) ?? 0);
501
+ // A refusal asking again will not change clears the rows; a passing failure keeps them.
502
+ const retainRows = !isPermanentClientError(error);
492
503
  setLookup((current) => ({
493
504
  ticker: lookupTicker,
494
505
  status: "error",
495
506
  calls: retainRows && current?.ticker === lookupTicker ? current.calls : [],
496
507
  fetchedAt: retainRows && current?.ticker === lookupTicker ? current.fetchedAt : undefined,
497
508
  stale: retainRows && current?.ticker === lookupTicker && current.calls.length > 0,
509
+ savedAt: retainRows && current?.ticker === lookupTicker ? current.savedAt : undefined,
498
510
  refreshError: error instanceof Error ? error.message : String(error),
499
511
  refreshRequest,
500
512
  }));
@@ -864,15 +876,14 @@ function CallsListView({
864
876
  }
865
877
  }
866
878
  const visibleLookup = lookup && (lookup.ticker === lookupTicker || (detailOpen && selected?.ticker === lookup.ticker)) ? lookup : null;
867
- if (visibleLookup?.stale) {
868
- info.push({ id: "lookup-stale", parts: [{ text: `${visibleLookup.ticker} stale cache`, tone: "warning" }] });
869
- }
870
879
  if (visibleLookup?.refreshError && lookupState !== "error") {
871
880
  info.push({ id: "lookup-error", parts: [{ text: `${visibleLookup.ticker}: ${visibleLookup.refreshError}`, tone: "warning" }] });
872
881
  }
873
- if (stale) {
874
- info.push({ id: "stale", parts: [{ text: "stale cache", tone: "warning" }] });
875
- }
882
+ // What a failed refresh left on screen: a saved copy says when it was saved; a list
883
+ // held from an earlier session, still refreshing, is only stale.
884
+ const savedCopy = savedCopyFooterInfo([listSavedAt, visibleLookup?.savedAt, readerOpen ? selectedTranscript?.savedAt : null]);
885
+ info.push(...savedCopy);
886
+ if (stale && listSavedAt == null) info.push(STALE_SEGMENT);
876
887
  if (listError) {
877
888
  info.push({ id: "error", parts: [{ text: listError.message, tone: "warning" }] });
878
889
  }
@@ -911,6 +922,8 @@ function CallsListView({
911
922
  lookupTicker,
912
923
  lookup,
913
924
  stale,
925
+ listSavedAt,
926
+ selectedTranscript,
914
927
  listError,
915
928
  proRequired,
916
929
  transcriptProRequired,
@@ -4,8 +4,8 @@ import { DEFAULT_GRAPH_OPTIONS, type GraphOptions, type GraphPayload } from "../
4
4
  import { errorMessage } from "../../../utils/errors";
5
5
  import { addDays, newYorkToday } from "../earnings/board-model";
6
6
  import { loadEarningsBoard } from "../earnings/client";
7
- import { loadSupplyChain } from "../supply-chain/client";
8
- import { cachedGraph, loadGraph } from "../supply-chain/graph-client";
7
+ import { loadSupplyChain, loadSupplyChainsAhead } from "../supply-chain/client";
8
+ import { cachedGraph, loadGraph, loadGraphsAhead } from "../supply-chain/graph-client";
9
9
  import { projectRipple, projectSecondHop, rippleCompanyTickers, RIPPLE_DAYS, secondHopLinks, type RippleRow, type SecondHopRow } from "./model";
10
10
 
11
11
  export interface RippleSources {
@@ -13,6 +13,10 @@ export interface RippleSources {
13
13
  calendar(query: EarningsCalendarQuery): Promise<EarningsCalendarPayload>;
14
14
  /** SPLC's graph, which returns both hops around a holding in one call. Absent when the account sees one hop only. */
15
15
  graph?(symbol: string, options: GraphOptions): Promise<GraphPayload>;
16
+ /** Reads every holding's disclosures ahead in batch requests, for `supplyChain` to find. */
17
+ supplyChainsAhead?(symbols: readonly string[]): Promise<void>;
18
+ /** Reads these holdings' graphs ahead in batch requests, for `graph` to find. */
19
+ graphsAhead?(symbols: readonly string[], options: GraphOptions): Promise<void>;
16
20
  }
17
21
 
18
22
  /**
@@ -60,14 +64,29 @@ export interface RippleSnapshot {
60
64
  */
61
65
  export function cachedRippleSources(accessKey: string, force = false, graph: "off" | "load" | "cached" = "off"): RippleSources & { staleFlags: boolean[] } {
62
66
  const staleFlags: boolean[] = [];
67
+ // A forced read takes what this refresh read ahead as fresh.
68
+ const chainsAhead = new Set<string>();
69
+ const graphsAhead = new Set<string>();
63
70
  return {
64
71
  staleFlags,
65
- supplyChain: async (symbol) => { const result = await loadSupplyChain(symbol, accessKey, force); staleFlags.push(result.stale); return result.payload; },
72
+ supplyChainsAhead: async (symbols) => {
73
+ for (const symbol of await loadSupplyChainsAhead(symbols, accessKey, force)) chainsAhead.add(symbol);
74
+ },
75
+ supplyChain: async (symbol) => {
76
+ const result = await loadSupplyChain(symbol, accessKey, force && !chainsAhead.has(symbol.toUpperCase()));
77
+ staleFlags.push(result.stale);
78
+ return result.payload;
79
+ },
66
80
  calendar: async (query) => { const result = await loadEarningsBoard(query, force); staleFlags.push(result.stale); return result.payload; },
67
81
  // Cached by the full query, apart from SPLC Graph's default one.
68
82
  ...(graph !== "off" ? { graph: async (symbol: string, options: GraphOptions) => {
69
83
  const cached = graph === "cached" ? cachedGraph(symbol, "", options, accessKey) : null;
70
- const result = cached ?? await loadGraph(symbol, "", options, accessKey, force); staleFlags.push(result.stale); return result.payload;
84
+ const result = cached ?? await loadGraph(symbol, "", options, accessKey, force && !graphsAhead.has(symbol.toUpperCase()));
85
+ staleFlags.push(result.stale);
86
+ return result.payload;
87
+ } } : {}),
88
+ ...(graph === "load" ? { graphsAhead: async (symbols: readonly string[], options: GraphOptions) => {
89
+ for (const symbol of await loadGraphsAhead(symbols, options, accessKey, force)) graphsAhead.add(symbol);
71
90
  } } : {}),
72
91
  };
73
92
  }
@@ -84,8 +103,12 @@ async function eachLimited(symbols: readonly string[], load: (symbol: string) =>
84
103
  const locked = () => ({ graphs: new Map<string, GraphPayload>(), failures: [] as Failure[], locked: true });
85
104
  const isPreview = (graphs: ReadonlyMap<string, GraphPayload>) => [...graphs.values()].some((graph) => graph.access === "preview");
86
105
 
87
- /** One graph call per holding. The first answer says whether the account sees two hops; a one-hop preview skips the rest. */
88
- async function loadGraphs(holdings: readonly string[], graph: NonNullable<RippleSources["graph"]>) {
106
+ /**
107
+ * One graph per holding. The first answer says whether the account sees two
108
+ * hops; a one-hop preview skips the rest, which are otherwise read ahead in
109
+ * batch requests.
110
+ */
111
+ async function loadGraphs(holdings: readonly string[], graph: NonNullable<RippleSources["graph"]>, ahead?: RippleSources["graphsAhead"]) {
89
112
  const graphs = new Map<string, GraphPayload>();
90
113
  const failures: Failure[] = [];
91
114
  const load = async (symbol: string) => {
@@ -95,6 +118,7 @@ async function loadGraphs(holdings: readonly string[], graph: NonNullable<Ripple
95
118
  const [first, ...rest] = holdings;
96
119
  if (first) await load(first);
97
120
  if (isPreview(graphs)) return locked();
121
+ await ahead?.(rest, RIPPLE_GRAPH_OPTIONS);
98
122
  await eachLimited(rest, load);
99
123
  // Only when the first holding failed can a preview get this far.
100
124
  if (isPreview(graphs)) return locked();
@@ -107,11 +131,12 @@ export async function loadRipple(holdings: readonly string[], sources: RippleSou
107
131
  const symbols = [...new Set(holdings.map((symbol) => symbol.toUpperCase()))];
108
132
  const chains = new Map<string, SupplyChainPayload>();
109
133
  const failures: Failure[] = [];
134
+ await sources.supplyChainsAhead?.(symbols);
110
135
  await eachLimited(symbols, async (symbol) => {
111
136
  try { chains.set(symbol, await sources.supplyChain(symbol)); }
112
137
  catch (error) { failures.push({ symbol, error: errorMessage(error) }); }
113
138
  });
114
- const second = !secondHop ? null : sources.graph ? await loadGraphs(symbols, sources.graph) : locked();
139
+ const second = !secondHop ? null : sources.graph ? await loadGraphs(symbols, sources.graph, sources.graphsAhead) : locked();
115
140
  const graphs = second?.graphs ?? new Map<string, GraphPayload>();
116
141
  const companies = [...rippleCompanyTickers(chains), ...secondHopLinks(chains, graphs).map((link) => link.company.ticker!.toUpperCase())];
117
142
  const reports: EarningsCalendarPayload["reports"] = [];
@@ -1,7 +1,6 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { formatTickerListInput, parseTickerListInput } from "../../../tickers/list";
3
3
  import { earningsRippleHeadless } from "./headless";
4
- import { EarningsRipplePane } from "./pane";
5
4
 
6
5
  function namedSymbols(arg: string | undefined): string[] {
7
6
  try { return arg?.trim() ? parseTickerListInput(arg) : []; } catch { return []; }
@@ -9,7 +8,7 @@ function namedSymbols(arg: string | undefined): string[] {
9
8
 
10
9
  export const earningsRippleModule: PluginModule = {
11
10
  panes: [{
12
- id: "earnings-ripple", name: "Earnings Ripple", icon: "E", component: EarningsRipplePane,
11
+ id: "earnings-ripple", name: "Earnings Ripple", icon: "E", get component(): typeof import("./pane").EarningsRipplePane { const { EarningsRipplePane } = require("./pane"); return EarningsRipplePane; },
13
12
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 20 },
14
13
  tableExport: true,
15
14
  // Without named tickers it lists what is in the person's portfolios.
@@ -256,17 +256,40 @@ export function econEventName(event: Pick<EconEvent, "country" | "event" | "actu
256
256
  /** Letters and digits only, lower case: "Non-Farm" and "nonfarm" read alike. */
257
257
  const compactName = (text: string) => text.toLowerCase().replace(/[^a-z0-9]+/g, "");
258
258
 
259
+ /** Words an initialism leaves out: the U.S. Department of Agriculture is USDA. */
260
+ const INITIALISM_SKIPS = new Set(["of", "the", "and", "for", "on", "in"]);
261
+
262
+ /**
263
+ * The ways a row's publisher is written, compacted: its name, its initials
264
+ * ("usda" for the U.S. Department of Agriculture, "bls" for the Bureau of
265
+ * Labor Statistics) and the name of its site (usda.gov).
266
+ */
267
+ function publisherNames(source: EconEvent["source"]): string[] {
268
+ if (!source?.name?.trim()) return [];
269
+ const initials = source.name.split(/[\s.,&/()-]+/).filter((word) => word && !INITIALISM_SKIPS.has(word.toLowerCase()))
270
+ .map((word) => word[0]!).join("");
271
+ let site = "";
272
+ try {
273
+ site = source.url ? new URL(source.url).hostname.replace(/^www\./i, "").split(".")[0] ?? "" : "";
274
+ } catch {
275
+ // Intentional ignore: a publisher's address that does not parse leaves its name and initials.
276
+ }
277
+ return [source.name, initials, site].map(compactName).filter(Boolean);
278
+ }
279
+
259
280
  /**
260
- * Whether a row's name carries every word of `query`, case, spaces and
261
- * punctuation aside: `nonfarm` finds Non Farm Payrolls and Non-Farm
262
- * Employment Change, `fed funds` the Federal Funds Rate. The name shown and the
263
- * feed's own are both read.
281
+ * Whether a row carries every word of `query` in its name or its publisher's,
282
+ * case, spaces and punctuation aside: `nonfarm` finds Non Farm Payrolls and
283
+ * Non-Farm Employment Change, `fed funds` the Federal Funds Rate, and `usda`
284
+ * every row the U.S. Department of Agriculture publishes. The name shown and
285
+ * the feed's own are both read.
264
286
  */
265
- export function matchesEventSearch(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior">, query: string): boolean {
287
+ export function matchesEventSearch(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior" | "source">, query: string): boolean {
266
288
  const words = query.split(/\s+/).map(compactName).filter(Boolean);
267
289
  if (words.length === 0) return true;
268
290
  const names = [event.event, econEventName(event)].map(compactName);
269
- return names.some((name) => words.every((word) => name.includes(word)));
291
+ const publisher = publisherNames(event.source);
292
+ return names.some((name) => words.every((word) => name.includes(word) || publisher.some((text) => text.includes(word))));
270
293
  }
271
294
 
272
295
  /**
@@ -18,6 +18,8 @@ import {
18
18
  type StatItem,
19
19
  } from "../../../components";
20
20
  import { spanDigits } from "../../../components/chart-table";
21
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
22
+ import { isPermanentClientError } from "../../../api-client/errors";
21
23
  import { getTableWidth, hasMeaningfulTableHorizontalOverflow } from "../../../components/ui/table-layout";
22
24
  import type { CompositeAxisDomain } from "../../../components/chart/composite/types";
23
25
  import {
@@ -123,6 +125,8 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
123
125
  const [loading, setLoading] = useState(false);
124
126
  const [error, setError] = useState<string | null>(null);
125
127
  const [freshnessWarning, setFreshnessWarning] = useState<string | null>(null);
128
+ // The history's refresh failed and the copy saved at this time (ms) answered instead.
129
+ const [savedAt, setSavedAt] = useState<number | null>(null);
126
130
  const [data, setData] = useState<FredSeriesData | null>(null);
127
131
  // Null follows the latest period.
128
132
  const [selectedDate, setSelectedDate] = useState<string | null>(null);
@@ -146,6 +150,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
146
150
  setLoading(false);
147
151
  setError(null);
148
152
  setFreshnessWarning(null);
153
+ setSavedAt(null);
149
154
  return;
150
155
  }
151
156
 
@@ -156,6 +161,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
156
161
  setLoading(false);
157
162
  setError(null);
158
163
  setFreshnessWarning(null);
164
+ setSavedAt(null);
159
165
  return;
160
166
  }
161
167
  } else {
@@ -165,6 +171,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
165
171
  setLoading(true);
166
172
  setError(null);
167
173
  setFreshnessWarning(null);
174
+ setSavedAt(null);
168
175
 
169
176
  loadCachedFredSeries(
170
177
  request,
@@ -174,12 +181,18 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
174
181
  }),
175
182
  )
176
183
  .then((entry) => {
184
+ // A refusal or a reply that fails validation is thrown by the load, never covered by a saved copy.
185
+ const saved = entry.source === "stale-fallback";
177
186
  setData(entry.data);
178
- setFreshnessWarning(entry.stale
179
- ? `Cached ${new Date(entry.fetchedAt).toISOString().slice(0, 10)} · refresh failed`
187
+ // A copy a failed refresh left says when it was saved, in the footer as every pane does;
188
+ // a series its source has not updated since is a note.
189
+ setSavedAt(saved ? entry.fetchedAt : null);
190
+ setFreshnessWarning(entry.stale && !saved
191
+ ? `Source not updated since ${new Date(entry.fetchedAt).toISOString().slice(0, 10)}.`
180
192
  : null);
181
193
  })
182
194
  .catch((err) => {
195
+ if (isPermanentClientError(err)) setData(null);
183
196
  setError(err instanceof Error ? err.message : String(err));
184
197
  })
185
198
  .finally(() => {
@@ -199,6 +212,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
199
212
  // which asks which one when there are several.
200
213
  const openTickerChoice = useOpenTickerChoice();
201
214
  const relatedTickers = useMemo(() => mapping?.relatedTickers ?? [], [mapping]);
215
+ usePaneFooter("econ-detail:saved", () => ({ info: savedCopyFooterInfo([savedAt]) }), [savedAt]);
202
216
  usePaneFooter("econ-detail:related", () => relatedTickers.length > 0 ? {
203
217
  hints: [{
204
218
  id: "related",
@@ -2,7 +2,6 @@ import type { PluginModule } from "../plugin-module";
2
2
  import { statsCache } from "./cache";
3
3
  import { STAT_CATEGORIES } from "./defs";
4
4
  import { econStatisticsHeadless } from "./headless";
5
- import { EconStatisticsPane } from "./pane";
6
5
  import { resolveStatArg, STATS, DEFAULT_STAT_ID } from "./stats";
7
6
 
8
7
 
@@ -13,7 +12,7 @@ export const econStatisticsModule: PluginModule = {
13
12
  id: ECON_STATISTICS_PANE_ID,
14
13
  name: "Economic Statistics",
15
14
  icon: "E",
16
- component: EconStatisticsPane,
15
+ get component(): typeof import("./pane").EconStatisticsPane { const { EconStatisticsPane } = require("./pane"); return EconStatisticsPane; },
17
16
  defaultPosition: "right",
18
17
  defaultMode: "floating",
19
18
  defaultFloatingSize: { width: 120, height: 36 },
@@ -21,7 +21,7 @@ const savedPath = (id: string) => `saved/${encodeURIComponent(id)}`;
21
21
  export const screenerApi = {
22
22
  query: (query: ScreenQuery, signal?: AbortSignal, client: ScreenerClient = apiClient) =>
23
23
  client.equityScreener<ScreenPayload>("query", json("POST", query, { signal })),
24
- fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis,volatility,dividends,technical,option-volatility,ownership", { signal }),
24
+ fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis,volatility,dividends,technical,option-volatility,ownership,quality", { signal }),
25
25
  export: (definition: ScreenDefinition, snapshotId: string) =>
26
26
  apiClient.equityScreener<ScreenExportResponse>("export", json("POST", { ...definition, snapshotId }, { headers: { Accept: "application/json" } })),
27
27
  saved: async (signal?: AbortSignal, client: ScreenerClient = apiClient) =>
@@ -251,6 +251,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
251
251
  "IV percentile is the share of the past 52 weeks' options closes with IV30 below the latest; IV30 changes are in volatility points (25% to 27% is +2). IV fields with no close in five days are unavailable. HV20 is 20-session realized volatility, annualized, and HV rank places it between its 52-week low and high.",
252
252
  "Technical fields (RSI, moving averages, MACD and golden crosses, new highs and lows, Bollinger width) read US listings' completed daily sessions: they describe the last close, not the session in progress. A days-since field counts trading days, 0 at the last session.",
253
253
  "Shares outstanding, float, short % of float and institutional ownership cover US filers: the 10-K or 10-Q cover count, the stored statistics float, the FINRA settlement, the 13F quarter. The source does not say when it measured the float, so float and short % of float are partial, dated by when the statistics were retrieved (within 30 days) and never above the dated shares outstanding. A stale or inconsistent input leaves a field unavailable with its reason. Institutional ownership is over the balance-sheet count at the 13F quarter end; over 100% is partial with its reason.",
254
+ "ROIC, FCF yield, net debt / EBITDA and debt / equity read FA's latest annual column: the trailing four quarters, or the fiscal year when those quarters are it. Revenue and EPS CAGRs run from the latest fiscal year back 3 or 5 years, both years from the stored statements or both from the SEC filings (US filers), never one of each. Forward growth is the consensus EE shows for the fiscal year in progress. N/M where FA prints it: EBITDA, equity, invested capital or a base year at or below zero.",
254
255
  ],
255
256
  },
256
257
  async load(args, ctx) {
@@ -23,7 +23,10 @@ const idea = (
23
23
  definition: { version: 1, currency: "USD", criteria: [...criteria, TRADED], sort },
24
24
  });
25
25
 
26
- /** Technical ideas, read at the last completed session. */
26
+ /**
27
+ * Technical ideas, read at the last completed session, then fundamental ones, read on `FA`'s bases (the trailing four
28
+ * quarters, or the latest fiscal year when those quarters are that year) and `EE`'s consensus.
29
+ */
27
30
  export const SCREEN_IDEAS: ScreenIdea[] = [
28
31
  idea("oversold-uptrend", "Oversold in an uptrend", [
29
32
  { field: "rsi14", op: "lte", value: 30 },
@@ -43,6 +46,32 @@ export const SCREEN_IDEAS: ScreenIdea[] = [
43
46
  { field: "macdBullishCrossDays", op: "lte", value: 3 },
44
47
  { field: "priceVsSma200Percent", op: "gt", value: 0 },
45
48
  ], { field: "macdBullishCrossDays", direction: "asc" }),
49
+ // A free cash flow yield of 6% or more, about twice the S&P 500's; EV / EBITDA of 10x or less, under the index's
50
+ // long-run 12x or so; net debt within 3x EBITDA, about where investment grade ends, so a cheap price is not a
51
+ // stretched balance sheet. Highest yield first.
52
+ idea("value", "Value", [
53
+ { field: "fcfYieldPercent", op: "gte", value: 6 },
54
+ { field: "evToEbitda", op: "lte", value: 10 },
55
+ { field: "netDebtToEbitda", op: "lte", value: 3 },
56
+ ], { field: "fcfYieldPercent", direction: "desc" }),
57
+ // A return on invested capital of 15% or more, above any company's cost of capital; net debt within 2x EBITDA;
58
+ // revenue compounding at 5% a year or more over five years; free cash flow above zero. Highest return first.
59
+ idea("quality", "Quality", [
60
+ { field: "roicPercent", op: "gte", value: 15 },
61
+ { field: "netDebtToEbitda", op: "lte", value: 2 },
62
+ { field: "revenueCagr5YPercent", op: "gte", value: 5 },
63
+ { field: "fcfYieldPercent", op: "gt", value: 0 },
64
+ ], { field: "roicPercent", direction: "desc" }),
65
+ // Growth at a reasonable price: consensus EPS growth of 10% to 25% for the fiscal year in progress (Peter Lynch's
66
+ // range; faster rarely lasts) on a three-year record of 10% a year or more; a forward P/E of 20x or less, so the
67
+ // price stays within twice the growth; ROIC of 10% or more, so the growth is earned, not a recovery from a loss.
68
+ // Fastest growth first.
69
+ idea("garp", "GARP", [
70
+ { field: "forwardEpsGrowthPercent", op: "between", value: [10, 25] },
71
+ { field: "epsCagr3YPercent", op: "gte", value: 10 },
72
+ { field: "forwardPE", op: "lte", value: 20 },
73
+ { field: "roicPercent", op: "gte", value: 10 },
74
+ ], { field: "forwardEpsGrowthPercent", direction: "desc" }),
46
75
  ];
47
76
 
48
77
  export const screenIdea = (id: string | undefined): ScreenIdea | undefined =>
@@ -1,6 +1,5 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
2
  import { equityScreenerHeadless } from "./headless";
3
- import { EquityScreenerPane } from "./pane";
4
3
 
5
4
  export const equityScreenerModule: PluginModule = {
6
5
  panes: [
@@ -8,7 +7,7 @@ export const equityScreenerModule: PluginModule = {
8
7
  id: "equity-screener",
9
8
  name: "Equity Screener",
10
9
  icon: "S",
11
- component: EquityScreenerPane,
10
+ get component(): typeof import("./pane").EquityScreenerPane { const { EquityScreenerPane } = require("./pane"); return EquityScreenerPane; },
12
11
  defaultPosition: "right",
13
12
  defaultMode: "floating",
14
13
  defaultFloatingSize: { width: 108, height: 30 },
@@ -6,6 +6,7 @@ import {
6
6
  NUMERIC_FIELDS,
7
7
  OPTION_VOLATILITY_FIELDS,
8
8
  OWNERSHIP_FIELDS,
9
+ QUALITY_FIELDS,
9
10
  RESEARCH_FIELDS,
10
11
  KPI_FIELDS,
11
12
  SOCIAL_FIELDS,
@@ -126,6 +127,16 @@ export const SHORT_LABELS: Record<NumericField, string> = {
126
127
  floatShares: "FLOAT",
127
128
  shortPercentOfFloat: "SI % FLOAT",
128
129
  institutionalOwnershipPercent: "INST OWN%",
130
+ roicPercent: "ROIC%",
131
+ fcfYieldPercent: "FCF YLD%",
132
+ netDebtToEbitda: "ND/EBITDA",
133
+ debtToEquity: "D/E",
134
+ revenueCagr3YPercent: "REV CAGR 3Y%",
135
+ revenueCagr5YPercent: "REV CAGR 5Y%",
136
+ epsCagr3YPercent: "EPS CAGR 3Y%",
137
+ epsCagr5YPercent: "EPS CAGR 5Y%",
138
+ forwardRevenueGrowthPercent: "FWD REV GR%",
139
+ forwardEpsGrowthPercent: "FWD EPS GR%",
129
140
  hiringOpenRoles: "OPEN ROLES",
130
141
  hiringNetAddsWeek: "NET ROLES/WK",
131
142
  hiringRemotePercent: "REMOTE%",
@@ -138,7 +149,8 @@ export const SHORT_LABELS: Record<NumericField, string> = {
138
149
  const COMPACT = new Set<NumericField>(["kpiArr", "kpiRpo", "kpiCrpo", "kpiSubscribers", "kpiDau", "kpiMau", "kpiGmv", "kpiStores", "kpiBacklog", "marketCap", "volume", "averageVolume20d", "shortInterestShares", "sharesOutstanding", "floatShares"]);
139
150
  /** Daily counts: whole numbers below a thousand, compact above. */
140
151
  const DAILY_COUNTS = new Set<NumericField>(["xPostsPerDay", "wikiViewsPerDay"]);
141
- const RATIOS = new Set<NumericField>(["kpiBookToBill", "xPostsVsMedian", "wikiViewsVsMedian", "priceToBook", "evToEbitda", "ivToHv"]);
152
+ const RATIOS = new Set<NumericField>(["kpiBookToBill", "xPostsVsMedian", "wikiViewsVsMedian", "priceToBook", "evToEbitda", "ivToHv",
153
+ "netDebtToEbitda", "debtToEquity"]);
142
154
  const RETURNS: NumericField[] = ["return1WPercent", "return1MPercent", "return3MPercent", "returnYtdPercent", "return1YPercent"];
143
155
  const SIGNED = new Set<NumericField>(["hiringNetAddsWeek", "hiringZScore", "appRankVelocity7d", "appRatingDrift7d", "appRatingCountGrowth7d",
144
156
  "guideRevenueChangePercent", "guideEpsChangePercent", "kpiSameStoreSalesPercent",
@@ -147,6 +159,8 @@ const SIGNED = new Set<NumericField>(["hiringNetAddsWeek", "hiringZScore", "appR
147
159
  ...RETURNS, "fromHigh52WPercent", "epsRevision30dPercent", "analystUpsidePercent",
148
160
  "priceVsSma20Percent", "priceVsSma50Percent", "priceVsSma200Percent",
149
161
  "iv30Change1d", "iv30Change5d",
162
+ "revenueCagr3YPercent", "revenueCagr5YPercent", "epsCagr3YPercent", "epsCagr5YPercent",
163
+ "forwardRevenueGrowthPercent", "forwardEpsGrowthPercent",
150
164
  ]);
151
165
  /** Moves whose sign is the news: they take the sign colour. */
152
166
  export const SIGN_COLORED = new Set<NumericField>(["changePercent", ...RETURNS, "epsRevision30dPercent", "analystUpsidePercent",
@@ -369,6 +383,16 @@ export const screenLabel = (field: string) =>
369
383
  floatShares: "Float",
370
384
  shortPercentOfFloat: "Short interest % of float",
371
385
  institutionalOwnershipPercent: "Institutional ownership % (13F)",
386
+ roicPercent: "ROIC %",
387
+ fcfYieldPercent: "FCF yield %",
388
+ netDebtToEbitda: "Net debt / EBITDA",
389
+ debtToEquity: "Debt / equity",
390
+ revenueCagr3YPercent: "Revenue CAGR 3Y %",
391
+ revenueCagr5YPercent: "Revenue CAGR 5Y %",
392
+ epsCagr3YPercent: "EPS CAGR 3Y %",
393
+ epsCagr5YPercent: "EPS CAGR 5Y %",
394
+ forwardRevenueGrowthPercent: "Forward revenue growth %",
395
+ forwardEpsGrowthPercent: "Forward EPS growth %",
372
396
  hiringOpenRoles: "Hiring open roles",
373
397
  hiringNetAddsWeek: "Net open roles per week",
374
398
  hiringRemotePercent: "Remote roles %",
@@ -682,9 +706,9 @@ export function validateScreenPayload(value: unknown): ScreenPayload {
682
706
  throw new Error("Invalid screener listing identity.");
683
707
  ids.add(screenRowId(row));
684
708
  for (const field of NUMERIC_FIELDS) {
685
- // A server from before social, research, volatility, dividend, technical, option volatility or ownership fields
686
- // omits them; they read as unavailable. A Pro field it left out was held back from this caller.
687
- if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS, ...VOLATILITY_FIELDS, ...TECHNICAL_FIELDS, ...OPTION_VOLATILITY_FIELDS, ...OWNERSHIP_FIELDS, ...ATTENTION_FIELDS, ...HIRING_APP_FIELDS, ...KPI_FIELDS, ...DIVIDEND_FIELDS] as readonly string[]).includes(field))
709
+ // A server from before social, research, volatility, dividend, technical, option volatility, ownership or quality
710
+ // fields omits them; they read as unavailable. A Pro field it left out was held back from this caller.
711
+ if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS, ...VOLATILITY_FIELDS, ...TECHNICAL_FIELDS, ...OPTION_VOLATILITY_FIELDS, ...OWNERSHIP_FIELDS, ...QUALITY_FIELDS, ...ATTENTION_FIELDS, ...HIRING_APP_FIELDS, ...KPI_FIELDS, ...DIVIDEND_FIELDS] as readonly string[]).includes(field))
688
712
  row.metrics[field] = PRO_FIELDS.has(field) ? withheldMetric() : unavailableMetric();
689
713
  const metric = row.metrics[field];
690
714
  if (
@@ -52,6 +52,7 @@ import { useResearchCloudSession } from "../shared/research-cloud-session";
52
52
  import { INITIAL_STREAM_RANGE, streamWindowRows } from "../shared/use-quote-board";
53
53
  import { useSampledValue } from "../../../state/hooks/live-ticker-financials";
54
54
  import { usePlanAccess } from "../../../api-client/plan-access";
55
+ import { useQuoteDelayFooter } from "../shared/cloud-upgrade";
55
56
  import { SignInWall } from "../cloud/auth-actions";
56
57
  import { CriterionEditor } from "./criterion-editor";
57
58
  import { SCREEN_IDEAS, screenIdea } from "./ideas";
@@ -584,6 +585,8 @@ function EquityScreenView({
584
585
  disabled: !selected,
585
586
  },
586
587
  ];
588
+ // Signed out or free, the quotes streamed over the snapshot's rows are delayed ones.
589
+ const quoteDelay = useQuoteDelayFooter(Array.from(liveEntries.values(), (entry) => entry.data ?? entry.lastGoodData), "screener-delay");
587
590
  usePaneFooter(
588
591
  "equity-screener",
589
592
  () => check ? null : ({
@@ -632,10 +635,12 @@ function EquityScreenView({
632
635
  },
633
636
  ]
634
637
  : []),
638
+ ...(quoteDelay ? [quoteDelay] : []),
635
639
  ],
636
640
  hints,
637
641
  }),
638
642
  [
643
+ quoteDelay,
639
644
  results.loading,
640
645
  results.loadingMore,
641
646
  data,
@@ -75,12 +75,34 @@ export function screenCheckSummary(data: ScreenCheckPayload): string {
75
75
  return data.matches === null ? "Not covered" : data.matches ? "Passes all criteria" : "Does not match";
76
76
  }
77
77
 
78
+ /** The thresholds a criterion compares a value with: one, or a range's two ends. */
79
+ function thresholds(criterion: ScreenCriterion): number[] {
80
+ if (!("value" in criterion)) return [];
81
+ const value = criterion.value as unknown;
82
+ return (Array.isArray(value) ? value : [value]).filter((entry): entry is number => typeof entry === "number");
83
+ }
84
+
85
+ /**
86
+ * A stored value as DVD writes its figures: two decimals (6.93%, 26.42 x), a
87
+ * whole number without them. A value that would round onto a threshold it is
88
+ * not (25.0001 against <= 25) keeps the decimals that tell it apart, so its
89
+ * Fail never reads as 25.00 failing <= 25.
90
+ */
91
+ function checkValueText(value: number, unit: string, criterion: ScreenCriterion): string {
92
+ const limits = thresholds(criterion);
93
+ const whole = Number.isInteger(value) && unit !== "%";
94
+ let digits = whole ? 0 : 2;
95
+ while (digits < 15 && limits.some((limit) => limit !== value && Number(value.toFixed(digits)) === limit)) digits += 1;
96
+ const text = value.toLocaleString("en-US", { minimumFractionDigits: whole ? 0 : 2, maximumFractionDigits: digits });
97
+ return unit === "%" ? `${text}%` : `${text}${unit ? ` ${unit}` : ""}`;
98
+ }
99
+
78
100
  export function checkRows(data: ScreenCheckPayload) {
79
101
  return data.criteria.map((entry, index) => ({
80
102
  id: String(index),
81
103
  criterion: checkCriterionText(entry.criterion),
82
104
  value: entry.state === "not-covered" ? "Not covered" : entry.value === null ? "Missing" : typeof entry.value === "number"
83
- ? `${number(entry.value)}${entry.unit ? ` ${entry.unit}` : ""}` : entry.value,
105
+ ? checkValueText(entry.value, entry.unit, entry.criterion) : entry.value,
84
106
  result: entry.passes === null ? "Not evaluated" : entry.passes ? "Pass" : "Fail",
85
107
  asOf: entry.asOf?.slice(0, 10) ?? (entry.observedAt ? `${entry.observedAt.slice(0, 10)} (collected)` : "--"),
86
108
  state: entry.state,
@@ -3,7 +3,6 @@ import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
3
3
  import { estimateRevisionsCache } from "./client";
4
4
  import { estimateRevisionsHeadless } from "./headless";
5
5
  import { estimateRevisionsLockNote } from "./lock-note";
6
- import { EstimateRevisionsPane } from "./pane";
7
6
  export const estimateRevisionsModule: PluginModule = {
8
7
  panes: [
9
8
  {
@@ -12,7 +11,7 @@ export const estimateRevisionsModule: PluginModule = {
12
11
  reportRequirements: ["Free Gloom sign-in"],
13
12
  name: "Estimate Revisions",
14
13
  icon: "E",
15
- component: EstimateRevisionsPane,
14
+ get component(): typeof import("./pane").EstimateRevisionsPane { const { EstimateRevisionsPane } = require("./pane"); return EstimateRevisionsPane; },
16
15
  defaultPosition: "right",
17
16
  tickerFollower: true,
18
17
  instruments: ["equity"],