@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +163 -8
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +8 -21
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +17 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +34 -12
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +5 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +10 -10
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +22 -4
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +48 -5
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +11 -2
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/version.ts +2 -2
- 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 {
|
|
5
|
-
import
|
|
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
|
|
107
|
-
*
|
|
108
|
-
* of
|
|
109
|
-
*
|
|
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
|
-
|
|
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) <=
|
|
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;
|