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
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
1
|
+
import type { HeadlessBundleSection, HeadlessPaneDefinition, HeadlessPaneFreshness, HeadlessPaneRow } from "../../../types/plugin";
|
|
2
|
+
import {
|
|
3
|
+
expirationOptionSeconds, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, readOptionExpiration,
|
|
4
|
+
} from "../../../utils/option-expiry";
|
|
2
5
|
import { formatStrikeLabel } from "../options/table";
|
|
3
6
|
import { createSurfaceDependencies, loadVolatilitySurface } from "./client";
|
|
4
|
-
import { buildSurfaceGrid, type SurfaceGridOptions, type SurfaceSettings } from "./model";
|
|
7
|
+
import { buildSurfaceGrid, type SurfaceGridOptions, type SurfaceSettings, type SurfaceSnapshot } from "./model";
|
|
8
|
+
import {
|
|
9
|
+
expiryFeedFields, expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT, HIDDEN_YIELD_TEXT, selectSurfaceExpiry, surfaceForwardRow, surfaceSkewRow,
|
|
10
|
+
surfaceSmileRows, surfaceTermRows, TERM_SLOPE_MIN_DAYS, type SurfaceForwardRow, type SurfaceSkewRow,
|
|
11
|
+
} from "./tables";
|
|
5
12
|
|
|
6
13
|
type SurfaceAxis = NonNullable<SurfaceGridOptions["axis"]>;
|
|
14
|
+
const SURFACE_TABS = ["surface", "table", "smile", "term", "skew", "forwards"] as const;
|
|
15
|
+
type SurfaceTab = typeof SURFACE_TABS[number];
|
|
7
16
|
|
|
8
17
|
/** Column label for a surface coordinate: strike/spot or strike/forward ratio, delta bucket, or listed strike. */
|
|
9
18
|
export function surfaceCoordinateLabel(axis: SurfaceAxis, coordinate: number, index: number): string {
|
|
@@ -11,18 +20,113 @@ export function surfaceCoordinateLabel(axis: SurfaceAxis, coordinate: number, in
|
|
|
11
20
|
: axis === "strike" ? formatStrikeLabel(coordinate) : `${Math.round(coordinate * 100)}%`;
|
|
12
21
|
}
|
|
13
22
|
|
|
23
|
+
/** The Skew view's sign conventions and units, one line each. */
|
|
24
|
+
const SKEW_NOTES = [
|
|
25
|
+
"RR is 25D call IV minus 25D put IV, BF the average 25D wing IV minus ATM IV, 90/110 the IV at 90% of spot minus the IV at 110%; all in vol points.",
|
|
26
|
+
`Slope is the ATM IV change per year to the next listed expiry, in vol points; left out under ${TERM_SLOPE_MIN_DAYS} days to expiry, where it annualises a few hours of noise.`,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const number = (value: unknown) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
30
|
+
const iv = (value: unknown) => formatIv(number(value));
|
|
31
|
+
const price = (value: unknown) => formatPrice(number(value));
|
|
32
|
+
const points = (value: unknown) => formatVolPoints(number(value));
|
|
33
|
+
const days = (value: unknown) => typeof value === "number" ? value.toFixed(value < 10 ? 1 : 0) : "--";
|
|
34
|
+
|
|
35
|
+
function surfaceSections(snapshot: SurfaceSnapshot, axis: SurfaceAxis, tenors: "listed" | "fixed"): HeadlessBundleSection[] {
|
|
36
|
+
const grid = buildSurfaceGrid(snapshot, { axis, tenors });
|
|
37
|
+
return [
|
|
38
|
+
// One row per tenor, one IV column per coordinate, as the pane's Table tab lays it out.
|
|
39
|
+
{ title: "Surface", columns: [
|
|
40
|
+
{ key: "tenor", header: "Expiry / tenor" },
|
|
41
|
+
...(grid.rows[0]?.cells.map((cell, index) => ({ key: String(index), header: surfaceCoordinateLabel(axis, cell.coordinate, index),
|
|
42
|
+
format: (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--" })) ?? []),
|
|
43
|
+
], rows: grid.rows.map((row) => ({ tenor: `${row.label}${row.extrapolated ? " E" : row.interpolated ? " I" : ""}`,
|
|
44
|
+
...Object.fromEntries(row.cells.map((cell, index) => [String(index), cell.volatility])) })) },
|
|
45
|
+
{ title: "Expiries", columns: [
|
|
46
|
+
{ key: "expiry", header: "Expiry" }, { key: "state", header: "State" }, { key: "forward", header: "Forward", format: price },
|
|
47
|
+
{ key: "rate", header: "Rate", format: (value) => typeof value === "number" ? `${(value * 100).toFixed(2)}%` : "--" }, { key: "fit", header: "Fit" }, { key: "asOf", header: "As of" },
|
|
48
|
+
], rows: snapshot.expiries.map((expiry) => ({ ...expiry, expiry: expiryLabel(expiry.expiration), fit: expiry.fit?.method ?? null })) },
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The selected expiry's clean quotes against its fitted smile. */
|
|
53
|
+
function smileSections(snapshot: SurfaceSnapshot, requested: number | null): {
|
|
54
|
+
sections: HeadlessBundleSection[]; errors: string[]; freshness?: HeadlessPaneFreshness;
|
|
55
|
+
} {
|
|
56
|
+
const expiry = selectSurfaceExpiry(snapshot, requested);
|
|
57
|
+
// An unlisted request is already an error naming the listed dates.
|
|
58
|
+
if (!expiry) return { sections: [], errors: requested == null ? ["No expiry loaded"] : [] };
|
|
59
|
+
// The quotes are one chain's, so its observation dates the report.
|
|
60
|
+
const freshness: HeadlessPaneFreshness = { asOf: expiry.asOf, ...expiry.stale ? { status: "stale" as const }
|
|
61
|
+
: expiry.dataSource ? { status: expiry.dataSource } : {}, ...expiry.delayMinutes ? { delayMinutes: expiry.delayMinutes } : {} };
|
|
62
|
+
if (!expiry.fit || expiry.points.length < 2) {
|
|
63
|
+
return { sections: [], freshness, errors: [`${expiryLabel(expiry.expiration)}: no clean quoted smile`] };
|
|
64
|
+
}
|
|
65
|
+
return { errors: [], freshness, sections: [
|
|
66
|
+
{ title: `Smile ${expiryLabel(expiry.expiration)}`, entries: [
|
|
67
|
+
{ key: "days", label: "Days to expiry", value: expiry.years * 365, formatted: days(expiry.years * 365) },
|
|
68
|
+
{ key: "forward", label: "Forward", value: expiry.forward, formatted: price(expiry.forward) },
|
|
69
|
+
{ key: "atm", label: "ATM IV (at spot)", value: expiry.atmIV, formatted: iv(expiry.atmIV) },
|
|
70
|
+
{ key: "fit", label: "Fit", value: expiry.fit.method, formatted: `${expiry.fit.method}, RMSE ${points(expiry.fit.residual)} vol pts` },
|
|
71
|
+
] },
|
|
72
|
+
{ title: "Clean quotes", columns: [
|
|
73
|
+
{ key: "strike", header: "Strike", format: (value) => typeof value === "number" ? formatStrikeLabel(value) : "--" },
|
|
74
|
+
{ key: "side", header: "Side" },
|
|
75
|
+
{ key: "moneyness", header: "Spot %", format: (value) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--" },
|
|
76
|
+
{ key: "volatility", header: "Quote IV", format: iv }, { key: "fitted", header: "Fitted IV", format: iv },
|
|
77
|
+
{ key: "residual", header: "Residual pts", format: points }, { key: "mid", header: "Mid", format: price },
|
|
78
|
+
{ key: "openInterest", header: "OI" },
|
|
79
|
+
], rows: surfaceSmileRows(expiry) },
|
|
80
|
+
] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function termSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
|
|
84
|
+
return { title: "Term structure", columns: [
|
|
85
|
+
{ key: "expiry", header: "Expiry" }, { key: "days", header: "Days", format: days },
|
|
86
|
+
{ key: "atm", header: "ATM IV", format: iv }, { key: "put25", header: "25D put IV", format: iv }, { key: "call25", header: "25D call IV", format: iv },
|
|
87
|
+
{ key: "straddle", header: "Straddle", format: price }, { key: "sigma", header: "1-sigma", format: price },
|
|
88
|
+
{ key: "sigmaPercent", header: "1-sigma %", format: (value) => typeof value === "number" ? `${value.toFixed(2)}%` : "--" },
|
|
89
|
+
], rows: surfaceTermRows(snapshot).map((row) => ({ ...row, expiry: expiryLabel(row.expiration) })) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function skewSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
|
|
93
|
+
const gap = (value: unknown, row: HeadlessPaneRow) => typeof value === "number" ? points(value) : (row as unknown as SurfaceSkewRow).gap ?? "--";
|
|
94
|
+
return { title: "Skew", columns: [
|
|
95
|
+
{ key: "expiry", header: "Expiry" }, { key: "days", header: "Days", format: days },
|
|
96
|
+
{ key: "put25", header: "25D put IV", format: iv }, { key: "call25", header: "25D call IV", format: iv },
|
|
97
|
+
{ key: "riskReversal", header: "RR pts", format: gap }, { key: "butterfly", header: "BF pts", format: gap },
|
|
98
|
+
{ key: "moneynessSkew", header: "90/110 pts", format: (value, row) => typeof value === "number" ? points(value)
|
|
99
|
+
: (row as unknown as SurfaceSkewRow).moneynessGap ?? "--" },
|
|
100
|
+
{ key: "termSlope", header: "Slope pts/yr", format: (value, row) => typeof value === "number" ? points(value)
|
|
101
|
+
: (row as unknown as SurfaceSkewRow).termSlopeHidden ? HIDDEN_SLOPE_TEXT : "--" },
|
|
102
|
+
], rows: snapshot.expiries.map((expiry) => ({ ...surfaceSkewRow(expiry), ...expiryFeedFields(expiry), expiry: expiryLabel(expiry.expiration) })) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function forwardsSection(snapshot: SurfaceSnapshot): HeadlessBundleSection {
|
|
106
|
+
const percent = (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(2)}%` : "--";
|
|
107
|
+
return { title: "Forwards", columns: [
|
|
108
|
+
{ key: "expiry", header: "Expiry" }, { key: "forward", header: "Forward", format: price },
|
|
109
|
+
{ key: "basis", header: "Basis", format: (value) => typeof value === "number" ? `${value > 0 ? "+" : ""}${formatPrice(value)}` : "--" },
|
|
110
|
+
{ key: "dividendYield", header: "Div yield", format: (value, row) => typeof value === "number" ? percent(value)
|
|
111
|
+
: (row as unknown as SurfaceForwardRow).dividendYieldHidden ? HIDDEN_YIELD_TEXT : "--" }, { key: "rate", header: "Rate", format: percent },
|
|
112
|
+
{ key: "pairs", header: "Pairs" }, { key: "asOf", header: "As of", format: (value) => typeof value === "string" ? value.slice(0, 10) : "--" },
|
|
113
|
+
], rows: snapshot.expiries.map((expiry) => ({ ...surfaceForwardRow(expiry, snapshot), ...expiryFeedFields(expiry), expiry: expiryLabel(expiry.expiration) })) };
|
|
114
|
+
}
|
|
115
|
+
|
|
14
116
|
export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
15
117
|
shape: "bundle", argument: { kind: "ticker", description: "Underlying ticker" },
|
|
16
118
|
describe: (args) => `OVDV ${args.symbols[0] ?? ""}`,
|
|
17
119
|
options: [
|
|
18
|
-
{ key: "tab", type: "enum", values:
|
|
19
|
-
defaultValue: "surface", description: "Initial view", pluginState: { pluginId: "ticker-research", key: "activeTabId" } },
|
|
120
|
+
{ key: "tab", type: "enum", values: SURFACE_TABS.map((value) => ({ value })),
|
|
121
|
+
defaultValue: "surface", description: "Initial view; the report prints that view's figures", pluginState: { pluginId: "ticker-research", key: "activeTabId" } },
|
|
20
122
|
{ key: "ivSource", type: "enum", values: [{ value: "recomputed" }, { value: "provider" }], defaultValue: "recomputed", description: "Quote-derived IV or provider comparison" },
|
|
21
123
|
{ key: "priceSide", type: "enum", values: [{ value: "mid" }, { value: "bid" }, { value: "ask" }], defaultValue: "mid", description: "Quote side used by the IV solver" },
|
|
22
124
|
{ key: "axis", type: "enum", values: ["spot", "forward", "delta", "strike"].map((value) => ({ value })), defaultValue: "spot", description: "Surface coordinates" },
|
|
23
125
|
{ key: "tenors", type: "enum", values: [{ value: "listed" }, { value: "fixed" }], defaultValue: "listed", description: "Listed or interpolated tenors" },
|
|
24
126
|
{ key: "limit", type: "integer", minimum: 1, maximum: 100, defaultValue: 18, description: "Representative expiry request limit; selected expiry may add one", pluginState: { pluginId: "ticker-research", key: "expiryLimit" } },
|
|
25
|
-
{ key: "expiration", type: "
|
|
127
|
+
{ key: "expiration", type: "string", placeholder: OPTION_EXPIRATION_PLACEHOLDER, example: "--expiration 2027-01-15 --tab smile",
|
|
128
|
+
description: `Selected expiry, as ${OPTION_EXPIRATION_FORMAT}; the first at least four weeks out when omitted`,
|
|
129
|
+
settingKey: "expiration", normalize: (value) => expirationOptionSeconds(value) },
|
|
26
130
|
],
|
|
27
131
|
async load(args, ctx) {
|
|
28
132
|
const symbol = args.symbols[0]!;
|
|
@@ -31,39 +135,39 @@ export const volSurfaceHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
31
135
|
if (!(quote.price > 0) || !Number.isFinite(quote.price) || quote.stale) {
|
|
32
136
|
return { sections: [], complete: false, unavailableSymbols: [symbol], errors: ["A current underlying price is required"] };
|
|
33
137
|
}
|
|
138
|
+
const requested = readOptionExpiration(args.options.expiration);
|
|
34
139
|
const snapshot = await loadVolatilitySurface({ instrument, spot: quote.price, spotAsOf: quote.lastUpdated,
|
|
35
140
|
settings: { ivSource: args.options.ivSource as SurfaceSettings["ivSource"], priceSide: args.options.priceSide as SurfaceSettings["priceSide"] },
|
|
36
141
|
limit: Number(args.options.limit), signal: ctx.signal,
|
|
37
|
-
requiredExpiries:
|
|
142
|
+
requiredExpiries: requested == null ? [] : [requested],
|
|
38
143
|
}, createSurfaceDependencies(ctx.marketData, ctx.apiClient));
|
|
39
144
|
const axis = args.options.axis as SurfaceAxis;
|
|
40
|
-
const
|
|
145
|
+
const tab = (SURFACE_TABS as readonly string[]).includes(String(args.options.tab)) ? args.options.tab as SurfaceTab : "surface";
|
|
41
146
|
const available = snapshot.expiries.some((expiry) => expiry.fit);
|
|
42
147
|
const expiryWarnings = snapshot.expiries.flatMap((expiry) => expiry.warnings.map((warning) =>
|
|
43
|
-
({ expiry:
|
|
148
|
+
({ expiry: expiryLabel(expiry.expiration), warning })));
|
|
44
149
|
const perExpiry = new Set(expiryWarnings.map((row) => row.warning));
|
|
45
150
|
// Calendar arbitrage compares expiries, so it belongs to the whole surface.
|
|
46
151
|
const warnings = [...expiryWarnings, ...snapshot.warnings.filter((warning) => !perExpiry.has(warning))
|
|
47
152
|
.map((warning) => ({ expiry: "All", warning }))];
|
|
153
|
+
const smile = tab === "smile" ? smileSections(snapshot, requested) : null;
|
|
154
|
+
// The report prints the view it was asked for, as the pane opens on it.
|
|
155
|
+
const view: HeadlessBundleSection[] = tab === "smile" ? smile!.sections : tab === "term" ? [termSection(snapshot)]
|
|
156
|
+
: tab === "skew" ? [skewSection(snapshot)] : tab === "forwards" ? [forwardsSection(snapshot)]
|
|
157
|
+
: surfaceSections(snapshot, axis, args.options.tenors as "listed" | "fixed");
|
|
48
158
|
return {
|
|
49
159
|
sections: [
|
|
50
|
-
|
|
51
|
-
{ title: "Surface", columns: [
|
|
52
|
-
{ key: "tenor", header: "Expiry / tenor" },
|
|
53
|
-
...(grid.rows[0]?.cells.map((cell, index) => ({ key: String(index), header: surfaceCoordinateLabel(axis, cell.coordinate, index),
|
|
54
|
-
format: (value: unknown) => typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "--" })) ?? []),
|
|
55
|
-
], rows: grid.rows.map((row) => ({ tenor: `${row.label}${row.extrapolated ? " E" : row.interpolated ? " I" : ""}`,
|
|
56
|
-
...Object.fromEntries(row.cells.map((cell, index) => [String(index), cell.volatility])) })) },
|
|
57
|
-
{ title: "Expiries", columns: [
|
|
58
|
-
{ key: "expiry", header: "Expiry" }, { key: "state", header: "State" }, { key: "forward", header: "Forward", format: (value) => typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "--" },
|
|
59
|
-
{ key: "rate", header: "Rate", format: (value) => typeof value === "number" ? `${(value * 100).toFixed(2)}%` : "--" }, { key: "fit", header: "Fit" }, { key: "asOf", header: "As of" },
|
|
60
|
-
], rows: snapshot.expiries.map((expiry) => ({ ...expiry, expiry: new Date(expiry.expiration * 1000).toISOString().slice(0, 10), fit: expiry.fit?.method ?? null })) },
|
|
160
|
+
...view,
|
|
61
161
|
// The pane lists these (arbitrage, SVI fallback) beside the surface; here each names its expiry.
|
|
62
162
|
...(warnings.length ? [{ title: "Warnings", columns: [{ key: "expiry", header: "Expiry" }, { key: "warning", header: "Warning" }],
|
|
63
163
|
rows: warnings }] : []),
|
|
64
164
|
],
|
|
65
165
|
complete: available && snapshot.failures.length === 0 && snapshot.expiries.every((expiry) => expiry.state === "ready"),
|
|
66
|
-
unavailableSymbols: available ? [] : [symbol],
|
|
166
|
+
unavailableSymbols: available ? [] : [symbol],
|
|
167
|
+
errors: [...snapshot.failures.map((failure) => failure.expiration == null ? failure.message
|
|
168
|
+
: `${expiryLabel(failure.expiration)}: ${failure.message}`), ...smile?.errors ?? []],
|
|
169
|
+
...(tab === "skew" ? { notes: SKEW_NOTES } : {}),
|
|
170
|
+
...(smile?.freshness ? { freshness: smile.freshness } : {}),
|
|
67
171
|
metadata: { ...snapshot, underlyingQuote: quote, unit: "decimal annualized IV", methodology: "docs/research-data.md#shared-volatility-calculations" },
|
|
68
172
|
};
|
|
69
173
|
},
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
daysToExpiryFrom, detectButterflyArbitrage, detectCalendarArbitrage, evaluateSmile, expectedMove,
|
|
4
4
|
extractImpliedForward, fitVolatilitySmile, FIXED_VOLATILITY_TENORS,
|
|
5
5
|
interpolateTotalVariance, logForwardMoneyness, optionDelta, optionMid,
|
|
6
|
-
|
|
6
|
+
solveImpliedVolatility, valueOption, volatilityTermSlope,
|
|
7
7
|
type ExpectedMove, type ImpliedForward, type OptionSide, type SkewMetrics, type SmileFit,
|
|
8
8
|
} from "../shared/volatility";
|
|
9
9
|
import type { YieldPoint } from "../yield-curve/treasury-data";
|
|
@@ -354,16 +354,7 @@ export function buildSurfaceExpiry(input: BuildSurfaceExpiryInput): SurfaceExpir
|
|
|
354
354
|
const smile = (strike: number) => evaluateSurfaceSmile(result, strike);
|
|
355
355
|
for (const point of result.points) point.fitResidual = point.volatility - (smile(point.strike) ?? point.volatility);
|
|
356
356
|
result.atmIV = smile(spot);
|
|
357
|
-
|
|
358
|
-
(strike) => evaluateSmile(fit, logForwardMoneyness(strike, forward)!));
|
|
359
|
-
const putCovered = deltaStrike(result, spot, -0.25) != null;
|
|
360
|
-
const callCovered = deltaStrike(result, spot, 0.25) != null;
|
|
361
|
-
const moneynessCovered = smile(spot * 0.9) != null && smile(spot * 1.1) != null;
|
|
362
|
-
result.skew = { ...skew, atm: result.atmIV, put25: putCovered ? skew.put25 : null, call25: callCovered ? skew.call25 : null,
|
|
363
|
-
putCallSkew: putCovered && callCovered ? skew.putCallSkew : null,
|
|
364
|
-
riskReversal: putCovered && callCovered ? skew.riskReversal : null,
|
|
365
|
-
butterfly: putCovered && callCovered && result.atmIV != null ? skew.butterfly : null,
|
|
366
|
-
moneynessSkew: moneynessCovered ? skew.moneynessSkew : null };
|
|
357
|
+
result.skew = surfaceExpirySkew(result, spot);
|
|
367
358
|
result.expectedMove = expectedMove(cleaned.calls, cleaned.puts, spot, result.years, result.atmIV);
|
|
368
359
|
const calls = result.points.map((point) => ({ strike: point.strike, callPrice: valueOption({
|
|
369
360
|
side: "call", spot: forward, strike: point.strike,
|
|
@@ -376,6 +367,27 @@ export function buildSurfaceExpiry(input: BuildSurfaceExpiryInput): SurfaceExpir
|
|
|
376
367
|
return result;
|
|
377
368
|
}
|
|
378
369
|
|
|
370
|
+
/**
|
|
371
|
+
* A fitted expiry's skew in decimal IV: RR is the 25-delta call minus put, BF
|
|
372
|
+
* their average minus ATM, 90/110 the IV at 90% of spot minus 110%. The
|
|
373
|
+
* 25-delta strikes are found on the fit inside the quoted strikes, as the
|
|
374
|
+
* Table's delta columns find them, so a fit that cannot price a far wing it
|
|
375
|
+
* was never quoted at does not blank a quoted expiry's skew.
|
|
376
|
+
*/
|
|
377
|
+
export function surfaceExpirySkew(expiry: SurfaceExpiry, spot: number): SkewMetrics {
|
|
378
|
+
const smile = (strike: number) => evaluateSurfaceSmile(expiry, strike);
|
|
379
|
+
const at = (delta: number) => {
|
|
380
|
+
const strike = deltaStrike(expiry, spot, delta);
|
|
381
|
+
return strike == null ? null : smile(strike);
|
|
382
|
+
};
|
|
383
|
+
const put25 = at(-0.25), call25 = at(0.25), atm = expiry.atmIV;
|
|
384
|
+
const low = smile(spot * 0.9), high = smile(spot * 1.1);
|
|
385
|
+
const both = put25 != null && call25 != null;
|
|
386
|
+
return { atm, put25, call25, putCallSkew: both ? put25 - call25 : null, riskReversal: both ? call25 - put25 : null,
|
|
387
|
+
butterfly: both && atm != null ? (put25 + call25) / 2 - atm : null,
|
|
388
|
+
moneynessSkew: low != null && high != null ? low - high : null };
|
|
389
|
+
}
|
|
390
|
+
|
|
379
391
|
export function surfaceCalendarWarnings(expiries: readonly SurfaceExpiry[]): string[] {
|
|
380
392
|
let count = 0;
|
|
381
393
|
for (const ratio of SURFACE_MONEYNESS) {
|
|
@@ -32,8 +32,11 @@ import {
|
|
|
32
32
|
DEFAULT_SURFACE3D_CAMERA, rotateSurface3DCamera, zoomSurface3DCamera, type Surface3DCamera,
|
|
33
33
|
} from "../../../components/chart/surface3d/model";
|
|
34
34
|
import { VolatilitySurface } from "./surface";
|
|
35
|
-
import {
|
|
35
|
+
import { SmileChart, TermChart } from "./charts";
|
|
36
36
|
import { surfaceCoordinateLabel } from "./headless";
|
|
37
|
+
import { expiryLabel, formatIv, formatPrice, formatVolPoints, HIDDEN_SLOPE_TEXT, HIDDEN_YIELD_TEXT, selectSurfaceExpiry, surfaceForwardRow,
|
|
38
|
+
surfaceSkewRow } from "./tables";
|
|
39
|
+
import { findListedExpiry, unlistedExpiryText } from "../../../utils/option-expiry";
|
|
37
40
|
|
|
38
41
|
const TABS = [
|
|
39
42
|
{ value: "surface", label: "3D surface" }, { value: "table", label: "Table" },
|
|
@@ -41,9 +44,6 @@ const TABS = [
|
|
|
41
44
|
{ value: "skew", label: "Skew" }, { value: "forwards", label: "Forwards" },
|
|
42
45
|
];
|
|
43
46
|
type Axis = "spot" | "forward" | "delta" | "strike";
|
|
44
|
-
const DEFAULT_EXPIRY_MIN_DAYS = 28;
|
|
45
|
-
/** Annualising a few days of parity carry produces meaningless yields. */
|
|
46
|
-
const DIVIDEND_YIELD_MIN_DAYS = 30;
|
|
47
47
|
|
|
48
48
|
export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
49
49
|
const colors = useThemeColors();
|
|
@@ -176,14 +176,17 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
176
176
|
] : [];
|
|
177
177
|
const tableHeight = Math.max(3, height - 1 - tabRows - statGridRows(ivStats, width));
|
|
178
178
|
const bitmapAvailable = !!useStaticChartBitmapSize(width, tableHeight);
|
|
179
|
+
// A typed date (`--expiration 2027-01-15`) is that day's UTC midnight; the
|
|
180
|
+
// catalogue may stamp the same expiry later in the day.
|
|
181
|
+
const pinned = expiration == null || !snapshot ? expiration : findListedExpiry(expiration, snapshot.catalogue) ?? expiration;
|
|
179
182
|
useEffect(() => {
|
|
180
183
|
// Existing slices only change selection. A new pin causes one load and then
|
|
181
184
|
// remains in the request identity after its partial snapshots arrive.
|
|
182
|
-
if (
|
|
183
|
-
|| snapshot.expiries.some((entry) => entry.expiration ===
|
|
184
|
-
|| !snapshot.catalogue.includes(
|
|
185
|
-
setRequestedExpiration(
|
|
186
|
-
}, [
|
|
185
|
+
if (pinned == null || pinned === requestedExpiration || resource.loading || !snapshot
|
|
186
|
+
|| snapshot.expiries.some((entry) => entry.expiration === pinned)
|
|
187
|
+
|| !snapshot.catalogue.includes(pinned)) return;
|
|
188
|
+
setRequestedExpiration(pinned);
|
|
189
|
+
}, [pinned, requestedExpiration, resource.loading, snapshot]);
|
|
187
190
|
const grid = useMemo(() => snapshot ? buildSurfaceGrid(snapshot, { axis, tenors }) : null, [axis, snapshot, tenors]);
|
|
188
191
|
// Delta columns give every expiry the same quoted wing span, so the 3D
|
|
189
192
|
// surface is a full sheet; moneyness keeps each row within 2.5 ATM sigmas.
|
|
@@ -207,19 +210,16 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
207
210
|
// Sheet rows may be constant maturities: pair them with the nearest listed expiry.
|
|
208
211
|
const nearestRow = (years: number | undefined) => years == null || !denseGrid?.tenors.length ? -1
|
|
209
212
|
: denseGrid.tenors.reduce((best, value, index) => Math.abs(value - years) < Math.abs(denseGrid.tenors[best]! - years) ? index : best, 0);
|
|
210
|
-
|
|
211
|
-
// selection is the first expiry at least four weeks out.
|
|
212
|
-
const defaultExpiry = snapshot?.expiries.find((entry) => entry.years * 365 >= DEFAULT_EXPIRY_MIN_DAYS) ?? snapshot?.expiries[0];
|
|
213
|
-
const selectedExpiry = snapshot?.expiries.find((entry) => entry.expiration === (expiration ?? defaultExpiry?.expiration)) ?? null;
|
|
213
|
+
const selectedExpiry = snapshot ? selectSurfaceExpiry(snapshot, expiration) : null;
|
|
214
214
|
const openChain = useCallback(() => {
|
|
215
215
|
if (!snapshot) return;
|
|
216
|
-
const selected =
|
|
216
|
+
const selected = pinned ?? selectedExpiry?.expiration;
|
|
217
217
|
createPaneFromTemplate("options-pane", { symbol: symbol ?? snapshot.symbol, ticker, instrument: target?.instrument,
|
|
218
218
|
...(ticker ? { listing: { name: ticker.metadata.name, exchange: ticker.metadata.exchange,
|
|
219
219
|
currency: ticker.metadata.currency, type: ticker.metadata.assetCategory ?? "STK" } } : {}),
|
|
220
220
|
values: selected == null ? {} : { expiration: String(selected) },
|
|
221
221
|
});
|
|
222
|
-
}, [createPaneFromTemplate,
|
|
222
|
+
}, [createPaneFromTemplate, pinned, selectedExpiry?.expiration, snapshot?.symbol, symbol, ticker, target?.instrument]);
|
|
223
223
|
useVolSurfaceEvidence({ snapshot, view: activeTab, grid: activeTab === "surface" && bitmapAvailable ? denseGrid : grid,
|
|
224
224
|
selectedExpiry, loading: active.loading, bitmapAvailable, axis: activeTab === "surface" && bitmapAvailable ? (deltaSurface ? "delta" : "forward") : axis,
|
|
225
225
|
tenors: activeTab === "surface" && bitmapAvailable ? "listed" : tenors, overlaySmiles });
|
|
@@ -323,8 +323,9 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
323
323
|
...(!spotAvailable && symbol && !historyDate ? ["Underlying price unavailable or stale"] : []),
|
|
324
324
|
...(historyDate && !stored.loading && !stored.data && !stored.error ? [`No stored ${underlying} surface for ${historyDate}`] : []),
|
|
325
325
|
...(fallbackDate ? [`Live chain has no two-sided quotes; showing the ${fallbackDate} close`] : []),
|
|
326
|
-
...(
|
|
327
|
-
|
|
326
|
+
...(pinned != null && snapshot && !resource.loading && !snapshot.catalogue.includes(pinned)
|
|
327
|
+
&& !snapshot.failures.some((failure) => failure.expiration === pinned)
|
|
328
|
+
? [`${expiryLabel(pinned)}: ${unlistedExpiryText(pinned, snapshot.catalogue)}`] : []),
|
|
328
329
|
...(active.error ? [active.error] : [])];
|
|
329
330
|
// What the quotes are and when they were observed; a reload moves the time.
|
|
330
331
|
const freshness = !snapshot ? null : snapshot.stored ? `stored close ${snapshot.stored.sessionDate}`
|
|
@@ -429,7 +430,7 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
429
430
|
error={!snapshot ? active.error : null} empty={!snapshot && !active.loading} subject="volatility surface">
|
|
430
431
|
<QueryBar width={width}
|
|
431
432
|
filters={[
|
|
432
|
-
{ id: "expiry", label: "Expiry", value: String(
|
|
433
|
+
{ id: "expiry", label: "Expiry", value: String(pinned ?? selectedExpiry?.expiration ?? ""),
|
|
433
434
|
options: snapshot?.catalogue.map((value) => ({ value: String(value), label: expiryLabel(value) })) ?? [],
|
|
434
435
|
onChange: (value: string) => setExpiration(Number(value)) },
|
|
435
436
|
...(storedDates.length ? [{ id: "date", label: "Date", value: shownDate ?? "", defaultValue: "",
|
|
@@ -468,25 +469,35 @@ function ExpiryTable({ snapshot, selected, onSelect, forwards, width, height, fo
|
|
|
468
469
|
forwards: boolean; width: number; height: number; focused: boolean;
|
|
469
470
|
onKey: (event: DataTableKeyEvent) => boolean; metadata: () => unknown[][];
|
|
470
471
|
}) {
|
|
471
|
-
|
|
472
|
+
// IVs in percent, IV differences in vol points; a missing 25-delta figure says why.
|
|
473
|
+
const fields = forwards ? ["Forward", "Basis", "Div %", "Rate %", "Pairs", "As of"] : ["25d put %", "25d call %", "RR pts", "BF pts", "90/110 pts", "Slope pts/yr"];
|
|
472
474
|
const integerColumn = forwards ? "4" : null;
|
|
473
475
|
const [sort, setSort] = useState<{ columnId: string; direction: SortDirection }>({ columnId: "expiry", direction: "asc" });
|
|
474
|
-
const
|
|
475
|
-
:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
476
|
+
const rowsByExpiry = useMemo(() => new Map(snapshot.expiries.map((entry) => [entry.expiration,
|
|
477
|
+
{ skew: surfaceSkewRow(entry), forward: surfaceForwardRow(entry, snapshot) }])), [snapshot]);
|
|
478
|
+
const value = (entry: SurfaceExpiry, id: string): number | string | null => {
|
|
479
|
+
if (id === "expiry") return entry.expiration;
|
|
480
|
+
const { skew, forward } = rowsByExpiry.get(entry.expiration)!;
|
|
481
|
+
return (forwards ? [forward.forward, forward.basis, forward.dividendYield == null ? null : forward.dividendYield * 100,
|
|
482
|
+
forward.rate == null ? null : forward.rate * 100, forward.pairs, entry.asOf?.slice(0, 10) ?? null]
|
|
483
|
+
: [skew.put25, skew.call25, skew.riskReversal, skew.butterfly, skew.moneynessSkew, skew.termSlope])[Number(id)] ?? null;
|
|
484
|
+
};
|
|
485
|
+
const missing = (entry: SurfaceExpiry, id: string) => {
|
|
486
|
+
const { skew, forward } = rowsByExpiry.get(entry.expiration)!;
|
|
487
|
+
if (forwards) return id === "2" && forward.dividendYieldHidden ? HIDDEN_YIELD_TEXT : "--";
|
|
488
|
+
return (id === "2" || id === "3" ? skew.gap : id === "4" ? skew.moneynessGap : id === "5" && skew.termSlopeHidden ? HIDDEN_SLOPE_TEXT : null) ?? "--";
|
|
489
|
+
};
|
|
479
490
|
const rows = [...snapshot.expiries].sort((a, b) => {
|
|
480
491
|
const x = value(a, sort.columnId), y = value(b, sort.columnId);
|
|
481
492
|
const difference = typeof x === "number" && typeof y === "number" ? x - y : String(x ?? "").localeCompare(String(y ?? ""));
|
|
482
493
|
return sort.direction === "asc" ? difference : -difference;
|
|
483
494
|
});
|
|
484
495
|
return <DataTableView<SurfaceExpiry> focused={focused} rootWidth={width} rootHeight={height} items={rows}
|
|
485
|
-
columns={[{ id: "expiry", label: "Expiry", width: 13, align: "left" }, ...fields.map((label, i) => ({ id: String(i), label, width:
|
|
496
|
+
columns={[{ id: "expiry", label: "Expiry", width: 13, align: "left" }, ...fields.map((label, i) => ({ id: String(i), label, width: 14, align: "right" as const }))]}
|
|
486
497
|
getItemKey={(entry) => String(entry.expiration)} selection={{ kind: "id", selectedId: String(selected?.expiration), getId: (entry) => String(entry.expiration), onChange: (_id, entry) => onSelect(entry) }}
|
|
487
498
|
onActivate={onSelect} onRootKeyDown={onKey} getExportMetadata={metadata} freezeFirstColumn
|
|
488
499
|
sortColumnId={sort.columnId} sortDirection={sort.direction} onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id))}
|
|
489
500
|
renderCell={(entry, column) => { const raw = value(entry, column.id); return { text: column.id === "expiry" ? expiryLabel(entry.expiration)
|
|
490
|
-
: raw == null ?
|
|
491
|
-
: forwards ? (column.id === "1" && raw > 0 ? `+${formatPrice(raw)}` : formatPrice(raw)) :
|
|
501
|
+
: raw == null ? missing(entry, column.id) : typeof raw === "string" ? raw : column.id === integerColumn ? String(raw)
|
|
502
|
+
: forwards ? (column.id === "1" && raw > 0 ? `+${formatPrice(raw)}` : formatPrice(raw)) : formatVolPoints(raw) }; }} emptyStateTitle="No expiry observations." />;
|
|
492
503
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { findListedExpiry } from "../../../utils/option-expiry";
|
|
2
|
+
import { evaluateSurfaceSmile, type SurfaceExpiry, type SurfaceSnapshot } from "./model";
|
|
3
|
+
|
|
4
|
+
export const formatIv = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : `${(value * 100).toFixed(2)}%`;
|
|
5
|
+
export const formatPrice = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : value.toFixed(2);
|
|
6
|
+
export const expiryLabel = (expiration: number) => new Date(expiration * 1000).toISOString().slice(0, 10);
|
|
7
|
+
|
|
8
|
+
/** A one-day front expiry is the noisiest smile on the board; the default selection is the first at least four weeks out. */
|
|
9
|
+
const DEFAULT_EXPIRY_MIN_DAYS = 28;
|
|
10
|
+
/** Annualising a few days of parity carry produces meaningless yields. */
|
|
11
|
+
const DIVIDEND_YIELD_MIN_DAYS = 30;
|
|
12
|
+
/**
|
|
13
|
+
* Below this many days to expiry, the ATM term slope to the next expiry
|
|
14
|
+
* annualises a few hours of IV change into hundreds of points a year.
|
|
15
|
+
*/
|
|
16
|
+
export const TERM_SLOPE_MIN_DAYS = 7;
|
|
17
|
+
/** What the Skew view prints in place of a slope it leaves out. */
|
|
18
|
+
export const HIDDEN_SLOPE_TEXT = `under ${TERM_SLOPE_MIN_DAYS}d`;
|
|
19
|
+
/** What the Forwards view prints in place of a dividend yield it leaves out. */
|
|
20
|
+
export const HIDDEN_YIELD_TEXT = `under ${DIVIDEND_YIELD_MIN_DAYS}d`;
|
|
21
|
+
/** A decimal IV difference in vol points, as the Skew view prints it. */
|
|
22
|
+
export const formatVolPoints = (value: number | null | undefined) =>
|
|
23
|
+
value == null || !Number.isFinite(value) ? "--" : (value * 100).toFixed(2);
|
|
24
|
+
|
|
25
|
+
/** Calendar days to the expiry's close, as the model prices it. */
|
|
26
|
+
const expiryDays = (expiry: Pick<SurfaceExpiry, "years">) => expiry.years * 365;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The expiry the smile, chain and pricer actions use: the requested one,
|
|
30
|
+
* matched by calendar date as OMON matches it, else the first at least four
|
|
31
|
+
* weeks out. A requested expiry that is not loaded selects nothing.
|
|
32
|
+
*/
|
|
33
|
+
export function selectSurfaceExpiry(snapshot: Pick<SurfaceSnapshot, "expiries">, requested: number | null | undefined): SurfaceExpiry | null {
|
|
34
|
+
if (requested != null) {
|
|
35
|
+
const listed = findListedExpiry(requested, snapshot.expiries.map((entry) => entry.expiration));
|
|
36
|
+
return snapshot.expiries.find((entry) => entry.expiration === listed) ?? null;
|
|
37
|
+
}
|
|
38
|
+
return snapshot.expiries.find((entry) => expiryDays(entry) >= DEFAULT_EXPIRY_MIN_DAYS) ?? snapshot.expiries[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Why an expiry has no 25-delta risk reversal or butterfly: no fitted smile,
|
|
43
|
+
* or clean quotes that stop short of a 25-delta strike (the fit is never
|
|
44
|
+
* extrapolated past them). Null when both are there, or while it loads.
|
|
45
|
+
*/
|
|
46
|
+
export function skewGap(expiry: SurfaceExpiry): string | null {
|
|
47
|
+
const { put25, call25, riskReversal, butterfly } = expiry.skew;
|
|
48
|
+
if (riskReversal != null && butterfly != null) return null;
|
|
49
|
+
if (!expiry.fit) return expiry.state === "loading" ? null : "no smile fit";
|
|
50
|
+
if (put25 == null && call25 == null) return "no 25D quotes";
|
|
51
|
+
if (put25 == null) return "no 25D put";
|
|
52
|
+
if (call25 == null) return "no 25D call";
|
|
53
|
+
return "no ATM IV";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SurfaceSkewRow {
|
|
57
|
+
expiration: number;
|
|
58
|
+
days: number;
|
|
59
|
+
/** Decimal IVs at the 25-delta put and call. */
|
|
60
|
+
put25: number | null;
|
|
61
|
+
call25: number | null;
|
|
62
|
+
/** Decimal IV differences: call minus put, wing average minus ATM, 90% of spot minus 110%. */
|
|
63
|
+
riskReversal: number | null;
|
|
64
|
+
butterfly: number | null;
|
|
65
|
+
moneynessSkew: number | null;
|
|
66
|
+
/** Decimal ATM IV change per year to the next expiry; null under TERM_SLOPE_MIN_DAYS, which `termSlopeHidden` marks. */
|
|
67
|
+
termSlope: number | null;
|
|
68
|
+
termSlopeHidden: boolean;
|
|
69
|
+
/** Why the risk reversal and butterfly are missing. */
|
|
70
|
+
gap: string | null;
|
|
71
|
+
/** Why the 90/110 figure is missing. */
|
|
72
|
+
moneynessGap: string | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function surfaceSkewRow(expiry: SurfaceExpiry): SurfaceSkewRow {
|
|
76
|
+
const days = expiryDays(expiry);
|
|
77
|
+
const termSlopeHidden = expiry.termSlope != null && days < TERM_SLOPE_MIN_DAYS;
|
|
78
|
+
return { expiration: expiry.expiration, days, put25: expiry.skew.put25, call25: expiry.skew.call25,
|
|
79
|
+
riskReversal: expiry.skew.riskReversal, butterfly: expiry.skew.butterfly, moneynessSkew: expiry.skew.moneynessSkew,
|
|
80
|
+
termSlope: termSlopeHidden ? null : expiry.termSlope, termSlopeHidden, gap: skewGap(expiry),
|
|
81
|
+
// 90% and 110% of spot lie past the clean quotes of a near expiry.
|
|
82
|
+
moneynessGap: expiry.skew.moneynessSkew != null ? null : !expiry.fit ? skewGap(expiry) : "not quoted" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SurfaceForwardRow {
|
|
86
|
+
expiration: number;
|
|
87
|
+
forward: number | null;
|
|
88
|
+
/** Forward minus spot, in price. */
|
|
89
|
+
basis: number | null;
|
|
90
|
+
/** Decimal implied dividend yield; null under 30 days out, which `dividendYieldHidden` marks. */
|
|
91
|
+
dividendYield: number | null;
|
|
92
|
+
dividendYieldHidden: boolean;
|
|
93
|
+
rate: number | null;
|
|
94
|
+
/** Put-call pairs behind the forward; a stored close keeps the forward, not its pairs. */
|
|
95
|
+
pairs: number | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function surfaceForwardRow(expiry: SurfaceExpiry, snapshot: SurfaceSnapshot & { stored?: unknown }): SurfaceForwardRow {
|
|
99
|
+
const dividendYieldHidden = expiry.dividendYield != null && expiryDays(expiry) < DIVIDEND_YIELD_MIN_DAYS;
|
|
100
|
+
return { expiration: expiry.expiration, forward: expiry.forward, basis: expiry.forward == null ? null : expiry.forward - snapshot.spot,
|
|
101
|
+
dividendYield: dividendYieldHidden ? null : expiry.dividendYield, dividendYieldHidden,
|
|
102
|
+
rate: expiry.rate, pairs: snapshot.stored ? null : expiry.parity.pairs.length };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** When and how an expiry's quotes were observed, as a report reads its freshness from each row. */
|
|
106
|
+
export const expiryFeedFields = (expiry: SurfaceExpiry) => ({
|
|
107
|
+
asOf: expiry.asOf, dataSource: expiry.dataSource, delayMinutes: expiry.delayMinutes, stale: expiry.stale,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** The Term view's points: ATM at spot and the 25-delta wings, with the expiry's straddle and one-sigma move. */
|
|
111
|
+
export function surfaceTermRows(snapshot: SurfaceSnapshot) {
|
|
112
|
+
return snapshot.expiries.filter((expiry) => expiry.atmIV != null).sort((a, b) => a.years - b.years).map((expiry) => ({
|
|
113
|
+
expiration: expiry.expiration, days: expiryDays(expiry), atm: expiry.atmIV, put25: expiry.skew.put25, call25: expiry.skew.call25,
|
|
114
|
+
straddle: expiry.expectedMove.straddle, sigma: expiry.expectedMove.sigma, sigmaPercent: expiry.expectedMove.sigmaPercent,
|
|
115
|
+
...expiryFeedFields(expiry),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The Smile view's clean quotes against the fitted smile, in strike order. */
|
|
120
|
+
export function surfaceSmileRows(expiry: SurfaceExpiry) {
|
|
121
|
+
return expiry.points.map((point) => ({ strike: point.strike, side: point.side, moneyness: point.moneyness,
|
|
122
|
+
volatility: point.volatility, fitted: evaluateSurfaceSmile(expiry, point.strike), residual: point.fitResidual,
|
|
123
|
+
mid: point.mid, openInterest: point.openInterest }));
|
|
124
|
+
}
|
|
@@ -2,10 +2,22 @@ import type { DataProvider } from "../../../types/data-provider";
|
|
|
2
2
|
import type { Quote } from "../../../types/financials";
|
|
3
3
|
import type { IndexEntry } from "./indices";
|
|
4
4
|
import { errorMessage } from "../../../utils/errors";
|
|
5
|
+
import { isNoProviderMessage } from "../../../sources/provider-errors";
|
|
6
|
+
|
|
7
|
+
/** What a row says in place of figures the feed does not carry. */
|
|
8
|
+
export const NOT_IN_FEED = "not available from the feed";
|
|
9
|
+
|
|
10
|
+
/** Whether the feed has no quote for a symbol, as opposed to a quote that failed to load. */
|
|
11
|
+
export function isFeedGap(error: string | null | undefined): boolean {
|
|
12
|
+
return !!error && isNoProviderMessage(error);
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
export interface WorldIndexQuoteResult {
|
|
7
16
|
quotes: Map<string, Quote | null>;
|
|
17
|
+
/** Failures to load: a timeout, an outage. A symbol the feed simply lacks is in `gaps`. */
|
|
8
18
|
errors: string[];
|
|
19
|
+
/** Symbols the feed has no quote for. They are a data gap, so their rows say so rather than carry an error. */
|
|
20
|
+
gaps: Set<string>;
|
|
9
21
|
}
|
|
10
22
|
|
|
11
23
|
export async function loadWorldIndexQuotes(
|
|
@@ -14,6 +26,12 @@ export async function loadWorldIndexQuotes(
|
|
|
14
26
|
): Promise<WorldIndexQuoteResult> {
|
|
15
27
|
const quotes = new Map<string, Quote | null>();
|
|
16
28
|
const errors: string[] = [];
|
|
29
|
+
const gaps = new Set<string>();
|
|
30
|
+
const fail = (symbol: string, error: unknown) => {
|
|
31
|
+
const message = errorMessage(error);
|
|
32
|
+
if (isFeedGap(message)) gaps.add(symbol);
|
|
33
|
+
else errors.push(`${symbol}: ${message}`);
|
|
34
|
+
};
|
|
17
35
|
if (provider.getQuotesBatch) {
|
|
18
36
|
const results = await provider.getQuotesBatch(
|
|
19
37
|
entries.map((entry) => ({ symbol: entry.symbol, exchange: "" })),
|
|
@@ -22,10 +40,10 @@ export async function loadWorldIndexQuotes(
|
|
|
22
40
|
for (const entry of entries) {
|
|
23
41
|
const result = bySymbol.get(entry.symbol);
|
|
24
42
|
quotes.set(entry.symbol, result?.quote ?? null);
|
|
25
|
-
if (result?.error)
|
|
43
|
+
if (result?.error) fail(entry.symbol, result.error);
|
|
26
44
|
else if (!result?.quote) errors.push(`${entry.symbol}: quote unavailable`);
|
|
27
45
|
}
|
|
28
|
-
return { quotes, errors };
|
|
46
|
+
return { quotes, errors, gaps };
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
await Promise.all(entries.map(async (entry) => {
|
|
@@ -33,8 +51,8 @@ export async function loadWorldIndexQuotes(
|
|
|
33
51
|
quotes.set(entry.symbol, await provider.getQuote(entry.symbol, ""));
|
|
34
52
|
} catch (error) {
|
|
35
53
|
quotes.set(entry.symbol, null);
|
|
36
|
-
|
|
54
|
+
fail(entry.symbol, error);
|
|
37
55
|
}
|
|
38
56
|
}));
|
|
39
|
-
return { quotes, errors };
|
|
57
|
+
return { quotes, errors, gaps };
|
|
40
58
|
}
|