@gloom-sh/terminal 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
@@ -15,6 +15,7 @@ import { isoDateDaysAgo, isoDateToday, isoDateYearsAgo } from "../../../utils/ca
15
15
  import { abortable, withDeadline } from "../../../utils/async-deadline";
16
16
  import { logFailure } from "../../../utils/log-failure";
17
17
  import {
18
+ amendmentKind,
18
19
  buildBrowserRows,
19
20
  filingGapFor,
20
21
  estimateFilingQuarterReturn,
@@ -33,6 +34,7 @@ import type {
33
34
  FundBrowserRow,
34
35
  FundDetailData,
35
36
  ThirteenFBrowserTab,
37
+ ThirteenFDetailLoading,
36
38
  ThirteenFFilingGap,
37
39
  ThirteenFFund,
38
40
  ThirteenFHoldingRecord,
@@ -106,6 +108,8 @@ export interface BrowserLoadResult {
106
108
  period?: string;
107
109
  quarter?: string;
108
110
  warning?: string;
111
+ /** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
112
+ savedAt?: number;
109
113
  hasMore?: boolean;
110
114
  nextOffset?: number;
111
115
  reportsLoaded?: Promise<void>;
@@ -118,11 +122,31 @@ export async function loadBrowserRows(
118
122
  options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { offset?: number; limit?: number } = {},
119
123
  ): Promise<BrowserLoadResult> {
120
124
  const warnings: string[] = [];
121
- const result = await loadBrowserPage(tab, query, signal, { ...options, onWarning: warning => warnings.push(warning) });
125
+ const page: { result?: BrowserLoadResult; savedAt?: number } = {};
126
+ const onSavedCopy = (savedAt: number) => {
127
+ page.savedAt = Math.min(page.savedAt ?? savedAt, savedAt);
128
+ // The rows' reports answer after the page does, and their copies date it too.
129
+ if (page.result) page.result.savedAt = page.savedAt;
130
+ options.onSavedCopy?.(savedAt);
131
+ };
132
+ const result = await loadBrowserPage(tab, query, signal, { ...options, onSavedCopy, onWarning: warning => warnings.push(warning) });
133
+ page.result = result;
134
+ if (page.savedAt != null) result.savedAt = page.savedAt;
122
135
  if (warnings.length) result.warning = [...new Set([result.warning, ...warnings].filter(Boolean))].join(" ") || undefined;
123
136
  return result;
124
137
  }
125
138
 
139
+ /** The oldest saved copy a load answered from (`savedAt`), each also passed on to `onSavedCopy`. */
140
+ function savedCopies(onSavedCopy?: (savedAt: number) => void) {
141
+ const copies: { savedAt?: number; note: (savedAt: number) => void } = {
142
+ note: (savedAt) => {
143
+ copies.savedAt = Math.min(copies.savedAt ?? savedAt, savedAt);
144
+ onSavedCopy?.(savedAt);
145
+ },
146
+ };
147
+ return copies;
148
+ }
149
+
126
150
  async function loadBrowserPage(
127
151
  tab: ThirteenFBrowserTab,
128
152
  query: string,
@@ -140,9 +164,12 @@ async function loadBrowserPage(
140
164
  const pageApiOptions = { ...apiOptions, offset };
141
165
 
142
166
  if (tab === "performance") {
143
- const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions);
144
167
  const quarters = priorQuarters(quarter);
145
- const history = await loadRankingHistory(quarters, signal, apiOptions);
168
+ // The prior quarters' rankings do not depend on this page, so they are read beside it.
169
+ const [topFunds, history] = await Promise.all([
170
+ listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions),
171
+ loadRankingHistory(quarters, signal, apiOptions),
172
+ ]);
146
173
  const funds: ThirteenFFund[] = topFunds.map((fund) => ({ cik: fund.cik, name: fund.name }));
147
174
  const rows = buildBrowserRows({ topFunds, source: "performance" }).map(row => ({ ...row, priorReturns: quarters.map((previous, index) => ({ quarter: previous, value: history[index]?.get(row.cik) ?? null })) }));
148
175
  const result: BrowserLoadResult = {
@@ -161,7 +188,10 @@ async function loadBrowserPage(
161
188
 
162
189
  if (tab === "latest") {
163
190
  const filings = await listThirteenFFilings(isoDateDaysAgo(21, now), to, latestLimit, signal, pageApiOptions);
164
- const selected = dedupeLatestForms(filings);
191
+ // An original filing or a restatement is its period's whole report, and an
192
+ // amendment of unknown kind shows no totals either way: only one that adds
193
+ // NEW HOLDINGS needs the fund's earlier filing for its quarter's totals.
194
+ const selected = dedupeLatestForms(filings).filter((form) => amendmentKind(form) === "new-holdings");
165
195
  const rows = buildBrowserRows({ latestFilings: filings, source: "latest" });
166
196
  const result: BrowserLoadResult = {
167
197
  rows,
@@ -440,7 +470,8 @@ export async function loadFundPerformance(
440
470
  options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { splitFactor?: SplitFactorLookup } = {},
441
471
  ): Promise<FundPerformanceResult> {
442
472
  const warnings: string[] = [];
443
- const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: options.onSavedCopy, onWarning: (warning: string) => warnings.push(warning) };
473
+ const saved = savedCopies(options.onSavedCopy);
474
+ const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
444
475
  const detail = await loadFundDetail(cik, fallbackName, signal, options);
445
476
  const period = detail.latestForm?.periodOfReport;
446
477
  const quarter = period ? periodToQuarter(period) : null;
@@ -482,7 +513,9 @@ export async function loadFundPerformance(
482
513
  : filed?.skipped.length ? `No share position held through the quarter to ${period} that could be priced across it.`
483
514
  : `No share position held through the quarter to ${period}.`;
484
515
  const warning = [...new Set([...(detail.warnings ?? []), ...warnings])].join(" ") || undefined;
516
+ const savedAt = Math.min(detail.savedAt ?? Infinity, saved.savedAt ?? Infinity);
485
517
  return {
518
+ ...(Number.isFinite(savedAt) ? { savedAt } : {}),
486
519
  rows: [row], ...(quarter ? { quarter } : {}), ...(period ? { period } : {}), hasMore: false, basis, ranking,
487
520
  ...(filed?.splits.length ? { splits: filed.splits } : {}),
488
521
  ...(filed?.skipped.length ? { skipped: filed.skipped } : {}),
@@ -491,16 +524,71 @@ export async function loadFundPerformance(
491
524
  };
492
525
  }
493
526
 
527
+ /** Rows a fund's two newest reports may hold together for their holdings to be read ahead. */
528
+ const PREFETCH_HOLDINGS_MAX = 2_000;
529
+
530
+ /**
531
+ * Reads ahead what opening a fund asks first, while the cursor rests on its
532
+ * row: its filing list, which every other request waits on, and the holdings
533
+ * of its two newest reports when they hold PREFETCH_HOLDINGS_MAX rows or fewer
534
+ * together. A large filer's holdings wait for Enter. These are loadFundDetail's
535
+ * own requests, so opening the fund reads them from cache or joins them.
536
+ */
537
+ export async function prefetchFundDetail(cik: string, signal: AbortSignal): Promise<void> {
538
+ const now = new Date();
539
+ const forms = await listThirteenFForms(cik, isoDateYearsAgo(4, now), isoDateToday(now), 100, signal);
540
+ const filings = buildPeriodReports(forms).slice(0, 2).flatMap((report) => report.filings);
541
+ const rows = filings.reduce((sum, form) => sum + (form.tableEntryTotal ?? Infinity), 0);
542
+ if (!filings.length || rows > PREFETCH_HOLDINGS_MAX) return;
543
+ await Promise.all(filings.map((form) => listThirteenFFormHoldings(cik, form.accessionNumber, signal, { entries: form.tableEntryTotal })));
544
+ }
545
+
546
+ /**
547
+ * One fund read ahead at a time (prefetchFundDetail): resting on another row
548
+ * stops the last one's requests, and `cancel` stops it when the list goes.
549
+ */
550
+ export function createFundPrefetcher(prefetch: (cik: string, signal: AbortSignal) => Promise<void> = prefetchFundDetail) {
551
+ let current: { cik: string; controller: AbortController } | null = null;
552
+ return {
553
+ prefetch(cik: string) {
554
+ if (current?.cik === cik) return;
555
+ current?.controller.abort();
556
+ const entry = { cik, controller: new AbortController() };
557
+ current = entry;
558
+ void prefetch(cik, entry.controller.signal).catch(() => {
559
+ // Intentional ignore: a fund read ahead that fails is read again, and the failure shown, when it opens.
560
+ }).finally(() => {
561
+ if (current === entry) current = null;
562
+ });
563
+ },
564
+ cancel() {
565
+ current?.controller.abort();
566
+ current = null;
567
+ },
568
+ };
569
+ }
570
+
571
+ /**
572
+ * A fund's latest report and the one before it, with their holdings. With
573
+ * `onProgress`, the fund as it fills in: the latest report's holdings as
574
+ * soon as they are read (a large filing's first rows before the rest), then
575
+ * the prior quarter they are compared with and EDGAR's filing list, each
576
+ * part still loading named in `loading`. The answer is the whole fund.
577
+ */
494
578
  export async function loadFundDetail(
495
579
  cik: string,
496
580
  fallbackName: string,
497
581
  signal?: AbortSignal,
498
582
  /** `filingGap: false` skips EDGAR's filing list, for a load that never shows it (an overlap). */
499
- options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { filingGap?: boolean } = {},
583
+ options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & {
584
+ filingGap?: boolean;
585
+ onProgress?: (detail: FundDetailData) => void;
586
+ } = {},
500
587
  ): Promise<FundDetailData> {
501
588
  const now = new Date();
502
589
  const warnings: string[] = [];
503
- const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: options.onSavedCopy, onWarning: (warning: string) => warnings.push(warning) };
590
+ const saved = savedCopies(options.onSavedCopy);
591
+ const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
504
592
 
505
593
  const forms = await listThirteenFForms(
506
594
  cik,
@@ -515,46 +603,87 @@ export async function loadFundDetail(
515
603
  const previousReport = reports[1];
516
604
  const latestForm = latestReport?.filings.at(-1) ?? null;
517
605
  const previousForm = previousReport?.filings.at(-1) ?? null;
518
- async function loadReport(report: ThirteenFPeriodReport | undefined): Promise<ThirteenFHoldingRecord[]> {
606
+ const checkGap = options.filingGap !== false;
607
+ const parts: Pick<FundDetailData, "latestHoldings" | "previousHoldings" | "filingGap"> & { loading: ThirteenFDetailLoading } = {
608
+ latestHoldings: [],
609
+ previousHoldings: [],
610
+ filingGap: null,
611
+ loading: { positions: true, ...(previousReport ? { comparison: true } : {}), ...(checkGap ? { filingGap: true } : {}) },
612
+ };
613
+ const detail = (): FundDetailData => {
614
+ const loading = Object.fromEntries(Object.entries(parts.loading).filter(([, value]) => value)) as ThirteenFDetailLoading;
615
+ return {
616
+ cik: normalizeCik(cik),
617
+ name: latestForm?.companyName || fallbackName,
618
+ forms,
619
+ latestForm,
620
+ previousForm,
621
+ latestHoldings: parts.latestHoldings,
622
+ previousHoldings: parts.previousHoldings,
623
+ latestReport,
624
+ previousReport,
625
+ warnings: [...warnings],
626
+ filingGap: parts.filingGap,
627
+ ...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}),
628
+ ...(Object.keys(loading).length ? { loading } : {}),
629
+ };
630
+ };
631
+ let failed = false;
632
+ // The fund is shown once its latest holdings, or a large filing's first rows, are in.
633
+ const progress = () => {
634
+ if (!options.onProgress || failed || signal?.aborted || (parts.loading.positions && !parts.latestHoldings.length)) return;
635
+ options.onProgress(detail());
636
+ };
637
+ async function loadReport(report: ThirteenFPeriodReport | undefined, onFirstRows?: (rows: ThirteenFHoldingRecord[]) => void): Promise<ThirteenFHoldingRecord[]> {
519
638
  if (!report) return [];
520
- const holdings: ThirteenFHoldingRecord[] = [];
521
- for (const form of report.filings) {
522
- const rows = await listThirteenFFormHoldings(cik, form.accessionNumber, signal, apiOptions);
523
- holdings.push(...rows);
639
+ // An amended period's filings are read at once: the original and each of its additions.
640
+ const filings = await Promise.all(report.filings.map((form, index) => listThirteenFFormHoldings(cik, form.accessionNumber, signal, {
641
+ ...apiOptions,
642
+ entries: form.tableEntryTotal,
643
+ ...(index === 0 && onFirstRows ? { onFirstRows } : {}),
644
+ })));
645
+ report.filings.forEach((form, index) => {
646
+ const rows = filings[index]!;
524
647
  if (form.tableEntryTotal != null && rows.length !== form.tableEntryTotal) {
525
648
  report.complete = false;
526
649
  warnings.push(`${report.periodOfReport}: loaded ${rows.length} of ${form.tableEntryTotal} disclosed entries for ${form.accessionNumber}.`);
527
650
  }
528
- }
651
+ });
529
652
  if (!report.complete && !warnings.some((warning) => warning.startsWith(report.periodOfReport))) {
530
653
  warnings.push(`${report.periodOfReport}: amendment type or original report unavailable; disclosed entries cannot be reconciled into a full report.`);
531
654
  }
532
- return holdings;
655
+ return filings.flat();
533
656
  }
534
- const [latestHoldings, previousHoldings, filingGap] = await Promise.all([
535
- loadReport(latestReport),
657
+ await Promise.all([
658
+ loadReport(latestReport, options.onProgress ? (rows) => {
659
+ if (!parts.loading.positions) return;
660
+ parts.latestHoldings = rows;
661
+ progress();
662
+ } : undefined).then((rows) => {
663
+ parts.latestHoldings = rows;
664
+ parts.loading.positions = false;
665
+ progress();
666
+ }),
536
667
  loadReport(previousReport).catch((error) => {
537
668
  if (signal?.aborted) throw error;
538
669
  if (previousReport) previousReport.complete = false;
539
670
  warnings.push(`${previousReport?.periodOfReport ?? "Prior quarter"}: holdings unavailable; comparison unavailable.`);
540
671
  return [];
672
+ }).then((rows) => {
673
+ parts.previousHoldings = rows;
674
+ parts.loading.comparison = false;
675
+ progress();
541
676
  }),
542
- options.filingGap === false ? null
543
- : loadFilingGap(cik, latestForm?.periodOfReport, signal, now, { forceRefresh: options.forceRefresh }),
544
- ]);
545
- return {
546
- cik: normalizeCik(cik),
547
- name: latestForm?.companyName || fallbackName,
548
- forms,
549
- latestForm,
550
- previousForm,
551
- latestHoldings,
552
- previousHoldings,
553
- latestReport,
554
- previousReport,
555
- warnings,
556
- filingGap,
557
- };
677
+ (checkGap ? loadFilingGap(cik, latestForm?.periodOfReport, signal, now, { forceRefresh: options.forceRefresh }) : Promise.resolve(null)).then((gap) => {
678
+ parts.filingGap = gap;
679
+ parts.loading.filingGap = false;
680
+ progress();
681
+ }),
682
+ ]).catch((error: unknown) => {
683
+ failed = true;
684
+ throw error;
685
+ });
686
+ return detail();
558
687
  }
559
688
 
560
689
  export async function loadFilingPositions(
@@ -562,12 +691,13 @@ export async function loadFilingPositions(
562
691
  accessionNumber: string,
563
692
  signal?: AbortSignal,
564
693
  options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
565
- ): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[] }> {
694
+ ): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[]; savedAt?: number }> {
566
695
  const warnings: string[] = [];
696
+ const saved = savedCopies();
567
697
  const result = await listThirteenFFormHoldingsPage(cik, accessionNumber, signal, {
568
- ...options, onWarning: warning => warnings.push(warning),
698
+ ...options, onSavedCopy: saved.note, onWarning: warning => warnings.push(warning),
569
699
  });
570
- return { ...result, warnings };
700
+ return { ...result, warnings, ...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}) };
571
701
  }
572
702
 
573
703
  /** `2026Q2` for a quarter end such as 2026-06-30. */
@@ -6,13 +6,11 @@ import {
6
6
  inferBrowserTabFromQuery,
7
7
  isCikQuery,
8
8
  } from "./model";
9
- import { ThirteenFPane } from "./pane";
10
9
  import {
11
10
  attachThirteenFApiPersistence,
12
11
  resetThirteenFApiPersistence,
13
12
  } from "./api";
14
13
  import { thirteenFHeadless } from "./headless";
15
- import { ThirteenFTickerPane } from "./signals-pane";
16
14
  import { isKnownNonUsListing } from "../../../utils/sec";
17
15
  import { isKnownMutualFund } from "../../../tickers/instrument-kind";
18
16
 
@@ -32,7 +30,7 @@ export const thirteenFModule: PluginModule = {
32
30
  id: "thirteenf",
33
31
  name: "13F",
34
32
  order: 39,
35
- component: ThirteenFTickerPane,
33
+ get component(): typeof import("./signals-pane").ThirteenFTickerPane { const { ThirteenFTickerPane } = require("./signals-pane"); return ThirteenFTickerPane; },
36
34
  instruments: ["equity", "fund"],
37
35
  isVisible: ({ ticker, financials }) => !isKnownNonUsListing(ticker) && !isKnownMutualFund(ticker, financials),
38
36
  });
@@ -47,7 +45,7 @@ export const thirteenFModule: PluginModule = {
47
45
  id: THIRTEENF_PANE_ID,
48
46
  name: "13F Funds",
49
47
  icon: "13F",
50
- component: ThirteenFPane,
48
+ get component(): typeof import("./pane").ThirteenFPane { const { ThirteenFPane } = require("./pane"); return ThirteenFPane; },
51
49
  defaultPosition: "right",
52
50
  defaultMode: "floating",
53
51
  defaultFloatingSize: { width: 112, height: 36 },
@@ -547,6 +547,8 @@ export function estimateFilingQuarterReturn(data: FundDetailData, splits: SplitF
547
547
 
548
548
  export function hasComparable13FQuarter(data: FundDetailData): boolean {
549
549
  if (data.latestReport?.complete === false || data.previousReport?.complete === false) return false;
550
+ // Until both quarters are in whole, a line one of them lacks is not a buy or a sale.
551
+ if (data.loading?.positions || data.loading?.comparison) return false;
550
552
  const current = data.latestForm?.periodOfReport;
551
553
  const previous = data.previousForm?.periodOfReport;
552
554
  return adjacentQuarterPeriods(current, previous);
@@ -10,6 +10,7 @@ import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
10
10
  import { useMineTickers } from "../shared/mine-tickers";
11
11
  import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
12
12
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
13
+ import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
13
14
  import { useAsyncResource } from "../../../react/async-resource";
14
15
  import { cachedFunds, cachedMembers, loadFunds, loadMembers } from "../members/client";
15
16
  import { coveredFund } from "../members/model";
@@ -115,7 +116,10 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
115
116
  const period = peer ? overlapPeriod(data, peer) : null;
116
117
  const mismatch = peer && !period;
117
118
  const listError = etf ? etfFile.error : error ?? paged.error?.message ?? paged.moreError?.message ?? null;
118
- usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading || (!!etf && etfFile.loading), error: listError, hints: open ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
119
+ // Either side answered from a copy a failed refresh left says when it was saved.
120
+ const etfSavedAt = etf ? etfFile.data?.savedAt : null;
121
+ const savedInfo = useMemo(() => savedCopyFooterInfo([data.savedAt, peer?.savedAt, etfSavedAt]), [data.savedAt, peer?.savedAt, etfSavedAt]);
122
+ usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading || (!!etf && etfFile.loading), error: listError, info: savedInfo, hints: open ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
119
123
  usePaneNoticeFooter({ registrationId: "13f-overlap-notice", focused, notices: [
120
124
  ...(peer?.warnings ?? []),
121
125
  ...(mismatch ? ["No reporting quarter is loaded for both funds; overlap is unavailable."] : []),
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
3
  DataTableStackView,
4
4
  DataTableView,
5
- EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
5
+ EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneFooter, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
6
6
  type DataTableRootKeyContext, type PageRequest, type PaneFooterSegment, type PaneHint, type StatItem
7
7
  } from "../../../components";
8
8
  import { useShortcut } from "../../../react/input";
@@ -23,10 +23,10 @@ import { nextHeaderSort } from "../../../utils/sort-values";
23
23
  import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../runtime";
24
24
  import { useMineTickers } from "../shared/mine-tickers";
25
25
  import { usePaneStatusFooter, usePaneStatusLinkFooter } from "../../../components/layout/pane/status-footer";
26
- import { corporateSplitFactor, loadBrowserRows, loadFilingPositions, loadFundDetail, loadSplitFactors } from "./data";
26
+ import { corporateSplitFactor, createFundPrefetcher, loadBrowserRows, loadFilingPositions, loadFundDetail, loadSplitFactors } from "./data";
27
27
  import { FundOverlapView } from "./overlap-pane";
28
28
  import { ThirteenFCrowdingPane, ThirteenFTickerHoldingsView } from "./signals-pane";
29
- import { PaneFooterScope } from "../../../components/layout/pane/footer";
29
+ import { PaneFooterScope, savedCopyFooterInfo } from "../../../components/layout/pane/footer";
30
30
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
31
31
  import { useClaimResearchTabKeys } from "../ticker-detail/research-tab-keys";
32
32
  import {
@@ -129,6 +129,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
129
129
  const notes = [...(pagesSettled ? browser.pages.map((page) => page.warning) : []), browser.moreError?.message];
130
130
  return [...new Set(notes.filter(Boolean))].join(" ") || null;
131
131
  }, [browser.moreError, browser.pages, pagesSettled, reportPaint]);
132
+ // A page a failed refresh answered from a saved copy says when it was saved, as every pane does.
133
+ const savedInfo = useMemo(() => savedCopyFooterInfo(pagesSettled ? browser.pages.map((page) => page.savedAt) : []),
134
+ [browser.pages, pagesSettled, reportPaint]);
132
135
  const [detailSeed, setDetailSeed] = useState<FundSeed | null>(() => (
133
136
  initialCik ? { cik: String(initialCik), name: normalizedQuery || String(initialCik) } : null
134
137
  ));
@@ -166,7 +169,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
166
169
  }, { allowEditable: true });
167
170
 
168
171
  const browserSort = useMemo(() => browserSortFor(sortPreference, browserMode), [sortPreference, browserMode]);
169
- const sortedRows = useMemo(() => sortBrowserRows(rows, browserSort), [rows, browserSort, reportPaint]);
172
+ // The rows' reports fill the rows in place after their page answers (from the network or a saved copy);
173
+ // a row the table drew before then is drawn again only as a new object.
174
+ const sortedRows = useMemo(() => sortBrowserRows(rows.map((row) => ({ ...row })), browserSort), [rows, browserSort, reportPaint]);
170
175
  const columns = useMemo(() => {
171
176
  if (browserMode !== "performance") return buildBrowserColumns(false);
172
177
  const history = rows[0]?.priorReturns ?? [];
@@ -191,18 +196,17 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
191
196
  blurSearch();
192
197
  setDetailSeed({ cik: row.cik, name: row.name });
193
198
  }, [blurSearch]);
194
- // Every request behind a fund detail is cached per path, so warming it
195
- // while the cursor rests on the row makes Enter read from cache.
196
- const prefetchDetail = useCallback((row: FundBrowserRow) => {
197
- void loadFundDetail(row.cik, row.name).catch(() => {
198
- // Intentional ignore: speculative prefetch is retried and failures are shown when the fund opens.
199
- });
200
- }, []);
199
+ // The fund's first requests are read ahead while the cursor rests on its
200
+ // row, one fund at a time, and stopped when the cursor moves on.
201
+ const prefetcher = useMemo(() => createFundPrefetcher(), []);
202
+ useEffect(() => () => prefetcher.cancel(), [prefetcher]);
203
+ const prefetchDetail = useCallback((row: FundBrowserRow) => prefetcher.prefetch(row.cik), [prefetcher]);
201
204
 
202
205
  const browserStatusInfo = useMemo<PaneFooterSegment[]>(() => [
203
206
  ...(loadingMore ? [{ id: "loading-more", parts: [{ text: "loading more", tone: "muted" as const }] }] : []),
204
207
  ...(warning ? [{ id: "warning", parts: [{ text: warning, tone: "warning" as const }] }] : []),
205
- ], [loadingMore, warning]);
208
+ ...savedInfo,
209
+ ], [loadingMore, warning, savedInfo]);
206
210
  const searchHints = useMemo(() => [{ id: "search", key: "/", label: "search", onPress: focusSearch }], [focusSearch]);
207
211
  usePaneStatusFooter({
208
212
  registrationId: THIRTEENF_PANE_ID,
@@ -366,7 +370,11 @@ export function FundDetailView({
366
370
  abortRef.current = controller;
367
371
  setStatus("loading");
368
372
  setError(null);
369
- void loadFundDetail(seed.cik, seed.name, controller.signal, { forceRefresh: refresh })
373
+ void loadFundDetail(seed.cik, seed.name, controller.signal, {
374
+ forceRefresh: refresh,
375
+ // A first load shows the fund as it fills in; a refresh keeps what is on screen until it answers whole.
376
+ ...(refresh ? {} : { onProgress: (partial: FundDetailData) => { if (abortRef.current === controller) setData(partial); } }),
377
+ })
370
378
  .then((nextData) => {
371
379
  if (abortRef.current !== controller) return;
372
380
  setData(nextData);
@@ -386,6 +394,8 @@ export function FundDetailView({
386
394
  .catch((loadError) => {
387
395
  if (abortRef.current !== controller) return;
388
396
  if (loadError instanceof Error && loadError.name === "AbortError") return;
397
+ // A first load that failed after its first rows shows the error, not a part of the filing as the fund.
398
+ setData((current) => current?.loading ? null : current);
389
399
  setError(loadError instanceof Error ? loadError.message : String(loadError));
390
400
  setStatus("error");
391
401
  });
@@ -516,7 +526,10 @@ export function FundDetailView({
516
526
  ...(staleFiling ? { tone: "warning" as const } : {}) },
517
527
  data && hasComparable13FQuarter(data)
518
528
  ? { id: "compared", label: "Compared with", value: data.previousForm!.periodOfReport }
519
- : { id: "compared", label: "Compared with", value: "No prior quarter", tone: "muted" },
529
+ // The quarter the changes will compare with, dim until both are in.
530
+ : data?.loading && hasComparable13FQuarter({ ...data, loading: undefined })
531
+ ? { id: "compared", label: "Compared with", value: data.previousForm!.periodOfReport, tone: "muted" }
532
+ : { id: "compared", label: "Compared with", value: "No prior quarter", tone: "muted" },
520
533
  ...(data?.latestReport && data.latestReport.filings.length > 1
521
534
  ? [{ id: "combined", label: "Combined", value: `${data.latestReport.filings.length} filings` }]
522
535
  : []),
@@ -544,6 +557,9 @@ export function FundDetailView({
544
557
  const detailHints = useMemo<PaneHint[]>(() => hasHoldingFiling
545
558
  ? [{ id: "filing", key: "f", label: "iling", title: "Open Filing", onPress: openSelectedFilingInPane }]
546
559
  : [], [hasHoldingFiling, openSelectedFilingInPane]);
560
+ // An open filing and the overlap tab say when their own saved copies are from.
561
+ const detailSavedInfo = useMemo(() => savedCopyFooterInfo(openFiling || activeTab === "overlap" ? [] : [data?.savedAt]),
562
+ [activeTab, data?.savedAt, openFiling]);
547
563
  usePaneStatusLinkFooter({
548
564
  registrationId: "thirteenf-detail",
549
565
  focused,
@@ -551,6 +567,7 @@ export function FundDetailView({
551
567
  source: openFiling ? "SEC 13F" : null,
552
568
  loading: activeTab !== "overlap" && status === "loading",
553
569
  error: activeTab === "overlap" ? null : error,
570
+ info: detailSavedInfo,
554
571
  showOpenHint: true,
555
572
  hints: detailHints,
556
573
  });
@@ -583,14 +600,16 @@ export function FundDetailView({
583
600
  ]}
584
601
  meta={data?.name && data.name !== seed.name ? data.name : undefined}
585
602
  />
586
- {activeTab === "overlap" && data ? <FundOverlapView data={data} focused={focused} width={width} /> : activeTab === "filings" ? (
603
+ {activeTab === "overlap" && data ? data.loading?.positions || data.loading?.comparison ? (
604
+ <PaneStatusBody loading align="center" loadingLabel="Loading 13F filing..." />
605
+ ) : <FundOverlapView data={data} focused={focused} width={width} /> : activeTab === "filings" ? (
587
606
  <DataTableStackView<FundTimelineRow, FundTimelineColumn>
588
607
  focused={focused}
589
608
  detailOpen={!!openFiling}
590
609
  onBack={closeOpenFiling}
591
610
  detailTitle={openFiling ? `${openFiling.periodOfReport} filing` : "13F filing"}
592
611
  detailContent={openFiling ? (
593
- <FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} />
612
+ <FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} sourceSavedAt={data?.savedAt} />
594
613
  ) : (
595
614
  <Box flexGrow={1} />
596
615
  )}
@@ -659,11 +678,14 @@ function FilingDetailView({
659
678
  filing,
660
679
  width,
661
680
  sourceWarnings,
681
+ sourceSavedAt,
662
682
  }: {
663
683
  focused: boolean;
664
684
  filing: FundTimelineRow;
665
685
  width: number;
666
686
  sourceWarnings: string[];
687
+ /** When the fund's saved copy was saved, when a failed refresh left it on screen. */
688
+ sourceSavedAt?: number;
667
689
  }) {
668
690
  const { pinTicker } = usePluginTickerActions();
669
691
  const [sortPreference, setSortPreference] = usePluginPaneState<FundSortPreference<FilingPositionColumnId>>(
@@ -711,6 +733,9 @@ function FilingDetailView({
711
733
  );
712
734
 
713
735
  usePaneNoticeFooter({ registrationId: "thirteenf-filing-notices", notices: [...new Set([...sourceWarnings, ...warnings])], focused });
736
+ const savedInfo = useMemo(() => savedCopyFooterInfo([sourceSavedAt, ...(pagesSettled ? positions.pages.map((page) => page.savedAt) : [])]),
737
+ [pagesSettled, positions.pages, sourceSavedAt]);
738
+ usePaneFooter("thirteenf-filing-saved", () => ({ info: savedInfo }), [savedInfo]);
714
739
 
715
740
  // An untyped amendment may replace or add to the report, so only it is a caveat.
716
741
  const kind = amendmentKind(filing);
@@ -99,6 +99,20 @@ export interface FundDetailData {
99
99
  warnings?: string[];
100
100
  /** Set when the latest report predates the newest quarter past its deadline. */
101
101
  filingGap?: ThirteenFFilingGap | null;
102
+ /** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
103
+ savedAt?: number;
104
+ /** The parts still loading, while the fund fills in (loadFundDetail's `onProgress`); absent once whole. */
105
+ loading?: ThirteenFDetailLoading;
106
+ }
107
+
108
+ /** Parts of a fund's detail still loading. */
109
+ export interface ThirteenFDetailLoading {
110
+ /** The latest report's holdings are a large filing's first rows. */
111
+ positions?: boolean;
112
+ /** The prior quarter's holdings, which the latest are compared with. */
113
+ comparison?: boolean;
114
+ /** EDGAR's filing list, for a fund behind on its filings. */
115
+ filingGap?: boolean;
102
116
  }
103
117
 
104
118
  /** A manager a 13F-NT names as reporting its holdings. */
@@ -24,7 +24,8 @@ import {
24
24
  barPriceDecimals, formatBarPrice, historyAllStart, historyBarsAfterHole, historyChangeBaseNote, historyMissingPeriodsNote, quotedPriceDivisor,
25
25
  } from "../../../../cli/history-rows";
26
26
  import {
27
- MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, priceHistoryIntegrityNotice,
27
+ MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, pricePointWickFlags, priceHistoryIntegrityNotice,
28
+ priceHistoryWickNotice,
28
29
  } from "../../../../utils/price-history-integrity";
29
30
  import {
30
31
  useAssetData,
@@ -130,7 +131,9 @@ function buildHistoricalPriceRows(points: PricePoint[], venue: string | undefine
130
131
  const sorted = points
131
132
  .flatMap((point, sourceIndex) => {
132
133
  const date = pricePointDate(point);
133
- return date ? [{ point: pricePointValues(point), date, sourceIndex, partial: point.partial === true, filled: point.filled === true }] : [];
134
+ return date ? [{
135
+ point: pricePointValues(point), date, sourceIndex, partial: point.partial === true, filled: point.filled === true, contract: point.contract,
136
+ }] : [];
134
137
  })
135
138
  .sort((left, right) => left.date.getTime() - right.date.getTime());
136
139
  const size = historyBarSize(sorted.map((entry) => entry.date.getTime()));
@@ -141,10 +144,21 @@ function buildHistoricalPriceRows(points: PricePoint[], venue: string | undefine
141
144
  const times = sorted.map((entry) => entry.date.getTime());
142
145
  const interval = size === "intraday" ? null : size;
143
146
  const afterGap = historyBarsAfterHole(times, interval);
144
- const gapNotices = [historyMissingPeriodsNote(times, interval), historyChangeBaseNote(times, interval)]
145
- .filter((notice): notice is string => notice != null);
147
+ // A continuous future's bar on another contract than the bar before has no change: two months' prices are no move.
148
+ let held: string | undefined;
149
+ const rolled = sorted.map((entry) => {
150
+ const roll = !!entry.contract && !!held && held !== entry.contract;
151
+ held = entry.contract ?? held;
152
+ return roll;
153
+ });
154
+ const rolls = sorted.flatMap((entry, index) => rolled[index] ? [`${entry.date.toISOString().slice(0, 10)} (${entry.contract})`] : []);
155
+ const gapNotices = [
156
+ historyMissingPeriodsNote(times, interval),
157
+ historyChangeBaseNote(times, interval),
158
+ rolls.length > 0 ? `No CHG at ${rolls.length === 1 ? "the roll" : "each roll"} to a new contract: ${rolls.join(", ")}` : null,
159
+ ].filter((notice): notice is string => notice != null);
146
160
  const rows = sorted.map((entry, index) => {
147
- const previous = sorted[index - 1]?.point;
161
+ const previous = rolled[index] ? undefined : sorted[index - 1]?.point;
148
162
  const { point, date, sourceIndex, filled } = entry;
149
163
  const newest = index === sorted.length - 1;
150
164
  const partial = newest && (!!forming || dayToDate);
@@ -257,6 +271,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
257
271
  [points, venue],
258
272
  );
259
273
  const integrityNotice = priceHistoryIntegrityNotice(rows.filter((row) => row.point.integrity).length);
274
+ const wickNotice = useMemo(() => priceHistoryWickNotice(points.filter((point) => pricePointWickFlags(point).length > 0).length), [points]);
260
275
  const volumeMissing = useMemo(() => missingVolumeCount(rows.map((row) => ({
261
276
  ...row.point, date: row.time, partial: row.partial, filled: row.filled,
262
277
  }))), [rows]);
@@ -356,7 +371,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
356
371
 
357
372
  usePaneNoticeFooter({
358
373
  registrationId: "historical-prices:integrity",
359
- notices: [integrityNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
374
+ notices: [integrityNotice, wickNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
360
375
  focused,
361
376
  title: "Price history data",
362
377
  });
@@ -404,6 +419,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
404
419
  : loading ? (rows.length ? "Refreshing" : "Loading") : rows.length ? "Available" : "Empty"],
405
420
  ...(allStart ? [["Source starts", allStart]] : []),
406
421
  ...(integrityNotice ? [["Warning", integrityNotice]] : []),
422
+ ...(wickNotice ? [["Warning", wickNotice]] : []),
407
423
  ...gapNotices.map((notice) => ["Gap note", notice]),
408
424
  ...(volumeNotice ? [["Volume note", volumeNotice]] : []),
409
425
  ...(error ? [["Error", error]] : []),