gloomberb 0.16.3 → 0.16.4
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/package.json +4 -1
- package/src/api-client/askg.ts +43 -0
- package/src/api-client/auth.ts +15 -0
- package/src/api-client/beneficial-owners.ts +4 -0
- package/src/api-client/chat.ts +19 -0
- package/src/api-client/company-kpis.ts +14 -2
- package/src/api-client/data.ts +34 -16
- package/src/api-client/index.ts +12 -0
- package/src/api-client/normalizers.ts +27 -0
- package/src/api-client/paths.ts +88 -23
- package/src/api-client/request.ts +2 -1
- package/src/api-client/research-activity.ts +31 -0
- package/src/api-client/socket.ts +39 -8
- package/src/api-client/types.ts +57 -1
- package/src/api-client/upgrade-experiment.ts +90 -0
- package/src/app/global-shortcuts.ts +11 -1
- package/src/app/keybindings/actions.ts +10 -0
- package/src/app/pane-runtime/index.tsx +8 -0
- package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +65 -25
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
- package/src/app/runtime/desktop-deeplink.ts +5 -0
- package/src/app/star-prompt/model.ts +98 -0
- package/src/app/star-prompt/runtime.ts +41 -0
- package/src/app/star-prompt/status-line.tsx +190 -0
- package/src/app.tsx +13 -7
- package/src/architecture/core-builtin-allowlist.json +70 -0
- package/src/brokers/signed-in/adapter.ts +33 -2
- package/src/brokers/signed-in/client.ts +10 -0
- package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
- package/src/brokers/signed-in/sign-in.ts +62 -5
- package/src/capabilities/registry.ts +8 -1
- package/src/capabilities/types.ts +2 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +359 -99
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +164 -26
- package/src/cli/commands/search.ts +29 -8
- package/src/cli/commands/system.ts +99 -19
- package/src/cli/commands/themes.ts +64 -0
- package/src/cli/commands/ticker.ts +62 -30
- package/src/cli/desktop-pane-shot.ts +97 -16
- package/src/cli/errors.ts +25 -2
- package/src/cli/freshness.ts +7 -4
- package/src/cli/fx-pair.ts +57 -0
- package/src/cli/help.ts +14 -1
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +9 -6
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +50 -6
- package/src/cli/pane-functions/capabilities.ts +8 -2
- package/src/cli/pane-functions/catalog.ts +13 -2
- package/src/cli/pane-functions/data.ts +36 -5
- package/src/cli/pane-functions/dom.ts +21 -13
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +67 -22
- package/src/cli/pane-functions/headless.ts +5 -4
- package/src/cli/pane-functions/index.ts +3 -0
- package/src/cli/pane-functions/market-session.ts +133 -0
- package/src/cli/pane-functions/options.ts +15 -4
- package/src/cli/pane-functions/report-notices.ts +63 -0
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +4 -6
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +105 -23
- package/src/cli/registry.ts +2 -1
- package/src/cli/result.ts +50 -13
- package/src/cli/row-window.ts +76 -0
- package/src/cli/scroll-truncation.ts +34 -0
- package/src/components/chart/composite/composite-chart.tsx +43 -4
- package/src/components/chart/composite/legend.tsx +39 -6
- package/src/components/chart/composite/rasterizer.ts +64 -4
- package/src/components/chart/composite/series-cues.ts +71 -0
- package/src/components/chart/composite/text-renderer.ts +16 -8
- package/src/components/chart/composite/types.ts +3 -0
- package/src/components/command-bar/commands/direct/index.ts +9 -1
- package/src/components/command-bar/commands/plugin/items.ts +3 -1
- package/src/components/command-bar/commands/registry.ts +7 -0
- package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/routes/root/command-items.ts +2 -0
- package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
- package/src/components/command-bar/routes/root/results.ts +40 -10
- package/src/components/command-bar/routes/root/runtime.ts +8 -0
- package/src/components/command-bar/routes/root/search-providers.ts +97 -14
- package/src/components/command-bar/surface/index.tsx +18 -4
- package/src/components/command-bar/theme-picker.tsx +48 -13
- package/src/components/command-bar/workflow/ops.ts +5 -3
- package/src/components/data-table/view.tsx +28 -9
- package/src/components/layout/floating-pane.tsx +6 -2
- package/src/components/layout/market-summary.ts +23 -9
- package/src/components/layout/pane/content.tsx +46 -29
- package/src/components/layout/pane/cycle-order.ts +2 -11
- package/src/components/layout/pane/header.tsx +2 -1
- package/src/components/layout/pane/title.ts +1 -1
- package/src/components/layout/presentation.ts +22 -0
- package/src/components/layout/shell/active-drag.ts +1 -1
- package/src/components/layout/shell/drag/index.ts +4 -0
- package/src/components/layout/shell/drag/live.ts +76 -5
- package/src/components/layout/shell/drag/runtime.ts +8 -8
- package/src/components/layout/shell/index.tsx +47 -6
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +51 -10
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/pane/new-layout.ts +116 -0
- package/src/components/layout/shell/shortcuts.ts +5 -2
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-text.tsx +9 -5
- package/src/components/metric-treemap/heat-surface.tsx +27 -8
- package/src/components/metric-treemap/heat-tiles.ts +48 -17
- package/src/components/onboarding/onboarding-frame.tsx +33 -10
- package/src/components/onboarding/onboarding-wizard.tsx +49 -40
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
- package/src/components/onboarding/pro-step-layout.ts +86 -0
- package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
- package/src/components/ui/button.tsx +10 -0
- package/src/components/ui/data-table/index.tsx +38 -0
- package/src/components/ui/data-table/types.ts +8 -1
- package/src/components/ui/data-table/visible-range.ts +2 -0
- package/src/components/ui/icon.tsx +11 -2
- package/src/components/ui/index.ts +1 -1
- package/src/core/connection-health.ts +1 -1
- package/src/core/state/app/layout-reducer.ts +89 -3
- package/src/core/state/app/state.ts +8 -5
- package/src/core/state/app/types.ts +13 -2
- package/src/data/config/layout.ts +55 -21
- package/src/data/config/store/fold-panes.ts +149 -0
- package/src/data/config/store/migrations.ts +7 -0
- package/src/data/config/store/node.ts +27 -6
- package/src/data/config/store/normalize.ts +37 -3
- package/src/i18n/es.ts +14 -1
- package/src/i18n/ja.ts +14 -1
- package/src/i18n/ko.ts +14 -1
- package/src/i18n/zh-cn.ts +14 -1
- package/src/i18n/zh-tw.ts +14 -1
- package/src/layout/desks.ts +1 -2
- package/src/layout/pane-layout-move.ts +264 -0
- package/src/layout/pane-manager/hidden-panes.ts +160 -0
- package/src/layout/pane-manager/index.ts +1 -0
- package/src/market-data/coordinator/entries.ts +7 -1
- package/src/market-data/coordinator/quotes.ts +2 -2
- package/src/market-data/market/freshness.ts +118 -18
- package/src/market-data/market/status.ts +40 -0
- package/src/market-data/options-alternatives.ts +69 -0
- package/src/market-data/quotes/contributions.ts +19 -2
- package/src/market-data/quotes/metadata.ts +16 -0
- package/src/market-data/quotes/screener-live-quotes.ts +56 -5
- package/src/plugins/builtin/account-management/keyboard.ts +3 -0
- package/src/plugins/builtin/account-management/model.ts +1 -0
- package/src/plugins/builtin/account-management/navigation.ts +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +31 -4
- package/src/plugins/builtin/alerts/quotes.ts +3 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
- package/src/plugins/builtin/analytics/risk-model.ts +37 -8
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/broker-manager/model.ts +8 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
- package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
- package/src/plugins/builtin/broker-manager/table.ts +5 -0
- package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +34 -36
- package/src/plugins/builtin/chat/command-bar.ts +138 -0
- package/src/plugins/builtin/chat/composer-commands.ts +21 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
- package/src/plugins/builtin/chat/content/footer.tsx +5 -3
- package/src/plugins/builtin/chat/content/index.tsx +21 -1
- package/src/plugins/builtin/chat/content/mentions.ts +2 -1
- package/src/plugins/builtin/chat/content/user-map.ts +3 -1
- package/src/plugins/builtin/chat/controller/index.ts +48 -0
- package/src/plugins/builtin/chat/conversations.ts +256 -0
- package/src/plugins/builtin/chat/discord-command.ts +91 -0
- package/src/plugins/builtin/chat/edit-window.ts +12 -6
- package/src/plugins/builtin/chat/ghost-user.ts +14 -0
- package/src/plugins/builtin/chat/index.tsx +40 -2
- package/src/plugins/builtin/chat/layout.ts +2 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/message/header.tsx +24 -15
- package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/pane-requests.ts +60 -0
- package/src/plugins/builtin/chat/pane-state.ts +32 -2
- package/src/plugins/builtin/chat/pane.tsx +8 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
- package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
- package/src/plugins/builtin/cloud/askg/model.ts +129 -70
- package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
- package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
- package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
- package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
- package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
- package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
- package/src/plugins/builtin/cloud/plugin.tsx +36 -1
- package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
- package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
- package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
- package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
- package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
- package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
- package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
- package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
- package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
- package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
- package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
- package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
- package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
- package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
- package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
- package/src/plugins/builtin/comment-letters/client.ts +1 -3
- package/src/plugins/builtin/company-kpis/client.ts +23 -8
- package/src/plugins/builtin/company-kpis/headless.ts +3 -1
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
- package/src/plugins/builtin/composite-plugins.ts +107 -64
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
- package/src/plugins/builtin/credit-boards/index.tsx +21 -21
- package/src/plugins/builtin/credit-boards/model.ts +0 -1
- package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
- package/src/plugins/builtin/debt-maturities/client.ts +37 -9
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/data.ts +28 -7
- package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
- package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +42 -17
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/index.tsx +68 -22
- package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +17 -0
- package/src/plugins/builtin/executives/data.ts +17 -6
- package/src/plugins/builtin/executives/pane.tsx +16 -7
- package/src/plugins/builtin/filing-events/pane.tsx +22 -5
- package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
- package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
- package/src/plugins/builtin/gpu/headless.ts +9 -3
- package/src/plugins/builtin/gpu/model.ts +14 -5
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/headless.ts +6 -1
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/insider/index.tsx +4 -4
- package/src/plugins/builtin/iv-history/charts.tsx +21 -7
- package/src/plugins/builtin/iv-history/client.ts +12 -2
- package/src/plugins/builtin/iv-history/format.ts +33 -2
- package/src/plugins/builtin/iv-history/headless.ts +22 -10
- package/src/plugins/builtin/iv-history/index.tsx +3 -3
- package/src/plugins/builtin/iv-history/model.ts +61 -13
- package/src/plugins/builtin/iv-history/pane.tsx +11 -3
- package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
- package/src/plugins/builtin/jobs/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
- package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
- package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
- package/src/plugins/builtin/layout-manager/index.tsx +9 -1
- package/src/plugins/builtin/macro-day/client.ts +24 -0
- package/src/plugins/builtin/macro-day/headless.ts +11 -8
- package/src/plugins/builtin/macro-day/index.tsx +3 -0
- package/src/plugins/builtin/macro-day/model.ts +11 -6
- package/src/plugins/builtin/macro-day/pane.tsx +12 -8
- package/src/plugins/builtin/macro-day/release-table.ts +40 -0
- package/src/plugins/builtin/macro-day/releases.ts +62 -44
- package/src/plugins/builtin/market-heatmap/data.ts +25 -5
- package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
- package/src/plugins/builtin/market-heatmap/model.ts +94 -3
- package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
- package/src/plugins/builtin/market-movers/headless.ts +31 -4
- package/src/plugins/builtin/market-movers/index.tsx +8 -1
- package/src/plugins/builtin/market-movers/model.ts +71 -17
- package/src/plugins/builtin/market-movers/table.ts +50 -23
- package/src/plugins/builtin/news/headless.ts +9 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
- package/src/plugins/builtin/openfda/client.ts +1 -1
- package/src/plugins/builtin/options/carry.ts +95 -0
- package/src/plugins/builtin/options/index.tsx +58 -5
- package/src/plugins/builtin/options/strike-window.ts +183 -0
- package/src/plugins/builtin/options/table.ts +55 -3
- package/src/plugins/builtin/options/types.ts +8 -1
- package/src/plugins/builtin/options/view.tsx +161 -12
- package/src/plugins/builtin/options-positioning/headless.ts +4 -2
- package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
- package/src/plugins/builtin/options-scenario/client.ts +73 -17
- package/src/plugins/builtin/options-scenario/headless.ts +71 -24
- package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
- package/src/plugins/builtin/options-scenario/index.tsx +9 -2
- package/src/plugins/builtin/options-scenario/model.ts +33 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
- package/src/plugins/builtin/pe-band/model.ts +2 -2
- package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
- package/src/plugins/builtin/perps/equity-row.tsx +8 -5
- package/src/plugins/builtin/perps/model.ts +16 -5
- package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
- package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
- package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
- package/src/plugins/builtin/plugin-module.ts +87 -4
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
- package/src/plugins/builtin/realized-vol/headless.ts +3 -0
- package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
- package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
- package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
- package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
- package/src/plugins/builtin/risk-factors/data.ts +23 -9
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
- package/src/plugins/builtin/seasonality/pane.tsx +1 -1
- package/src/plugins/builtin/sec/index.tsx +5 -5
- package/src/plugins/builtin/sectors/client.ts +10 -2
- package/src/plugins/builtin/sectors/headless.ts +35 -6
- package/src/plugins/builtin/sectors/index.tsx +18 -1
- package/src/plugins/builtin/sectors/sector-model.ts +150 -41
- package/src/plugins/builtin/shared/command-tab.ts +51 -0
- package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
- package/src/plugins/builtin/shared/members-menu.tsx +4 -4
- package/src/plugins/builtin/shared/report-freshness.ts +50 -2
- package/src/plugins/builtin/shared/ticker-request.ts +25 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
- package/src/plugins/builtin/shared/volatility/options.ts +1 -46
- package/src/plugins/builtin/short-interest/index.tsx +19 -4
- package/src/plugins/builtin/short-interest/surface.tsx +4 -4
- package/src/plugins/builtin/short-volume/index.tsx +1 -10
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
- package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
- package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
- package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
- package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
- package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
- package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
- package/src/plugins/builtin/vol-surface/client.ts +6 -2
- package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
- package/src/plugins/builtin/vol-surface/headless.ts +124 -20
- package/src/plugins/builtin/vol-surface/model.ts +23 -11
- package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
- package/src/plugins/builtin/vol-surface/tables.ts +124 -0
- package/src/plugins/builtin/world-indices/client.ts +22 -4
- package/src/plugins/builtin/world-indices/headless.ts +54 -27
- package/src/plugins/builtin/world-indices/model.ts +4 -2
- package/src/plugins/builtin/world-indices/table.tsx +13 -2
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/builtin-editorial.ts +126 -0
- package/src/plugins/catalog-backend.ts +16 -9
- package/src/plugins/catalog-browser.ts +127 -63
- package/src/plugins/catalog-manifest.ts +73 -40
- package/src/plugins/catalog-ui.ts +26 -6
- package/src/plugins/extracted-plugins.ts +7 -1
- package/src/plugins/ownership.ts +150 -7
- package/src/plugins/plugin-functions.ts +54 -0
- package/src/plugins/plugin-off.ts +36 -0
- package/src/plugins/registry/host-actions.ts +16 -0
- package/src/plugins/registry/index.ts +84 -24
- package/src/plugins/registry/plugin-state.ts +25 -1
- package/src/plugins/runtime/context.tsx +29 -0
- package/src/react/input.ts +48 -1
- package/src/remote/app-host.tsx +5 -0
- package/src/remote/controller.ts +3 -53
- package/src/remote/matching.ts +69 -0
- package/src/remote/schema.ts +1 -1
- package/src/remote/semantic-tree.tsx +14 -0
- package/src/remote/types.ts +2 -0
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/query-bar.tsx +59 -32
- package/src/renderers/dom/host/box.tsx +1 -1
- package/src/renderers/dom/host/drag-shield.ts +17 -6
- package/src/renderers/dom/host/live-frame.ts +10 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +13 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +11 -5
- package/src/sources/provider-router/financials.ts +42 -15
- package/src/sources/provider-router/history.ts +27 -11
- package/src/sources/provider-router/index.ts +1 -1
- package/src/sources/provider-router/primary.ts +23 -5
- package/src/sources/sec-edgar.ts +68 -8
- package/src/sync/core-contributors.ts +4 -3
- package/src/test-support/cli-context.ts +2 -1
- package/src/test-support/data-provider.ts +34 -0
- package/src/test-support/shell-pane-keys.tsx +1 -0
- package/src/theme/series-colors.ts +46 -0
- package/src/theme/themes.ts +149 -0
- package/src/tickers/search/index.ts +22 -2
- package/src/tickers/search/ranking.ts +45 -1
- package/src/tickers/search/types.ts +3 -1
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/headless.ts +11 -0
- package/src/types/instrument.ts +8 -0
- package/src/types/plugin.ts +49 -3
- package/src/ui/host.tsx +8 -4
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history.ts +39 -6
- package/src/utils/rpc-timeout-error.ts +6 -2
- package/src/utils/sec-eps-basis.ts +159 -30
- package/src/utils/sec.ts +38 -34
- package/src/utils/throttled-fetch.ts +51 -22
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { StatItem } from "../../../components";
|
|
2
|
+
import type { OptionsChain } from "../../../types/financials";
|
|
3
|
+
import type { HeadlessPaneEntry } from "../../../types/headless";
|
|
4
|
+
import { formatCompact } from "../../../utils/format";
|
|
5
|
+
import { UNKNOWN_CURRENCY, type ScenarioPosition } from "./model";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hedge budget sizing (docs/research-data.md#options-scenario-analysis): how
|
|
9
|
+
* many of the position as entered a budget of NAV buys, what that spends, and
|
|
10
|
+
* how much of the underlying it protects.
|
|
11
|
+
*
|
|
12
|
+
* The position is scaled as a whole, in sets of its smallest whole ratio (a
|
|
13
|
+
* put, a 1x1 spread, a 2x1 ratio), so the hedge keeps its shape. A set costs
|
|
14
|
+
* its net debit: each leg's price times its signed contracts and multiplier.
|
|
15
|
+
* The price is the entry price as entered, except for a leg a strategy seed
|
|
16
|
+
* entered at its contract's midpoint (`priceSource: "mid"`, its id the
|
|
17
|
+
* contract's symbol in the loaded chain), which buys at the ask and sells at
|
|
18
|
+
* the bid, so a seeded hedge never spends past its budget.
|
|
19
|
+
* The protecting legs are the long puts, or the long calls when the position
|
|
20
|
+
* holds no long put; notional protected is their contracts times multiplier
|
|
21
|
+
* times spot.
|
|
22
|
+
*/
|
|
23
|
+
export interface HedgeBudgetInputs {
|
|
24
|
+
/** Portfolio NAV, in the position's currency. */
|
|
25
|
+
nav: number;
|
|
26
|
+
/** The budget as basis points of NAV: 50 is 0.5%. */
|
|
27
|
+
budgetBps: number;
|
|
28
|
+
/** The equity sleeve the hedge protects; coverage is notional over it. */
|
|
29
|
+
sleeve: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** How the set was priced: the legs as entered, chain quotes (long at the ask, short at the bid), or some of each. */
|
|
33
|
+
type HedgePriceBasis = "entry" | "quote" | "mixed";
|
|
34
|
+
|
|
35
|
+
export interface HedgeBudget extends HedgeBudgetInputs {
|
|
36
|
+
currency: string;
|
|
37
|
+
/** NAV times budget bps. */
|
|
38
|
+
budget: number;
|
|
39
|
+
basis: HedgePriceBasis;
|
|
40
|
+
/** True for a single long leg: the price is that leg's own, not a net debit. */
|
|
41
|
+
singleLeg: boolean;
|
|
42
|
+
/** Per underlying unit of one protecting contract; null when the legs' multipliers differ. */
|
|
43
|
+
sizingPrice: number | null;
|
|
44
|
+
/** What one protecting contract costs, all legs of its set included. */
|
|
45
|
+
costPerContract: number;
|
|
46
|
+
/** Units of the underlying one protecting contract covers. */
|
|
47
|
+
multiplier: number;
|
|
48
|
+
/** Protecting contracts bought: whole sets, rounded down, times the protecting contracts in a set. */
|
|
49
|
+
contracts: number;
|
|
50
|
+
/** Premium spent: whole sets times what a set costs. */
|
|
51
|
+
premium: number;
|
|
52
|
+
/** Premium over NAV in basis points: what the budget actually spends. */
|
|
53
|
+
premiumBps: number;
|
|
54
|
+
/** The underlying price notional is read at. */
|
|
55
|
+
spot: number;
|
|
56
|
+
/** Protecting contracts times multiplier times spot. */
|
|
57
|
+
notional: number;
|
|
58
|
+
/** Notional over NAV, as a fraction. */
|
|
59
|
+
notionalOfNav: number;
|
|
60
|
+
/** Notional over the sleeve, as a fraction; null without a sleeve. */
|
|
61
|
+
coverage: number | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const supplied = (value: unknown) => value != null && value !== "";
|
|
65
|
+
|
|
66
|
+
const AMOUNT_SCALES: Record<string, number> = { k: 1e3, m: 1e6, mm: 1e6, mn: 1e6, b: 1e9, bn: 1e9, t: 1e12, tn: 1e12 };
|
|
67
|
+
const AMOUNT = /^\$?\s*(\d{1,3}(?:,\d{3})+|\d+)(\.\d+)?\s*([a-z]{1,2})?$/i;
|
|
68
|
+
const AMOUNT_FORMS = "such as 100m, 2.5bn, 250k, 1,000,000 or $100m";
|
|
69
|
+
|
|
70
|
+
/** `100m`, `2.5bn`, `250k`, `1,000,000` and `$100m` as a positive amount; anything else names the flag and the forms. */
|
|
71
|
+
export function parseHedgeAmount(value: unknown, flag: string): number {
|
|
72
|
+
const text = typeof value === "number" ? String(value) : typeof value === "string" ? value.trim() : "";
|
|
73
|
+
if (/^-/.test(text)) throw new Error(`--${flag} must be greater than zero.`);
|
|
74
|
+
const match = AMOUNT.exec(text);
|
|
75
|
+
const scale = match ? match[3] ? AMOUNT_SCALES[match[3].toLowerCase()] : 1 : undefined;
|
|
76
|
+
const amount = match && scale ? Number(`${match[1]!.replaceAll(",", "")}${match[2] ?? ""}`) * scale : Number.NaN;
|
|
77
|
+
if (!Number.isFinite(amount)) throw new Error(`--${flag} must be an amount ${AMOUNT_FORMS}; got "${text}".`);
|
|
78
|
+
if (!(amount > 0)) throw new Error(`--${flag} must be greater than zero.`);
|
|
79
|
+
return amount;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseBudgetBps(value: unknown): number {
|
|
83
|
+
const text = typeof value === "number" ? String(value) : typeof value === "string" ? value.trim() : "";
|
|
84
|
+
const bps = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/.test(text) ? Number(text) : Number.NaN;
|
|
85
|
+
if (!Number.isFinite(bps)) throw new Error(`--budget-bps must be a number of basis points, such as 50 for 0.5% of NAV; got "${text}".`);
|
|
86
|
+
if (!(bps > 0)) throw new Error("--budget-bps must be greater than zero.");
|
|
87
|
+
if (bps > 10_000) throw new Error("--budget-bps must be at most 10,000 (all of NAV).");
|
|
88
|
+
return bps;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `--nav`, `--budget-bps` and `--sleeve` as pane settings or report options.
|
|
93
|
+
* Null when none is set; each needs the others it depends on.
|
|
94
|
+
*/
|
|
95
|
+
export function hedgeInputsFromSettings(settings: Record<string, unknown>): HedgeBudgetInputs | null {
|
|
96
|
+
const hasNav = supplied(settings.nav);
|
|
97
|
+
const hasBudget = supplied(settings.budgetBps);
|
|
98
|
+
const hasSleeve = supplied(settings.sleeve);
|
|
99
|
+
if (!hasNav && !hasBudget) {
|
|
100
|
+
if (hasSleeve) throw new Error("--sleeve needs --nav and --budget-bps: coverage is the notional a hedge budget buys over the sleeve.");
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (!hasBudget) throw new Error("--nav needs --budget-bps: the share of NAV to spend, in basis points (50 is 0.5%).");
|
|
104
|
+
if (!hasNav) throw new Error("--budget-bps needs --nav: the portfolio value the budget is a share of, such as 100m.");
|
|
105
|
+
return { nav: parseHedgeAmount(settings.nav, "nav"), budgetBps: parseBudgetBps(settings.budgetBps),
|
|
106
|
+
sleeve: hasSleeve ? parseHedgeAmount(settings.sleeve, "sleeve") : null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function gcd(left: number, right: number): number {
|
|
110
|
+
return right === 0 ? left : gcd(right, left % right);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Float noise off a cent-quoted premium times a multiplier, so 5.495 x 100 is 549.5. */
|
|
114
|
+
const clean = (value: number) => Math.round(value * 1e6) / 1e6;
|
|
115
|
+
|
|
116
|
+
export interface HedgeQuote { bid: number; ask: number }
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The bid and ask of each leg a strategy seeded from the chain, by leg id: the
|
|
120
|
+
* chain's, or the leg's streamed quote when the pane follows the market.
|
|
121
|
+
*/
|
|
122
|
+
function hedgeLegQuotes(
|
|
123
|
+
position: ScenarioPosition, chain: OptionsChain | null | undefined, liveQuotes?: ReadonlyMap<string, HedgeQuote>,
|
|
124
|
+
): Map<string, HedgeQuote> {
|
|
125
|
+
const contracts = new Map([...(chain?.calls ?? []), ...(chain?.puts ?? [])].map((contract) => [contract.contractSymbol, contract]));
|
|
126
|
+
const quotes = new Map<string, HedgeQuote>();
|
|
127
|
+
for (const leg of position.legs) {
|
|
128
|
+
const contract = leg.priceSource === "mid" ? contracts.get(leg.id) : undefined;
|
|
129
|
+
if (contract && contract.strike === leg.strike && contract.expiration === leg.expiration
|
|
130
|
+
&& contract.bid > 0 && contract.ask >= contract.bid && Number.isFinite(contract.ask)) {
|
|
131
|
+
const live = liveQuotes?.get(leg.id);
|
|
132
|
+
quotes.set(leg.id, live && live.bid > 0 && live.ask >= live.bid ? live : { bid: contract.bid, ask: contract.ask });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return quotes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The underlying price notional is read at: the last price when the scenario
|
|
140
|
+
* starts at a spot the option quotes imply (a seeded strategy) and no spot was
|
|
141
|
+
* typed, otherwise the scenario's own spot.
|
|
142
|
+
*/
|
|
143
|
+
function hedgeSpot(position: ScenarioPosition, marketSpot: number | null | undefined, spotTyped: boolean): number {
|
|
144
|
+
const quoteImplied = position.legs.some((leg) => leg.volatilitySource === "mid");
|
|
145
|
+
return quoteImplied && !spotTyped && marketSpot != null && Number.isFinite(marketSpot) && marketSpot > 0 ? marketSpot : position.spot;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const money = (value: number) => value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
149
|
+
|
|
150
|
+
export function sizeHedgeBudget(
|
|
151
|
+
position: ScenarioPosition,
|
|
152
|
+
inputs: HedgeBudgetInputs,
|
|
153
|
+
{ quotes = new Map(), spot = position.spot }: { quotes?: ReadonlyMap<string, HedgeQuote>; spot?: number } = {},
|
|
154
|
+
): HedgeBudget {
|
|
155
|
+
const longs = position.legs.filter((leg) => leg.quantity > 0);
|
|
156
|
+
if (!longs.length) throw new Error("A hedge budget buys options; this position has no long option.");
|
|
157
|
+
const step = position.legs.reduce((divisor, leg) => gcd(divisor, Math.abs(leg.quantity)), 0);
|
|
158
|
+
const protecting = longs.some((leg) => leg.side === "put") ? longs.filter((leg) => leg.side === "put") : longs;
|
|
159
|
+
const priced = position.legs.map((leg) => {
|
|
160
|
+
const quote = quotes.get(leg.id);
|
|
161
|
+
return { leg, quoted: !!quote, price: quote ? leg.quantity > 0 ? quote.ask : quote.bid : leg.price };
|
|
162
|
+
});
|
|
163
|
+
const quotedCount = priced.filter((entry) => entry.quoted).length;
|
|
164
|
+
const basis: HedgePriceBasis = quotedCount === 0 ? "entry" : quotedCount === priced.length ? "quote" : "mixed";
|
|
165
|
+
const setCost = clean(priced.reduce((sum, { leg, price }) => sum + price * leg.quantity / step * leg.multiplier, 0));
|
|
166
|
+
const currency = position.currency === UNKNOWN_CURRENCY ? "" : ` ${position.currency}`;
|
|
167
|
+
if (!(setCost > 0)) {
|
|
168
|
+
throw new Error(setCost < 0
|
|
169
|
+
? `This position takes in a net credit of ${money(-setCost)}${currency} per set, so a budget does not size it; a hedge budget buys a net debit.`
|
|
170
|
+
: "This position costs nothing at these prices, so a budget does not size it.");
|
|
171
|
+
}
|
|
172
|
+
const contractsPerSet = protecting.reduce((sum, leg) => sum + leg.quantity / step, 0);
|
|
173
|
+
const unitsPerSet = protecting.reduce((sum, leg) => sum + leg.quantity / step * leg.multiplier, 0);
|
|
174
|
+
const multipliers = new Set(position.legs.map((leg) => leg.multiplier));
|
|
175
|
+
const multiplier = unitsPerSet / contractsPerSet;
|
|
176
|
+
const costPerContract = setCost / contractsPerSet;
|
|
177
|
+
const sizingPrice = multipliers.size === 1 ? clean(costPerContract / multiplier) : null;
|
|
178
|
+
const budget = clean(inputs.nav * inputs.budgetBps / 10_000);
|
|
179
|
+
// A budget that is an exact multiple of the set's cost buys that many sets, despite float division.
|
|
180
|
+
const sets = Math.floor(budget / setCost * (1 + 1e-12));
|
|
181
|
+
if (sets < 1) {
|
|
182
|
+
const single = position.legs.length === 1 && contractsPerSet === 1;
|
|
183
|
+
const at = single && sizingPrice != null ? ` at ${describePrice(basis, true, sizingPrice)}` : "";
|
|
184
|
+
throw new Error(`A ${formatBps(inputs.budgetBps)} budget of ${money(inputs.nav)}${currency} NAV is ${money(budget)}${currency}; `
|
|
185
|
+
+ `one ${single ? "contract" : "set of the position"} costs ${money(setCost)}${currency}${at}.`);
|
|
186
|
+
}
|
|
187
|
+
const contracts = sets * contractsPerSet;
|
|
188
|
+
const premium = clean(sets * setCost);
|
|
189
|
+
const notional = sets * unitsPerSet * spot;
|
|
190
|
+
return { ...inputs, currency: position.currency, budget, basis, singleLeg: position.legs.length === 1, sizingPrice,
|
|
191
|
+
costPerContract, multiplier, contracts, premium, premiumBps: premium / inputs.nav * 10_000, spot, notional,
|
|
192
|
+
notionalOfNav: notional / inputs.nav, coverage: inputs.sleeve == null ? null : notional / inputs.sleeve };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Hedge sizing for a scenario, read the same way by the report and the pane:
|
|
197
|
+
* null without `--nav` and `--budget-bps`; throws on a bad input or a
|
|
198
|
+
* position a budget cannot size.
|
|
199
|
+
*/
|
|
200
|
+
export function scenarioHedgeBudget(
|
|
201
|
+
settings: Record<string, unknown>,
|
|
202
|
+
position: ScenarioPosition | null,
|
|
203
|
+
market: { chain?: OptionsChain | null; spot?: number | null; liveQuotes?: ReadonlyMap<string, HedgeQuote> } | null | undefined,
|
|
204
|
+
): HedgeBudget | null {
|
|
205
|
+
const inputs = hedgeInputsFromSettings(settings);
|
|
206
|
+
if (!inputs) return null;
|
|
207
|
+
if (!position?.legs.length) throw new Error("A hedge budget needs a position; add --legs or select --strategy.");
|
|
208
|
+
return sizeHedgeBudget(position, inputs, { quotes: hedgeLegQuotes(position, market?.chain, market?.liveQuotes),
|
|
209
|
+
spot: hedgeSpot(position, market?.spot, supplied(settings.spot)) });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** `50 bps`, `12.5 bps`. */
|
|
213
|
+
function formatBps(value: number): string {
|
|
214
|
+
return `${Number(value.toFixed(2))} bps`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A per-unit premium with the decimals it was quoted to: 5.51, 5.495. */
|
|
218
|
+
export function formatHedgePrice(value: number): string {
|
|
219
|
+
return value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 4 });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The price a budget is sized at, and where it came from: `5.51 per share, the ask`, `2.30 per share net debit at entry prices`. */
|
|
223
|
+
function describePrice(basis: HedgePriceBasis, singleLeg: boolean, price: number): string {
|
|
224
|
+
const value = `${formatHedgePrice(price)} per share`;
|
|
225
|
+
if (singleLeg) return basis === "quote" ? `${value}, the ask` : `${value}, the entry price`;
|
|
226
|
+
return basis === "quote" ? `${value} net debit: long legs at the ask, short at the bid`
|
|
227
|
+
: basis === "mixed" ? `${value} net debit: quoted legs at the ask or bid, the rest at entry`
|
|
228
|
+
: `${value} net debit at entry prices`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The price basis in a word or two, for a figure's detail: `ask 5.51`, `entry 5.495`, `net debit 2.30`. */
|
|
232
|
+
function shortPrice(hedge: HedgeBudget): string {
|
|
233
|
+
if (hedge.sizingPrice == null) return `${money(hedge.costPerContract)} each`;
|
|
234
|
+
const word = !hedge.singleLeg ? "net debit" : hedge.basis === "quote" ? "ask" : "entry";
|
|
235
|
+
return `${word} ${formatHedgePrice(hedge.sizingPrice)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const percent = (fraction: number) => `${(fraction * 100).toFixed(1)}%`;
|
|
239
|
+
|
|
240
|
+
/** `551.00 USD (5.51 x 100)`: what one protecting contract costs, and the price behind it. */
|
|
241
|
+
function costText(cost: number, sizingPrice: number | null, multiplier: number, unit: string): string {
|
|
242
|
+
return `${money(cost)}${unit}${sizingPrice == null ? "" : ` (${formatHedgePrice(sizingPrice)} x ${multiplier})`}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The report's Hedge budget section; `value` keeps full precision, `formatted` is what text output prints. */
|
|
246
|
+
export function hedgeReportEntries(hedge: HedgeBudget): HeadlessPaneEntry[] {
|
|
247
|
+
const unit = hedge.currency && hedge.currency !== UNKNOWN_CURRENCY ? ` ${hedge.currency}` : "";
|
|
248
|
+
return [
|
|
249
|
+
{ key: "nav", label: "NAV", value: hedge.nav, formatted: `${money(hedge.nav)}${unit}` },
|
|
250
|
+
{ key: "budget", label: "Budget", value: hedge.budget, formatted: `${money(hedge.budget)}${unit} (${formatBps(hedge.budgetBps)} of NAV)` },
|
|
251
|
+
{ key: "sizingPrice", label: "Sizing price", value: hedge.sizingPrice,
|
|
252
|
+
formatted: hedge.sizingPrice == null ? "--" : describePrice(hedge.basis, hedge.singleLeg, hedge.sizingPrice) },
|
|
253
|
+
{ key: "costPerContract", label: "Cost per contract", value: hedge.costPerContract, formatted: costText(hedge.costPerContract, hedge.sizingPrice, hedge.multiplier, unit) },
|
|
254
|
+
{ key: "contracts", label: "Contracts", value: hedge.contracts, formatted: hedge.contracts.toLocaleString("en-US") },
|
|
255
|
+
{ key: "premium", label: "Premium spent", value: hedge.premium, formatted: `${money(hedge.premium)}${unit}` },
|
|
256
|
+
{ key: "premiumBps", label: "Premium / NAV", value: hedge.premiumBps, formatted: `${formatBps(hedge.premiumBps)} (${(hedge.premiumBps / 100).toFixed(2)}% of NAV)` },
|
|
257
|
+
{ key: "notional", label: "Notional protected", value: hedge.notional,
|
|
258
|
+
formatted: `${money(hedge.notional)}${unit} (${hedge.contracts.toLocaleString("en-US")} x ${hedge.multiplier} x ${formatHedgePrice(hedge.spot)})` },
|
|
259
|
+
{ key: "notionalOfNav", label: "Notional / NAV", value: hedge.notionalOfNav, formatted: percent(hedge.notionalOfNav) },
|
|
260
|
+
hedge.coverage == null || hedge.sleeve == null
|
|
261
|
+
? { key: "coverage", label: "Coverage", value: null, formatted: "No equity sleeve given; add --sleeve <amount> to read notional against it" }
|
|
262
|
+
: { key: "coverage", label: "Coverage", value: hedge.coverage, formatted: `${percent(hedge.coverage)} of a ${money(hedge.sleeve)}${unit} sleeve` },
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** `100M`, `70.62M`: an amount short enough for a figure. */
|
|
267
|
+
const short = (value: number) => formatCompact(value);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The pane's Hedge budget figures: the same numbers as the report, sized for a
|
|
271
|
+
* StatGrid. The currency is the P&L figure's, so it is not repeated here.
|
|
272
|
+
*/
|
|
273
|
+
export function hedgeStatItems(hedge: HedgeBudget): StatItem[] {
|
|
274
|
+
return [
|
|
275
|
+
{ id: "hedge-budget", label: "Hedge budget", value: money(hedge.budget), detail: `${formatBps(hedge.budgetBps)} of ${short(hedge.nav)}` },
|
|
276
|
+
{ id: "hedge-contracts", label: "Contracts", value: hedge.contracts.toLocaleString("en-US"), detail: shortPrice(hedge) },
|
|
277
|
+
{ id: "hedge-premium", label: "Premium", value: money(hedge.premium), detail: `${formatBps(hedge.premiumBps)} of NAV` },
|
|
278
|
+
{ id: "hedge-notional", label: "Notional", value: short(hedge.notional),
|
|
279
|
+
detail: `${percent(hedge.notionalOfNav)} of NAV at ${formatHedgePrice(hedge.spot)}` },
|
|
280
|
+
hedge.coverage == null || hedge.sleeve == null
|
|
281
|
+
? { id: "hedge-coverage", label: "Coverage", value: "--", detail: "no sleeve set" }
|
|
282
|
+
: { id: "hedge-coverage", label: "Coverage", value: percent(hedge.coverage), detail: `of ${short(hedge.sleeve)} sleeve` },
|
|
283
|
+
];
|
|
284
|
+
}
|
|
@@ -3,7 +3,8 @@ import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
|
3
3
|
import { OptionsScenarioPane } from "./pane";
|
|
4
4
|
import { optionsScenarioHeadless } from "./headless";
|
|
5
5
|
|
|
6
|
-
const scenarioSettingKeys = ["legs", "strategy", "expiration", "spot", "rate", "dividendYield", "currency", "asOf", "date", "volShift",
|
|
6
|
+
const scenarioSettingKeys = ["legs", "strategy", "strike", "expiration", "spot", "rate", "dividendYield", "currency", "asOf", "date", "volShift",
|
|
7
|
+
"spotRange", "seedLeg", "nav", "budgetBps", "sleeve"];
|
|
7
8
|
|
|
8
9
|
export const optionsScenarioModule: PluginModule = {
|
|
9
10
|
panes: [{
|
|
@@ -14,13 +15,19 @@ export const optionsScenarioModule: PluginModule = {
|
|
|
14
15
|
title: "Options Scenario Settings",
|
|
15
16
|
fields: [
|
|
16
17
|
{ key: "spotRange", label: "Spot grid range (%)", type: "text" },
|
|
18
|
+
{ key: "nav", label: "Hedge NAV", type: "text", placeholder: "100m",
|
|
19
|
+
description: "Portfolio NAV a hedge budget is a share of, such as 100m, 2.5bn or 1,000,000. Set with a budget to size the position." },
|
|
20
|
+
{ key: "budgetBps", label: "Hedge budget (bps)", type: "text", placeholder: "50",
|
|
21
|
+
description: "Share of NAV to spend on the position, in basis points: 50 is 0.5%." },
|
|
22
|
+
{ key: "sleeve", label: "Equity sleeve", type: "text", placeholder: "100m",
|
|
23
|
+
description: "The equity the hedge protects, for coverage: notional protected over the sleeve." },
|
|
17
24
|
],
|
|
18
25
|
},
|
|
19
26
|
}],
|
|
20
27
|
paneTemplates: [createTickerSurfacePaneTemplate({
|
|
21
28
|
id: "options-scenario-pane", paneId: "options-scenario", label: "Options Scenario",
|
|
22
29
|
description: "Build multi-leg positions and compare scenario P&L, payoff and aggregate Greeks.",
|
|
23
|
-
keywords: ["osa", "options", "scenario", "payoff", "strategy", "greeks"],
|
|
30
|
+
keywords: ["osa", "options", "scenario", "payoff", "strategy", "greeks", "hedge", "hedging", "collar", "protective put", "put spread"],
|
|
24
31
|
shortcut: "OSA", publicShare: true,
|
|
25
32
|
settings: (symbol, _context, options) => ({ symbol, ...Object.fromEntries(scenarioSettingKeys
|
|
26
33
|
.filter((key) => options?.values?.[key] != null).map((key) => [key, options!.values![key]])) }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { daysToExpiryFrom, valueOption, type OptionValuation } from "../shared/volatility";
|
|
2
2
|
import { isFiniteNumber } from "../../../utils/guards";
|
|
3
|
+
import { isUsListingExchange } from "../../../utils/exchanges";
|
|
3
4
|
|
|
4
5
|
const DAY_MS = 86_400_000;
|
|
5
6
|
const MAX_DATE_MS = Date.UTC(9999, 11, 31);
|
|
@@ -19,6 +20,12 @@ export interface ScenarioLeg {
|
|
|
19
20
|
volatility: number;
|
|
20
21
|
/** "mid" when the IV was solved from the quote midpoint rather than taken from the provider. */
|
|
21
22
|
volatilitySource?: "mid";
|
|
23
|
+
/**
|
|
24
|
+
* "mid" when a strategy seed entered the leg at the midpoint of the chain
|
|
25
|
+
* contract its id names; a hedge budget then buys it at the ask. Saving the
|
|
26
|
+
* leg from the editor drops it: the price is then as typed.
|
|
27
|
+
*/
|
|
28
|
+
priceSource?: "mid";
|
|
22
29
|
multiplier: number;
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -129,6 +136,32 @@ export function validateScenarioInputs(position: ScenarioPosition): string | nul
|
|
|
129
136
|
return null;
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
/** The stored stand-in for a currency nothing names; shown as `currency unknown`. */
|
|
140
|
+
export const UNKNOWN_CURRENCY = "UNKNOWN";
|
|
141
|
+
export const currencyLabel = (currency: string) => currency === UNKNOWN_CURRENCY ? "currency unknown" : currency;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The position's currency: the one typed, else the underlying's quote
|
|
145
|
+
* currency, else USD for a US listing (a bare symbol is one, as Gloom Cloud
|
|
146
|
+
* lists only US options). A listing abroad with no quote stays unknown.
|
|
147
|
+
*/
|
|
148
|
+
export function scenarioCurrency(typed: unknown, quoted: string | null | undefined, exchange: string | undefined): string {
|
|
149
|
+
const explicit = typed == null ? "" : String(typed).trim().toUpperCase();
|
|
150
|
+
if (explicit) return explicit;
|
|
151
|
+
if (quoted?.trim()) return quoted.trim().toUpperCase();
|
|
152
|
+
return !exchange || isUsListingExchange(exchange) ? "USD" : UNKNOWN_CURRENCY;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What the position's money figures (value, P&L, risk, Greeks in currency) are
|
|
157
|
+
* for: they add every contract times its multiplier, while entry prices and
|
|
158
|
+
* spot are per share. One contract reads `per contract`.
|
|
159
|
+
*/
|
|
160
|
+
export function scenarioValueUnit(position: Pick<ScenarioPosition, "legs">): string {
|
|
161
|
+
const contracts = position.legs.reduce((sum, leg) => sum + Math.abs(leg.quantity), 0);
|
|
162
|
+
return contracts === 1 ? "per contract" : `for ${contracts.toLocaleString("en-US")} contracts`;
|
|
163
|
+
}
|
|
164
|
+
|
|
132
165
|
/** Also validates restored persistence values at the model boundary. */
|
|
133
166
|
export function validatePosition(position: ScenarioPosition): string | null {
|
|
134
167
|
return validateScenarioInputs(position) ?? (position.legs.length ? null : "Add at least one option leg.");
|
|
@@ -21,10 +21,14 @@ import { buildOptionQuoteKey, freshOptionQuote, OPTIONS_QUOTE_EXCHANGE } from ".
|
|
|
21
21
|
import { liveScenarioPosition, scenarioLegContractSymbol } from "./live";
|
|
22
22
|
import { ScenarioPayoffChart } from "./charts";
|
|
23
23
|
import { ScenarioLegEditor, ScenarioSaveForm, ScenarioInputsForm } from "./editor";
|
|
24
|
-
import { loadScenarioMarket,
|
|
25
|
-
import { buildScenario,
|
|
24
|
+
import { loadScenarioMarket, scenarioControlsFromSettings, scenarioPositionFromSettings, type ScenarioMarketSnapshot } from "./client";
|
|
25
|
+
import { buildScenario, currencyLabel, scenarioCurrency, scenarioValueUnit, UNKNOWN_CURRENCY, type ScenarioLeg, type ScenarioPosition,
|
|
26
|
+
type ScenarioControls, validatePosition } from "./model";
|
|
26
27
|
import { EMPTY_SAVED_STRATEGIES, restoreSavedStrategies, type SavedScenarioStrategy } from "./state";
|
|
27
28
|
import { useScenarioEvidence } from "./evidence";
|
|
29
|
+
import { hedgeStatItems, scenarioHedgeBudget, type HedgeQuote } from "./hedge";
|
|
30
|
+
import { readOptionExpiration } from "../../../utils/option-expiry";
|
|
31
|
+
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
28
32
|
|
|
29
33
|
const EMPTY_ERRORS: string[] = [];
|
|
30
34
|
const TABS = [{ value: "payoff", label: "Payoff" }, { value: "grid", label: "P&L grid" }, { value: "legs", label: "Legs" }];
|
|
@@ -44,9 +48,12 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
44
48
|
const [snapshotErrors] = usePaneSettingValue<string[]>("scenarioSnapshotErrors", EMPTY_ERRORS);
|
|
45
49
|
const [frozenMarket] = usePaneSettingValue<ScenarioMarketSnapshot | null>("scenarioMarketSnapshot", null);
|
|
46
50
|
const target = resolveOptionsTarget(ticker);
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
51
|
+
// A listing is keyed "SPY:ARCX", both in the pane's binding and in the ticker record opened under it: split either.
|
|
52
|
+
const listed = parsePublicTickerKey(target?.effectiveTicker ?? symbol ?? String(settings.symbol ?? ""));
|
|
53
|
+
const underlying = listed.symbol;
|
|
54
|
+
const exchange = listed.exchange || target?.effectiveExchange || undefined;
|
|
55
|
+
// A strategy seeded for an expiry (`OSA SPY --strategy put --expiration 2027-01-15`) opens that expiry's chain.
|
|
56
|
+
const [expiration, setExpiration] = usePluginPaneState<number | null>("chainExpiration", readOptionExpiration(settings.expiration));
|
|
50
57
|
const [tab, setTab] = usePluginPaneState("activeTabId", "payoff");
|
|
51
58
|
const [detail, setDetail] = useState<"chain" | "leg" | "save" | "inputs" | null>(null);
|
|
52
59
|
const [editingLeg, setEditingLeg] = useState<ScenarioLeg | null>(null);
|
|
@@ -62,9 +69,9 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
62
69
|
const loadMarket = useCallback(async (force: boolean) => {
|
|
63
70
|
controller.current?.abort();
|
|
64
71
|
const abort = new AbortController(); controller.current = abort;
|
|
65
|
-
return loadScenarioMarket({ instrument: { symbol: underlying, exchange
|
|
72
|
+
return loadScenarioMarket({ instrument: { symbol: underlying, exchange },
|
|
66
73
|
expiration: expiration ?? undefined, forceRefresh: force, signal: abort.signal });
|
|
67
|
-
}, [underlying,
|
|
74
|
+
}, [underlying, exchange, expiration]);
|
|
68
75
|
// Fully specified calculations and screenshot snapshots never need a quote refresh.
|
|
69
76
|
const needsMarket = !frozen && (!["spot", "rate", "dividendYield"].every((key) => settings[key] != null && settings[key] !== "") || detail === "chain");
|
|
70
77
|
const resource = useAsyncResource(underlying && needsMarket ? loadMarket : null);
|
|
@@ -98,18 +105,20 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
98
105
|
if (!follow || !storedPosition) return null;
|
|
99
106
|
const spotQuote = resolveEntryData(liveEntries.get(buildQuoteKey({ symbol: underlying, exchange: exchange ?? "" })));
|
|
100
107
|
if (!spotQuote || spotQuote.stale || !(spotQuote.price > 0) || !Number.isFinite(spotQuote.price)
|
|
101
|
-
|| (spotQuote.currency && storedPosition.currency !==
|
|
108
|
+
|| (spotQuote.currency && storedPosition.currency !== UNKNOWN_CURRENCY && spotQuote.currency !== storedPosition.currency)) return null;
|
|
102
109
|
const freshness = { now: Math.max(freshnessNow, Date.now()), subscriptionStartedAt };
|
|
103
110
|
const mids = new Map<string, number>();
|
|
111
|
+
const quotes = new Map<string, HedgeQuote>();
|
|
104
112
|
let delayed = spotQuote.dataSource !== "live";
|
|
105
113
|
for (const [id, symbol] of legSymbols) {
|
|
106
114
|
const quote = freshOptionQuote(liveEntries.get(buildOptionQuoteKey(symbol)), freshness);
|
|
107
115
|
const mid = quote && quote.bid != null && quote.ask != null ? optionMid({ bid: quote.bid, ask: quote.ask }) : null;
|
|
108
116
|
if (mid == null) continue;
|
|
109
117
|
mids.set(id, mid);
|
|
118
|
+
quotes.set(id, { bid: quote!.bid!, ask: quote!.ask! });
|
|
110
119
|
delayed ||= quote!.dataSource !== "live";
|
|
111
120
|
}
|
|
112
|
-
return { position: liveScenarioPosition(storedPosition, spotQuote.price, Date.now(), mids),
|
|
121
|
+
return { position: liveScenarioPosition(storedPosition, spotQuote.price, Date.now(), mids), quotes,
|
|
113
122
|
observedAt: spotQuote.lastUpdated, basis: delayed ? "delayed" : "real-time" };
|
|
114
123
|
}, [follow, storedPosition, liveEntries, underlying, exchange, legSymbols, freshnessNow, subscriptionStartedAt]);
|
|
115
124
|
const position = live?.position ?? storedPosition;
|
|
@@ -132,7 +141,16 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
132
141
|
}, [frozen, position, controls, seeded.error, controlState, baseControls.error]);
|
|
133
142
|
const scenario = result.scenario;
|
|
134
143
|
const error = localError ?? result.error;
|
|
135
|
-
|
|
144
|
+
// The report's Hedge budget, from the same position and quotes; a live position sizes at its live quotes and spot.
|
|
145
|
+
const hedge = useMemo(() => {
|
|
146
|
+
if (!scenario) return { value: null, error: null };
|
|
147
|
+
try {
|
|
148
|
+
return { value: scenarioHedgeBudget(settings, scenario.position,
|
|
149
|
+
live ? { chain: market?.chain, liveQuotes: live.quotes } : market), error: null };
|
|
150
|
+
} catch (error) { return { value: null, error: error instanceof Error ? error.message : String(error) }; }
|
|
151
|
+
}, [settings, scenario, market, live]);
|
|
152
|
+
const baseline = (): ScenarioPosition => position ?? { symbol: underlying, exchange,
|
|
153
|
+
currency: scenarioCurrency(settings.currency, market?.currency || ticker?.metadata.currency, exchange ?? market?.exchange),
|
|
136
154
|
spot: Number.isFinite(explicitSpot) ? explicitSpot : market?.spot ?? NaN,
|
|
137
155
|
rate: settings.rate != null ? Number(settings.rate) / 100 : market?.rate ?? NaN,
|
|
138
156
|
dividendYield: settings.dividendYield != null ? Number(settings.dividendYield) / 100 : market?.dividendYield ?? NaN,
|
|
@@ -215,9 +233,10 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
215
233
|
usePaneFooter("osa", () => ({ info: [
|
|
216
234
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading chain", tone: "muted" as const }] }] : []),
|
|
217
235
|
...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
236
|
+
...(hedge.error ? [{ id: "hedge-error", parts: [{ text: hedge.error, tone: "warning" as const }] }] : []),
|
|
218
237
|
...(live ? [{ id: "asof", parts: [{ text: `${live.basis} · as of ${new Date(live.observedAt).toISOString().slice(11, 19)} UTC`, tone: "muted" as const }] }]
|
|
219
238
|
: position ? [{ id: "asof", parts: [{ text: `${dateLabel(position.asOf)} · ${market?.source ? "market" : "input assumptions"}${midVolatility}`, tone: "muted" as const }] }] : []),
|
|
220
|
-
], hints }), [hints, position, market?.source, resource.loading, error, live?.basis, live?.observedAt, midVolatility]);
|
|
239
|
+
], hints }), [hints, position, market?.source, resource.loading, error, hedge.error, live?.basis, live?.observedAt, midVolatility]);
|
|
221
240
|
useScenarioEvidence({ scenario, view: tab, loading: !!resource.loading && !scenario, error: error ?? (snapshotErrors.join("; ") || null), notices });
|
|
222
241
|
// A choice dialog (scenario date, saved strategies) owns the keys while open.
|
|
223
242
|
const dialogOpen = useDialogState((state) => state.isOpen);
|
|
@@ -238,11 +257,13 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
238
257
|
const tabsFocused = focused && !volActive && !detail;
|
|
239
258
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused: tabsFocused, dense: true });
|
|
240
259
|
const risk = scenario?.expiryRisk;
|
|
260
|
+
// Value and P&L add every contract; spot and entry prices are per share.
|
|
261
|
+
const valueUnit = scenario ? `${currencyLabel(scenario.position.currency)} ${scenarioValueUnit(scenario.position)}` : "";
|
|
241
262
|
const stats: StatItem[] = scenario ? [
|
|
242
|
-
{ id: "pnl", label: "P&L", value: money(scenario.valuation.pnl), detail:
|
|
263
|
+
{ id: "pnl", label: "P&L", value: money(scenario.valuation.pnl), detail: valueUnit,
|
|
243
264
|
tone: cents(scenario.valuation.pnl) >= 0 ? "positive" : "negative" },
|
|
244
|
-
{ id: "spot", label: "Spot", value: money(scenario.position.spot) },
|
|
245
|
-
{ id: "value", label: "Value", value: money(scenario.valuation.price) },
|
|
265
|
+
{ id: "spot", label: "Spot", value: money(scenario.position.spot), detail: "per share" },
|
|
266
|
+
{ id: "value", label: "Value", value: money(scenario.valuation.price), detail: valueUnit },
|
|
246
267
|
{ id: "max-profit", label: "Max profit", value: risk?.unlimitedProfit ? "Unlimited" : money(risk?.maxProfit ?? null) },
|
|
247
268
|
{ id: "max-loss", label: "Max loss", value: risk?.unlimitedLoss ? "Unlimited" : money(risk?.maxLoss ?? null) },
|
|
248
269
|
{ id: "breakevens", label: "Breakevens", value: risk?.breakevens.map(money).join(", ") || "--" },
|
|
@@ -252,11 +273,13 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
252
273
|
{ id: "vega", label: "Vega", value: money(scenario.valuation.vegaPerPoint), detail: "per pt" },
|
|
253
274
|
{ id: "rho", label: "Rho", value: money(scenario.valuation.rhoPerPoint), detail: "per pt" },
|
|
254
275
|
] : [];
|
|
276
|
+
const hedgeItems = hedge.value ? hedgeStatItems(hedge.value) : [];
|
|
255
277
|
// The query bar and the stat band sit above the content only once a scenario prices.
|
|
256
|
-
const bodyHeight = Math.max(3, height - tabRows - (scenario ? 1 + statGridRows(stats, width) : 0)
|
|
278
|
+
const bodyHeight = Math.max(3, height - tabRows - (scenario ? 1 + statGridRows(stats, width) : 0)
|
|
279
|
+
- (hedgeItems.length ? statGridRows(hedgeItems, width) : 0));
|
|
257
280
|
const legColumns: DataTableColumn[] = [{ id: "quantity", label: "Contracts", width: 10, align: "right" },
|
|
258
281
|
{ id: "side", label: "Option", width: 7, align: "left" }, { id: "strike", label: "Strike", width: 11, align: "right" },
|
|
259
|
-
{ id: "expiration", label: "Expiry", width: 12, align: "left" }, { id: "price", label: "Entry /
|
|
282
|
+
{ id: "expiration", label: "Expiry", width: 12, align: "left" }, { id: "price", label: "Entry / share", width: 14, align: "right" },
|
|
260
283
|
{ id: "volatility", label: "IV %", width: 9, align: "right" }, { id: "multiplier", label: "Units", width: 7, align: "right" }];
|
|
261
284
|
const activeContent = scenario && tab === "payoff" ? <ScenarioPayoffChart scenario={scenario} width={width} height={bodyHeight} focused={tabsFocused} />
|
|
262
285
|
: scenario && tab === "grid" ? <DataTableView focused={focused && !volActive} rootWidth={width}
|
|
@@ -308,6 +331,7 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
308
331
|
]}
|
|
309
332
|
/>
|
|
310
333
|
<StatGrid items={stats} width={width} />
|
|
334
|
+
{hedgeItems.length > 0 && <StatGrid items={hedgeItems} width={width} />}
|
|
311
335
|
</>}
|
|
312
336
|
{!position?.legs.length && !scenario ? <PaneStatusBody loading={!!resource.loading && !market} error={seeded.error} subject="scenario inputs">
|
|
313
337
|
<Box paddingX={1}><EmptyState title="Build an options position." actions={<Button label="Add leg" onPress={addTyped} />} /></Box>
|
|
@@ -120,8 +120,8 @@ function epsSeries(financials: TickerFinancials, period: "ttm" | "annual"): Time
|
|
|
120
120
|
* by the shared statement series, which keeps declared gaps and broken runs
|
|
121
121
|
* of quarters unavailable. A reported fiscal year is itself a trailing
|
|
122
122
|
* twelve-month figure; at a fiscal year end it replaces the quarterly sum.
|
|
123
|
-
* A figure
|
|
124
|
-
*
|
|
123
|
+
* A figure is known no later than the company's report of that quarter, when
|
|
124
|
+
* `reports` has it, including one with no publication date of its own.
|
|
125
125
|
*/
|
|
126
126
|
export function trailingEpsSteps(financials: TickerFinancials, reports: readonly ReportDate[] = []): EpsStep[] {
|
|
127
127
|
const known = (row: FinancialStatement) => datedByReport(knownWhenFiled(row), reports);
|
|
@@ -65,14 +65,18 @@ function reportedWhen(periodEnd: string, reports: readonly ReportDate[]): string
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* The statement with its EPS dated by the company's report
|
|
69
|
-
* publication date of its own
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
68
|
+
* The statement with its EPS dated by the company's report when it has no
|
|
69
|
+
* publication date of its own or a later one: the figure was public once the
|
|
70
|
+
* period was reported, and a filing that repeats or restates it afterwards (a
|
|
71
|
+
* split onto a new share count, a comparative in a later 10-K) does not delay
|
|
72
|
+
* it. A fiscal year is dated by the report of its last quarter, which shares
|
|
73
|
+
* its period end. A trailing sum takes the latest of its four quarters from
|
|
74
|
+
* the shared series, so it cannot be known before all four are.
|
|
73
75
|
*/
|
|
74
76
|
export function datedByReport(row: FinancialStatement, reports: readonly ReportDate[]): FinancialStatement {
|
|
75
|
-
if (!reports.length || typeof row.eps !== "number" || !Number.isFinite(row.eps)
|
|
77
|
+
if (!reports.length || typeof row.eps !== "number" || !Number.isFinite(row.eps)) return row;
|
|
76
78
|
const known = reportedWhen(row.date, reports);
|
|
77
|
-
|
|
79
|
+
const onRecord = statementFieldAvailability(row, "eps");
|
|
80
|
+
if (!known || (onRecord !== undefined && Date.parse(onRecord) <= Date.parse(known))) return row;
|
|
81
|
+
return { ...row, fieldAvailability: { ...row.fieldAvailability, eps: known } };
|
|
78
82
|
}
|
|
@@ -6,7 +6,7 @@ import { useAsyncResource, useAutoRefresh, usePluginAppActions } from "../../../
|
|
|
6
6
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
7
7
|
import { perpEquityIdentity } from "./equity-identity";
|
|
8
8
|
import { loadPerpsEquity } from "./client";
|
|
9
|
-
import { compact,
|
|
9
|
+
import { compact, percent, perpDescription, price, readingTime } from "./model";
|
|
10
10
|
|
|
11
11
|
/** A failed optional market comparison never blocks a stock's quote. */
|
|
12
12
|
export function PerpEquityRow({ symbol, exchange, instrumentType, maxRows = 3 }: { symbol: string; exchange?: string; instrumentType?: string; maxRows?: number }) {
|
|
@@ -23,8 +23,11 @@ export function PerpEquityRow({ symbol, exchange, instrumentType, maxRows = 3 }:
|
|
|
23
23
|
useAutoRefresh(resource.updatedAt, resource.load, { intervalMs: 60_000 });
|
|
24
24
|
const rows = resource.data?.payload.rows.filter((row) => !row.delisted) ?? [];
|
|
25
25
|
if (!enabled || !rows.length) return null;
|
|
26
|
-
return <>{rows.slice(0, maxRows).map((row) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
return <>{rows.slice(0, maxRows).map((row) => {
|
|
27
|
+
const read = readingTime(row.observedAt);
|
|
28
|
+
return <ActionRow key={row.marketId} height={2}
|
|
29
|
+
// Funding reads per 8h here as it does in PERP, whatever interval the venue pays on.
|
|
30
|
+
label={`${row.baseAsset} · ${perpDescription(row)} ${price(row.markPrice)} ${row.quoteCurrency} ${percent(row.underlyingPremium, 2)} vs last${row.stale || resource.data?.stale ? " · stale" : ""}\nFunding ${percent(row.fundingRate8h, 4)} per 8 h · OI ${compact(row.openInterestUsd)} USD · oracle ${percent(row.premium, 2)}${read ? ` · as of ${read}` : ""}`}
|
|
31
|
+
onPress={() => createPaneFromTemplate("perps-pane", { arg: row.marketId })} />;
|
|
32
|
+
})}</>;
|
|
30
33
|
}
|
|
@@ -22,6 +22,16 @@ export const time = (s: string | null | undefined) => s ? `${s.slice(0, 10)} ${s
|
|
|
22
22
|
/** A list row's observation, month to minute: the header names UTC and the footer the full as-of. */
|
|
23
23
|
const observed = (s: string | null | undefined) => s ? `${s.slice(5, 10)} ${s.slice(11, 16)}` : "--";
|
|
24
24
|
export const marketLabel = (row: PerpBoardRow) => row.dex && row.dex !== "default" ? `${row.baseAsset} · ${row.dex.toUpperCase()}` : row.baseAsset;
|
|
25
|
+
/** What the contract is, in plain words: perpetual futures never expire and trade around the clock. */
|
|
26
|
+
export const perpDescription = (row: Pick<PerpBoardRow, "venue">) =>
|
|
27
|
+
`Perpetual futures (24/7 ${row.venue === "hyperliquid" || row.venue === "dydx" ? "on-chain" : venueName(row.venue)} market)`;
|
|
28
|
+
/** When the venue's figures were read, in UTC: the time alone for today, with the date for an older reading. */
|
|
29
|
+
export function readingTime(observedAt: string | null | undefined, now = Date.now()): string | null {
|
|
30
|
+
const read = observedAt ? Date.parse(observedAt) : Number.NaN;
|
|
31
|
+
if (!Number.isFinite(read)) return null;
|
|
32
|
+
const iso = new Date(read).toISOString();
|
|
33
|
+
return `${iso.slice(0, 10) === new Date(now).toISOString().slice(0, 10) ? "" : `${iso.slice(0, 10)} `}${iso.slice(11, 16)} UTC`;
|
|
34
|
+
}
|
|
25
35
|
export const fundingInterval = (row: Pick<PerpBoardRow, "fundingIntervalHours" | "fundingKind">) => row.fundingIntervalHours ? `${row.fundingIntervalHours}h${row.fundingKind === "last-paid" ? " paid" : row.fundingKind === "continuous" ? " continuous" : ""}` : "--";
|
|
26
36
|
export const HISTORY_METRICS = ["funding", "oi", "premium", "price", "long-short"] as const;
|
|
27
37
|
export type HistoryMetric = typeof HISTORY_METRICS[number];
|
|
@@ -116,12 +126,13 @@ const PERP_COLUMNS: Record<PerpColumnId, DataTableColumn> = {
|
|
|
116
126
|
value: { id: "value", label: "Value %", width: 10, align: "right" },
|
|
117
127
|
};
|
|
118
128
|
// Columns in reading order, then the order a narrow pane gives them up in.
|
|
119
|
-
export const BOARD_COLUMNS: PerpColumnId[] = ["market", "venue", "markPrice", "fundingRate", "
|
|
129
|
+
export const BOARD_COLUMNS: PerpColumnId[] = ["market", "venue", "markPrice", "fundingRate", "premium", "openInterestUsd", "oiChange24h", "longShare", "fundingApr", "priceChange24h", "observedAt"];
|
|
120
130
|
// The venue goes last: the same market trades on several, and only the venue tells those rows apart. The long share
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
131
|
+
// is what the board and the comparison are read for, so a 100-column pane keeps it and the annualised funding, which
|
|
132
|
+
// only restates the funding rate, goes first (the 24h OI change next on the board, the per-interval rate on Compare).
|
|
133
|
+
const BOARD_DROPS: PerpColumnId[] = ["observedAt", "fundingApr", "priceChange24h", "oiChange24h", "longShare", "premium", "markPrice", "openInterestUsd", "venue"];
|
|
134
|
+
export const COMPARE_COLUMNS: PerpColumnId[] = ["venue", "symbol", "markPrice", "fundingRate", "fundingRate8h", "premium", "openInterestUsd", "oiChange24h", "longShare", "fundingApr", "observedAt"];
|
|
135
|
+
const COMPARE_DROPS: PerpColumnId[] = ["observedAt", "fundingApr", "oiChange24h", "fundingRate", "longShare", "premium", "markPrice", "openInterestUsd", "symbol"];
|
|
125
136
|
export const RANKING_COLUMNS: PerpColumnId[] = ["market", "venue", "value", "openInterestUsd"];
|
|
126
137
|
|
|
127
138
|
/** The columns that fit the pane, the least read going first; before the last of them goes, a long market name gives up its tail. */
|