@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -54,6 +54,7 @@ import { useSampledValue } from "../../../state/hooks/live-ticker-financials";
54
54
  import { usePlanAccess } from "../../../api-client/plan-access";
55
55
  import { SignInWall } from "../cloud/auth-actions";
56
56
  import { CriterionEditor } from "./criterion-editor";
57
+ import { SCREEN_IDEAS, screenIdea } from "./ideas";
57
58
  import { ScreenTickerCheck } from "./ticker-check";
58
59
  import {
59
60
  fetchSavedScreens,
@@ -70,7 +71,9 @@ import {
70
71
  formatScreenValue,
71
72
  metricDate,
72
73
  parseScreenDefinition,
74
+ pickerFields,
73
75
  resultFields,
76
+ screenMetricSetting,
74
77
  SIGN_COLORED,
75
78
  screenColumnLabel,
76
79
  screenCurrencyEmpty,
@@ -80,6 +83,7 @@ import {
80
83
  screenYtdBase,
81
84
  } from "./model";
82
85
  import { useScreenResults } from "./results";
86
+ import { unmatchedCategoryText, unmatchedCategoryValues } from "./categories";
83
87
  import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, sortLiveScreenRows, type LiveScreenRow } from "./live";
84
88
  import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
85
89
  import { buildScreenerQuoteTargets } from "../../../market-data/quotes/screener-live-quotes";
@@ -92,6 +96,7 @@ const TABS = [
92
96
  { value: "results", label: "Results" },
93
97
  { value: "criteria", label: "Criteria" },
94
98
  { value: "saved", label: "Saved" },
99
+ { value: "ideas", label: "Ideas" },
95
100
  ];
96
101
  const errorText = (error: unknown) =>
97
102
  error instanceof Error ? error.message : "Request failed.";
@@ -115,8 +120,10 @@ export function resultColumns(
115
120
  asOf: boolean,
116
121
  extendedSessions: readonly ExtendedSession[] = [],
117
122
  ytdBase: string | null = null,
123
+ /** Other metrics `--metric` asked for, shown first after the focus metric. */
124
+ otherMetrics: readonly NumericField[] = [],
118
125
  ): DataTableColumn[] {
119
- const [focus, ...others] = resultFields(definition, metric);
126
+ const [focus, ...others] = resultFields(definition, metric, otherMetrics);
120
127
  const metricColumn = (field: NumericField, form: "pane" | "compact"): DataTableColumn => {
121
128
  const label = screenColumnLabel(field, ytdBase, form);
122
129
  return { id: `m:${field}`, label, width: columnWidth(field, label), align: "right" };
@@ -222,9 +229,12 @@ export function EquityScreenerPane(props: PaneProps) {
222
229
  "definition",
223
230
  undefined,
224
231
  );
232
+ // A ready screen from Ideas (`--preset`), which opens on its own metric.
233
+ const [presetId] = usePaneSettingValue<string | undefined>("preset", undefined);
234
+ const preset = screenIdea(presetId);
225
235
  let initial: ScreenDefinition;
226
236
  try {
227
- initial = seed ? parseScreenDefinition(JSON.parse(seed)) : DEFAULT_SCREEN;
237
+ initial = preset?.definition ?? (seed ? parseScreenDefinition(JSON.parse(seed)) : DEFAULT_SCREEN);
228
238
  } catch (error) {
229
239
  return (
230
240
  <PaneStatusBody subject="screen definition" error={errorText(error)} />
@@ -232,9 +242,10 @@ export function EquityScreenerPane(props: PaneProps) {
232
242
  }
233
243
  return (
234
244
  <EquityScreenView
235
- key={`${seed ?? "default"}:${session.requestKey}`}
245
+ key={`${preset?.id ?? seed ?? "default"}:${session.requestKey}`}
236
246
  {...props}
237
247
  initial={initial}
248
+ initialMetric={preset?.metric ?? "marketCap"}
238
249
  />
239
250
  );
240
251
  }
@@ -243,7 +254,8 @@ function EquityScreenView({
243
254
  height,
244
255
  focused,
245
256
  initial,
246
- }: PaneProps & { initial: ScreenDefinition }) {
257
+ initialMetric,
258
+ }: PaneProps & { initial: ScreenDefinition; initialMetric: NumericField }) {
247
259
  const colors = useThemeColors(),
248
260
  dialog = useDialog(),
249
261
  host = useRendererHost();
@@ -253,10 +265,12 @@ function EquityScreenView({
253
265
  const session = useResearchCloudSession(),
254
266
  access = usePlanAccess();
255
267
  const [definition, setDefinition] = usePluginPaneState("definition", initial);
256
- const [metric, setMetric] = usePaneSettingValue<NumericField>(
268
+ const [metricSetting, setMetric] = usePaneSettingValue<string>(
257
269
  "metric",
258
- "marketCap",
270
+ initialMetric,
259
271
  );
272
+ // `--metric trailingPE,dividendYieldPercent` leads with the first and adds the others as columns.
273
+ const [metric, ...otherMetrics] = screenMetricSetting(metricSetting, initialMetric);
260
274
  const [mode, setMode] = usePluginPaneState("mode", "results");
261
275
  const [selectedId, setSelectedId] = usePluginPaneState<string | null>(
262
276
  "selected",
@@ -271,6 +285,10 @@ function EquityScreenView({
271
285
  const [savedSelectedId, setSavedSelectedId] = usePluginPaneState<
272
286
  string | null
273
287
  >("savedSelected", null);
288
+ const [ideaSelectedId, setIdeaSelectedId] = usePluginPaneState(
289
+ "ideaSelected",
290
+ SCREEN_IDEAS[0]!.id,
291
+ );
274
292
  const [currentSavedId, setCurrentSavedId] = usePluginPaneState<string | null>(
275
293
  "currentSaved",
276
294
  null,
@@ -356,8 +374,10 @@ function EquityScreenView({
356
374
  !opened && !results.loading && !results.loadingMore && !!data?.nextCursor,
357
375
  results.loadMore,
358
376
  );
359
- const metricFields =
360
- fields.data?.fields.filter((field) => field.kind === "number") ?? [];
377
+ const metricFields = pickerFields(
378
+ fields.data?.fields.filter((field) => field.kind === "number") ?? [],
379
+ [metric],
380
+ );
361
381
  // AS OF earns its column only when some row's focus metric is stale; the
362
382
  // snapshot decides, so a live quote freshening a row does not reflow the table.
363
383
  const staleFocus = snapshotRows.some((row) => row.metrics[metric]?.state === "stale");
@@ -367,7 +387,11 @@ function EquityScreenView({
367
387
  const ytdBase = screenYtdBase(snapshotRows, data?.snapshot?.assembledAt);
368
388
  // A screen its currency emptied says so, not that no company matches.
369
389
  const currencyEmpty = data && data.rows.length === 0 ? screenCurrencyEmpty(data.definition, data.universe) : null;
370
- const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions, ytdBase);
390
+ // A criterion's name that matches nothing (`Utility`) says so with the names it was likely meant as.
391
+ const unmatchedNames = data && data.rows.length === 0
392
+ ? unmatchedCategoryValues(data.definition, data).map(unmatchedCategoryText).join(" ") || null
393
+ : null;
394
+ const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions, ytdBase, otherMetrics);
371
395
  const apply = (next: ScreenDefinition) => {
372
396
  try {
373
397
  setDefinition(parseScreenDefinition(next));
@@ -494,7 +518,7 @@ function EquityScreenView({
494
518
  useAutoRefresh(results.updatedAt, results.load);
495
519
  const openQueryControl = () => queryControl.current?.open();
496
520
  const hints =
497
- saveForm || editing !== null || check
521
+ saveForm || editing !== null || check || mode === "ideas"
498
522
  ? []
499
523
  : mode === "criteria"
500
524
  ? [
@@ -783,7 +807,7 @@ function EquityScreenView({
783
807
  editing !== null ? (
784
808
  <CriterionEditor
785
809
  key={editing}
786
- fields={fields.data.fields}
810
+ fields={pickerFields(fields.data.fields, editingCriterion ? [editingCriterion.field] : [])}
787
811
  value={editingCriterion ?? null}
788
812
  focused={focused}
789
813
  width={width}
@@ -863,6 +887,36 @@ function EquityScreenView({
863
887
  />
864
888
  ) : null}
865
889
  </PaneStatusBody>
890
+ ) : mode === "ideas" ? (
891
+ <DataTableView
892
+ columns={[
893
+ { id: "name", label: "SCREEN", width: 28, align: "left" },
894
+ { id: "criteria", label: "CRITERIA", width: 20, flexGrow: 1, align: "left" },
895
+ ]}
896
+ items={SCREEN_IDEAS}
897
+ focused={focused && !saveForm}
898
+ rootWidth={width}
899
+ rootHeight={bodyHeight}
900
+ getItemKey={(row) => row.id}
901
+ selection={{
902
+ kind: "id",
903
+ selectedId: ideaSelectedId,
904
+ getId: (row) => row.id,
905
+ onChange: setIdeaSelectedId,
906
+ }}
907
+ onActivate={(row) => {
908
+ setMetric(row.metric);
909
+ apply(row.definition);
910
+ setCurrentSavedId(null);
911
+ setMode("results");
912
+ }}
913
+ renderCell={(row, column) => ({
914
+ text: column.id === "name" ? row.name : row.definition.criteria.map(criterionText).join("; "),
915
+ })}
916
+ sortColumnId={null}
917
+ sortDirection="asc"
918
+ emptyStateTitle="No ideas."
919
+ />
866
920
  ) : mode === "saved" ? (
867
921
  !access.emailVerified ? (
868
922
  <SignInWall
@@ -1042,8 +1096,9 @@ function EquityScreenView({
1042
1096
  },
1043
1097
  });
1044
1098
  }}
1045
- emptyStateTitle={currencyEmpty?.title ?? "No equities match this screen."}
1046
- emptyStateHint={currencyEmpty ? `${currencyEmpty.detail} Choose ${currencyEmpty.base.currency} to screen in the base currency.` : undefined}
1099
+ emptyStateTitle={(unmatchedNames ? null : currencyEmpty?.title) ?? "No equities match this screen."}
1100
+ emptyStateHint={unmatchedNames
1101
+ ?? (currencyEmpty ? `${currencyEmpty.detail} Choose ${currencyEmpty.base.currency} to screen in the base currency.` : undefined)}
1047
1102
  />
1048
1103
  </PaneStatusBody>
1049
1104
  </>
@@ -51,7 +51,8 @@ const SECTION_VIEWS: Record<FixedIncomeSection, { defaultView: string; views: Vi
51
51
  defaultView: "documents",
52
52
  views: [
53
53
  { value: "documents", label: "Documents", paneType: "credit-documents", component: ResearchCreditDocumentsPane },
54
- { value: "maturities", label: "Maturities", paneType: "debt-maturities", component: ResearchDebtMaturitiesPane },
54
+ // CRDOC's own Maturities tab is the contractual calendar-year wall; this is the filed fiscal-bucket schedule.
55
+ { value: "maturities", label: "Debt Schedule", paneType: "debt-maturities", component: ResearchDebtMaturitiesPane },
55
56
  ],
56
57
  },
57
58
  bonds: {
@@ -1,12 +1,15 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
+ import { ApiRequestError } from "../../../api-client/errors";
2
3
  import type { FuturesContractTermsPayload } from "../../../api-client/futures-curve";
3
4
  import type { FuturesContract } from "../../../market-data/futures-board";
5
+ import { createTtlCache } from "../../../data/ttl-cache";
4
6
  import { loadFuturesContractTerms } from "../../../market-data/futures-contract";
7
+ import { logFailure } from "../../../utils/log-failure";
5
8
 
6
9
  interface FuturesTermsState {
7
10
  /** The row's terms are still being asked for. */
8
11
  loading: boolean;
9
- /** Null when the server has no terms for the symbol, or could not answer. */
12
+ /** Null while they are being asked for, when the server has none for the symbol, or when the read failed. */
10
13
  terms: FuturesContractTermsPayload | null;
11
14
  }
12
15
 
@@ -14,32 +17,35 @@ export type FuturesTermsMap = ReadonlyMap<string, FuturesTermsState>;
14
17
 
15
18
  /** The terms change with a roll, and a roll is decided once a day. */
16
19
  const TERMS_TTL_MS = 30 * 60_000;
20
+ /**
21
+ * A read that failed is asked again after this: one blip leaves a row without
22
+ * its terms for a minute, not for the TTL, and a service that is down is asked
23
+ * once a minute, not once a tick.
24
+ */
25
+ const TERMS_RETRY_MS = 60_000;
17
26
  /** Forty-odd rows open at once; a few requests at a time keep the board's quotes first. */
18
27
  const TERMS_CONCURRENCY = 4;
19
28
 
20
- interface CachedTerms {
21
- at: number;
22
- /** Null when the server has no terms for the symbol, or could not answer. */
23
- terms: FuturesContractTermsPayload | null;
24
- }
25
-
26
- const termsCache = new Map<string, CachedTerms>();
27
- const inFlight = new Map<string, Promise<void>>();
29
+ /**
30
+ * Null when the server has no terms for the symbol, which is an answer and is
31
+ * kept like any other. A read that fails is not kept as one: it rejects, and
32
+ * the cache holds the rejection for `TERMS_RETRY_MS`.
33
+ */
34
+ const termsCache = createTtlCache<FuturesContractTermsPayload | null>({ ttlMs: TERMS_TTL_MS, rejectionMs: TERMS_RETRY_MS });
28
35
 
29
- function fresh(symbol: string, now: number): CachedTerms | undefined {
30
- const cached = termsCache.get(symbol);
31
- return cached && now - cached.at < TERMS_TTL_MS ? cached : undefined;
36
+ async function readTerms(symbol: string): Promise<FuturesContractTermsPayload | null> {
37
+ try {
38
+ return await loadFuturesContractTerms(symbol);
39
+ } catch (error) {
40
+ if (error instanceof ApiRequestError && error.status === 404) return null;
41
+ throw error;
42
+ }
32
43
  }
33
44
 
34
- function loadTerms(symbol: string): Promise<void> {
35
- const pending = inFlight.get(symbol);
36
- if (pending) return pending;
37
- const request = loadFuturesContractTerms(symbol)
38
- .then((terms) => { termsCache.set(symbol, { at: Date.now(), terms }); })
39
- .catch(() => { termsCache.set(symbol, { at: Date.now(), terms: null }); })
40
- .finally(() => { inFlight.delete(symbol); });
41
- inFlight.set(symbol, request);
42
- return request;
45
+ /** Whether the row has an answer; false when the read failed. */
46
+ function loadTerms(symbol: string): Promise<boolean> {
47
+ return termsCache.load(symbol, () => readTerms(symbol))
48
+ .then(() => true, logFailure("futures", "Read a futures row's contract terms failed", false));
43
49
  }
44
50
 
45
51
  /**
@@ -51,32 +57,40 @@ function loadTerms(symbol: string): Promise<void> {
51
57
  export function useFuturesBoardTerms(contracts: readonly FuturesContract[], enabled: boolean): FuturesTermsMap {
52
58
  const symbolKey = contracts.map((contract) => contract.symbol).join(",");
53
59
  const [version, setVersion] = useState(0);
60
+ // Bumped once the retry delay has passed after a failed read, to ask again.
61
+ const [retry, setRetry] = useState(0);
54
62
 
55
63
  useEffect(() => {
56
64
  if (!enabled || !symbolKey) return;
57
- const now = Date.now();
58
- const queue = symbolKey.split(",").filter((symbol) => !fresh(symbol, now));
65
+ const queue = symbolKey.split(",").filter((symbol) => termsCache.peek(symbol) === undefined);
59
66
  if (queue.length === 0) return;
60
67
  let cancelled = false;
68
+ let failed = false;
69
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
61
70
  const worker = async () => {
62
71
  for (let symbol = queue.shift(); symbol; symbol = queue.shift()) {
63
- await loadTerms(symbol);
72
+ if (!await loadTerms(symbol)) failed = true;
64
73
  if (!cancelled) setVersion((current) => current + 1);
65
74
  }
66
75
  };
67
- void Promise.all(Array.from({ length: Math.min(TERMS_CONCURRENCY, queue.length) }, worker));
76
+ void Promise.all(Array.from({ length: Math.min(TERMS_CONCURRENCY, queue.length) }, worker)).then(() => {
77
+ if (failed && !cancelled) retryTimer = setTimeout(() => setRetry((current) => current + 1), TERMS_RETRY_MS);
78
+ });
68
79
  return () => {
69
80
  cancelled = true;
70
81
  queue.length = 0;
82
+ clearTimeout(retryTimer);
71
83
  };
72
- }, [enabled, symbolKey]);
84
+ }, [enabled, symbolKey, retry]);
73
85
 
74
86
  return useMemo(() => {
75
87
  const result = new Map<string, FuturesTermsState>();
76
88
  if (!enabled) return result;
77
89
  for (const symbol of symbolKey ? symbolKey.split(",") : []) {
78
- const cached = termsCache.get(symbol);
79
- result.set(symbol, { loading: !cached, terms: cached?.terms ?? null });
90
+ // A row keeps the terms it had past their TTL until the next load lands, and
91
+ // one whose read failed draws without them rather than waiting on a retry.
92
+ const terms = termsCache.peek(symbol, { stale: true });
93
+ result.set(symbol, { loading: terms === undefined && !termsCache.failed(symbol), terms: terms ?? null });
80
94
  }
81
95
  return result;
82
96
  // `version` re-reads the cache as loads land.
@@ -1,6 +1,9 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
+ import { createTtlCache } from "../../../data/ttl-cache";
2
3
  import type { DataProvider } from "../../../types/data-provider";
4
+ import { isNoSourceAnswer } from "../../../sources/provider-errors";
3
5
  import type { PricePoint } from "../../../types/financials";
6
+ import { logFailure } from "../../../utils/log-failure";
4
7
  import type { BoardQuoteMap } from "../shared/use-quote-board";
5
8
  import type { FuturesContract } from "../../../market-data/futures-board";
6
9
  import {
@@ -24,32 +27,33 @@ export type FrontReturnsMap = ReadonlyMap<string, FrontReturnsState>;
24
27
  * for half an hour; the live quote moves the returns in between.
25
28
  */
26
29
  const HISTORY_TTL_MS = 30 * 60_000;
30
+ /**
31
+ * A read that failed is asked again after this: one blip leaves a row without
32
+ * returns for a minute, not for the TTL, and a service that is down is asked
33
+ * once a minute, not once a tick.
34
+ */
35
+ const HISTORY_RETRY_MS = 60_000;
27
36
  /** Forty-odd contracts open at once; a few requests at a time keep the board's quotes first. */
28
37
  const HISTORY_CONCURRENCY = 4;
29
38
 
30
- interface CachedHistory {
31
- at: number;
32
- /** Null when the contract has no history to read (delisted, unknown). */
33
- points: readonly PricePoint[] | null;
34
- }
35
-
36
- const historyCache = new Map<string, CachedHistory>();
37
- const inFlight = new Map<string, Promise<void>>();
39
+ /**
40
+ * Null when the source has no history for the contract (delisted, unknown),
41
+ * which is an answer and is kept like any other. A read that fails is not kept
42
+ * as one: it rejects, and the cache holds the rejection for `HISTORY_RETRY_MS`.
43
+ */
44
+ const historyCache = createTtlCache<readonly PricePoint[] | null>({ ttlMs: HISTORY_TTL_MS, rejectionMs: HISTORY_RETRY_MS });
38
45
 
39
- function freshHistory(symbol: string, now: number): CachedHistory | undefined {
40
- const cached = historyCache.get(symbol);
41
- return cached && now - cached.at < HISTORY_TTL_MS ? cached : undefined;
46
+ function readHistory(provider: DataProvider, symbol: string): Promise<readonly PricePoint[] | null> {
47
+ return provider.getPriceHistory(symbol, "", "1Y").then((points) => points.length > 0 ? points : null, (error: unknown) => {
48
+ if (isNoSourceAnswer(error)) return null;
49
+ throw error;
50
+ });
42
51
  }
43
52
 
44
- function loadHistory(provider: DataProvider, symbol: string): Promise<void> {
45
- const pending = inFlight.get(symbol);
46
- if (pending) return pending;
47
- const request = provider.getPriceHistory(symbol, "", "1Y")
48
- .then((points) => { historyCache.set(symbol, { at: Date.now(), points: points.length > 0 ? points : null }); })
49
- .catch(() => { historyCache.set(symbol, { at: Date.now(), points: null }); })
50
- .finally(() => { inFlight.delete(symbol); });
51
- inFlight.set(symbol, request);
52
- return request;
53
+ /** Whether the contract has an answer; false when the read failed. */
54
+ function loadHistory(provider: DataProvider, symbol: string): Promise<boolean> {
55
+ return historyCache.load(symbol, () => readHistory(provider, symbol))
56
+ .then(() => true, logFailure("futures", "Read a futures contract's history failed", false));
53
57
  }
54
58
 
55
59
  /**
@@ -73,27 +77,33 @@ export function useFrontContractReturns(
73
77
  return [symbol, list ? list.split(",") : []] as const;
74
78
  })), [candidateKey]);
75
79
  const [version, setVersion] = useState(0);
80
+ // Bumped once the retry delay has passed after a failed read, to ask again.
81
+ const [retry, setRetry] = useState(0);
76
82
  // A board left open overnight reloads once the day turns.
77
83
  const today = new Date().toISOString().slice(0, 10);
78
84
 
79
85
  useEffect(() => {
80
86
  if (!enabled || !provider) return;
81
- const now = Date.now();
82
- const queue = [...new Set([...candidates.values()].flat())].filter((symbol) => !freshHistory(symbol, now));
87
+ const queue = [...new Set([...candidates.values()].flat())].filter((symbol) => historyCache.peek(symbol) === undefined);
83
88
  if (queue.length === 0) return;
84
89
  let cancelled = false;
90
+ let failed = false;
91
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
85
92
  const worker = async () => {
86
93
  for (let symbol = queue.shift(); symbol; symbol = queue.shift()) {
87
- await loadHistory(provider, symbol);
94
+ if (!await loadHistory(provider, symbol)) failed = true;
88
95
  if (!cancelled) setVersion((current) => current + 1);
89
96
  }
90
97
  };
91
- void Promise.all(Array.from({ length: Math.min(HISTORY_CONCURRENCY, queue.length) }, worker));
98
+ void Promise.all(Array.from({ length: Math.min(HISTORY_CONCURRENCY, queue.length) }, worker)).then(() => {
99
+ if (failed && !cancelled) retryTimer = setTimeout(() => setRetry((current) => current + 1), HISTORY_RETRY_MS);
100
+ });
92
101
  return () => {
93
102
  cancelled = true;
94
103
  queue.length = 0;
104
+ clearTimeout(retryTimer);
95
105
  };
96
- }, [candidates, enabled, provider, today]);
106
+ }, [candidates, enabled, provider, today, retry]);
97
107
 
98
108
  return useMemo(() => {
99
109
  const result = new Map<string, FrontReturnsState>();
@@ -101,12 +111,14 @@ export function useFrontContractReturns(
101
111
  for (const contract of contracts) {
102
112
  const quote = quotes.get(contract.symbol)?.quote;
103
113
  const symbols = candidates.get(contract.symbol) ?? [];
104
- const loaded = symbols.map((symbol) => ({ symbol, cached: historyCache.get(symbol) }));
105
- if (loaded.some((entry) => !entry.cached)) {
114
+ // A contract keeps the history it had past its TTL until the next load lands,
115
+ // and one whose read failed counts as having none rather than waiting on a retry.
116
+ const loaded = symbols.map((symbol) => ({ symbol, history: historyCache.peek(symbol, { stale: true }), failed: historyCache.failed(symbol) }));
117
+ if (loaded.some((entry) => entry.history === undefined && !entry.failed)) {
106
118
  result.set(contract.symbol, { loading: true, values: NO_FUTURES_RETURNS });
107
119
  continue;
108
120
  }
109
- const available = loaded.flatMap((entry) => entry.cached?.points ? [{ symbol: entry.symbol, history: entry.cached.points }] : []);
121
+ const available = loaded.flatMap((entry) => entry.history ? [{ symbol: entry.symbol, history: entry.history }] : []);
110
122
  const front = pickFrontContract(available, quote?.price);
111
123
  const history = available.find((entry) => entry.symbol === front)?.history;
112
124
  result.set(contract.symbol, {
@@ -118,3 +130,8 @@ export function useFrontContractReturns(
118
130
  // `version` re-reads the history cache as loads land.
119
131
  }, [candidates, contracts, enabled, quotes, version]);
120
132
  }
133
+
134
+ /** Forgets every cached contract's history (tests). */
135
+ export function resetFrontContractReturns(): void {
136
+ historyCache.clear();
137
+ }
@@ -5,7 +5,7 @@ import { resolveEntryData } from "../../../market-data/selectors";
5
5
  import type { DataProvider } from "../../../types/data-provider";
6
6
  import type { Quote } from "../../../types/financials";
7
7
  import { quoteChangeBasis, type QuoteChangeBasis } from "../../../market-data/market/change-basis";
8
- import { fxLegMoveReference, fxLegs, fxLegSavedAt, liveFxLegEntry, type FxLeg } from "./live-legs";
8
+ import { fxBoardMoveDay, fxLegMoveReference, fxLegs, fxLegSavedAt, liveFxLegEntry, type FxLeg } from "./live-legs";
9
9
 
10
10
  /** One USD leg as the board reads it: the rate it draws and where that rate came from. */
11
11
  export interface FxLegReading {
@@ -17,7 +17,8 @@ export interface FxLegReading {
17
17
  /**
18
18
  * USD per unit at the pair quote's previous daily close, for the rate drawn
19
19
  * whether it is the quote's or a loaded rate of the same FX trading day
20
- * (fxLegMoveReference); null when the quote is stale or has none.
20
+ * (fxLegMoveReference); null when the quote is stale, has none, or is not
21
+ * of the board's trading day (fxBoardMoveDay).
21
22
  */
22
23
  reference: number | null;
23
24
  /** The close `reference` is, as `quote` names it ("vs Wed 21 Oct close"). */
@@ -51,12 +52,14 @@ export async function loadFxBoard(
51
52
  coordinator.loadQuotesBatch(legs.map((leg) => ({ symbol: leg.symbol, exchange: "" })), { forceRefresh })
52
53
  .catch(logFailure("fx-matrix.client", "Load FX pair quotes failed", [] as QueryEntry<Quote>[])),
53
54
  ]);
55
+ // Every leg moves from the board's one close, read off the clock, as the pane's tint does.
56
+ const boardDay = fxBoardMoveDay(quotes);
54
57
  return new Map(legs.map((leg, index) => {
55
58
  const rate = rates[index]!;
56
59
  const quote = quotes[index];
57
60
  const live = liveFxLegEntry(leg, quote, rate);
58
61
  const entry = live ?? (resolveEntryData(rate) != null ? rate : null);
59
- const close = fxLegMoveReference(leg, quote, entry);
62
+ const close = fxLegMoveReference(leg, quote, entry, boardDay);
60
63
  // A pair quote or a loaded rate whose refresh failed is the copy saved before, not a current answer.
61
64
  const savedAt = fxLegSavedAt(live ? resolveEntryData(quote) : null, rate);
62
65
  return [leg.currency, {
@@ -94,7 +94,7 @@ export function projectFxCrosses(
94
94
  }, saved.length > 0 ? Math.min(...saved) : Number.NaN);
95
95
  }));
96
96
 
97
- // The closes the moves are read from: one day for a board read at once, "vs mixed closes" otherwise.
97
+ // The close the moves are read from: the board's one trading day (fxBoardMoveDay), never mixed closes.
98
98
  const moveBasis = combinedChangeBasisLabel(currencies.map((currency) => readings.get(currency)?.changeBasis));
99
99
  const unavailable = currencies.filter((currency) => !rates.has(currency));
100
100
  const errors = unavailable.flatMap((currency) => {
@@ -26,7 +26,7 @@ import { fxRateObservations } from "../shared/fx-rate-freshness";
26
26
  import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
27
27
  import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
28
28
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
29
- import { fxLegMoveReference, fxLegQuoteKey, fxLegReferenceRate, fxLegSavedAt, fxLegTargets, fxLegs, fxLegsBehind, liveFxLegEntry } from "./live-legs";
29
+ import { fxBoardMoveDay, fxLegMoveReference, fxLegQuoteKey, fxLegReferenceRate, fxLegSavedAt, fxLegTargets, fxLegs, fxLegsBehind, liveFxLegEntry } from "./live-legs";
30
30
  import { CURRENCY_FLAG_REGIONS, FX_CURRENCIES, formatRate, resolveCurrencies, type FxCurrency } from "./pairs";
31
31
  import { crossMovePercent, directionTint, nextTintLevel } from "./direction";
32
32
  import { fxMatrixSettings } from "./settings";
@@ -101,9 +101,11 @@ function FxMatrixPane({ focused, width, height }: PaneProps) {
101
101
  const directionReferences = useMemo(() => {
102
102
  const coordinator = getSharedMarketDataCoordinator();
103
103
  const references = new Map<string, number>([["USD", 1]]);
104
+ // One close for the board, read off the clock (fxBoardMoveDay): a leg on another day has no move.
105
+ const boardDay = fxBoardMoveDay(legs.map((leg) => legEntries.get(fxLegQuoteKey(leg))), Date.now());
104
106
  for (const leg of legs) {
105
107
  const drawn = liveEntries.get(leg.currency) ?? coordinator?.getFxEntry(leg.currency);
106
- const reference = fxLegMoveReference(leg, legEntries.get(fxLegQuoteKey(leg)), drawn);
108
+ const reference = fxLegMoveReference(leg, legEntries.get(fxLegQuoteKey(leg)), drawn, boardDay);
107
109
  if (reference) references.set(leg.currency, reference.rate);
108
110
  }
109
111
  return references;
@@ -3,7 +3,8 @@ import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors"
3
3
  import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
4
4
  import type { Quote } from "../../../types/financials";
5
5
  import { isStreamCarryingQuote } from "../shared/use-quote-board";
6
- import { fxTradingDay } from "../../../utils/fx-market-hours";
6
+ import { fxPrintTradingDay, fxTradingDay, fxTradingDayAt } from "../../../utils/fx-market-hours";
7
+ import { delayedFeedLagMs } from "../../../market-data/market/freshness";
7
8
  import { savedCopyAt } from "../../../data/saved-copies";
8
9
 
9
10
  /** Currencies the market quotes as USD per unit; the rest quote as units per USD. */
@@ -106,22 +107,60 @@ export function fxLegReferenceRate(leg: FxLeg, entry: QueryEntry<Quote> | undefi
106
107
  return leg.inverted ? 1 / close : close;
107
108
  }
108
109
 
110
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
111
+
112
+ /**
113
+ * The FX trading day a pair quote's move is of: the day its source dated
114
+ * (`changeSessionDate`), else the day of its print, which turns at 17:00 New
115
+ * York. Null for a stale quote, or an undated print made while the market was
116
+ * shut, which is of no trading day.
117
+ */
118
+ function fxLegMoveDay(quote: Quote | null | undefined): string | null {
119
+ if (!quote || quote.stale === true || !Number.isFinite(quote.lastUpdated)) return null;
120
+ if (quote.changeSessionDate && ISO_DATE.test(quote.changeSessionDate)) return quote.changeSessionDate;
121
+ return fxPrintTradingDay(quote.lastUpdated);
122
+ }
123
+
124
+ /** How far behind the present a leg's quote is held: a delayed feed's lag, none for a live one. */
125
+ function legDelayMs(quote: Quote | null | undefined): number {
126
+ return quote?.dataSource === "delayed" ? delayedFeedLagMs(quote.listingExchangeName || quote.exchangeName || "CCY") : 0;
127
+ }
128
+
129
+ /**
130
+ * The one FX trading day a board's moves are read on, from the clock: the
131
+ * day the market is on as the legs' feed shows it, `now` less the longest
132
+ * delay among their quotes, or the Friday that closed the week while FX is
133
+ * shut (fxTradingDayAt). A leg's quote is never trusted to set it, so a leg
134
+ * dated ahead of the clock, or on a weekend, cannot move the board's close:
135
+ * only its own move goes blank (fxLegMoveReference).
136
+ */
137
+ export function fxBoardMoveDay(quotes: Iterable<QueryEntry<Quote> | undefined>, now = Date.now()): string | null {
138
+ let delay = 0;
139
+ for (const entry of quotes) delay = Math.max(delay, legDelayMs(resolveEntryData(entry)));
140
+ return fxTradingDayAt(now - delay);
141
+ }
142
+
109
143
  /**
110
144
  * What a leg's move is read against: its pair quote's previous daily close
111
145
  * (USD per unit), the close `quote` measures the pair from, whichever rate
112
146
  * the board draws. A loaded rate a minute newer than the quote moves from the
113
147
  * same close, so it reads against it too, as long as it is of the quote's FX
114
- * trading day. Null when the quote is stale or carries no previous close, or
115
- * when the drawn rate is of a later trading day (or undated) than the quote.
148
+ * trading day. Null when the quote is stale or carries no previous close,
149
+ * when the drawn rate is of a later trading day (or undated) than the quote,
150
+ * or when the quote's move is not of the board's day (`boardDay`,
151
+ * fxBoardMoveDay): a quote dated ahead of it or on a weekend, or a thin pair
152
+ * that has not printed since the day turned, whose move is the day before's.
116
153
  */
117
154
  export function fxLegMoveReference(
118
155
  leg: FxLeg,
119
156
  entry: QueryEntry<Quote> | undefined,
120
157
  drawn: Pick<QueryEntry<number>, "asOf"> | null | undefined,
158
+ boardDay: string | null,
121
159
  ): { rate: number; quote: Quote } | null {
122
160
  const quote = resolveEntryData(entry);
123
161
  const rate = fxLegReferenceRate(leg, entry);
124
162
  if (!quote || quote.stale === true || rate == null || !Number.isFinite(quote.lastUpdated)) return null;
163
+ if (boardDay == null || fxLegMoveDay(quote) !== boardDay) return null;
125
164
  const drawnAt = drawn?.asOf;
126
165
  if (drawnAt == null || !Number.isFinite(drawnAt) || fxTradingDay(drawnAt) !== fxTradingDay(quote.lastUpdated)) return null;
127
166
  return { rate, quote };
@@ -1,6 +1,6 @@
1
1
  import type { GpuObservation } from "../../../api-client/gpu";
2
2
  import type { HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/plugin";
3
- import { formatUtcTime } from "../../../utils/utc-time";
3
+ import { formatUtcTime, parseReportTime } from "../../../utils/utc-time";
4
4
  import { fetchGpuBoard, fetchGpuEvents, fetchGpuHistory, fetchGpuModelPreviews, gpuPreviewModels, loadGpuEquityHistory } from "./client";
5
5
  import {
6
6
  GPU_TABS, gpuBasisLabel, gpuChange, gpuChangesLocked, gpuEquityGap, gpuEquityModels, gpuEquityRows, gpuEventDate, gpuFamilies, gpuFamily,
@@ -16,7 +16,13 @@ const observationRow = (row: GpuObservation) => ({ gpu: gpuLabel(row), source: g
16
16
  const percent = (value: unknown) => gpuChange(typeof value === "number" ? value : null);
17
17
  /** Prices to the cent and times to the minute, as the pane draws them; exports keep the full value. */
18
18
  const price = (value: unknown) => typeof value === "number" ? gpuPrice(value) : "-";
19
- const minute = (value: unknown) => typeof value === "string" ? formatUtcTime(value) : "-";
19
+ const DAY_MS = 86_400_000;
20
+ /** A reference index's daily figure is stamped at 00:00 UTC: that is a day, not a time to the minute. */
21
+ const minute = (value: unknown) => {
22
+ if (typeof value !== "string") return "-";
23
+ const time = parseReportTime(value);
24
+ return time && !time.dateOnly && time.time % DAY_MS === 0 ? formatUtcTime({ ...time, dateOnly: true }) : formatUtcTime(value);
25
+ };
20
26
  /** A move the free preview leaves to Pro says so, instead of reading as missing. */
21
27
  const change = (value: unknown, row: HeadlessPaneRow) => row.changesLocked === true ? "Pro" : percent(value);
22
28
  const columns = [
@@ -24,6 +24,8 @@ export interface HelpFunction {
24
24
  argPlaceholder?: string;
25
25
  argOptional?: boolean;
26
26
  help: FunctionHelp | null;
27
+ /** Left out of `HELP <words>` matches, as of every list (`listed: false`); `HELP` with its code still answers. */
28
+ unlisted?: true;
27
29
  }
28
30
 
29
31
  export interface HelpFunctionSources {
@@ -97,6 +99,7 @@ export function buildHelpFunctionIndex(sources: HelpFunctionSources): HelpFuncti
97
99
  argPlaceholder: template.shortcut?.argPlaceholder
98
100
  ?? (template.shortcut?.argKind === "ticker-list" ? "tickers" : template.shortcut?.argKind),
99
101
  argOptional: template.shortcut?.argOptional,
102
+ ...(template.listed === false ? { unlisted: true as const } : {}),
100
103
  }));
101
104
  }
102
105
 
@@ -165,7 +168,7 @@ export function parseHelpArgument(arg: string, index: HelpFunctionIndex): HelpRe
165
168
 
166
169
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
167
170
  const matches = listHelpFunctions(index)
168
- .filter((fn) => fn.code !== HELP_CODE)
171
+ .filter((fn) => fn.code !== HELP_CODE && !fn.unlisted)
169
172
  .map((fn) => ({ fn, score: searchScore(fn, terms) }))
170
173
  .filter(({ score }) => score > 0)
171
174
  .sort((left, right) => right.score - left.score || left.fn.code.localeCompare(right.fn.code))