@gloom-sh/terminal 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -51,6 +51,7 @@ import {
51
51
  } from "./client";
52
52
  import {
53
53
  ALL_EXPIRIES,
54
+ axisRatioOfDate,
54
55
  chartWindow,
55
56
  expiryAxis,
56
57
  expiryLabel,
@@ -63,6 +64,7 @@ import {
63
64
  formatDistance,
64
65
  formatGamma,
65
66
  formatLevel,
67
+ gammaTimesFigure,
66
68
  positioningSpotDetail,
67
69
  formatPayout,
68
70
  formatRatio,
@@ -160,13 +162,18 @@ interface PositioningChartProps {
160
162
 
161
163
  /** Columns across a numeric axis (strikes, or expiries one slot apart), with spot and the other levels as guide lines. */
162
164
  function PositioningChart(props: PositioningChartProps) {
165
+ // The legend's values are one column's, under the cursor (the last column's
166
+ // without one), not the totals the figures above show: it names the column.
167
+ const column = props.formatCursor(props.selection.cursorDate ? axisRatioOfDate(props.selection.cursorDate) : 1);
168
+ const legendSeries = useMemo(() => column
169
+ ? props.series.map((entry) => ({ ...entry, label: `${entry.label} at ${column}` })) : undefined, [column, props.series]);
163
170
  const xAxis = useMemo(() => ({
164
171
  ticks: props.ticks(Math.max(1, props.width - VALUE_GUTTER)),
165
172
  markers: props.markers.flatMap((marker) => marker.ratio != null && marker.ratio >= 0 && marker.ratio <= 1
166
173
  ? [{ id: marker.id, xRatio: marker.ratio, label: marker.label, color: marker.color }] : []),
167
174
  formatCursor: props.formatCursor,
168
175
  }), [props.formatCursor, props.markers, props.ticks, props.width]);
169
- return <CompositeChart series={props.series} panels={PANELS} width={props.width} height={props.height}
176
+ return <CompositeChart series={props.series} legendSeries={legendSeries} panels={PANELS} width={props.width} height={props.height}
170
177
  focused={false} navigable={false} showLegend showTimeAxis xAxis={xAxis}
171
178
  formatValue={(value) => props.formatValue(value)} formatAxisValue={props.formatAxisValue}
172
179
  cursorDate={props.selection.cursorDate} onCursorDateChange={props.selection.onCursorDateChange}
@@ -217,7 +224,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
217
224
  const data = openInterest.data;
218
225
  const spot = data?.spot ?? null;
219
226
  const venue = data?.venue;
220
- const spotStamp = useMemo(() => positioningSpotDetail(spot, data?.spotAsOf, venue), [spot, data?.spotAsOf, venue]);
227
+ const spotStamp = useMemo(() => positioningSpotDetail(spot, data?.spotAsOf, venue, data?.spotBasis),
228
+ [spot, data?.spotAsOf, data?.spotBasis, venue]);
221
229
  // A coin's options (Deribit's) count open interest in coins, to tenths.
222
230
  const coin = isCoinContract(data?.contract);
223
231
  const unit = coin ? ` ${data!.contract!.unit}` : "";
@@ -496,6 +504,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
496
504
  const band = gammaData?.band;
497
505
  const shares = band ? `${Math.round(band.shareLow * 100)}-${Math.round(band.shareHigh * 100)}%` : "";
498
506
  const walls = useMemo(() => gammaWalls(gammaStrikes), [gammaStrikes]);
507
+ const gammaTimes = useMemo(() => gammaTimesFigure(gammaData?.gammaAsOf, gammaData?.quotesAsOf),
508
+ [gammaData?.gammaAsOf, gammaData?.quotesAsOf]);
499
509
  const gammaFigures = useMemo<StatItem[]>(() => !gammaData?.total ? [] : [
500
510
  { id: "net", label: "Net GEX", value: formatGamma(gammaData.total.net), detail: "$ per 1% move",
501
511
  color: gammaData.total.net >= 0 ? colors.positive : colors.negative },
@@ -507,8 +517,11 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
507
517
  })),
508
518
  ...(band ? [{ id: "band", label: "Dealer range", value: `${formatGamma(band.low)} to ${formatGamma(band.high)}`,
509
519
  detail: `${shares} with dealers${gammaData.boxes?.length ? ", boxes out" : ""}` }] : []),
510
- { id: "spot", label: "Spot", value: formatLevel(gammaSpot), ...positioningSpotDetail(gammaSpot, gammaData.spotAsOf, gammaData.venue) },
511
- ], [band, colors.negative, colors.positive, gammaData, gammaSpot, shares, walls]);
520
+ { id: "spot", label: "Spot", value: formatLevel(gammaSpot),
521
+ ...positioningSpotDetail(gammaSpot, gammaData.spotAsOf, gammaData.venue, gammaData.spotBasis) },
522
+ // Gamma is evaluated at the spot's time; before the open its volatilities are the last session's.
523
+ ...(gammaTimes ? [{ id: "gamma-at", ...gammaTimes }] : []),
524
+ ], [band, colors.negative, colors.positive, gammaData, gammaSpot, gammaTimes, shares, walls]);
512
525
  // The assumption rides in the legend, where the numbers it changes are read; the
513
526
  // dealer range figure names its own share. It gives way before the series name does.
514
527
  const assumption = width >= 60 ? "dealers long calls, short puts" : width >= 48 ? "long calls, short puts" : null;
@@ -580,7 +593,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
580
593
  onHeaderClick={(id) => setExpirySort((current) => nextHeaderSort(current, id))}
581
594
  emptyStateTitle="No listed expiries."
582
595
  getExportMetadata={() => [["open interest as of", data?.oiDate ?? ""], ["change since", data?.previousOiDate ?? ""], ["spot", spot ?? ""],
583
- ...(hasGamma ? [["net GEX unit", "dollars of dealer delta per 1% move"], ["quotes as of", netGammas ? gamma.data?.quotesAsOf ?? "" : ""]] : [])]}
596
+ ...(hasGamma ? [["net GEX unit", "dollars of dealer delta per 1% move"], ["gamma at", netGammas ? gamma.data?.gammaAsOf ?? "" : ""],
597
+ ["vols from", netGammas ? gamma.data?.quotesAsOf ?? "" : ""]] : [])]}
584
598
  rootBefore={<ChartTableHeader width={width} height={bodyHeight} figures={ladderFigures}
585
599
  tableRows={expiryRows.length} tableChromeRows={chartTableChromeRows(expiryColumnList, width)}
586
600
  chart={expiryChart ? {
@@ -619,7 +633,8 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
619
633
  ["range with boxes high", gammaData.bandWithBoxes.high],
620
634
  ["box legs left out", (gammaData.boxes ?? []).map((leg) => `${leg.expiry} ${leg.strike} x${leg.contracts}`).join("; ")]] : []),
621
635
  ["flip", gammaData.flip ?? ""], ["call wall", walls.callWall ?? ""], ["put wall", walls.putWall ?? ""],
622
- ["open interest as of", gammaData.oiDate ?? ""], ["quotes as of", gammaData.quotesAsOf ?? ""]]}
636
+ ["open interest as of", gammaData.oiDate ?? ""], ["gamma at", gammaData.gammaAsOf ?? ""],
637
+ ["vols from", gammaData.quotesAsOf ?? ""]]}
623
638
  rootBefore={<ChartTableHeader width={width} height={bodyHeight} query={query} figures={gammaFigures}
624
639
  tableRows={gammaRows.length} tableChromeRows={chartTableChromeRows(GAMMA_COLUMNS, width)}
625
640
  chart={gammaChart ? {
@@ -384,11 +384,52 @@ export const SCENARIO_STRATEGIES = ["vertical", "straddle", "put", "csp", "colla
384
384
  type ScenarioStrategy = typeof SCENARIO_STRATEGIES[number];
385
385
  const isScenarioStrategy = (value: unknown): value is ScenarioStrategy => SCENARIO_STRATEGIES.includes(value as ScenarioStrategy);
386
386
 
387
- interface StrategyPricing { spot: number; asOf: number; rate: number; dividendYield: number }
387
+ export interface StrategyPricing { spot: number; asOf: number; rate: number; dividendYield: number }
388
388
 
389
389
  const usableVolatility = (value: unknown): value is number => positive(value) && value <= 5;
390
390
 
391
- interface StrategyParity { forward: number; daysToExpiry: number }
391
+ export interface StrategyParity { forward: number; daysToExpiry: number }
392
+
393
+ /**
394
+ * The contracts a strategy seed picks from: every two-sided quote with the IV
395
+ * its midpoint implies against its expiry's parity forward (or a usable
396
+ * provider IV), each side nearest `spot` first. `parityFor` is the forward an
397
+ * expiry's IVs were solved on, null without a rate to solve them by.
398
+ */
399
+ export interface QuotedChain {
400
+ calls: QuotedContract[];
401
+ puts: QuotedContract[];
402
+ parityFor(expiration: number): StrategyParity | null;
403
+ }
404
+
405
+ /** The quoted chain every seed reads, as `scenarioStrategyLegs` describes it. */
406
+ function quoteStrategyChain(chain: OptionsChain, spot: number, pricing: StrategyPricing | null): QuotedChain {
407
+ const parity = new Map<number, StrategyParity | null>();
408
+ const parityFor = (expiration: number) => {
409
+ if (!pricing) return null;
410
+ if (!parity.has(expiration)) {
411
+ const daysToExpiry = daysToExpiryFrom(expiration, pricing.asOf);
412
+ const { forward } = extractImpliedForward(chain.calls.filter((call) => call.expiration === expiration),
413
+ chain.puts.filter((put) => put.expiration === expiration), pricing.spot, daysToExpiry / 365, pricing.rate,
414
+ chain.underlyingSymbol);
415
+ parity.set(expiration, forward == null ? null : { forward, daysToExpiry });
416
+ }
417
+ return parity.get(expiration)!;
418
+ };
419
+ const quoted = (contracts: OptionsChain["calls"], side: ScenarioLeg["side"]) => contracts.flatMap((contract): QuotedContract[] => {
420
+ const mid = optionMid(contract);
421
+ if (!positive(contract.strike) || mid == null) return [];
422
+ const expiry = parityFor(contract.expiration);
423
+ // q=r turns the spot pricer into discounted forward pricing.
424
+ const volatility = expiry ? solveImpliedVolatility({ side, spot: expiry.forward, strike: contract.strike,
425
+ daysToExpiry: expiry.daysToExpiry, rate: pricing!.rate, dividendYield: pricing!.rate }, mid).volatility : null;
426
+ if (usableVolatility(volatility)) return [{ contract, side, mid, volatility, solved: true }];
427
+ return usableVolatility(contract.impliedVolatility) ? [{ contract, side, mid, volatility: contract.impliedVolatility, solved: false }] : [];
428
+ });
429
+ const nearest = (a: { contract: { strike: number } }, b: { contract: { strike: number } }) =>
430
+ Math.abs(a.contract.strike - spot) - Math.abs(b.contract.strike - spot);
431
+ return { calls: quoted(chain.calls, "call").sort(nearest), puts: quoted(chain.puts, "put").sort(nearest), parityFor };
432
+ }
392
433
 
393
434
  /**
394
435
  * An explicit strategy request uses only two-sided quotes and enters each leg
@@ -418,33 +459,8 @@ function scenarioStrategyLegs(
418
459
  chain: OptionsChain, spot: number, strategy: ScenarioStrategy, pricing: StrategyPricing | null, strike?: number,
419
460
  collar: { put?: number; call?: number } = {}, delta?: number, unusable: UnusableChainText = defaultUnusableChainText,
420
461
  ): { legs: ScenarioLeg[]; parity: StrategyParity | null } {
421
- const parity = new Map<number, StrategyParity | null>();
422
- const parityFor = (expiration: number) => {
423
- if (!pricing) return null;
424
- if (!parity.has(expiration)) {
425
- const daysToExpiry = daysToExpiryFrom(expiration, pricing.asOf);
426
- const { forward } = extractImpliedForward(chain.calls.filter((call) => call.expiration === expiration),
427
- chain.puts.filter((put) => put.expiration === expiration), pricing.spot, daysToExpiry / 365, pricing.rate,
428
- chain.underlyingSymbol);
429
- parity.set(expiration, forward == null ? null : { forward, daysToExpiry });
430
- }
431
- return parity.get(expiration)!;
432
- };
433
- const quoted = (contracts: OptionsChain["calls"], side: ScenarioLeg["side"]) => contracts.flatMap((contract) => {
434
- const mid = optionMid(contract);
435
- if (!positive(contract.strike) || mid == null) return [];
436
- const expiry = parityFor(contract.expiration);
437
- // q=r turns the spot pricer into discounted forward pricing.
438
- const volatility = expiry ? solveImpliedVolatility({ side, spot: expiry.forward, strike: contract.strike,
439
- daysToExpiry: expiry.daysToExpiry, rate: pricing!.rate, dividendYield: pricing!.rate }, mid).volatility : null;
440
- if (usableVolatility(volatility)) return [{ contract, mid, volatility, solved: true }];
441
- return usableVolatility(contract.impliedVolatility) ? [{ contract, mid, volatility: contract.impliedVolatility, solved: false }] : [];
442
- });
443
- const nearest = (a: { contract: { strike: number } }, b: { contract: { strike: number } }) =>
444
- Math.abs(a.contract.strike - spot) - Math.abs(b.contract.strike - spot);
462
+ const { calls, puts, parityFor } = quoteStrategyChain(chain, spot, pricing);
445
463
  const anchored = ({ contract }: { contract: { strike: number } }) => strike == null || Math.abs(contract.strike - strike) < 1e-9;
446
- const calls = quoted(chain.calls, "call").sort(nearest);
447
- const puts = quoted(chain.puts, "put").sort(nearest);
448
464
  const missing = (candidates: { contract: { strike: number } }[], at = strike, what: string = strategy === "csp" ? "put to sell" : strategy) => {
449
465
  if (at == null) return new Error(unusable("strategy"));
450
466
  const listed = [...new Set(candidates.map(({ contract }) => contract.strike))]
@@ -455,7 +471,7 @@ function scenarioStrategyLegs(
455
471
  };
456
472
  if (strategy === "collar") return collarLegs(calls, puts, spot, collar, missing, parityFor, unusable);
457
473
  if (strategy === "put" || strategy === "csp") {
458
- const put = delta == null ? puts.find(anchored) : nearestDelta(puts, -delta, spot, pricing, parityFor);
474
+ const put = delta == null ? puts.find(anchored) : nearestDeltaContract(puts, -delta, spot, pricing, parityFor);
459
475
  if (!put) throw missing(puts);
460
476
  return { legs: [{ id: put.contract.contractSymbol, side: "put", quantity: strategy === "csp" ? -1 : 1, strike: put.contract.strike,
461
477
  expiration: put.contract.expiration, price: put.mid, priceSource: "mid", volatility: put.volatility, multiplier: 100,
@@ -480,10 +496,10 @@ function scenarioStrategyLegs(
480
496
  return { legs, parity: first.solved || second.solved ? parityFor(first.contract.expiration) : null };
481
497
  }
482
498
 
483
- interface QuotedContract { contract: OptionsChain["calls"][number]; mid: number; volatility: number; solved: boolean }
499
+ export interface QuotedContract { contract: OptionsChain["calls"][number]; side: ScenarioLeg["side"]; mid: number; volatility: number; solved: boolean }
484
500
 
485
501
  /** Why a chain seeds nothing: `what` it has no usable quote for, then the next live expiry to try unless `next` is false. */
486
- type UnusableChainText = (what: string, next?: boolean) => string;
502
+ export type UnusableChainText = (what: string, next?: boolean) => string;
487
503
  const defaultUnusableChainText: UnusableChainText = (what) => `The selected chain has no complete quoted ${what} with usable IV`;
488
504
 
489
505
  /**
@@ -502,38 +518,91 @@ function unusableChainText(market: ScenarioMarketSnapshot, exchange: string | un
502
518
  }
503
519
 
504
520
  /**
505
- * A put's spot delta at the IV it trades at: against the parity forward its
506
- * IV was solved on when it was (forward delta, carried to spot), else at the
507
- * seed's spot. Null when it cannot be valued.
521
+ * An option's spot delta at the IV it trades at: against the parity forward
522
+ * its IV was solved on when it was (forward delta, carried to spot), else at
523
+ * the seed's spot. Null when it cannot be valued.
508
524
  */
509
- function quotedPutDelta(quoted: QuotedContract, spot: number, pricing: StrategyPricing | null, parity: StrategyParity | null): number | null {
510
- const { contract, volatility, solved } = quoted;
525
+ function quotedDelta(quoted: QuotedContract, spot: number, pricing: StrategyPricing | null, parity: StrategyParity | null): number | null {
526
+ const { contract, side, volatility, solved } = quoted;
511
527
  if (!pricing) return null;
512
528
  if (solved && parity) {
513
529
  // On the forward, with the dividend yield set to the rate so it carries no drift, as the IV was solved.
514
- const forwardDelta = valueOption({ side: "put", spot: parity.forward, strike: contract.strike, daysToExpiry: parity.daysToExpiry,
530
+ const forwardDelta = valueOption({ side, spot: parity.forward, strike: contract.strike, daysToExpiry: parity.daysToExpiry,
515
531
  rate: pricing.rate, dividendYield: pricing.rate, volatility }).delta;
516
532
  return Number.isFinite(forwardDelta) ? forwardDelta * Math.exp((pricing.rate - pricing.dividendYield) * parity.daysToExpiry / 365) : null;
517
533
  }
518
534
  const years = daysToExpiryFrom(contract.expiration, pricing.asOf) / 365;
519
- return optionDelta({ spot, years, volatility, rate: pricing.rate, dividendYield: pricing.dividendYield }, contract.strike, "put");
535
+ return optionDelta({ spot, years, volatility, rate: pricing.rate, dividendYield: pricing.dividendYield }, contract.strike, side);
520
536
  }
521
537
 
522
- /** The put whose delta is nearest `target` (negative, as a put's delta reads), the lower strike on a tie; null when none can be valued. */
523
- function nearestDelta(
524
- puts: readonly QuotedContract[], target: number, spot: number, pricing: StrategyPricing | null,
538
+ /**
539
+ * The contract whose delta is nearest `target` (negative for a put, as its
540
+ * delta reads), the one further out of the money on a tie (the lower strike
541
+ * of two puts, the higher of two calls); null when none can be valued.
542
+ */
543
+ export function nearestDeltaContract(
544
+ candidates: readonly QuotedContract[], target: number, spot: number, pricing: StrategyPricing | null,
525
545
  parityFor: (expiration: number) => StrategyParity | null,
526
546
  ): QuotedContract | null {
527
- let best: { put: QuotedContract; distance: number } | null = null;
528
- for (const put of puts) {
529
- const delta = quotedPutDelta(put, spot, pricing, put.solved ? parityFor(put.contract.expiration) : null);
547
+ let best: { quoted: QuotedContract; distance: number } | null = null;
548
+ for (const quoted of candidates) {
549
+ const delta = quotedDelta(quoted, spot, pricing, quoted.solved ? parityFor(quoted.contract.expiration) : null);
530
550
  if (delta == null) continue;
531
551
  const distance = Math.abs(delta - target);
532
- if (!best || distance < best.distance - 1e-12 || (Math.abs(distance - best.distance) <= 1e-12 && put.contract.strike < best.put.contract.strike)) {
533
- best = { put, distance };
552
+ const further = quoted.side === "put" ? quoted.contract.strike < (best?.quoted.contract.strike ?? Infinity)
553
+ : quoted.contract.strike > (best?.quoted.contract.strike ?? -Infinity);
554
+ if (!best || distance < best.distance - 1e-12 || (Math.abs(distance - best.distance) <= 1e-12 && further)) {
555
+ best = { quoted, distance };
534
556
  }
535
557
  }
536
- return best?.put ?? null;
558
+ return best?.quoted ?? null;
559
+ }
560
+
561
+ /** A seeded leg: `quantity` contracts of a quoted option, entered at its midpoint with the IV that midpoint implies, as every seed enters it. */
562
+ export function seededLeg({ contract, side, mid, volatility, solved }: QuotedContract, quantity: number): ScenarioLeg {
563
+ return { id: contract.contractSymbol, side, quantity, strike: contract.strike, expiration: contract.expiration, price: mid,
564
+ priceSource: "mid", volatility, multiplier: 100, ...(solved ? { volatilitySource: "mid" as const } : {}) };
565
+ }
566
+
567
+ /** What a strategy screen picks from a quoted chain: its legs, and the shares it holds with them (a covered call's). */
568
+ export interface ScreenSeed { legs: ScenarioLeg[]; shares?: number }
569
+
570
+ /**
571
+ * A position a strategy screen seeds as OSA seeds `--strategy`: legs picked
572
+ * from the market's chain (or `merged`, a calendar's two expiries together) at
573
+ * their quote midpoints, valued at the market's rate and dividend yield from
574
+ * the spot the first expiry's parity forward implies when a leg's IV was solved
575
+ * against it. Shares are bought at that spot. `pick` reads the quoted chain
576
+ * with its pricing, so a leg can be chosen by delta.
577
+ */
578
+ export function seedScreenPosition(
579
+ market: ScenarioMarketSnapshot,
580
+ pick: (quoted: QuotedChain, pricing: StrategyPricing, unusable: UnusableChainText) => ScreenSeed,
581
+ merged?: OptionsChain,
582
+ ): ScenarioPosition {
583
+ if (!market.chain || market.warnings.includes("Options chain is stale")) {
584
+ throw new Error(market.missingExpiry ?? "A current options chain is required to seed a strategy");
585
+ }
586
+ const chain = merged ?? market.chain;
587
+ if (market.spot == null) throw new Error("A current underlying price is required");
588
+ if (market.rate == null) throw new Error("Choosing legs by delta needs the Treasury rate");
589
+ if (market.dividendYield == null) throw new Error("Dividend yield unavailable");
590
+ const pricing = { spot: market.spot, asOf: market.asOf, rate: market.rate, dividendYield: market.dividendYield };
591
+ const quoted = quoteStrategyChain(chain, market.spot, pricing);
592
+ const { legs, shares } = pick(quoted, pricing, unusableChainText(market, market.exchange));
593
+ const first = Math.min(...legs.map((leg) => leg.expiration));
594
+ const parity = legs.some((leg) => leg.expiration === first && leg.volatilitySource === "mid") ? quoted.parityFor(first) : null;
595
+ const spot = parity ? parity.forward * Math.exp(-(pricing.rate - pricing.dividendYield) * parity.daysToExpiry / 365) : market.spot;
596
+ const position: ScenarioPosition = {
597
+ symbol: market.symbol, exchange: market.exchange, currency: scenarioCurrency(undefined, market.currency, market.exchange),
598
+ spot, rate: pricing.rate, dividendYield: pricing.dividendYield, asOf: market.asOf, legs,
599
+ ...(shares ? { underlying: { quantity: shares, price: spot } } : {}),
600
+ };
601
+ const error = validatePosition(position);
602
+ if (error) throw new Error(error);
603
+ const contracts = [...chain.calls, ...chain.puts].filter((contract) => legs.some((leg) => leg.id === contract.contractSymbol));
604
+ if (contracts.some((contract) => contract.currency && contract.currency !== position.currency)) throw new Error("Strategy currency differs from the underlying quote currency");
605
+ return position;
537
606
  }
538
607
 
539
608
  /** How far out of the money a collar's put and call sit when their strikes are not given. */
@@ -8,6 +8,10 @@ import {
8
8
  CSP_SCREEN_DEFAULTS, CSP_SCREEN_LIMIT, cspEarningsText, cspScreenCriteriaText, loadCspScreen, readCspScreenCriteria,
9
9
  type CspScreen, type CspScreenRow,
10
10
  } from "./csp-screen";
11
+ import {
12
+ loadStrategyScreen, readStrategyFamily, readStrategyScreenCriteria, STRATEGY_FAMILY_DEFS, strategyFamilyDef,
13
+ strategyRankedBy, strategyScreenCriteriaText, type ScreenFamily, type StrategyScreen, type StrategyScreenCriteria, type StrategyScreenRow,
14
+ } from "./strategy-screen";
11
15
 
12
16
  const percentCell = (digits: number) => (value: unknown) => typeof value === "number" ? `${value.toFixed(digits)}%` : "--";
13
17
  const priceCell = (value: unknown) => typeof value === "number" ? formatHedgePrice(value) : "--";
@@ -69,42 +73,146 @@ const listText = (items: readonly string[]) => items.length <= 1 ? items.join(""
69
73
  * sessions).`), and a bare symbol, which has none to give, is just not ranked
70
74
  * yet (`No IV rank for DAL yet.`).
71
75
  */
72
- export function cspScreenNotes(screen: Pick<CspScreen, "criteria" | "missingRanks">): string[] {
76
+ export function cspScreenNotes(
77
+ screen: { criteria: Pick<CspScreen["criteria"], "maxSpreadPercent">; missingRanks: string[] }, rankedBy = "annualised yield at the bid",
78
+ ): string[] {
73
79
  const reasoned = screen.missingRanks.filter((entry) => entry.includes(" "));
74
80
  const bare = screen.missingRanks.filter((entry) => !entry.includes(" "));
75
81
  return [
76
- `Ranked by annualised yield at the bid. Wide is a spread over ${screen.criteria.maxSpreadPercent}% of the mid (--max-spread).`,
82
+ `Ranked by ${rankedBy}. Wide is a spread over ${screen.criteria.maxSpreadPercent}% of the mid (--max-spread).`,
77
83
  ...(reasoned.length ? [`No IV rank: ${reasoned.join(", ")}.`] : []),
78
84
  ...(bare.length ? [`No IV rank for ${listText(bare)} yet.`] : []),
79
85
  ];
80
86
  }
81
87
 
88
+ const money = (value: number) => value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
89
+
90
+ /**
91
+ * One name's trade, flat: money (credit or debit, max gain and loss) for one
92
+ * contract a leg as OSA counts it, break-evens per share, every percent in
93
+ * percent, reward to risk as a multiple.
94
+ */
95
+ export type StrategyScreenReportRow = {
96
+ rank: number | null; symbol: string; expiry: string | null; farExpiry: string | null; days: number | null; trade: string | null;
97
+ net: number | null; maxGain: number | null; maxLoss: number | null; unlimited: "gain" | "loss" | null;
98
+ breakEven: number | null; secondBreakEven: number | null; breakEvens: string; pop: number | null; returnToExpiry: number | null;
99
+ returnPerYear: number | null; rewardRisk: number | null; ivRank: number | null; spread: number | null; wide: boolean | null;
100
+ earnings: string; earningsBeforeExpiry: boolean | null; earningsDate: string | null; spot: number | null; note: string | null;
101
+ };
102
+
103
+ export function strategyScreenReportRow(row: StrategyScreenRow): StrategyScreenReportRow {
104
+ const { trade } = row;
105
+ return {
106
+ rank: row.rank, symbol: row.symbol, expiry: trade?.expiry ?? null, farExpiry: trade?.farExpiry ?? null, days: trade?.daysToExpiry ?? null,
107
+ // The screen keeps only trades whose mids open the way the family does, so the amount is the column's own credit or debit.
108
+ trade: trade?.text ?? null, net: trade ? Math.abs(trade.netAmount) : null, maxGain: trade?.maxGain ?? null, maxLoss: trade?.maxLoss ?? null,
109
+ unlimited: trade?.unlimited ?? null, breakEven: trade?.breakEvens[0] ?? null, secondBreakEven: trade?.breakEvens[1] ?? null,
110
+ breakEvens: trade?.breakEvens.length ? trade.breakEvens.map((value) => value.toFixed(2)).join(" / ") : "--",
111
+ pop: exportPercent(trade?.probabilityOfProfit), returnToExpiry: exportPercent(trade?.returnToExpiry),
112
+ returnPerYear: exportPercent(trade?.annualizedReturn), rewardRisk: trade?.rewardToRisk ?? null,
113
+ ivRank: row.ivRank, spread: exportPercent(trade?.quote?.spreadOfMid), wide: trade?.quote ? row.wideSpread : null,
114
+ earnings: cspEarningsText(trade), earningsBeforeExpiry: trade ? trade.earnings.beforeExpiry : null, earningsDate: trade?.earnings.date ?? null,
115
+ spot: trade?.spot ?? null, note: row.note,
116
+ };
117
+ }
118
+
119
+ /** A gain or loss for one contract a leg, `unlimited` on the side that has no limit. */
120
+ const riskCell = (side: "gain" | "loss") => (value: unknown, row: Record<string, unknown>) =>
121
+ typeof value === "number" ? money(value) : row.unlimited === side ? "unlimited" : "--";
122
+ /** `2.68x`, or `unlimited` where the gain has no limit. */
123
+ export const rewardRiskText = (value: unknown, unlimited: unknown) =>
124
+ typeof value === "number" ? `${value.toFixed(2)}x` : unlimited === "gain" ? "unlimited" : "--";
125
+
126
+ /** The unit every money column of a trade is in, said once: OSA's own for position money. */
127
+ export const STRATEGY_MONEY_UNIT = "USD per contract";
128
+
129
+ /**
130
+ * A family's columns: its money column is `Credit` or `Debit`; an income trade
131
+ * reads its annualised return and a debit trade its reward to risk. A
132
+ * calendar's trade names the expiry it buys, which exports also give alone.
133
+ */
134
+ function strategyScreenColumns(strategy: ScreenFamily): { text: HeadlessPaneColumn[]; exported: HeadlessPaneColumn[] } {
135
+ const def = strategyFamilyDef(strategy);
136
+ const lead: HeadlessPaneColumn[] = [COLUMNS[0]!, COLUMNS[1]!, COLUMNS[2]!];
137
+ const farExpiry: HeadlessPaneColumn[] = strategy === "calendar" ? [{ key: "farExpiry", header: "Bought expiry" }] : [];
138
+ const body: HeadlessPaneColumn[] = [
139
+ COLUMNS[3]!,
140
+ { key: "trade", header: "Trade", format: (value) => typeof value === "string" ? value : "--" },
141
+ { key: "net", header: def.net === "credit" ? "Credit" : "Debit", align: "right", format: (value) => typeof value === "number" ? money(value) : "--" },
142
+ { key: "maxGain", header: "Max gain", align: "right", format: riskCell("gain") },
143
+ { key: "maxLoss", header: "Max loss", align: "right", format: riskCell("loss") },
144
+ ];
145
+ const measure: HeadlessPaneColumn = def.net === "credit"
146
+ ? { key: "returnPerYear", header: "Return/yr", align: "right", format: percentCell(1) }
147
+ : { key: "rewardRisk", header: "Reward/risk", align: "right", format: (value, row) => rewardRiskText(value, row.unlimited) };
148
+ const odds: HeadlessPaneColumn[] = [{ key: "pop", header: "PoP", align: "right", format: percentCell(0) }, measure, COLUMNS[12]!, COLUMNS[7]!, COLUMNS[13]!];
149
+ const breakEven: HeadlessPaneColumn = { key: "breakEven", header: "Break-even", align: "right", format: priceCell,
150
+ textFormat: (_value, row) => String(row.breakEvens) };
151
+ const toExpiry: HeadlessPaneColumn[] = def.net === "credit"
152
+ ? [{ key: "returnToExpiry", header: "Return to expiry", align: "right", format: percentCell(2) }] : [];
153
+ return {
154
+ text: [...lead, ...body, breakEven, ...odds, NOTE],
155
+ exported: [...lead, ...farExpiry, ...body, breakEven, { key: "secondBreakEven", header: "Second break-even", format: priceCell }, ...odds,
156
+ ...toExpiry, ...EXPORT_ONLY, NOTE],
157
+ };
158
+ }
159
+
160
+ function strategyMetadata(screen: StrategyScreen) {
161
+ return { strategy: screen.criteria.strategy, criteria: screen.criteria, rankedBy: strategyRankedBy(screen.criteria.strategy),
162
+ unit: `credit, debit, max gain and max loss in ${STRATEGY_MONEY_UNIT} (one contract a leg, 100 shares); break-evens per share; percent; reward/risk as a multiple`,
163
+ methodology: "docs/research-data.md#options-strategy-screen" };
164
+ }
165
+
82
166
  export const cspScreenHeadless: HeadlessPaneDefinition<"rows"> = {
83
167
  shape: "rows",
84
168
  argument: { kind: "tickers", maximum: CSP_SCREEN_LIMIT, description: "US option underlyings to rank" },
85
169
  usListingsOnly: "Options data",
86
170
  failsWithoutData: true,
87
- description: "Cash-secured puts across a list, ranked by annualised yield on cash at the bid: for each name, the put nearest a delta "
88
- + "(0.30) on the listed expiry nearest a date or a number of days out (30), with its strike, bid, ask, spread (flagged wide over "
89
- + "--max-spread, never left out), delta, distance out of the money, yield at the mid and at the bid, IV rank and whether earnings fall before expiry.",
171
+ description: "Option strategies across a list, one family at a time (--strategy): cash-secured puts by default, ranked by annualised yield on "
172
+ + "cash at the bid, the put nearest a delta (0.30) on the listed expiry nearest a date or a number of days out (30), with its strike, bid, ask, "
173
+ + "spread (flagged wide over --max-spread, never left out), delta, distance out of the money, yield at the mid and at the bid, IV rank and whether "
174
+ + "earnings fall before expiry. Covered calls, verticals, iron condors, straddles, strangles and calendars pick their legs by delta on the same "
175
+ + "expiry and show the trade, credit or debit, max gain and loss per contract, break-evens, probability of profit, and an income trade's "
176
+ + "annualised return or a debit trade's reward to risk.",
90
177
  discovery: { screenshotReadiness: "live-dom", dataRequirements: ["Current option chains", "Cloud stored implied volatility", "Earnings calendar"],
91
- limitations: ["Assignment, margin and the interest the cash earns are not modelled."] },
178
+ limitations: ["Assignment, margin and the interest the cash earns are not modelled.",
179
+ "Probability of profit is the Black-Scholes chance at each break-even's nearest leg IV, not a forecast."] },
92
180
  describe: (_args, result) => {
93
- const criteria = (result?.metadata as { criteria?: CspScreen["criteria"] } | undefined)?.criteria;
94
- return ["Cash-secured puts", ...(criteria ? [cspScreenCriteriaText(criteria, Date.now())] : [])].join(" · ");
181
+ const metadata = result?.metadata as { criteria?: CspScreen["criteria"] | StrategyScreenCriteria; strategy?: ScreenFamily } | undefined;
182
+ const criteria = metadata?.criteria;
183
+ if (metadata?.strategy && criteria) {
184
+ return [strategyFamilyDef(metadata.strategy).title, strategyScreenCriteriaText(criteria as StrategyScreenCriteria, Date.now()), STRATEGY_MONEY_UNIT]
185
+ .join(" · ");
186
+ }
187
+ return ["Cash-secured puts", ...(criteria ? [cspScreenCriteriaText(criteria as CspScreen["criteria"], Date.now())] : [])].join(" · ");
95
188
  },
96
189
  options: [
190
+ { key: "strategy", type: "enum", example: "--strategy iron-condor",
191
+ values: STRATEGY_FAMILY_DEFS.map((def) => ({ value: def.id, ...(def.aliases.length ? { aliases: def.aliases } : {}) })),
192
+ description: "The strategy each name trades, a cash-secured put (csp) when not given; also cc, put-credit, call-credit, call-debit, "
193
+ + "put-debit, ic and call-calendar" },
97
194
  { key: "delta", type: "string", placeholder: "0.30", example: "--delta 0.20",
98
- description: `Each name's put is the one whose delta is nearest this, as a put's delta reads without its sign. Default ${CSP_SCREEN_DEFAULTS.delta.toFixed(2)}` },
195
+ description: `Each name's put is the one whose delta is nearest this, as a put's delta reads without its sign. Default ${CSP_SCREEN_DEFAULTS.delta.toFixed(2)}. `
196
+ + "Other strategies pick their legs the same way: a covered call's call, a vertical's leg nearer the money, a condor's or a strangle's put and call, "
197
+ + "a calendar's call, each strategy at its own default (0.30; 0.50 for debit verticals and calendars; 0.20 for condors and short strangles)" },
198
+ { key: "wingDelta", aliases: ["wing-delta"], type: "string", placeholder: "0.15", example: "--strategy iron-condor --wing-delta 0.05",
199
+ description: "A vertical's or an iron condor's legs further out of the money are the ones whose delta is nearest this: the legs a credit spread "
200
+ + "buys and a debit spread sells. Default 0.15 (0.30 for debit verticals, 0.10 for condors), or half --delta when that is not below it" },
99
201
  { key: "days", type: "string", placeholder: "days", example: "--days 45",
100
202
  description: `Each name's expiry is its listed one nearest this many calendar days out. Default ${CSP_SCREEN_DEFAULTS.days}` },
101
203
  { key: "expiration", type: "string", placeholder: OPTION_EXPIRATION_PLACEHOLDER, example: "--expiration 2026-11-20",
102
204
  description: `Each name's expiry is its listed one nearest this date, in place of --days, as ${OPTION_EXPIRATION_FORMAT}`,
103
205
  normalize: (value) => expirationOptionSeconds(value) },
104
206
  { key: "maxSpread", aliases: ["max-spread"], type: "string", placeholder: "percent", example: "--max-spread 15",
105
- description: `A put whose bid/ask spread is over this percent of its mid is marked wide; it is still ranked. Default ${CSP_SCREEN_DEFAULTS.maxSpreadPercent}` },
207
+ description: "A put whose bid/ask spread is over this percent of its mid is marked wide; it is still ranked. A strategy of several legs adds "
208
+ + `their spreads up against its net mid. Default ${CSP_SCREEN_DEFAULTS.maxSpreadPercent}` },
106
209
  ],
107
210
  async load(args, ctx) {
211
+ const strategy = readStrategyFamily(args.options.strategy);
212
+ if (strategy !== "csp") return loadStrategyReport(strategy, args, ctx);
213
+ if (args.options.wingDelta != null && args.options.wingDelta !== "") {
214
+ throw new Error("--wing-delta sets the far legs of a vertical or an iron condor; a cash-secured put has none");
215
+ }
108
216
  const criteria = readCspScreenCriteria(args.options);
109
217
  const instruments = await Promise.all(args.symbols.map((symbol) => resolveHeadlessInstrument(ctx, symbol)));
110
218
  const screen = await loadCspScreen(instruments, criteria, {
@@ -125,3 +233,27 @@ export const cspScreenHeadless: HeadlessPaneDefinition<"rows"> = {
125
233
  };
126
234
  },
127
235
  };
236
+
237
+ /** `fn CSP --strategy <family>`: each name's trade of the family, ranked, with its own columns. */
238
+ async function loadStrategyReport(
239
+ strategy: ScreenFamily, args: Parameters<HeadlessPaneDefinition["load"]>[0], ctx: Parameters<HeadlessPaneDefinition["load"]>[1],
240
+ ) {
241
+ const criteria = readStrategyScreenCriteria(strategy, args.options);
242
+ const instruments = await Promise.all(args.symbols.map((symbol) => resolveHeadlessInstrument(ctx, symbol)));
243
+ const screen = await loadStrategyScreen(instruments, criteria, {
244
+ scenario: createScenarioDependencies(ctx.marketData, ctx.apiClient), earnings: ctx.marketData, ivApi: ctx.apiClient,
245
+ }, ctx.signal);
246
+ const unavailable = screen.rows.filter((row) => !row.trade);
247
+ const columns = strategyScreenColumns(strategy);
248
+ return {
249
+ freshness: screen.freshness,
250
+ columns: columns.exported,
251
+ textColumns: columns.text,
252
+ rows: screen.rows.map(strategyScreenReportRow),
253
+ complete: unavailable.length === 0 && !screen.ivRankError,
254
+ unavailableSymbols: unavailable.map((row) => row.symbol),
255
+ errors: screen.ivRankError ? [`IV rank unavailable: ${screen.ivRankError}`] : [],
256
+ notes: cspScreenNotes(screen, strategyRankedBy(strategy)),
257
+ metadata: strategyMetadata(screen),
258
+ };
259
+ }