@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
@@ -1,5 +1,9 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { DataTableView, PaneStatusBody, QueryBar, usePaneFooter, usePaneNoticeFooter, type DataTableColumn, type DataTableKeyEvent } from "../../../components";
2
+ import {
3
+ DataTableView, PaneStatusBody, QueryBar, usePaneFooter, usePaneNoticeFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type DataTableKeyEvent,
4
+ type QueryBarFilter,
5
+ } from "../../../components";
6
+ import { dropColumnsToFit } from "../../../components/ui/table-layout";
3
7
  import { useAsyncResource } from "../../../react/async-resource";
4
8
  import { useAutoRefresh } from "../../../react/auto-refresh";
5
9
  import { usePaneSettingValue, usePluginAppActions } from "../../../public/react";
@@ -10,11 +14,19 @@ import { parsePublicTickerKey } from "../../../utils/exchanges";
10
14
  import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
11
15
  import { useAssetData } from "../../runtime";
12
16
  import { createScenarioDependencies } from "./client";
13
- import { cspScreenNotes, cspScreenReportRow, type CspScreenReportRow } from "./csp-screen-headless";
17
+ import {
18
+ cspScreenNotes, cspScreenReportRow, rewardRiskText, STRATEGY_MONEY_UNIT, strategyScreenReportRow, type CspScreenReportRow,
19
+ type StrategyScreenReportRow,
20
+ } from "./csp-screen-headless";
14
21
  import { CSP_SCREEN_DEFAULTS, CSP_SCREEN_LIMIT, cspScreenCriteriaText, cspTargetDate, loadCspScreen, readCspScreenCriteria, type CspScreenRow } from "./csp-screen";
15
22
  import { formatHedgePrice } from "./hedge";
23
+ import {
24
+ loadStrategyScreen, readStrategyFamily, readStrategyScreenCriteria, STRATEGY_TABS, strategyFamilyDef, strategyRankedBy,
25
+ strategyScreenCriteriaText, tabFamilies, type ScreenFamily, type StrategyFamily, type StrategyScreenRow, type StrategyTab,
26
+ } from "./strategy-screen";
16
27
 
17
28
  type ScreenRow = CspScreenReportRow & { source: CspScreenRow };
29
+ type TradeRow = StrategyScreenReportRow & { source: StrategyScreenRow };
18
30
 
19
31
  const COLUMNS: DataTableColumn[] = [
20
32
  { id: "rank", label: "#", width: 3, align: "right" },
@@ -33,7 +45,50 @@ const COLUMNS: DataTableColumn[] = [
33
45
  { id: "earnings", label: "Earnings", width: 17, align: "left" },
34
46
  ];
35
47
 
48
+ /** At a narrow width the put screen keeps the put, its yield and its risks; its expiry and the mid's yield give way first. */
49
+ const PUT_DROP_ORDER = ["midYield", "expiry", "otm", "ask", "bid", "rank", "delta"];
50
+
51
+ /** The Trade column fits the longest trade listed, so a condor's bought wings show beside its sold legs. */
52
+ const TRADE_WIDTH = { min: 14, max: 40 };
53
+ const tradeWidth = (rows: readonly { trade: string | null }[]) =>
54
+ Math.min(TRADE_WIDTH.max, Math.max(TRADE_WIDTH.min, ...rows.map((row) => row.trade?.length ?? 0)));
55
+
56
+ /**
57
+ * A family's columns. DTE dates the trade (a calendar's bought expiry is in
58
+ * its words). Money is per contract, said once in the bar. An income trade
59
+ * reads its annualised return, a debit trade its reward to risk.
60
+ */
61
+ const tradeColumns = (strategy: ScreenFamily, tradeColumnWidth: number): DataTableColumn[] => {
62
+ const credit = strategyFamilyDef(strategy).net === "credit";
63
+ return [
64
+ { id: "rank", label: "#", width: 3, align: "right" },
65
+ { id: "symbol", label: "Symbol", width: 7, align: "left" },
66
+ { id: "days", label: "DTE", width: 4, align: "right" },
67
+ { id: "trade", label: "Trade", width: tradeColumnWidth, align: "left", flexGrow: 1 },
68
+ { id: "net", label: credit ? "Credit" : "Debit", width: 8, align: "right", title: STRATEGY_MONEY_UNIT },
69
+ { id: "maxGain", label: "Max gain", width: 9, align: "right", title: STRATEGY_MONEY_UNIT },
70
+ { id: "maxLoss", label: "Max loss", width: 9, align: "right", title: STRATEGY_MONEY_UNIT },
71
+ { id: "breakEven", label: "Break-even", width: 15, align: "right" },
72
+ { id: "pop", label: "PoP", width: 4, align: "right" },
73
+ credit ? { id: "returnPerYear", label: "Return/yr", width: 9, align: "right" }
74
+ : { id: "rewardRisk", label: "Reward/risk", width: 9, align: "right", title: "Max gain over max loss" },
75
+ { id: "ivRank", label: "IVR", width: 4, align: "right" },
76
+ { id: "spread", label: "Spread", width: 11, align: "right" },
77
+ { id: "earnings", label: "Earnings", width: 13, align: "left" },
78
+ ];
79
+ };
80
+ /**
81
+ * What a narrow pane gives up first: the max gain or loss that repeats the
82
+ * credit or debit, then the break-evens, the rank, the other max, the
83
+ * earnings flag and the spread. Symbol, DTE, trade, credit or debit, PoP,
84
+ * the return and IV rank stay at every width.
85
+ */
86
+ const tradeDropOrder = (strategy: ScreenFamily) => strategyFamilyDef(strategy).net === "credit"
87
+ ? ["maxGain", "breakEven", "rank", "maxLoss", "earnings", "spread"]
88
+ : ["maxLoss", "breakEven", "rank", "maxGain", "earnings", "spread"];
89
+
36
90
  const DELTA_OPTIONS = ["0.10", "0.15", "0.20", "0.25", "0.30", "0.35", "0.40", "0.50"];
91
+ const WING_OPTIONS = ["0.05", "0.10", "0.15", "0.20", "0.25", "0.30", "0.35", "0.40"];
37
92
  const DAYS_OPTIONS = ["7", "14", "21", "30", "45", "60", "90"];
38
93
  const SYMBOLS_FIELD_WIDTH = 24;
39
94
  /** Symbols apply once typing pauses, so a half-typed ticker does not start a screen. */
@@ -42,79 +97,128 @@ const SYMBOLS_DEBOUNCE_MS = 700;
42
97
  const symbolList = (text: string) => [...new Set(text.split(/[\s,;]+/).map((symbol) => symbol.trim().toUpperCase()).filter(Boolean))];
43
98
  const percentText = (value: unknown, digits: number) => typeof value === "number" ? `${value.toFixed(digits)}%` : "--";
44
99
  const priceText = (value: unknown) => typeof value === "number" ? formatHedgePrice(value) : "--";
45
- /** A column's value to sort on: earnings by date, a flag as a number. */
46
- function sortValue(row: CspScreenReportRow, columnId: string): string | number | null {
47
- if (columnId === "earnings") return row.earningsDate;
48
- const value = row[columnId as keyof CspScreenReportRow];
49
- return typeof value === "boolean" ? Number(value) : value;
100
+ const moneyText = (value: number) => value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
101
+ const riskText = (value: number | null, unlimited: boolean) => value != null ? moneyText(value) : unlimited ? "unlimited" : "--";
102
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
103
+ /** `before Oct 28`: the flag and the date without its year, which the next report never needs. */
104
+ const earningsCell = (row: { earningsDate: string | null; earningsBeforeExpiry: boolean | null; earnings: string }) => {
105
+ const date = row.earningsDate;
106
+ if (!date || row.earningsBeforeExpiry == null) return row.earnings;
107
+ return `${row.earningsBeforeExpiry ? "before" : "after"} ${MONTHS[Number(date.slice(5, 7)) - 1]} ${Number(date.slice(8, 10))}`;
108
+ };
109
+ /** A column's value to sort on: earnings by date, a flag as a number, an unlimited gain or loss above every limit. */
110
+ function sortValue(row: Record<string, unknown>, columnId: string): string | number | null {
111
+ if (columnId === "earnings") return typeof row.earningsDate === "string" ? row.earningsDate : null;
112
+ if ((columnId === "maxGain" || columnId === "rewardRisk") && row.unlimited === "gain") return Number.POSITIVE_INFINITY;
113
+ if (columnId === "maxLoss" && row.unlimited === "loss") return Number.POSITIVE_INFINITY;
114
+ const value = row[columnId];
115
+ return typeof value === "boolean" ? Number(value) : typeof value === "number" || typeof value === "string" ? value : null;
50
116
  }
51
117
  /** A select's options with the current value kept when it is not a preset (a delta typed on the command line). */
52
118
  const withCurrent = (presets: readonly string[], current: string, label: (value: string) => string) =>
53
119
  [...new Set([...presets, current])].sort((a, b) => Number(a) - Number(b)).map((value) => ({ value, label: label(value) }));
54
120
 
121
+ /** OSA's typed legs for a trade (`call,105,2026-11-20,-1,1.64,28.1`), each at its entry price and IV. */
122
+ const osaLegs = (row: StrategyScreenRow) => row.trade!.position.legs.map((leg) => [leg.side, leg.strike, new Date(leg.expiration * 1000).toISOString().slice(0, 10),
123
+ leg.quantity, Number(leg.price.toFixed(6)), Number((leg.volatility * 100).toFixed(4)), leg.multiplier].join(",")).join(";");
124
+
55
125
  export function CspScreenPane({ width, height, focused }: PaneProps) {
56
126
  const colors = useThemeColors();
57
127
  const [symbolsText, setSymbolsText] = usePaneSettingValue<string>("symbols", "");
128
+ const [strategySetting, setStrategySetting] = usePaneSettingValue<string>("strategy", "");
58
129
  const [delta, setDelta] = usePaneSettingValue<string | number>("delta", "");
130
+ const [wingDelta, setWingDelta] = usePaneSettingValue<string | number>("wingDelta", "");
59
131
  const [days, setDays] = usePaneSettingValue<string | number>("days", "");
60
132
  const [expiration, setExpiration] = usePaneSettingValue<string | number>("expiration", "");
61
133
  const [maxSpread] = usePaneSettingValue<string | number>("maxSpread", "");
62
134
  const [symbolsActive, setSymbolsActive] = useState(false);
63
135
  const { createPaneFromTemplate } = usePluginAppActions();
64
136
  const assetData = useAssetData();
137
+ const strategy = useMemo((): StrategyFamily => {
138
+ try { return readStrategyFamily(strategySetting); } catch { return "csp"; }
139
+ }, [strategySetting]);
140
+ const family = strategyFamilyDef(strategy);
141
+ // Each strategy opens at its own deltas; the names and the expiry carry over.
142
+ const chooseStrategy = (next: StrategyFamily) => {
143
+ if (next === strategy) return;
144
+ setStrategySetting(next === "csp" ? "" : next);
145
+ setDelta("");
146
+ setWingDelta("");
147
+ };
148
+ const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: STRATEGY_TABS.map((tab) => ({ ...tab })), activeValue: family.tab,
149
+ onSelect: (tab) => chooseStrategy(tabFamilies(tab as StrategyTab)[0]!.id), focused, dense: true });
65
150
  const typed = useMemo(() => symbolList(String(symbolsText ?? "")), [symbolsText]);
66
151
  // The screen reads the first names it can rank; the footer names the rest.
67
152
  const symbols = useMemo(() => typed.slice(0, CSP_SCREEN_LIMIT), [typed]);
68
153
  const leftOut = typed.slice(CSP_SCREEN_LIMIT);
69
154
  const criteria = useMemo(() => {
70
- try { return { value: readCspScreenCriteria({ delta, days, expiration, maxSpread }), error: null }; }
71
- catch (error) { return { value: null, error: error instanceof Error ? error.message : String(error) }; }
72
- }, [delta, days, expiration, maxSpread]);
155
+ try {
156
+ const options = { delta, wingDelta, days, expiration, maxSpread };
157
+ if (strategy === "csp") return { value: readCspScreenCriteria(options), trade: null, error: null };
158
+ return { value: null, trade: readStrategyScreenCriteria(strategy, options), error: null };
159
+ } catch (error) { return { value: null, trade: null, error: error instanceof Error ? error.message : String(error) }; }
160
+ }, [strategy, delta, wingDelta, days, expiration, maxSpread]);
161
+ const ready = criteria.value ?? criteria.trade;
73
162
  const controller = useRef<AbortController | null>(null);
74
- const key = JSON.stringify([symbols, criteria.value]);
163
+ const key = JSON.stringify([symbols, strategy, ready]);
75
164
  const loader = useCallback(async () => {
76
165
  controller.current?.abort();
77
166
  const current = new AbortController();
78
167
  controller.current = current;
79
- return loadCspScreen(symbols.map((symbol) => parsePublicTickerKey(symbol)), criteria.value!,
80
- { scenario: createScenarioDependencies(), earnings: assetData }, current.signal);
168
+ const instruments = symbols.map((symbol) => parsePublicTickerKey(symbol));
169
+ const dependencies = { scenario: createScenarioDependencies(), earnings: assetData };
170
+ if (criteria.trade) return { kind: "trade" as const, screen: await loadStrategyScreen(instruments, criteria.trade, dependencies, current.signal) };
171
+ return { kind: "csp" as const, screen: await loadCspScreen(instruments, criteria.value!, dependencies, current.signal) };
81
172
  }, [key, assetData]);
82
- const resource = useAsyncResource(symbols.length && criteria.value ? loader : null);
173
+ const resource = useAsyncResource(symbols.length && ready ? loader : null);
83
174
  useEffect(() => () => controller.current?.abort(), [loader]);
84
175
  useAutoRefresh(resource.updatedAt, resource.load);
85
176
  const [sort, setSort] = useState<{ columnId: string; direction: SortDirection }>({ columnId: "rank", direction: "asc" });
86
- const rows = useMemo<ScreenRow[]>(() => {
87
- const flat = (resource.data?.rows ?? []).map((row) => ({ ...cspScreenReportRow(row), source: row }));
177
+ // A strategy's answer shows only under its own tab: the last one stays until the next has loaded only for the same strategy.
178
+ const data = resource.data && (resource.data.kind === "csp") === (strategy === "csp")
179
+ && (resource.data.kind === "csp" || resource.data.screen.criteria.strategy === strategy) ? resource.data : null;
180
+ const order = useCallback(<Row extends { rank: number | null } & Record<string, unknown>>(flat: Row[]) => flat.sort((left, right) => sort.columnId === "rank"
88
181
  // Unranked names stay under the ranked ones whichever way rank sorts.
89
- return flat.sort((left, right) => sort.columnId === "rank"
90
- ? compareSortValues(left.rank ?? Number.POSITIVE_INFINITY, right.rank ?? Number.POSITIVE_INFINITY, sort.direction)
91
- : compareSortValues(sortValue(left, sort.columnId), sortValue(right, sort.columnId), sort.direction));
92
- }, [resource.data, sort]);
182
+ ? compareSortValues(left.rank ?? Number.POSITIVE_INFINITY, right.rank ?? Number.POSITIVE_INFINITY, sort.direction)
183
+ : compareSortValues(sortValue(left, sort.columnId), sortValue(right, sort.columnId), sort.direction)), [sort]);
184
+ const putRows = useMemo<ScreenRow[]>(() => data?.kind !== "csp" ? []
185
+ : order(data.screen.rows.map((row) => ({ ...cspScreenReportRow(row), source: row }))), [data, order]);
186
+ const tradeRows = useMemo<TradeRow[]>(() => data?.kind !== "trade" ? []
187
+ : order(data.screen.rows.map((row) => ({ ...strategyScreenReportRow(row), source: row }))), [data, order]);
93
188
  const [selected, setSelected] = useState<string | null>(null);
94
- const screen = resource.data;
95
- const notices = [criteria.error, resource.data ? null : resource.error,
189
+ const screen = data?.screen;
190
+ const rankedBy = strategy === "csp" ? undefined : strategyRankedBy(strategy);
191
+ const notices = [criteria.error, data ? null : resource.error,
96
192
  leftOut.length ? `Ranks the first ${CSP_SCREEN_LIMIT} symbols; ${leftOut.length} left out: ${leftOut.join(", ")}` : null,
97
193
  ...(screen?.ivRankError ? [`IV rank unavailable: ${screen.ivRankError}`] : []),
98
194
  ...(screen?.rows ?? []).flatMap((row) => row.note ? [`${row.symbol}: ${row.note}`] : []),
99
- ...(screen ? cspScreenNotes(screen).slice(1) : [])].filter((value): value is string => !!value);
195
+ ...(screen ? cspScreenNotes(screen, rankedBy).slice(1) : [])].filter((value): value is string => !!value);
100
196
  usePaneNoticeFooter({ registrationId: "csp-screen-notices", notices, focused });
101
197
  // The bar names the delta and the expiry; the footer says only what changes.
102
198
  usePaneFooter("csp-screen", () => ({ info: [
103
199
  ...(resource.loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
104
200
  ] }), [resource.loading]);
105
- const criteriaText = criteria.value ? cspScreenCriteriaText(criteria.value, Date.now()) : "";
106
- // Enter opens the name's put in OSA, at the strike and expiry the screen read.
107
- const openScenario = (row: ScreenRow) => {
108
- const csp = row.source.csp;
109
- if (!csp) return;
110
- createPaneFromTemplate("options-scenario-pane", { symbol: row.symbol, values: { strategy: "csp", strike: String(csp.strike), expiration: csp.expiry } });
201
+ const now = Date.now();
202
+ const criteriaText = criteria.value ? cspScreenCriteriaText(criteria.value, now) : criteria.trade ? strategyScreenCriteriaText(criteria.trade, now) : "";
203
+ // Enter opens the name's trade in OSA: the put at the strike and expiry the screen read, any other trade as its legs.
204
+ const openScenario = (row: ScreenRow | TradeRow) => {
205
+ if ("csp" in row.source) {
206
+ const csp = row.source.csp;
207
+ if (!csp) return;
208
+ createPaneFromTemplate("options-scenario-pane", { symbol: row.symbol, values: { strategy: "csp", strike: String(csp.strike), expiration: csp.expiry } });
209
+ return;
210
+ }
211
+ const trade = row.source.trade;
212
+ if (!trade) return;
213
+ createPaneFromTemplate("options-scenario-pane", { symbol: row.symbol, values: { legs: osaLegs(row.source),
214
+ ...(trade.position.underlying ? { shares: String(trade.position.underlying.quantity) } : {}) } });
111
215
  };
112
216
  const handleKey = (event: DataTableKeyEvent): boolean => {
113
217
  if (event.ctrl || event.alt || event.meta || event.name !== "r") return false;
114
218
  void resource.reload();
115
219
  event.preventDefault?.(); event.stopPropagation?.(); return true;
116
220
  };
117
- const cell = (row: ScreenRow, id: string): { text: string; color?: string } => {
221
+ const putCell = (row: ScreenRow, id: string): { text: string; color?: string } => {
118
222
  switch (id) {
119
223
  case "rank": return { text: row.rank == null ? "" : String(row.rank), color: colors.textDim };
120
224
  case "symbol": return { text: row.symbol, color: colors.textBright };
@@ -133,33 +237,80 @@ export function CspScreenPane({ width, height, focused }: PaneProps) {
133
237
  default: return { text: "" };
134
238
  }
135
239
  };
136
- const deltaValue = criteria.value ? criteria.value.delta.toFixed(2) : CSP_SCREEN_DEFAULTS.delta.toFixed(2);
137
- const daysValue = String(criteria.value?.days ?? CSP_SCREEN_DEFAULTS.days);
240
+ const tradeCell = (row: TradeRow, id: string): DataTableCell => {
241
+ switch (id) {
242
+ case "rank": return { text: row.rank == null ? "" : String(row.rank), color: colors.textDim };
243
+ case "symbol": return { text: row.symbol, color: colors.textBright };
244
+ case "days": return { text: row.days == null ? "--" : String(row.days) };
245
+ case "trade": return { text: row.trade ?? "--" };
246
+ case "net": return { text: row.net == null ? "--" : moneyText(row.net) };
247
+ case "maxGain": return { text: riskText(row.maxGain, row.unlimited === "gain") };
248
+ case "maxLoss": return { text: riskText(row.maxLoss, row.unlimited === "loss") };
249
+ case "breakEven": return { text: row.breakEvens };
250
+ case "pop": return { text: percentText(row.pop, 0) };
251
+ // A return above zero reads as a gain; none, or nothing to rank, stays neutral.
252
+ case "returnPerYear": return { text: percentText(row.returnPerYear, 1), color: (row.returnPerYear ?? 0) > 0 ? colors.positive : undefined };
253
+ case "rewardRisk": return { text: rewardRiskText(row.rewardRisk, row.unlimited), value: row.rewardRisk };
254
+ case "ivRank": return { text: typeof row.ivRank === "number" ? row.ivRank.toFixed(0) : "--" };
255
+ case "spread": return { text: `${percentText(row.spread, 1)}${row.wide ? " wide" : ""}`, color: row.wide ? colors.warning : undefined };
256
+ case "earnings": return { text: earningsCell(row), value: row.earningsDate,
257
+ color: row.earningsBeforeExpiry === true ? colors.warning : colors.textDim };
258
+ default: return { text: "" };
259
+ }
260
+ };
261
+ const familyDelta = family.delta ?? CSP_SCREEN_DEFAULTS.delta;
262
+ const deltaValue = (criteria.value ?? criteria.trade)?.delta?.toFixed(2) ?? familyDelta.toFixed(2);
263
+ const wingValue = criteria.trade?.wingDelta?.toFixed(2) ?? (family.wing != null ? family.wing.toFixed(2) : "");
264
+ const defaultWing = family.wing == null ? "" : (family.wing < Number(deltaValue) ? family.wing : Number(deltaValue) / 2).toFixed(2);
265
+ const daysValue = String(ready?.days ?? CSP_SCREEN_DEFAULTS.days);
138
266
  // A date typed on the command line is the expiry's choice until a number of days is picked.
139
- const typedDate = criteria.value?.expiration != null ? cspTargetDate(criteria.value, Date.now()) : null;
267
+ const typedDate = ready?.expiration != null ? cspTargetDate(ready, now) : null;
268
+ const choices = tabFamilies(family.tab);
269
+ const filters: QueryBarFilter[] = [
270
+ { id: "symbols", kind: "text" as const, label: "Symbols", value: String(symbolsText ?? ""), placeholder: "JPM, GS, KO",
271
+ width: SYMBOLS_FIELD_WIDTH, debounceMs: SYMBOLS_DEBOUNCE_MS, focused, active: symbolsActive, onActiveChange: setSymbolsActive,
272
+ onChange: setSymbolsText },
273
+ ...(choices.length > 1 ? [{ id: "variant", label: family.tab === "vertical" ? "Spread" : "Side", value: strategy,
274
+ options: choices.map((choice) => ({ value: choice.id, label: choice.choice ?? choice.title })),
275
+ onChange: (value: string) => chooseStrategy(value as ScreenFamily) }] : []),
276
+ ...(family.delta != null ? [{ id: "delta", label: "Delta", value: deltaValue, defaultValue: familyDelta.toFixed(2),
277
+ options: withCurrent(DELTA_OPTIONS, deltaValue, (value) => value), onChange: (value: string) => setDelta(value) }] : []),
278
+ ...(family.wing != null ? [{ id: "wing", label: "Wing", value: wingValue, defaultValue: defaultWing,
279
+ options: withCurrent(WING_OPTIONS.filter((value) => Number(value) < Number(deltaValue)), wingValue, (value) => value),
280
+ onChange: (value: string) => setWingDelta(value) }] : []),
281
+ { id: "expiry", label: "Expiry", value: typedDate ?? daysValue, defaultValue: String(CSP_SCREEN_DEFAULTS.days),
282
+ options: [...withCurrent(DAYS_OPTIONS, daysValue, (value) => `${value} days`), ...(typedDate ? [{ value: typedDate, label: typedDate }] : [])],
283
+ onChange: (value: string) => { if (value === typedDate) return; setExpiration(""); setDays(value); } },
284
+ ];
285
+ const subject = strategy === "csp" ? "cash-secured puts" : family.title.toLowerCase();
286
+ const table = {
287
+ focused: focused && !symbolsActive, rootWidth: width, rootHeight: Math.max(2, height - 1 - tabRows),
288
+ getItemKey: (row: { symbol: string }) => row.symbol, sortColumnId: sort.columnId, sortDirection: sort.direction,
289
+ emptyStateTitle: symbols.length ? `No ${strategy === "csp" ? "puts" : subject} to rank.` : `Type symbols to rank their ${subject}.`,
290
+ onHeaderClick: (id: string) => setSort((current) => nextHeaderSort(current, id, { firstDirection: id === "rank" || id === "symbol" ? "asc" : "desc" })),
291
+ onActivate: openScenario, onRootKeyDown: handleKey,
292
+ getExportMetadata: (): Array<[string, string | undefined]> => [[strategy === "csp" ? "puts" : subject, criteriaText || undefined],
293
+ ["ranked by", strategy === "csp" ? "annualised yield on cash at the bid" : rankedBy],
294
+ ["wide", ready ? `spread over ${ready.maxSpreadPercent}% of the mid` : undefined],
295
+ ...(strategy === "csp" ? [] : [["money", STRATEGY_MONEY_UNIT] as [string, string]])],
296
+ };
297
+ // Whole columns give way, least needed first, so nothing scrolls sideways; a vertical scrollbar takes a cell.
298
+ const putColumns = useMemo(() => dropColumnsToFit(COLUMNS, width - 1, PUT_DROP_ORDER), [width]);
299
+ const tradeColumnWidth = tradeWidth(tradeRows);
300
+ const shownTradeColumns = useMemo(() => strategy === "csp" ? [] : dropColumnsToFit(tradeColumns(strategy, tradeColumnWidth), width - 1,
301
+ tradeDropOrder(strategy)), [strategy, tradeColumnWidth, width]);
302
+ const firstSymbol = (strategy === "csp" ? putRows : tradeRows)[0]?.symbol ?? "";
303
+ const selection = { kind: "id" as const, selectedId: selected ?? firstSymbol, getId: (row: { symbol: string }) => row.symbol, onChange: (id: string) => setSelected(id) };
140
304
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
141
- <QueryBar width={width} filters={[
142
- { id: "symbols", kind: "text" as const, label: "Symbols", value: String(symbolsText ?? ""), placeholder: "JPM, GS, KO",
143
- width: SYMBOLS_FIELD_WIDTH, debounceMs: SYMBOLS_DEBOUNCE_MS, focused, active: symbolsActive, onActiveChange: setSymbolsActive,
144
- onChange: setSymbolsText },
145
- { id: "delta", label: "Delta", value: deltaValue, defaultValue: CSP_SCREEN_DEFAULTS.delta.toFixed(2),
146
- options: withCurrent(DELTA_OPTIONS, deltaValue, (value) => value), onChange: (value: string) => setDelta(value) },
147
- { id: "expiry", label: "Expiry", value: typedDate ?? daysValue, defaultValue: String(CSP_SCREEN_DEFAULTS.days),
148
- options: [...withCurrent(DAYS_OPTIONS, daysValue, (value) => `${value} days`), ...(typedDate ? [{ value: typedDate, label: typedDate }] : [])],
149
- onChange: (value: string) => { if (value === typedDate) return; setExpiration(""); setDays(value); } },
150
- ]} />
151
- <PaneStatusBody subject="cash-secured puts" loading={resource.loading && !resource.data}
152
- error={criteria.error && !criteria.value ? criteria.error : !resource.data ? resource.error : null}>
153
- <DataTableView<ScreenRow> focused={focused && !symbolsActive} columns={COLUMNS} items={rows} rootWidth={width}
154
- rootHeight={Math.max(2, height - 1)}
155
- getItemKey={(row) => row.symbol} sortColumnId={sort.columnId} sortDirection={sort.direction}
156
- emptyStateTitle={symbols.length ? "No puts to rank." : "Type symbols to rank their cash-secured puts."}
157
- onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id, { firstDirection: id === "rank" || id === "symbol" ? "asc" : "desc" }))}
158
- selection={{ kind: "id", selectedId: selected ?? rows[0]?.symbol ?? "", getId: (row) => row.symbol, onChange: (id) => setSelected(id) }}
159
- onActivate={openScenario} onRootKeyDown={handleKey}
160
- getExportMetadata={() => [["puts", criteriaText || undefined], ["ranked by", "annualised yield on cash at the bid"],
161
- ["wide", criteria.value ? `spread over ${criteria.value.maxSpreadPercent}% of the mid` : undefined]]}
162
- renderCell={(row, column) => cell(row, column.id)} />
305
+ {tabStrip}
306
+ <QueryBar width={width} filters={filters} meta={strategy === "csp" ? undefined : STRATEGY_MONEY_UNIT} />
307
+ <PaneStatusBody subject={subject} loading={resource.loading && !data}
308
+ error={criteria.error && !ready ? criteria.error : !data ? resource.error : null}>
309
+ {strategy === "csp"
310
+ ? <DataTableView<ScreenRow> {...table} columns={putColumns} items={putRows} selection={selection}
311
+ renderCell={(row, column) => putCell(row, column.id)} />
312
+ : <DataTableView<TradeRow> {...table} columns={shownTradeColumns} items={tradeRows} selection={selection}
313
+ renderCell={(row, column) => tradeCell(row, column.id)} />}
163
314
  </PaneStatusBody>
164
315
  </Box>;
165
316
  }
@@ -31,7 +31,7 @@ export interface CspScreenCriteria {
31
31
  }
32
32
 
33
33
  export const CSP_SCREEN_DEFAULTS: Readonly<Omit<CspScreenCriteria, "expiration">> = { delta: 0.3, days: 30, maxSpreadPercent: 10 };
34
- /** The most names one screen reads: each is two chain requests. */
34
+ /** The most names one screen reads: each is up to two chain requests (the first lists the expiries, the next is the one traded), a calendar one more. */
35
35
  export const CSP_SCREEN_LIMIT = 20;
36
36
  const DAY_MS = 86_400_000;
37
37
  const CONCURRENCY = 4;
@@ -61,9 +61,9 @@ export interface CspScreen {
61
61
  freshness: HeadlessPaneFreshness;
62
62
  }
63
63
 
64
- const supplied = (value: unknown) => value != null && value !== "";
64
+ export const supplied = (value: unknown) => value != null && value !== "";
65
65
 
66
- function readNumber(value: unknown, flag: string, example: string): number {
66
+ export function readNumber(value: unknown, flag: string, example: string): number {
67
67
  const text = typeof value === "number" ? String(value) : typeof value === "string" ? value.trim().replace(/%$/, "") : "";
68
68
  const number = Number(text);
69
69
  if (!text || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text) || !Number.isFinite(number)) {
@@ -143,11 +143,16 @@ export interface CspScreenDependencies {
143
143
 
144
144
  interface ScreenedPut { csp: CashSecuredPut; delta: number | null; market: ScenarioMarketSnapshot }
145
145
 
146
- /** One name's put: OSA's csp seed at `--delta` on the listed expiry nearest the target, read as OSA reads it. */
147
- async function screenPut(
148
- instrument: InstrumentRef, criteria: CspScreenCriteria, dependencies: ScenarioLoaderDependencies, target: string,
149
- earningsDate: string | null, signal: AbortSignal | undefined,
150
- ): Promise<ScreenedPut> {
146
+ /**
147
+ * One name's chain on its listed expiry nearest `target` (YYYY-MM-DD), the
148
+ * catalogue's own chain when it is already on that expiry. A typed
149
+ * `expiration` whose last session has closed is refused with the live ones
150
+ * nearest it.
151
+ */
152
+ export async function loadScreenMarket(
153
+ instrument: InstrumentRef, criteria: Pick<CspScreenCriteria, "expiration">, dependencies: ScenarioLoaderDependencies, target: string,
154
+ signal: AbortSignal | undefined,
155
+ ): Promise<ScenarioMarketSnapshot> {
151
156
  const first = await loadScenarioMarket({ instrument, signal }, dependencies);
152
157
  // A typed date whose last session has closed is not stood in for by the next one: it says so, and names it.
153
158
  const now = dependencies.now?.() ?? Date.now();
@@ -160,8 +165,16 @@ async function screenPut(
160
165
  throw new Error(first.warnings.find((warning) => warning.startsWith("Options chain")) ?? "No listed option expiries");
161
166
  }
162
167
  const loaded = first.chain ? [...first.chain.puts, ...first.chain.calls][0]?.expiration : undefined;
163
- const market = loaded != null && expiryIsoDate(loaded) === expiryIsoDate(expiration)
168
+ return loaded != null && expiryIsoDate(loaded) === expiryIsoDate(expiration)
164
169
  ? first : await loadScenarioMarket({ instrument, expiration, signal }, dependencies);
170
+ }
171
+
172
+ /** One name's put: OSA's csp seed at `--delta` on the listed expiry nearest the target, read as OSA reads it. */
173
+ async function screenPut(
174
+ instrument: InstrumentRef, criteria: CspScreenCriteria, dependencies: ScenarioLoaderDependencies, target: string,
175
+ earningsDate: string | null, signal: AbortSignal | undefined,
176
+ ): Promise<ScreenedPut> {
177
+ const market = await loadScreenMarket(instrument, criteria, dependencies, target, signal);
165
178
  const settings = { symbol: instrument.symbol, exchange: instrument.exchange, strategy: "csp", delta: criteria.delta };
166
179
  const position = scenarioPositionFromSettings(settings, market);
167
180
  const leg = position?.legs[0];
@@ -201,13 +214,26 @@ function screenFreshness(markets: readonly ScenarioMarketSnapshot[], now: number
201
214
  };
202
215
  }
203
216
 
217
+ /** One name of a screen: what it found, or why it found nothing, with its IV rank either way. */
218
+ interface ScreenedName<T> { symbol: string; ivRank: number | null; value: T | null; note: string | null }
219
+
220
+ export interface ScreenedNames<T> {
221
+ names: ScreenedName<T>[];
222
+ missingRanks: string[];
223
+ ivRankError: string | null;
224
+ freshness: HeadlessPaneFreshness;
225
+ }
226
+
204
227
  /**
205
- * Each name's cash-secured put, ranked: one IV rank request, one earnings
206
- * calendar request and one Treasury curve for all of them.
228
+ * Each name screened by `screen`, four at a time: one IV rank request, one
229
+ * earnings calendar request and one Treasury curve for all of them. A name
230
+ * `screen` throws on is kept with the error as its note (`fallback` when the
231
+ * error has no message). `screen` returns the market that dates its row.
207
232
  */
208
- export async function loadCspScreen(
209
- instruments: readonly InstrumentRef[], criteria: CspScreenCriteria, dependencies: CspScreenDependencies, signal?: AbortSignal,
210
- ): Promise<CspScreen> {
233
+ export async function screenNames<T>(
234
+ instruments: readonly InstrumentRef[], dependencies: CspScreenDependencies, signal: AbortSignal | undefined, fallback: string,
235
+ screen: (instrument: InstrumentRef, earningsDate: string | null, scenario: ScenarioLoaderDependencies) => Promise<{ value: T; market: ScenarioMarketSnapshot }>,
236
+ ): Promise<ScreenedNames<T>> {
211
237
  const now = dependencies.scenario.now?.() ?? Date.now();
212
238
  // Every name reads the same curve, up to twice (the catalogue, then its expiry): asked for once, when the first name needs it.
213
239
  let curve: ReturnType<ScenarioLoaderDependencies["loadYieldCurve"]> | null = null;
@@ -218,29 +244,43 @@ export async function loadCspScreen(
218
244
  loadNextEarningsDates(dependencies.earnings, symbols, zonedDateKey(now, "America/New_York")),
219
245
  ]);
220
246
  signal?.throwIfAborted();
221
- const target = cspTargetDate(criteria, now);
222
247
  const markets: ScenarioMarketSnapshot[] = [];
223
248
  const missingRanks: string[] = [];
224
- const rows = await mapWithConcurrency(instruments, CONCURRENCY, async (instrument, index): Promise<Omit<CspScreenRow, "rank">> => {
249
+ const names = await mapWithConcurrency(instruments, CONCURRENCY, async (instrument, index): Promise<ScreenedName<T>> => {
225
250
  const symbol = symbols[index]!;
226
251
  const ivRow = ivRanks.rows.get(symbol);
227
252
  const rank = ivRow?.iv30?.rank;
228
253
  const ivRank = rank != null && Number.isFinite(rank) ? rank : null;
229
254
  try {
230
- const screened = await screenPut(instrument, criteria, scenario, target, earnings.get(symbol) ?? null, signal);
255
+ const screened = await screen(instrument, earnings.get(symbol) ?? null, scenario);
231
256
  markets.push(screened.market);
232
257
  if (ivRank == null && !ivRanks.error) {
233
258
  const reason = ivRankReason(ivRow);
234
259
  missingRanks.push(reason ? `${symbol} (${reason})` : symbol);
235
260
  }
236
- return { symbol, csp: screened.csp, delta: screened.delta, ivRank, wideSpread: cspWideSpread(screened.csp, criteria.maxSpreadPercent), note: null };
261
+ return { symbol, ivRank, value: screened.value, note: null };
237
262
  } catch (error) {
238
263
  if (signal?.aborted) throw error;
239
- return { symbol, csp: null, delta: null, ivRank, wideSpread: false, note: errorMessage(error) ?? "No put to sell" };
264
+ return { symbol, ivRank, value: null, note: errorMessage(error) ?? fallback };
240
265
  }
241
266
  });
242
- return { rows: rankCspScreen(rows), criteria, missingRanks: missingRanks.sort(), ivRankError: ivRanks.error,
243
- freshness: screenFreshness(markets, now) };
267
+ return { names, missingRanks: missingRanks.sort(), ivRankError: ivRanks.error, freshness: screenFreshness(markets, now) };
268
+ }
269
+
270
+ /** Each name's cash-secured put, ranked. */
271
+ export async function loadCspScreen(
272
+ instruments: readonly InstrumentRef[], criteria: CspScreenCriteria, dependencies: CspScreenDependencies, signal?: AbortSignal,
273
+ ): Promise<CspScreen> {
274
+ const target = cspTargetDate(criteria, dependencies.scenario.now?.() ?? Date.now());
275
+ const screened = await screenNames(instruments, dependencies, signal, "No put to sell", async (instrument, earningsDate, scenario) => {
276
+ const put = await screenPut(instrument, criteria, scenario, target, earningsDate, signal);
277
+ return { value: put, market: put.market };
278
+ });
279
+ const rows = screened.names.map(({ symbol, ivRank, value, note }): Omit<CspScreenRow, "rank"> => ({
280
+ symbol, csp: value?.csp ?? null, delta: value?.delta ?? null, ivRank,
281
+ wideSpread: value ? cspWideSpread(value.csp, criteria.maxSpreadPercent) : false, note }));
282
+ return { rows: rankCspScreen(rows), criteria, missingRanks: screened.missingRanks, ivRankError: screened.ivRankError,
283
+ freshness: screened.freshness };
244
284
  }
245
285
 
246
286
  /** `nearest 0.30 delta · expiry nearest 2026-11-21 (30 days)`: what each name's put is. */
@@ -249,8 +289,8 @@ export function cspScreenCriteriaText(criteria: CspScreenCriteria, now: number):
249
289
  + (criteria.expiration == null ? ` (${criteria.days} days)` : "");
250
290
  }
251
291
 
252
- /** `before 2026-11-04`, `after 2027-01-19`, `unknown`: whether the next report falls before the put expires, and its date. */
253
- export function cspEarningsText(csp: CashSecuredPut | null): string {
292
+ /** `before 2026-11-04`, `after 2027-01-19`, `unknown`: whether the next report falls before the put (or a trade's first leg) expires, and its date. */
293
+ export function cspEarningsText(csp: Pick<CashSecuredPut, "earnings"> | null): string {
254
294
  if (!csp) return "--";
255
295
  const { beforeExpiry, date } = csp.earnings;
256
296
  return beforeExpiry == null ? "unknown" : `${beforeExpiry ? "before" : "after"} ${date}`;
@@ -87,6 +87,13 @@ function newYorkCalendarDate(time: number): string {
87
87
  return newYorkDate.format(time);
88
88
  }
89
89
 
90
+ /** The valuation date in New York, and the calendar days from it to `expiry` (YYYY-MM-DD), 0 once it is reached. */
91
+ export function calendarDaysToExpiry(asOf: number, expiry: string): { valuationDate: string; daysToExpiry: number } {
92
+ const valuationDate = newYorkCalendarDate(asOf);
93
+ return { valuationDate,
94
+ daysToExpiry: Math.max(0, Math.round((Date.parse(`${expiry}T00:00:00Z`) - Date.parse(`${valuationDate}T00:00:00Z`)) / DAY_MS)) };
95
+ }
96
+
90
97
  /** Whether the next report falls before the put expires; unknown without an upcoming date. */
91
98
  export function earningsBeforeExpiry(earningsDate: string | null | undefined, valuationDate: string, expiry: string): CspEarnings {
92
99
  if (!earningsDate || !/^\d{4}-\d{2}-\d{2}$/.test(earningsDate) || earningsDate < valuationDate) return { date: null, beforeExpiry: null };
@@ -114,8 +121,7 @@ export function cashSecuredPut(
114
121
  if (position.legs.length !== 1 || !leg || leg.side !== "put" || leg.quantity >= 0 || position.underlying) return null;
115
122
  const contracts = -leg.quantity;
116
123
  const expiry = new Date(leg.expiration * 1000).toISOString().slice(0, 10);
117
- const valuationDate = newYorkCalendarDate(position.asOf);
118
- const daysToExpiry = Math.max(0, Math.round((Date.parse(`${expiry}T00:00:00Z`) - Date.parse(`${valuationDate}T00:00:00Z`)) / DAY_MS));
124
+ const { valuationDate, daysToExpiry } = calendarDaysToExpiry(position.asOf, expiry);
119
125
  const cashSecured = leg.strike * leg.multiplier * contracts;
120
126
  const sharesSold = leg.multiplier * contracts;
121
127
  const atEntry = putYield(leg.price, cashSecured, sharesSold, daysToExpiry);
@@ -26,24 +26,26 @@ export const optionsScenarioModule: PluginModule = {
26
26
  ],
27
27
  },
28
28
  }, {
29
- id: "csp-screen", name: "Cash-Secured Put Screen", icon: "V", component: CspScreenPane,
29
+ id: "csp-screen", name: "Options Strategy Screen", icon: "V", component: CspScreenPane,
30
30
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 144, height: 24 },
31
31
  tableExport: true, headless: cspScreenHeadless,
32
32
  settings: {
33
- title: "Cash-Secured Put Screen",
33
+ title: "Options Strategy Screen",
34
34
  fields: [
35
35
  { key: "symbols", label: "Symbols", type: "text", placeholder: "JPM, GS, KO" },
36
36
  { key: "maxSpread", label: "Wide spread over (%)", type: "text", placeholder: "10",
37
- description: "A put whose bid/ask spread is over this percent of its mid is marked wide. It is still ranked." },
37
+ description: "A trade whose bid/ask spread is over this percent of its mid is marked wide. It is still ranked." },
38
38
  ],
39
39
  },
40
40
  }],
41
41
  paneTemplates: [{
42
- id: "csp-screen-pane", paneId: "csp-screen", label: "Cash-Secured Put Screen",
43
- description: "Cash-secured puts across a list, ranked by annualised yield at the bid, with % OTM, delta, IV rank, spread and an earnings flag.",
44
- // The put-selling phrases lead here first, then to OSA for one name.
42
+ id: "csp-screen-pane", paneId: "csp-screen", label: "Options Strategy Screen",
43
+ description: "Cash-secured puts, covered calls, verticals, iron condors, straddles, strangles and calendars across a list, ranked, "
44
+ + "with max gain and loss, break-evens, probability of profit, IV rank, spread and an earnings flag.",
45
+ // The put-selling phrases lead here first, then to OSA for one name; so do the strategies it screens.
45
46
  keywords: ["csp", "cash secured put", "cash-secured puts", "put selling", "selling puts", "sell put", "sell puts", "short put", "wheel",
46
- "put screen", "premium yield"],
47
+ "put screen", "premium yield", "strategy screen", "options screener", "covered call", "covered calls", "iron condor", "strangle",
48
+ "straddle screen", "calendar spread", "bull put spread", "bear call spread", "vertical spread"],
47
49
  shortcut: { prefix: "CSP", argKind: "ticker-list" as const }, headless: cspScreenHeadless,
48
50
  createInstance: (_context, options) => ({
49
51
  title: "CSP", placement: "floating" as const,