@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
@@ -0,0 +1,532 @@
1
+ import type { InstrumentRef } from "../../../market-data/request-types";
2
+ import type { OptionsChain } from "../../../types/financials";
3
+ import { expiryIsoDate } from "../../../utils/option-expiry";
4
+ import { probabilityAbove } from "../shared/volatility";
5
+ import {
6
+ loadScenarioMarket, nearestDeltaContract, seededLeg, seedScreenPosition,
7
+ type QuotedChain, type QuotedContract, type ScenarioLoaderDependencies, type ScenarioMarketSnapshot, type ScreenSeed, type StrategyPricing,
8
+ type UnusableChainText,
9
+ } from "./client";
10
+ import { calendarDaysToExpiry, earningsBeforeExpiry, type CspEarnings } from "./csp";
11
+ import {
12
+ cspTargetDate, loadScreenMarket, nearestListedExpiry, readCspScreenCriteria, readNumber, screenNames, supplied,
13
+ type CspScreen, type CspScreenCriteria, type CspScreenDependencies,
14
+ } from "./csp-screen";
15
+ import { seededLegQuotes } from "./hedge";
16
+ import { expiryRisk, optionExpirationClose, scenarioValue, type ScenarioPosition } from "./model";
17
+
18
+ /**
19
+ * The options strategy screen (docs/research-data.md#options-strategy-screen):
20
+ * the cash-secured put screen's names, expiry and limits, with each family's
21
+ * legs picked from the chain the way the put is, by delta on the listed expiry
22
+ * nearest the target, entered at their midpoints as OSA's seeds are, and read
23
+ * with OSA's expiry risk.
24
+ */
25
+ export const STRATEGY_FAMILIES = ["csp", "covered-call", "bull-put", "bear-call", "bull-call", "bear-put", "iron-condor",
26
+ "short-straddle", "long-straddle", "short-strangle", "long-strangle", "calendar"] as const;
27
+ export type StrategyFamily = typeof STRATEGY_FAMILIES[number];
28
+ /** Every family but the cash-secured put, which keeps its own screen. */
29
+ export type ScreenFamily = Exclude<StrategyFamily, "csp">;
30
+
31
+ export const STRATEGY_TABS = [
32
+ { value: "csp", label: "Cash-secured put" }, { value: "covered-call", label: "Covered call" }, { value: "vertical", label: "Vertical" },
33
+ { value: "iron-condor", label: "Iron condor" }, { value: "straddle", label: "Straddle" }, { value: "strangle", label: "Strangle" },
34
+ { value: "calendar", label: "Calendar" },
35
+ ] as const;
36
+ export type StrategyTab = typeof STRATEGY_TABS[number]["value"];
37
+
38
+ export interface StrategyFamilyDef {
39
+ id: StrategyFamily;
40
+ /** The report's title: `Iron condors`. */
41
+ title: string;
42
+ tab: StrategyTab;
43
+ /** Its name among its tab's choices, when the tab has several: `Bull put`. */
44
+ choice?: string;
45
+ /** Other words `--strategy` takes for it. */
46
+ aliases: string[];
47
+ /** The delta its legs are nearest by default; null when it takes the strike nearest the money. */
48
+ delta: number | null;
49
+ /** The delta its legs further out of the money (a vertical's or a condor's wings) are nearest by default; null without them. */
50
+ wing: number | null;
51
+ /**
52
+ * How its rows rank: an income trade's annualised return, a debit trade's
53
+ * reward to risk, or IV rank high or low first.
54
+ */
55
+ rank: "return" | "reward-risk" | "iv-rank-high" | "iv-rank-low";
56
+ /**
57
+ * Whether it takes in premium or pays it. That names its money column and its
58
+ * measure: an income trade's annualised return, a debit trade's reward to risk.
59
+ */
60
+ net: "credit" | "debit";
61
+ /** Which side of the trade its plain words start with. */
62
+ lead: "sell" | "buy";
63
+ }
64
+
65
+ const family = (def: StrategyFamilyDef) => def;
66
+ export const STRATEGY_FAMILY_DEFS: readonly StrategyFamilyDef[] = [
67
+ family({ id: "csp", title: "Cash-secured puts", tab: "csp", aliases: ["cash-secured-put"], delta: 0.3, wing: null, rank: "return", net: "credit", lead: "sell" }),
68
+ family({ id: "covered-call", title: "Covered calls", tab: "covered-call", aliases: ["cc", "covered"], delta: 0.3, wing: null, rank: "return", net: "credit", lead: "sell" }),
69
+ family({ id: "bull-put", title: "Bull put spreads", tab: "vertical", choice: "Bull put", aliases: ["put-credit", "bull-put-spread"],
70
+ delta: 0.3, wing: 0.15, rank: "return", net: "credit", lead: "sell" }),
71
+ family({ id: "bear-call", title: "Bear call spreads", tab: "vertical", choice: "Bear call", aliases: ["call-credit", "bear-call-spread"],
72
+ delta: 0.3, wing: 0.15, rank: "return", net: "credit", lead: "sell" }),
73
+ family({ id: "bull-call", title: "Bull call spreads", tab: "vertical", choice: "Bull call", aliases: ["call-debit", "bull-call-spread"],
74
+ delta: 0.5, wing: 0.3, rank: "reward-risk", net: "debit", lead: "buy" }),
75
+ family({ id: "bear-put", title: "Bear put spreads", tab: "vertical", choice: "Bear put", aliases: ["put-debit", "bear-put-spread"],
76
+ delta: 0.5, wing: 0.3, rank: "reward-risk", net: "debit", lead: "buy" }),
77
+ family({ id: "iron-condor", title: "Iron condors", tab: "iron-condor", aliases: ["ic", "condor"], delta: 0.2, wing: 0.1, rank: "return", net: "credit", lead: "sell" }),
78
+ family({ id: "short-straddle", title: "Short straddles", tab: "straddle", choice: "Short", aliases: [], delta: null, wing: null, rank: "iv-rank-high", net: "credit", lead: "sell" }),
79
+ family({ id: "long-straddle", title: "Long straddles", tab: "straddle", choice: "Long", aliases: [], delta: null, wing: null, rank: "iv-rank-low", net: "debit", lead: "buy" }),
80
+ family({ id: "short-strangle", title: "Short strangles", tab: "strangle", choice: "Short", aliases: [], delta: 0.2, wing: null, rank: "iv-rank-high", net: "credit", lead: "sell" }),
81
+ family({ id: "long-strangle", title: "Long strangles", tab: "strangle", choice: "Long", aliases: [], delta: 0.3, wing: null, rank: "iv-rank-low", net: "debit", lead: "buy" }),
82
+ family({ id: "calendar", title: "Call calendars", tab: "calendar", aliases: ["call-calendar", "calendar-spread"], delta: 0.5, wing: null, rank: "reward-risk", net: "debit", lead: "sell" }),
83
+ ];
84
+
85
+ export function strategyFamilyDef(id: StrategyFamily): StrategyFamilyDef {
86
+ return STRATEGY_FAMILY_DEFS.find((def) => def.id === id)!;
87
+ }
88
+
89
+ /** The families under a tab, its first one the tab's default. */
90
+ export const tabFamilies = (tab: StrategyTab) => STRATEGY_FAMILY_DEFS.filter((def) => def.tab === tab);
91
+
92
+ /** `--strategy`, by its name or an alias, in any case; the cash-secured put when not set. */
93
+ export function readStrategyFamily(value: unknown): StrategyFamily {
94
+ if (!supplied(value)) return "csp";
95
+ const text = String(value).trim().toLowerCase().replaceAll(/[\s_]+/g, "-");
96
+ const def = STRATEGY_FAMILY_DEFS.find((entry) => entry.id === text || entry.aliases.includes(text));
97
+ if (!def) throw new Error(`Invalid --strategy "${String(value)}". Use one of ${STRATEGY_FAMILIES.join(", ")}.`);
98
+ return def.id;
99
+ }
100
+
101
+ /** Calendar days after the sold expiry a calendar's bought one is nearest. */
102
+ const CALENDAR_GAP_DAYS = 30;
103
+ const DAY_MS = 86_400_000;
104
+
105
+ export interface StrategyScreenCriteria extends Omit<CspScreenCriteria, "delta"> {
106
+ strategy: ScreenFamily;
107
+ /** The absolute delta the legs are nearest; null for a straddle, which takes the strike nearest the money. */
108
+ delta: number | null;
109
+ /** The absolute delta a vertical's or a condor's legs further out of the money are nearest; null without them. */
110
+ wingDelta: number | null;
111
+ }
112
+
113
+ const deltaExample = "a delta between 0 and 1, such as 0.30";
114
+
115
+ /**
116
+ * A family's criteria from `fn CSP --strategy` options or the pane's settings:
117
+ * the expiry and the spread limit as the put screen reads them, `--delta` and
118
+ * `--wing-delta` at the family's defaults where not set. A wing's default
119
+ * that is not further out of the money than `--delta` is half of it.
120
+ */
121
+ export function readStrategyScreenCriteria(strategy: ScreenFamily, options: Record<string, unknown>): StrategyScreenCriteria {
122
+ const def = strategyFamilyDef(strategy);
123
+ const { days, expiration, maxSpreadPercent } = readCspScreenCriteria({ ...options, delta: undefined });
124
+ if (def.delta == null && supplied(options.delta)) {
125
+ throw new Error(`--delta does not apply to ${def.title.toLowerCase()}: they take the strike nearest the money`);
126
+ }
127
+ if (def.wing == null && supplied(options.wingDelta)) {
128
+ throw new Error(`--wing-delta sets the far legs of a vertical or an iron condor; ${def.title.toLowerCase()} have none`);
129
+ }
130
+ if (def.delta == null) return { strategy, delta: null, wingDelta: null, days, expiration, maxSpreadPercent };
131
+ const delta = supplied(options.delta) ? Math.abs(readNumber(options.delta, "delta", deltaExample)) : def.delta;
132
+ if (!(delta > 0 && delta < 1)) throw new Error(`Invalid --delta "${String(options.delta)}". Use ${deltaExample}.`);
133
+ let wingDelta: number | null = null;
134
+ if (def.wing != null) {
135
+ wingDelta = supplied(options.wingDelta) ? Math.abs(readNumber(options.wingDelta, "wing-delta", "a delta between 0 and 1, such as 0.15"))
136
+ : def.wing < delta ? def.wing : delta / 2;
137
+ if (!(wingDelta > 0 && wingDelta < 1)) throw new Error(`Invalid --wing-delta "${String(options.wingDelta)}". Use a delta between 0 and 1, such as 0.15.`);
138
+ if (!(wingDelta < delta)) {
139
+ throw new Error(`--wing-delta ${wingDelta.toFixed(2)} must be below --delta ${delta.toFixed(2)}: the wings sit further out of the money`);
140
+ }
141
+ }
142
+ return { strategy, delta, wingDelta, days, expiration, maxSpreadPercent };
143
+ }
144
+
145
+ /**
146
+ * Each family's legs from one quoted chain, one contract each: a short side's
147
+ * strike nearest `delta` (a put's as its delta reads without its sign), a
148
+ * vertical's or a condor's far legs nearest `wingDelta` and strictly further
149
+ * out of the money, a straddle at the strike nearest spot quoted on both
150
+ * sides when it is near the money (`straddleReach`), a calendar's call nearest
151
+ * `delta` among the strikes quoted on both expiries, sold on `near` and bought
152
+ * on `far`. `unusable` words a pick the chain has no quote for.
153
+ */
154
+ function pickStrategyLegs(
155
+ criteria: Pick<StrategyScreenCriteria, "strategy" | "delta" | "wingDelta">, quoted: QuotedChain, pricing: StrategyPricing,
156
+ unusable: UnusableChainText, near: number, far?: number,
157
+ ): ScreenSeed {
158
+ const { strategy } = criteria;
159
+ const delta = criteria.delta ?? 0.5;
160
+ const wing = criteria.wingDelta ?? delta / 2;
161
+ const onNear = (list: QuotedContract[]) => list.filter(({ contract }) => contract.expiration === near);
162
+ const calls = onNear(quoted.calls);
163
+ const puts = onNear(quoted.puts);
164
+ const at = (list: QuotedContract[], target: number, what: string) => {
165
+ const picked = nearestDeltaContract(list, list[0]?.side === "put" ? -target : target, pricing.spot, pricing, quoted.parityFor);
166
+ if (!picked) throw new Error(unusable(what));
167
+ return picked;
168
+ };
169
+ const below = (list: QuotedContract[], strike: number) => list.filter(({ contract }) => contract.strike < strike);
170
+ const above = (list: QuotedContract[], strike: number) => list.filter(({ contract }) => contract.strike > strike);
171
+ const strike = (leg: QuotedContract) => strikeText(leg.contract.strike);
172
+ switch (strategy) {
173
+ case "covered-call":
174
+ return { legs: [seededLeg(at(calls, delta, "call to sell"), -1)], shares: 100 };
175
+ case "bull-put": {
176
+ const short = at(puts, delta, "put to sell");
177
+ return { legs: [seededLeg(short, -1), seededLeg(at(below(puts, short.contract.strike), wing, `put to buy below the ${strike(short)} put`), 1)] };
178
+ }
179
+ case "bear-call": {
180
+ const short = at(calls, delta, "call to sell");
181
+ return { legs: [seededLeg(short, -1), seededLeg(at(above(calls, short.contract.strike), wing, `call to buy above the ${strike(short)} call`), 1)] };
182
+ }
183
+ case "bull-call": {
184
+ const long = at(calls, delta, "call to buy");
185
+ return { legs: [seededLeg(long, 1), seededLeg(at(above(calls, long.contract.strike), wing, `call to sell above the ${strike(long)} call`), -1)] };
186
+ }
187
+ case "bear-put": {
188
+ const long = at(puts, delta, "put to buy");
189
+ return { legs: [seededLeg(long, 1), seededLeg(at(below(puts, long.contract.strike), wing, `put to sell below the ${strike(long)} put`), -1)] };
190
+ }
191
+ case "iron-condor": {
192
+ const put = at(puts, delta, "put to sell");
193
+ const call = at(above(calls, put.contract.strike), delta, `call to sell above the ${strike(put)} put`);
194
+ return { legs: [seededLeg(put, -1), seededLeg(call, -1),
195
+ seededLeg(at(below(puts, put.contract.strike), wing, `put to buy below the ${strike(put)} put`), 1),
196
+ seededLeg(at(above(calls, call.contract.strike), wing, `call to buy above the ${strike(call)} call`), 1)] };
197
+ }
198
+ case "short-straddle":
199
+ case "long-straddle": {
200
+ // OSA's straddle: the strike nearest spot that has a call and a put quoted.
201
+ const call = calls.find(({ contract }) => puts.some((put) => put.contract.strike === contract.strike));
202
+ const put = call && puts.find(({ contract }) => contract.strike === call.contract.strike);
203
+ if (!call || !put) throw new Error(unusable("straddle"));
204
+ const reach = straddleReach([...calls, ...puts].map(({ contract }) => contract.strike), pricing.spot);
205
+ if (Math.abs(call.contract.strike - pricing.spot) > reach) {
206
+ throw new Error(`No put and call both quoted within ${reach.toFixed(2)} of spot ${pricing.spot.toFixed(2)}: `
207
+ + `the nearest is the ${strike(call)} strike`);
208
+ }
209
+ const quantity = strategy === "short-straddle" ? -1 : 1;
210
+ return { legs: [seededLeg(put, quantity), seededLeg(call, quantity)] };
211
+ }
212
+ case "short-strangle":
213
+ case "long-strangle": {
214
+ const quantity = strategy === "short-strangle" ? -1 : 1;
215
+ const verb = quantity < 0 ? "sell" : "buy";
216
+ const put = at(puts, delta, `put to ${verb}`);
217
+ return { legs: [seededLeg(put, quantity), seededLeg(at(above(calls, put.contract.strike), delta, `call to ${verb} above the ${strike(put)} put`), quantity)] };
218
+ }
219
+ case "calendar": {
220
+ // Only a strike quoted on both expiries can be sold on one and bought on the other.
221
+ const later = new Map(quoted.calls.filter(({ contract }) => contract.expiration === far).map((call) => [call.contract.strike, call]));
222
+ const sold = at(calls.filter(({ contract }) => later.has(contract.strike)), delta,
223
+ `call to sell at a strike also quoted on ${far == null ? "a later expiry" : expiryIsoDate(far)}`);
224
+ return { legs: [seededLeg(sold, -1), seededLeg(later.get(sold.contract.strike)!, 1)] };
225
+ }
226
+ }
227
+ }
228
+
229
+ /** A straddle's strike is at the money when it is this share of spot from it, or half a strike step where strikes are wider. */
230
+ const STRADDLE_REACH = 0.025;
231
+
232
+ /**
233
+ * How far from spot a straddle's strike may sit: 2.5% of spot, or half the
234
+ * usual step between the quoted strikes where they are listed wider apart, so
235
+ * a chain whose strikes near the money are missing quotes skips the name
236
+ * rather than trade a strike well away from it.
237
+ */
238
+ function straddleReach(strikes: readonly number[], spot: number): number {
239
+ const sorted = [...new Set(strikes)].sort((a, b) => a - b);
240
+ const steps = sorted.slice(1).map((value, index) => value - sorted[index]!).sort((a, b) => a - b);
241
+ const step = steps.length ? steps[Math.floor(steps.length / 2)]! : 0;
242
+ return Math.max(spot * STRADDLE_REACH, step / 2);
243
+ }
244
+
245
+ /** A strike as a trader says it: `92.5`, `105`. */
246
+ const strikeText = (value: number) => String(Number(value.toFixed(4)));
247
+ const legExpiry = (expiration: number) => new Date(expiration * 1000).toISOString().slice(0, 10);
248
+
249
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
250
+ /** `Dec 18`: a later leg's expiry, the row's own being its DTE. */
251
+ const monthDay = (expiration: number) => {
252
+ const date = new Date(expiration * 1000);
253
+ return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
254
+ };
255
+
256
+ /**
257
+ * The trade in plain words, one part per side, the side the family leads
258
+ * with first, each leg its strike and `p` or `c`: `Sell 95p`, `buy 90p`;
259
+ * `Sell 105c`, `buy 100 shares`; `Sell 95p/105c`, `buy 90p/110c`. A leg on a
260
+ * later expiry than the row's names it: a calendar's `buy Dec 18 95c`.
261
+ */
262
+ function strategyTradeParts(strategy: StrategyFamily, position: Pick<ScenarioPosition, "legs" | "underlying">): string[] {
263
+ const first = Math.min(...position.legs.map((leg) => leg.expiration));
264
+ const name = (leg: ScenarioPosition["legs"][number]) =>
265
+ `${leg.expiration === first ? "" : `${monthDay(leg.expiration)} `}${strikeText(leg.strike)}${leg.side === "put" ? "p" : "c"}`;
266
+ const sides = {
267
+ sell: position.legs.filter((leg) => leg.quantity < 0).map(name),
268
+ buy: [...(position.underlying ? [`${position.underlying.quantity} shares`] : []), ...position.legs.filter((leg) => leg.quantity > 0).map(name)],
269
+ };
270
+ const order = strategyFamilyDef(strategy).lead === "sell" ? ["sell", "buy"] as const : ["buy", "sell"] as const;
271
+ return order.filter((side) => sides[side].length)
272
+ .map((side, index) => `${index ? side : side === "sell" ? "Sell" : "Buy"} ${sides[side].join("/")}`);
273
+ }
274
+
275
+ /** The trade in plain words, its parts in one line: `Sell 95p/105c · buy 90p/110c`. */
276
+ const strategyTradeText = (strategy: StrategyFamily, position: Pick<ScenarioPosition, "legs" | "underlying">) =>
277
+ strategyTradeParts(strategy, position).join(" · ");
278
+
279
+ const finite = (value: number | null | undefined): value is number => typeof value === "number" && Number.isFinite(value);
280
+
281
+ export interface StrategyTrade {
282
+ strategy: ScreenFamily;
283
+ position: ScenarioPosition;
284
+ /** The legs in plain words, the side the family leads with first: `Sell 95p/105c · buy 90p/110c`. */
285
+ text: string;
286
+ /** The first expiry, YYYY-MM-DD: when the trade is read. */
287
+ expiry: string;
288
+ /** A calendar's bought expiry; null for the rest. */
289
+ farExpiry: string | null;
290
+ daysToExpiry: number;
291
+ /** Valuation date in New York, YYYY-MM-DD. */
292
+ valuationDate: string;
293
+ /** The spot the option quotes imply, which the trade is valued from. */
294
+ spot: number;
295
+ /** Per share: the options' premium at their midpoints, taken in when positive, paid when negative. */
296
+ net: number;
297
+ /** The same premium for one contract a leg, in currency, as OSA counts money: what the Credit or Debit column shows. */
298
+ netAmount: number;
299
+ /** In currency for one contract a leg; null when unlimited. */
300
+ maxGain: number | null;
301
+ maxLoss: number | null;
302
+ /** Which side has no limit: a long straddle's gain, a short one's loss. */
303
+ unlimited: "gain" | "loss" | null;
304
+ breakEvens: number[];
305
+ probabilityOfProfit: number | null;
306
+ /**
307
+ * An income trade's return to expiry: max gain over max loss, or for a short
308
+ * straddle or strangle, whose loss has no limit, the credit over the cash
309
+ * that secures its put (the put screen's yield on cash). Null for a debit trade.
310
+ */
311
+ returnToExpiry: number | null;
312
+ /** Return to expiry x 365 / days to expiry; null on the expiry day. */
313
+ annualizedReturn: number | null;
314
+ /** A debit trade's max gain over its max loss; null for an income trade, an unlimited gain, or nothing at risk. */
315
+ rewardToRisk: number | null;
316
+ /** The legs' spreads added up and over the net midpoint; null when a leg has no two-sided quote. */
317
+ quote: { spread: number; spreadOfMid: number } | null;
318
+ /** The next report against the last expiry: a calendar's bought one, which an earnings move between its expiries reaches. */
319
+ earnings: CspEarnings;
320
+ }
321
+
322
+ interface TradeRisk { maxGain: number | null; maxLoss: number | null; unlimited: StrategyTrade["unlimited"]; breakEvens: number[] }
323
+
324
+ const firstClose = (position: ScenarioPosition) => Math.min(...position.legs.map((leg) => optionExpirationClose(leg.expiration)));
325
+
326
+ /**
327
+ * A calendar read at its sold leg's expiry, with the bought one still open at
328
+ * its IV (OSA's scenario value): the most it makes is where the sold call
329
+ * expires worthless at its strike, the most it loses the debit, and its
330
+ * break-evens are where that value crosses the debit.
331
+ */
332
+ function calendarRisk(position: ScenarioPosition, net: number): TradeRisk {
333
+ const close = firstClose(position);
334
+ const pnl = (spot: number) => scenarioValue(position, spot, close).pnl;
335
+ const strikes = [...new Set(position.legs.map((leg) => leg.strike))];
336
+ const multiplier = position.legs[0]?.multiplier ?? 100;
337
+ const steps = 400;
338
+ const low = position.spot * 0.2;
339
+ const high = position.spot * 3;
340
+ const grid = [...new Set([...Array.from({ length: steps + 1 }, (_, index) => low + (high - low) * index / steps), ...strikes])].sort((a, b) => a - b);
341
+ const values = grid.map(pnl);
342
+ const breakEvens: number[] = [];
343
+ for (let index = 1; index < grid.length; index++) {
344
+ let [left, right] = [grid[index - 1]!, grid[index]!];
345
+ let leftValue = values[index - 1]!;
346
+ if ((leftValue < 0) === (values[index]! < 0)) continue;
347
+ for (let step = 0; step < 60; step++) {
348
+ const middle = (left + right) / 2;
349
+ const value = pnl(middle);
350
+ if ((value < 0) === (leftValue < 0)) { left = middle; leftValue = value; } else right = middle;
351
+ }
352
+ breakEvens.push((left + right) / 2);
353
+ }
354
+ return { maxGain: Math.max(0, ...strikes.map(pnl)), maxLoss: Math.max(0, -net) * multiplier, unlimited: null, breakEvens };
355
+ }
356
+
357
+ /**
358
+ * The chance the trade is in profit when its first leg expires: the
359
+ * Black-Scholes-Merton probability the underlying ends in each stretch
360
+ * between break-evens whose P&L is above zero (OSA's scenario value at that
361
+ * close), each break-even at the IV of the expiring leg whose strike is
362
+ * nearest it, from the spot the quotes imply at the scenario's rate and
363
+ * dividend yield.
364
+ */
365
+ function probabilityOfProfit(position: ScenarioPosition, breakEvens: readonly number[]): number | null {
366
+ if (!position.legs.length) return null;
367
+ const close = firstClose(position);
368
+ const days = Math.max(0, (close - position.asOf) / DAY_MS);
369
+ const expiring = position.legs.filter((leg) => optionExpirationClose(leg.expiration) === close);
370
+ const pnl = (spot: number) => scenarioValue(position, spot, close).pnl;
371
+ const above = (level: number) => {
372
+ if (level <= 0) return 1;
373
+ if (!Number.isFinite(level)) return 0;
374
+ const leg = expiring.reduce((best, entry) => Math.abs(entry.strike - level) < Math.abs(best.strike - level) ? entry : best);
375
+ return probabilityAbove({ spot: position.spot, strike: level, daysToExpiry: days, rate: position.rate,
376
+ dividendYield: position.dividendYield, volatility: leg.volatility });
377
+ };
378
+ const bounds = [0, ...breakEvens.filter((value) => value > 0), Number.POSITIVE_INFINITY];
379
+ let total = 0;
380
+ for (let index = 1; index < bounds.length; index++) {
381
+ const [low, high] = [bounds[index - 1]!, bounds[index]!];
382
+ const probe = Number.isFinite(high) ? (low + high) / 2 : low > 0 ? low * 1.5 : position.spot;
383
+ if (pnl(probe) > 0) total += Math.max(0, above(low) - above(high));
384
+ }
385
+ return Math.min(1, total);
386
+ }
387
+
388
+ /**
389
+ * A trade's figures, as the screen's row and its export read them; `quotes`
390
+ * are its legs' bids and asks. Mids that cross (an income trade that would pay
391
+ * to open, a debit trade that would be paid) leave the name out.
392
+ */
393
+ function strategyTrade(
394
+ strategy: ScreenFamily, position: ScenarioPosition, quotes: ReadonlyMap<string, { bid: number; ask: number }>, earningsDate: string | null | undefined,
395
+ ): StrategyTrade {
396
+ const def = strategyFamilyDef(strategy);
397
+ const expirations = [...new Set(position.legs.map((leg) => leg.expiration))].sort((a, b) => a - b);
398
+ const expiry = legExpiry(expirations[0]!);
399
+ const farExpiry = expirations.length > 1 ? legExpiry(expirations.at(-1)!) : null;
400
+ const { valuationDate, daysToExpiry } = calendarDaysToExpiry(position.asOf, expiry);
401
+ const net = Math.round(position.legs.reduce((sum, leg) => sum - leg.quantity * leg.price, 0) * 1e8) / 1e8;
402
+ if (def.net === "credit" ? !(net > 0) : !(net < 0)) {
403
+ throw new Error(`Crossed mids: the legs ${net > 0 ? `take in ${net.toFixed(2)}` : net < 0 ? `cost ${(-net).toFixed(2)}` : "net to nothing"} `
404
+ + `where the trade ${def.net === "credit" ? "takes in a credit" : "pays a debit"}`);
405
+ }
406
+ const multiplier = position.legs[0]!.multiplier;
407
+ let risk: TradeRisk;
408
+ if (expirations.length > 1) risk = calendarRisk(position, net);
409
+ else {
410
+ const expiryRisks = expiryRisk(position);
411
+ risk = { maxGain: expiryRisks.unlimitedProfit ? null : expiryRisks.maxProfit, maxLoss: expiryRisks.unlimitedLoss ? null : expiryRisks.maxLoss,
412
+ unlimited: expiryRisks.unlimitedProfit ? "gain" : expiryRisks.unlimitedLoss ? "loss" : null, breakEvens: expiryRisks.breakevens };
413
+ }
414
+ const riskRatio = finite(risk.maxGain) && finite(risk.maxLoss) && risk.maxLoss > 0 ? risk.maxGain / risk.maxLoss : null;
415
+ // A short straddle or strangle has no max loss to divide by: its credit is read on the cash its put would take, as the put screen's yield is.
416
+ const securedPut = risk.unlimited === "loss" ? position.legs.find((leg) => leg.side === "put" && leg.quantity < 0) : undefined;
417
+ const returnToExpiry = def.net === "debit" ? null
418
+ : riskRatio ?? (securedPut ? net * multiplier / (securedPut.strike * securedPut.multiplier * -securedPut.quantity) : null);
419
+ const legQuotes = position.legs.map((leg) => quotes.get(leg.id));
420
+ const spread = legQuotes.every((quote) => quote && quote.ask >= quote.bid && quote.bid > 0)
421
+ ? legQuotes.reduce((sum, quote) => sum + quote!.ask - quote!.bid, 0) : null;
422
+ return {
423
+ strategy, position, text: strategyTradeText(strategy, position), expiry, farExpiry, daysToExpiry, valuationDate, spot: position.spot,
424
+ net, netAmount: Math.round(net * multiplier * 1e6) / 1e6,
425
+ ...risk, probabilityOfProfit: probabilityOfProfit(position, risk.breakEvens), returnToExpiry,
426
+ annualizedReturn: finite(returnToExpiry) && daysToExpiry > 0 ? returnToExpiry * 365 / daysToExpiry : null,
427
+ rewardToRisk: def.net === "debit" ? riskRatio : null,
428
+ quote: spread != null ? { spread, spreadOfMid: spread / Math.abs(net) } : null,
429
+ earnings: earningsBeforeExpiry(earningsDate, valuationDate, farExpiry ?? expiry),
430
+ };
431
+ }
432
+
433
+ export interface StrategyScreenRow {
434
+ symbol: string;
435
+ /** 1 for the first by the family's ranking; null for a name with no trade or nothing to rank it by. */
436
+ rank: number | null;
437
+ trade: StrategyTrade | null;
438
+ /** 30-day IV rank, 0 to 100, against the prior 52 weeks; null when IV history has none. */
439
+ ivRank: number | null;
440
+ /** Its spread is over the screen's `maxSpreadPercent` of the net midpoint. */
441
+ wideSpread: boolean;
442
+ /** Why the name has no trade. */
443
+ note: string | null;
444
+ }
445
+
446
+ export interface StrategyScreen extends Omit<CspScreen, "rows" | "criteria"> {
447
+ rows: StrategyScreenRow[];
448
+ criteria: StrategyScreenCriteria;
449
+ }
450
+
451
+ /** What a row ranks by: an income trade's annualised return, a debit trade's reward to risk, or its IV rank. */
452
+ function rankValue(row: Omit<StrategyScreenRow, "rank">, def: StrategyFamilyDef): number | null {
453
+ if (!row.trade) return null;
454
+ const value = def.rank === "return" ? row.trade.annualizedReturn : def.rank === "reward-risk" ? row.trade.rewardToRisk : row.ivRank;
455
+ return finite(value) ? value : null;
456
+ }
457
+
458
+ /**
459
+ * The family's order: an income trade's highest annualised return first, a
460
+ * debit trade's highest reward to risk, or for a straddle or strangle the
461
+ * richest IV rank first when selling and the cheapest when buying. A wide spread is ranked and flagged. A trade with
462
+ * nothing to rank it by follows, then the names with no trade, by symbol.
463
+ */
464
+ export function rankStrategyScreen(rows: readonly Omit<StrategyScreenRow, "rank">[], strategy: ScreenFamily): StrategyScreenRow[] {
465
+ const def = strategyFamilyDef(strategy);
466
+ const direction = def.rank === "iv-rank-low" ? 1 : -1;
467
+ const tier = (row: Omit<StrategyScreenRow, "rank">) => rankValue(row, def) != null ? 0 : row.trade ? 1 : 2;
468
+ const sorted = [...rows].sort((left, right) => tier(left) - tier(right)
469
+ || direction * ((rankValue(left, def) ?? 0) - (rankValue(right, def) ?? 0))
470
+ || left.symbol.localeCompare(right.symbol));
471
+ let rank = 0;
472
+ return sorted.map((row) => ({ ...row, rank: tier(row) === 0 ? ++rank : null }));
473
+ }
474
+
475
+ /** The date a calendar's bought expiry is nearest: `CALENDAR_GAP_DAYS` after the sold one. */
476
+ const farTarget = (near: number) => new Date(Date.parse(`${expiryIsoDate(near)}T00:00:00Z`) + CALENDAR_GAP_DAYS * DAY_MS).toISOString().slice(0, 10);
477
+
478
+ /** One name's trade: its legs picked from the chain on the listed expiry nearest the target, and a calendar's from the expiry after it. */
479
+ async function screenStrategy(
480
+ instrument: InstrumentRef, criteria: StrategyScreenCriteria, dependencies: ScenarioLoaderDependencies, target: string,
481
+ earningsDate: string | null, signal: AbortSignal | undefined,
482
+ ): Promise<{ value: StrategyTrade; market: ScenarioMarketSnapshot }> {
483
+ const market = await loadScreenMarket(instrument, criteria, dependencies, target, signal);
484
+ const near = market.chain ? [...market.chain.calls, ...market.chain.puts][0]?.expiration : undefined;
485
+ let merged: OptionsChain | undefined;
486
+ let far: number | undefined;
487
+ if (criteria.strategy === "calendar" && market.chain && near != null) {
488
+ far = nearestListedExpiry(market.expirationDates.filter((expiration) => expiryIsoDate(expiration) > expiryIsoDate(near)), farTarget(near));
489
+ if (far == null) throw new Error(`No expiry listed after ${expiryIsoDate(near)} to buy`);
490
+ const back = await loadScenarioMarket({ instrument, expiration: far, signal }, dependencies);
491
+ if (!back.chain || back.warnings.includes("Options chain is stale")) {
492
+ throw new Error(back.missingExpiry ?? `No current ${expiryIsoDate(far)} chain to buy the calendar on`);
493
+ }
494
+ // The bought contracts by the date they are listed on in the catalogue.
495
+ const backExpiration = [...back.chain.calls, ...back.chain.puts][0]?.expiration ?? far;
496
+ far = backExpiration;
497
+ merged = { ...market.chain, calls: [...market.chain.calls, ...back.chain.calls], puts: [...market.chain.puts, ...back.chain.puts] };
498
+ }
499
+ const position = seedScreenPosition(market, (quoted, pricing, unusable) => pickStrategyLegs(criteria, quoted, pricing, unusable, near!, far), merged);
500
+ return { value: strategyTrade(criteria.strategy, position, seededLegQuotes(position, merged ?? market.chain), earningsDate), market };
501
+ }
502
+
503
+ /** Each name's trade of the family, ranked. */
504
+ export async function loadStrategyScreen(
505
+ instruments: readonly InstrumentRef[], criteria: StrategyScreenCriteria, dependencies: CspScreenDependencies, signal?: AbortSignal,
506
+ ): Promise<StrategyScreen> {
507
+ const target = cspTargetDate(criteria, dependencies.scenario.now?.() ?? Date.now());
508
+ const screened = await screenNames(instruments, dependencies, signal, "No trade on this chain", async (instrument, earningsDate, scenario) =>
509
+ screenStrategy(instrument, criteria, scenario, target, earningsDate, signal));
510
+ const rows = screened.names.map(({ symbol, ivRank, value, note }): Omit<StrategyScreenRow, "rank"> => ({
511
+ symbol, trade: value, ivRank, note,
512
+ wideSpread: value?.quote != null && value.quote.spreadOfMid * 100 > criteria.maxSpreadPercent }));
513
+ return { rows: rankStrategyScreen(rows, criteria.strategy), criteria, missingRanks: screened.missingRanks,
514
+ ivRankError: screened.ivRankError, freshness: screened.freshness };
515
+ }
516
+
517
+ /** `nearest 0.30 delta, wings 0.15 · expiry nearest 2026-11-20 (30 days)`: what each name's trade is. */
518
+ export function strategyScreenCriteriaText(criteria: StrategyScreenCriteria, now: number): string {
519
+ const def = strategyFamilyDef(criteria.strategy);
520
+ const legs = def.delta == null || criteria.delta == null ? "strike nearest the money"
521
+ : `nearest ${criteria.delta.toFixed(2)} delta${criteria.wingDelta != null ? `, wings ${criteria.wingDelta.toFixed(2)}` : ""}`;
522
+ return `${legs} · expiry nearest ${cspTargetDate(criteria, now)}${criteria.expiration == null ? ` (${criteria.days} days)` : ""}`
523
+ + (criteria.strategy === "calendar" ? `, bought about ${CALENDAR_GAP_DAYS} days later` : "");
524
+ }
525
+
526
+ /** How a family's rows are ranked, in the words the notes and exports use. */
527
+ export function strategyRankedBy(strategy: ScreenFamily): string {
528
+ const def = strategyFamilyDef(strategy);
529
+ if (def.rank === "reward-risk") return "reward to risk (max gain over max loss) at the mid";
530
+ if (def.rank === "return") return "annualised return on risk at the mid";
531
+ return def.rank === "iv-rank-high" ? "IV rank, highest first" : "IV rank, lowest first";
532
+ }
@@ -1,40 +1,21 @@
1
1
  import { fxLegForCurrency } from "../../../market-data/coordinator/fx-legs";
2
2
  import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
3
3
  import { fxCloseOnOrBefore, type FxCloses } from "../../../market-data/fx-closes";
4
- import { extractFundamentalSeries, knownWhenFiled } from "../../../time-series/fundamentals";
5
- import type { SecuritySeriesSource, TimeSeriesPoint } from "../../../time-series/types";
4
+ import { knownWhenFiled } from "../../../time-series/fundamentals";
5
+ import { STALE_TRAILING_EPS_MS, trailingEpsSteps as sharedTrailingEpsSteps, type EpsStep } from "../../../time-series/filed-trailing-eps";
6
6
  import { createValuationCurrencyContext } from "../../../time-series/valuation-currency";
7
7
  import type { FinancialStatement, PricePoint, TickerFinancials } from "../../../types/financials";
8
8
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
9
- import { areNearbyFinancialPeriodEnds } from "../../../utils/financial-statements";
10
9
  import { datedByReport, type ReportDate } from "./report-dates";
11
10
 
11
+ export type { EpsStep };
12
+
12
13
  const DAY_MS = 86_400_000;
13
14
  const YEAR_MS = 365.25 * DAY_MS;
14
- /**
15
- * A figure stops pricing weeks this long after its period end. Sixteen months
16
- * covers a fiscal year plus its filing, so a year of unavailable quarterly
17
- * sums falls back on the last annual figure but never on one long overdue.
18
- */
19
- const STALE_EPS_MS = 487 * DAY_MS;
20
15
  /** Spacing between band multiples, finest first; the first that needs four lines or fewer wins. */
21
16
  const MULTIPLE_STEPS = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
22
17
  const MAX_BANDS = 4, MIN_BANDS = 3;
23
18
 
24
- /** One trailing-twelve-month EPS figure and when it became known. */
25
- export interface EpsStep {
26
- /** ISO period end. */
27
- periodEnd: string;
28
- /** Four reported quarters summed, or a reported fiscal year. */
29
- basis: "ttm" | "annual";
30
- knownAt: Date;
31
- /** False when no publication date or report is on record and `knownAt` is the period end. */
32
- dated: boolean;
33
- /** Statement currency units; null when unavailable (a declared gap or an incomplete run of quarters). */
34
- eps: number | null;
35
- currency: string | null;
36
- }
37
-
38
19
  interface PeBandWeek {
39
20
  date: Date;
40
21
  close: number;
@@ -97,34 +78,14 @@ export interface PeBandModel {
97
78
  notice: string | null;
98
79
  }
99
80
 
100
- function epsSeries(financials: TickerFinancials, period: "ttm" | "annual"): TimeSeriesPoint[] {
101
- const source: SecuritySeriesSource = { kind: "security", instrument: { symbol: "" }, fieldId: "fundamental.eps", period, timestampMode: "available-at" };
102
- return extractFundamentalSeries(financials, source);
103
- }
104
-
105
81
  /**
106
- * Trailing EPS through time, oldest first. Four reported quarters are summed
107
- * by the shared statement series, which keeps declared gaps and broken runs
108
- * of quarters unavailable. A reported fiscal year is itself a trailing
109
- * twelve-month figure; at a fiscal year end it replaces the quarterly sum.
110
- * A figure is known no later than the company's report of that quarter, when
111
- * `reports` has it, including one with no publication date of its own.
82
+ * Trailing EPS through time, oldest first, as the headline EPS (TTM) reads it
83
+ * (time-series/filed-trailing-eps). A figure is known no later than the
84
+ * company's report of that quarter, when `reports` has it, including one with
85
+ * no publication date of its own.
112
86
  */
113
87
  export function trailingEpsSteps(financials: TickerFinancials, reports: readonly ReportDate[] = []): EpsStep[] {
114
- const known = (row: FinancialStatement) => datedByReport(knownWhenFiled(row), reports);
115
- const adjusted = { ...financials, quarterlyStatements: financials.quarterlyStatements.map(known),
116
- annualStatements: financials.annualStatements.map(known) };
117
- const toStep = (basis: EpsStep["basis"]) => (point: TimeSeriesPoint): EpsStep => ({
118
- periodEnd: point.observedAt.toISOString().slice(0, 10), basis, knownAt: point.date, dated: point.availableAt !== undefined,
119
- eps: point.value != null && Number.isFinite(point.value) ? point.value : null, currency: point.provenance?.currency ?? null,
120
- });
121
- const steps = epsSeries(adjusted, "annual").map(toStep("annual"));
122
- for (const step of epsSeries(adjusted, "ttm").map(toStep("ttm"))) {
123
- const index = steps.findIndex((other) => areNearbyFinancialPeriodEnds(other.periodEnd, step.periodEnd));
124
- if (index < 0) steps.push(step);
125
- else if (steps[index]!.eps == null && step.eps != null) steps[index] = step;
126
- }
127
- return steps.sort((a, b) => a.knownAt.getTime() - b.knownAt.getTime() || a.periodEnd.localeCompare(b.periodEnd));
88
+ return sharedTrailingEpsSteps(financials, (row: FinancialStatement) => datedByReport(knownWhenFiled(row), reports));
128
89
  }
129
90
 
130
91
  /**
@@ -230,7 +191,7 @@ export function stepAt(steps: readonly EpsStep[], time: number): EpsStep | null
230
191
  if (candidate.eps == null || candidate.knownAt.getTime() > time) continue;
231
192
  if (!step || candidate.periodEnd > step.periodEnd) step = candidate;
232
193
  }
233
- return step && time - Date.parse(step.periodEnd) <= STALE_EPS_MS ? step : null;
194
+ return step && time - Date.parse(step.periodEnd) <= STALE_TRAILING_EPS_MS ? step : null;
234
195
  }
235
196
 
236
197
  export function projectPeBand(
@@ -311,6 +272,8 @@ export function projectPeBand(
311
272
  const ordered = [...pes].sort((a, b) => a - b);
312
273
  // The regular session's price, as the quote headlines it: after the close, the close rather than an after-hours print.
313
274
  const price = regularSessionValuationPrice(financials.quote) ?? sorted.at(-1)?.close ?? null;
275
+ // Today's figure reads like every week's: one whose newest quarter has no EPS keeps the last figure, dated in the
276
+ // table, where the headline EPS (TTM) needs that quarter's own (filed-trailing-eps) and shows Gloom Cloud's, labelled.
314
277
  const currentStep = stepAt(allSteps, now);
315
278
  // Today's EPS at the latest completed FX close.
316
279
  const currentEps = priced(currentStep, day(now))?.eps ?? null;