gloomberb 0.18.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) 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 +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  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 +31 -6
  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/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  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/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -36,7 +36,7 @@ import {
36
36
  } from "../../../public/react";
37
37
  import { useDialog } from "../../../ui/dialog";
38
38
  import { useThemeColors } from "../../../theme/theme-context";
39
- import { priceColor } from "../../../theme/colors";
39
+ import { priceColor, shownPriceColor } from "../../../theme/colors";
40
40
  import { apiClient } from "../../../api-client";
41
41
  import {
42
42
  NUMERIC_FIELDS,
@@ -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
@@ -987,7 +1041,7 @@ function EquityScreenView({
987
1041
  const move = screenExtendedMove(row, column.id.slice(EXTENDED_COLUMN_PREFIX.length) as ExtendedSession);
988
1042
  return move == null
989
1043
  ? { text: "--", color: colors.textDim }
990
- : { text: formatPercentRaw(move), color: priceColor(move, colors) };
1044
+ : { text: formatPercentRaw(move), color: shownPriceColor(move, 2, colors) };
991
1045
  }
992
1046
  const field = column.id.startsWith("m:")
993
1047
  ? (column.id.slice(2) as NumericField)
@@ -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
  </>
@@ -4,7 +4,7 @@ import { Box, ChartSurface, Text, useUiCapabilities } from "../../../ui";
4
4
  import { useThemeColors } from "../../../theme/theme-context";
5
5
  import type { ChartVectorShape } from "../../../ui/host";
6
6
  import type { ExposureComponent } from "../../../api-client/exposure";
7
- import { basisLabel, rangeText, type ExposureRow } from "./model";
7
+ import { basisLabel, hopsText, rangeText, type ExposureRow } from "./model";
8
8
  import { truncateToDisplayWidth } from "../../../utils/format";
9
9
 
10
10
  /** A disclosure-bound interval, not a probability band. Same numerical domain on both renderers. */
@@ -53,7 +53,7 @@ export function ExposurePathDiagram({ component, width, height }: { component: E
53
53
  const nodeWidth = Math.max(8, Math.floor((width - 2) / nodes.length));
54
54
  const vectors: ChartVectorShape[] = hops.map((hop, i) => ({ id: `${i}`, points: [{ x: (i + .5) / nodes.length, y: .5 }, { x: (i + 1.5) / nodes.length, y: .5 }], color: hop.pct === null ? colors.textDim : colors.warning, strokeWidth: 2, handles: true }));
55
55
  return <Box flexDirection="column" height={height} overflow="hidden">
56
- <Box paddingX={1} height={1} flexShrink={0}><Text fg={colors.textDim}>{hops.length} hop{hops.length === 1 ? "" : "s"} · {component.classification}{component.exposurePct ? ` · ${rangeText(component.exposurePct)}` : ""}</Text></Box>
56
+ <Box paddingX={1} height={1} flexShrink={0}><Text fg={colors.textDim}>{hopsText(hops.length)} · {component.classification}{component.exposurePct ? ` · ${rangeText(component.exposurePct)}` : ""}</Text></Box>
57
57
  <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((node, i) => <Box key={`${node.id}:${i}`} width={nodeWidth} alignItems="center"><ClippedText fg={colors.textBright} text={node.symbol ?? node.name} width={nodeWidth - 2} /></Box>)}</Box>
58
58
  <Box height={1} flexShrink={0} paddingX={1}>
59
59
  {desktop ? <ChartSurface width={width - 2} height={1} vectors={vectors} /> : <Text fg={colors.textDim}>{nodes.map((_, i) => i < hops.length ? "─".repeat(Math.max(1, nodeWidth - 2)) + "→ " : "").join("")}</Text>}
@@ -1,4 +1,5 @@
1
1
  import type { ExposureBasis, ExposureComponent, ExposureExtensionObservation, ExposureHolding, ExposurePayload, ExposureRange, ExposureScenario } from "../../../api-client/exposure";
2
+ import { quarterLabel } from "../shared/earnings-report";
2
3
  import { humanLabel } from "../shared/research-cells";
3
4
 
4
5
  export const TABS = [{ value: "table", label: "Table" }, { value: "drivers", label: "Drivers" }, { value: "paths", label: "Paths" }, { value: "portfolio", label: "Portfolio" }];
@@ -14,6 +15,17 @@ export const basisLabel = (basis: string | null) => basis?.replaceAll("_", " ")
14
15
  const pct = (value: number) => `${value.toFixed(1)}%`;
15
16
  export const rangeText = (range: ExposureRange | null) => !range ? "Unknown" : range.low === range.high ? pct(range.low) : `${range.low.toFixed(1)} to ${range.high.toFixed(1)}%`;
16
17
  export const fraction = (value: number) => pct(value * 100);
18
+ /** "1 hop", "2 hops". */
19
+ export const hopsText = (count: number) => `${count} hop${count === 1 ? "" : "s"}`;
20
+ /**
21
+ * A reporting period as the tables word it. The server keys a quarter by its end date (`quarter:2026-07-31`), which
22
+ * reads as the month it ends in, Jul 2026, as the earnings events label a quarter; a date or a source's own words
23
+ * stay as they are.
24
+ */
25
+ export function periodText(period: string): string {
26
+ const quarter = /^quarter:(\d{4}-\d{2}(?:-\d{2})?)$/.exec(period);
27
+ return quarter ? quarterLabel(quarter[1]) : period;
28
+ }
17
29
 
18
30
  /** A weighted list is entirely explicit. Never silently fill missing weights or normalize shorts. */
19
31
  export function parseHoldings(text: string): ExposureHolding[] {
@@ -78,7 +90,7 @@ export function exposureCellText(row: ExposureRow, column: "label" | "basis" | "
78
90
  // What the number rests on, in words: disclosed by the company, estimated by a chain, or not visible.
79
91
  case "classification": return `${humanLabel(row.classification)}${row.incomplete && row.classification !== "unknown" ? " · partial" : ""}`;
80
92
  case "impact": return row.impact ? rangeText(row.impact) : "--";
81
- case "period": return row.period ?? "--";
93
+ case "period": return row.period ? periodText(row.period) : "--";
82
94
  case "weight": return fraction(row.weight ?? 0);
83
95
  }
84
96
  }
@@ -20,7 +20,7 @@ import { ExposurePathDiagram, ExposureRanges } from "./charts";
20
20
  import { fetchExposure, fetchScenarios, validateExposure } from "./client";
21
21
  import { ExposureEditor, type ExposureInputs } from "./editor";
22
22
  import { portfolioHoldings, watchlistHoldings } from "./holdings";
23
- import { basisLabel, DEFAULT_SCENARIO, exposureCellText, fraction, parseCustomScenario, parseHoldings, pathRows, portfolioRows, rangeText, tableRows, TABS, type ExposureRow } from "./model";
23
+ import { basisLabel, DEFAULT_SCENARIO, exposureCellText, fraction, hopsText, parseCustomScenario, parseHoldings, pathRows, periodText, portfolioRows, rangeText, tableRows, TABS, type ExposureRow } from "./model";
24
24
  import { useExposureEvidence } from "./evidence";
25
25
  import { driverRows, driverScope, driverUnits, driverValue } from "./drivers";
26
26
 
@@ -29,7 +29,7 @@ function EvidenceItem({ item }: { item: ExposureEvidence }) {
29
29
  const host = useRendererHost();
30
30
  return <Box flexDirection="column" paddingY={1}>
31
31
  <KeyValueRow labelWidth={20} label="Evidence" value={`${item.tier} · ${item.asOf ?? "undated"}`} detail={item.confidence === null ? undefined : `${Math.round(item.confidence * 100)}% confidence`} />
32
- <KeyValueRow labelWidth={20} label="Period / units" value={`${item.period ?? "unknown"} · ${item.units}${item.currency ? ` ${item.currency}` : ""}`} />
32
+ <KeyValueRow labelWidth={20} label="Period / units" value={`${item.period ? periodText(item.period) : "unknown"} · ${item.units}${item.currency ? ` ${item.currency}` : ""}`} />
33
33
  {item.revision ? <KeyValueRow labelWidth={20} label="Revision" value={item.revision} /> : null}
34
34
  {item.quote ? <Text fg={colors.textBright}>{item.quote}</Text> : <Text fg={colors.textDim}>{item.tier === "user" ? "User-supplied assumption" : "Structured observation; no verbatim quote"}</Text>}
35
35
  {item.url ? <ActionRow label="Open primary evidence" onPress={() => void host.openExternal(item.url!)} /> : null}
@@ -49,7 +49,7 @@ function ExposureDetail({ row, data, width }: { row: ExposureRow | null; data: E
49
49
  {Object.entries(row.driver.dimensions ?? {}).map(([key, value]) => <KeyValueRow key={key} labelWidth={20} label={humanLabel(key)} value={value} />)}
50
50
  {row.driver.evidence.map(item => <EvidenceItem key={item.id} item={item} />)}
51
51
  </> : row ? <>
52
- <KeyValueRow labelWidth={20} label="Basis / period" value={`${basisLabel(row.basis)} · ${row.period ?? "unknown"}`} />
52
+ <KeyValueRow labelWidth={20} label="Basis / period" value={`${basisLabel(row.basis)} · ${row.period ? periodText(row.period) : "unknown"}`} />
53
53
  <KeyValueRow labelWidth={20} label={row.concentration ? "Gross exposure" : "Exposure"} value={rangeText(row.exposure)} detail={`${row.classification}${row.incomplete ? " · incomplete bound" : ""}`} />
54
54
  <KeyValueRow labelWidth={20} label={row.concentration ? "Signed exposure" : "Operating stress"} value={rangeText(row.impact)} detail={row.concentration ? "NAV-weighted operating exposure; not a scenario change" : "Estimated change in the stated denominator"} />
55
55
  {row.components.map(component => <Box key={component.id} flexDirection="column" paddingY={1}>
@@ -249,7 +249,7 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
249
249
  const query = <QueryBar width={width} filters={[
250
250
  { id: "scenario", label: "Scenario", value: scenarioId, options: [...(library.data ?? [DEFAULT_SCENARIO]).map(s => ({ value: s.id ?? (snapshot ? "captured" : "custom"), label: s.label })), { value: "custom", label: "Custom" }].filter((s, i, all) => all.findIndex(v => v.value === s.value) === i), onChange: (value: string) => { if (value === "custom") setEditing(true); else setScenarioId(value); } },
251
251
  ...(tab === "portfolio" ? [{ id: "group", label: "View", value: view, options: [{ value: "stress", label: "Stress" }, { value: "country", label: "Country" }, { value: "supplier", label: "Supplier" }, { value: "customer", label: "Customer" }], onChange: setView }] : []),
252
- ]} meta={tab === "drivers" ? "Company disclosures" : tab === "portfolio" ? "NAV-weighted operating exposure" : `${data?.depth ?? inputs.depth} hops`} />;
252
+ ]} meta={tab === "drivers" ? "Company disclosures" : tab === "portfolio" ? "NAV-weighted operating exposure" : hopsText(data?.depth ?? inputs.depth)} />;
253
253
  if (!data && isCloudSessionRequired(resource.error) && !editing) return <SignInWall placement="exposure-signin" action="analyze exposures" needsVerification={session.needsVerification} />;
254
254
  return <Box width={width} height={height} flexDirection="column">
255
255
  {strip}
@@ -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: {
@@ -274,6 +274,7 @@ export function FuturesPane({ focused, width, height }: PaneProps) {
274
274
  noQuotes,
275
275
  status.latestTs,
276
276
  status.loading,
277
+ status.savedAt,
277
278
  status.stale,
278
279
  status.unavailable,
279
280
  ]);
@@ -5,6 +5,7 @@ import type { BoardQuoteMap } from "../shared/use-quote-board";
5
5
  import { FUTURES_SECTOR_ORDER, type FuturesContract, type FuturesSector } from "../../../market-data/futures-board";
6
6
  import { futuresPriceUnit } from "../../../market-data/futures-contract";
7
7
  import { contractMonthLabel } from "../../../utils/contract-month";
8
+ import { quoteDotMarketState } from "../../../market-data/market/status";
8
9
  import type { FuturesReturnHorizon } from "./returns";
9
10
  import type { FuturesTermsMap } from "./use-board-terms";
10
11
  import type { FrontReturnsMap } from "./use-front-returns";
@@ -169,7 +170,7 @@ function getSortValue(
169
170
  case "unit":
170
171
  return futuresUnitLabel(contract, terms?.get(contract.symbol)?.terms);
171
172
  case "status":
172
- return quote?.marketState === "REGULAR" ? 0 : 1;
173
+ return quoteDotMarketState(quote) === "REGULAR" ? 0 : 1;
173
174
  case "code":
174
175
  return contract.code;
175
176
  case "name":
@@ -1,5 +1,5 @@
1
1
  import type { DataTableCell, DataTableColumn } from "../../../components";
2
- import { colors, priceColor } from "../../../theme/colors";
2
+ import { colors, shownPriceColor } from "../../../theme/colors";
3
3
  import type { Quote } from "../../../types/financials";
4
4
  import { TextAttributes } from "../../../ui";
5
5
  import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
@@ -253,7 +253,7 @@ export function renderFuturesCell(
253
253
  if (isBoardRowLoading(state) || returns?.loading) return { text: "…", color: colors.textDim };
254
254
  const value = returns?.values[FUTURES_RETURN_COLUMNS[column.id]!];
255
255
  if (value == null || !Number.isFinite(value)) return { text: "—", color: colors.textDim };
256
- return { text: formatPercentRaw(value), color: priceColor(value) };
256
+ return { text: formatPercentRaw(value), color: shownPriceColor(value, 2) };
257
257
  }
258
258
  case "prevClose":
259
259
  if (isBoardRowLoading(state)) return { text: "…", color: colors.textDim };
@@ -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
+ }