gloomberb 0.16.2 → 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/account-cache.ts +45 -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/command-options.ts +74 -0
- package/src/cli/commands/command-utils.ts +29 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +524 -148
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +223 -42
- 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 +200 -73
- package/src/cli/desktop-pane-shot.ts +100 -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 +40 -7
- package/src/cli/history-rows.ts +187 -15
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +21 -13
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +55 -7
- package/src/cli/pane-functions/capabilities.ts +13 -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 +89 -28
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +71 -24
- package/src/cli/pane-functions/headless.ts +168 -35
- package/src/cli/pane-functions/index.ts +12 -2
- 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 +12 -1
- package/src/cli/pane-functions/resolver.ts +32 -8
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +109 -23
- package/src/cli/registry.ts +8 -4
- package/src/cli/report-tables.ts +460 -0
- package/src/cli/result.ts +63 -16
- 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/export.ts +6 -51
- package/src/components/data-table/view.tsx +31 -10
- 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 +67 -35
- package/src/components/layout/shell/cursor-occlusion.ts +6 -5
- package/src/components/layout/shell/drag/index.ts +16 -7
- package/src/components/layout/shell/drag/live.ts +193 -0
- package/src/components/layout/shell/drag/overlays.tsx +179 -35
- package/src/components/layout/shell/drag/runtime.ts +62 -50
- package/src/components/layout/shell/drag/slide.ts +10 -0
- package/src/components/layout/shell/index.tsx +84 -57
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
- package/src/components/layout/shell/native/surfaces.ts +30 -14
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +461 -180
- 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/terminal-pointer-runtime.ts +18 -18
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
- 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/data/json-persistence.ts +17 -3
- package/src/data/json-storage.ts +2 -0
- package/src/data/memory-resource-store.ts +46 -0
- 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 +156 -11
- 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/freshness.ts +5 -0
- 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/pane-model.ts +10 -6
- 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/attention/pane.tsx +1 -1
- 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/catalysts/detail.tsx +1 -1
- 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-attention/pane.tsx +1 -1
- 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/client.ts +3 -6
- package/src/plugins/builtin/holders/format.ts +31 -1
- package/src/plugins/builtin/holders/headless.ts +32 -6
- package/src/plugins/builtin/holders/pane.tsx +25 -13
- package/src/plugins/builtin/holders/report-header.ts +46 -5
- 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 +121 -45
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
- 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/members/pane.tsx +10 -0
- 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/perps/pane.tsx +1 -1
- 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/account-metrics.ts +16 -1
- package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
- 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/research/relative-valuation-model.ts +7 -3
- 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/model.ts +2 -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 +260 -0
- 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/short-volume/pane.tsx +1 -1
- package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -0
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
- package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
- 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/basemap-layer.tsx +2 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
- package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -6
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
- package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
- package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
- package/src/plugins/builtin/world-venue-map/model.ts +16 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
- 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/browser/app-services.ts +3 -1
- package/src/renderers/dom/data-table/dom.ts +31 -0
- package/src/renderers/dom/data-table/index.tsx +5 -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 +113 -17
- package/src/renderers/dom/host/drag-shield.ts +35 -0
- package/src/renderers/dom/host/live-frame.ts +62 -0
- package/src/renderers/dom/host/mouse.ts +8 -1
- package/src/renderers/dom/host/scroll-box.tsx +6 -5
- package/src/renderers/dom/host/tabs.tsx +3 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +26 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
- package/src/renderers/electrobun/shared/protocol.ts +12 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/host/native.ts +0 -10
- package/src/renderers/electrobun/view/host/window-move.ts +28 -0
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
- package/src/renderers/electrobun/view/ui-host.tsx +2 -5
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/renderers/opentui/ui-host.tsx +7 -1
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/batches.ts +3 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +13 -7
- package/src/sources/provider-router/financials.ts +106 -16
- 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 +24 -6
- package/src/sources/sec-edgar.ts +68 -8
- package/src/state/pane-scroll-registry.tsx +67 -35
- 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/margin-account.ts +108 -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/resolution.ts +15 -0
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/financials.ts +32 -3
- 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 +16 -0
- package/src/ui/index.tsx +10 -3
- package/src/ui/scroll-box-axis.ts +26 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/currency-units.ts +8 -3
- package/src/utils/depositary-receipt.ts +54 -10
- package/src/utils/display-number.ts +63 -0
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fundamentals.ts +17 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history-integrity.ts +19 -6
- 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,183 @@
|
|
|
1
|
+
import type { OptionsChain } from "../../../types/financials";
|
|
2
|
+
import { calculateOptionGreeks, solveChainVolatilities } from "./analytics";
|
|
3
|
+
import { findNearestStrikeIndex, formatStrikeLabel } from "./table";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which strikes of an expiry the chain lists. Every strike by default, as
|
|
7
|
+
* layouts saved before the setting existed showed; or a count either side of
|
|
8
|
+
* the money; or the strikes whose call or put delta falls in a band, which
|
|
9
|
+
* reaches deep in-the-money LEAPS without scrolling past the wings.
|
|
10
|
+
*/
|
|
11
|
+
export type StrikeWindow =
|
|
12
|
+
| { kind: "all" }
|
|
13
|
+
| { kind: "around"; count: number }
|
|
14
|
+
| { kind: "delta"; min: number; max: number };
|
|
15
|
+
|
|
16
|
+
/** The pane setting, and the `fn OMON --strikes` option that sets it. */
|
|
17
|
+
export const STRIKE_WINDOW_SETTING = "strikes";
|
|
18
|
+
export const ALL_STRIKES = "all";
|
|
19
|
+
const MAX_STRIKES_AROUND = 200;
|
|
20
|
+
|
|
21
|
+
const ALL: StrikeWindow = { kind: "all" };
|
|
22
|
+
|
|
23
|
+
/** Choices the query bar and the settings dialog offer; the CLI takes any count or band. */
|
|
24
|
+
export const STRIKE_WINDOW_PRESETS: ReadonlyArray<{ value: string; label: string; short: string; description: string }> = [
|
|
25
|
+
{ value: ALL_STRIKES, label: "All strikes", short: "All", description: "Every listed strike." },
|
|
26
|
+
{ value: "10", label: "10 either side of the money", short: "±10", description: "The strike nearest spot and 10 on each side." },
|
|
27
|
+
{ value: "20", label: "20 either side of the money", short: "±20", description: "The strike nearest spot and 20 on each side." },
|
|
28
|
+
{ value: "40", label: "40 either side of the money", short: "±40", description: "The strike nearest spot and 40 on each side." },
|
|
29
|
+
{ value: "0.70-0.90", label: "Delta .70 to .90", short: "Δ .70-.90", description: "In the money: calls below spot, puts above. A stock replacement band." },
|
|
30
|
+
{ value: "0.40-0.60", label: "Delta .40 to .60", short: "Δ .40-.60", description: "Near the money." },
|
|
31
|
+
{ value: "0.10-0.30", label: "Delta .10 to .30", short: "Δ .10-.30", description: "Out of the money on both wings." },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const BAND = /^(-?\d*\.?\d+)(?:-|to|,|:|\.\.)(-?\d*\.?\d+)$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A band of absolute deltas as typed: `0.70-0.90`, `.7-.9`, `70-90` (read as
|
|
38
|
+
* percent), `-0.9--0.7` (a put's signed deltas). Null when it is not one.
|
|
39
|
+
*/
|
|
40
|
+
export function parseDeltaBand(raw: string): { min: number; max: number } | null {
|
|
41
|
+
const band = BAND.exec(raw.trim().toLowerCase().replace(/\s+/g, "").replace(/^(?:δ|delta:?)/, ""));
|
|
42
|
+
if (!band) return null;
|
|
43
|
+
let [low, high] = [Math.abs(Number(band[1])), Math.abs(Number(band[2]))];
|
|
44
|
+
// 70-90 reads as a percent band, the way traders say it.
|
|
45
|
+
if (low > 1 || high > 1) [low, high] = [low / 100, high / 100];
|
|
46
|
+
if (!Number.isFinite(low) || !Number.isFinite(high) || low > 1 || high > 1) return null;
|
|
47
|
+
return low <= high ? { min: low, max: high } : { min: high, max: low };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseWindowText(raw: string): StrikeWindow | null {
|
|
51
|
+
const text = raw.trim().toLowerCase().replace(/\s+/g, "");
|
|
52
|
+
if (text === ALL_STRIKES) return ALL;
|
|
53
|
+
if (/^\d+$/.test(text)) {
|
|
54
|
+
const count = Number(text);
|
|
55
|
+
return count >= 1 && count <= MAX_STRIKES_AROUND ? { kind: "around", count } : null;
|
|
56
|
+
}
|
|
57
|
+
const band = parseDeltaBand(text);
|
|
58
|
+
return band ? { kind: "delta", ...band } : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The window a pane setting holds; a missing or unreadable one is every strike. */
|
|
62
|
+
export function resolveStrikeWindow(value: unknown): StrikeWindow {
|
|
63
|
+
return typeof value === "string" || typeof value === "number" ? parseWindowText(String(value)) ?? ALL : ALL;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The setting's stored text: `all`, `10`, `0.70-0.90`. */
|
|
67
|
+
export function strikeWindowValue(window: StrikeWindow): string {
|
|
68
|
+
if (window.kind === "all") return ALL_STRIKES;
|
|
69
|
+
if (window.kind === "around") return String(window.count);
|
|
70
|
+
return `${window.min.toFixed(2)}-${window.max.toFixed(2)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `fn OMON --strikes`: the window as typed, checked, in the setting's text. */
|
|
74
|
+
export function normalizeStrikeWindowOption(raw: string): string {
|
|
75
|
+
const window = parseWindowText(raw);
|
|
76
|
+
if (!window) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Invalid strike window "${raw}". Use all, a count of strikes either side of the money (1 to ${MAX_STRIKES_AROUND}), `
|
|
79
|
+
+ "or a call or put delta band such as 0.70-0.90.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return strikeWindowValue(window);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const deltaText = (value: number) => value.toFixed(2).replace(/^0\./, ".");
|
|
86
|
+
|
|
87
|
+
/** The window on the query bar chip: All, ±10, Δ .70-.90. */
|
|
88
|
+
export function strikeWindowShortLabel(window: StrikeWindow): string {
|
|
89
|
+
if (window.kind === "all") return "All";
|
|
90
|
+
if (window.kind === "around") return `±${window.count}`;
|
|
91
|
+
return `Δ ${deltaText(window.min)}-${deltaText(window.max)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The absolute call and put delta at each strike, where the chain values them. */
|
|
95
|
+
export type StrikeDeltas = ReadonlyMap<number, { call?: number; put?: number }>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Deltas from the same solver and Greeks the chain shows, against one spot.
|
|
99
|
+
* A strike without a two-sided quote on a side has no delta there.
|
|
100
|
+
*/
|
|
101
|
+
export function chainDeltasByStrike(
|
|
102
|
+
chain: OptionsChain,
|
|
103
|
+
spot: number | undefined,
|
|
104
|
+
dividendYield: number | undefined,
|
|
105
|
+
now: number = Date.now(),
|
|
106
|
+
): StrikeDeltas {
|
|
107
|
+
const deltas = new Map<number, { call?: number; put?: number }>();
|
|
108
|
+
if (spot == null) return deltas;
|
|
109
|
+
const volatilities = solveChainVolatilities(chain, spot, dividendYield, now);
|
|
110
|
+
for (const [side, contracts] of [["call", chain.calls], ["put", chain.puts]] as const) {
|
|
111
|
+
for (const contract of contracts) {
|
|
112
|
+
const delta = calculateOptionGreeks(contract, side, spot, dividendYield, volatilities)?.delta;
|
|
113
|
+
if (delta == null || !Number.isFinite(delta)) continue;
|
|
114
|
+
deltas.set(contract.strike, { ...deltas.get(contract.strike), [side]: Math.abs(delta) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return deltas;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface StrikeWindowResult {
|
|
121
|
+
/** The strikes the chain lists, ascending. */
|
|
122
|
+
strikes: number[];
|
|
123
|
+
/** Strikes the expiry has. */
|
|
124
|
+
total: number;
|
|
125
|
+
/** Why the window could not be applied, so every strike is listed instead. */
|
|
126
|
+
fallback: "no-price" | "no-delta" | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Displayed deltas round to three places; a strike that reads .700 is in a .70 band.
|
|
130
|
+
const DELTA_TOLERANCE = 5e-4;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The strikes a window keeps. `center` is the price the money is read at (the
|
|
134
|
+
* held strike, else spot); `keep` is a strike that always stays, the held
|
|
135
|
+
* contract's. A window that cannot be read without a price or deltas lists
|
|
136
|
+
* every strike and says why.
|
|
137
|
+
*/
|
|
138
|
+
export function applyStrikeWindow(
|
|
139
|
+
strikes: readonly number[],
|
|
140
|
+
window: StrikeWindow,
|
|
141
|
+
{ center = null, deltas = null, keep = null }: {
|
|
142
|
+
center?: number | null;
|
|
143
|
+
deltas?: StrikeDeltas | null;
|
|
144
|
+
keep?: number | null;
|
|
145
|
+
} = {},
|
|
146
|
+
): StrikeWindowResult {
|
|
147
|
+
const total = strikes.length;
|
|
148
|
+
const every = (fallback: StrikeWindowResult["fallback"]): StrikeWindowResult => ({ strikes: [...strikes], total, fallback });
|
|
149
|
+
if (window.kind === "all" || total === 0) return every(null);
|
|
150
|
+
const kept = new Set<number>();
|
|
151
|
+
if (window.kind === "around") {
|
|
152
|
+
if (center == null || !Number.isFinite(center)) return every("no-price");
|
|
153
|
+
const index = findNearestStrikeIndex([...strikes], center);
|
|
154
|
+
for (const strike of strikes.slice(Math.max(0, index - window.count), index + window.count + 1)) kept.add(strike);
|
|
155
|
+
} else {
|
|
156
|
+
if (!deltas || deltas.size === 0) return every("no-delta");
|
|
157
|
+
const inBand = (value: number | undefined) => value != null
|
|
158
|
+
&& value >= window.min - DELTA_TOLERANCE && value <= window.max + DELTA_TOLERANCE;
|
|
159
|
+
for (const strike of strikes) {
|
|
160
|
+
const delta = deltas.get(strike);
|
|
161
|
+
if (inBand(delta?.call) || inBand(delta?.put)) kept.add(strike);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (keep != null && strikes.includes(keep)) kept.add(keep);
|
|
165
|
+
return { strikes: strikes.filter((strike) => kept.has(strike)), total, fallback: null };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* What a report says about the strikes it lists: how many of how many, and
|
|
170
|
+
* the window that chose them.
|
|
171
|
+
*/
|
|
172
|
+
export function strikeWindowNotice(window: StrikeWindow, result: StrikeWindowResult): string {
|
|
173
|
+
const { strikes, total } = result;
|
|
174
|
+
const range = strikes.length > 0 ? ` (${formatStrikeLabel(strikes[0]!)} to ${formatStrikeLabel(strikes.at(-1)!)})` : "";
|
|
175
|
+
const more = " --strikes all lists every strike.";
|
|
176
|
+
if (result.fallback === "no-price") return `All ${total} strikes${range}: no price to centre the window on.`;
|
|
177
|
+
if (result.fallback === "no-delta") return `All ${total} strikes${range}: a delta band needs a current underlying quote.`;
|
|
178
|
+
if (window.kind === "all") return `All ${total} strikes${range}.`;
|
|
179
|
+
if (window.kind === "around") {
|
|
180
|
+
return `${strikes.length} of ${total} strikes, ${window.count} either side of the money${range}.${more}`;
|
|
181
|
+
}
|
|
182
|
+
return `${strikes.length} of ${total} strikes with a call or put delta of ${window.min.toFixed(2)} to ${window.max.toFixed(2)}${range}.${more}`;
|
|
183
|
+
}
|
|
@@ -5,6 +5,7 @@ import { blendHex, colors } from "../../../theme/colors";
|
|
|
5
5
|
import { blendForContrast, blendForSeparation, contrastRatio } from "../../../theme/color-utils";
|
|
6
6
|
import { formatCompact } from "../../../utils/format";
|
|
7
7
|
import { formatOptionPrice, optionQuoteSide, optionSpread } from "./market-reference";
|
|
8
|
+
import { formatCarryPercent, formatCostOfSpot } from "./carry";
|
|
8
9
|
import type {
|
|
9
10
|
OptionColumn,
|
|
10
11
|
OptionFieldId,
|
|
@@ -20,6 +21,8 @@ type OptionFieldDef = {
|
|
|
20
21
|
header: string;
|
|
21
22
|
width: number;
|
|
22
23
|
description: string;
|
|
24
|
+
/** Drawn on the put side of the strike only; the call side leaves it out. */
|
|
25
|
+
putOnly?: true;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
const OPTION_TEXT_MIN_CONTRAST = 4.5;
|
|
@@ -36,7 +39,7 @@ const MONEYNESS_MAX_TINT = 0.42;
|
|
|
36
39
|
export const OPTION_FIELD_DEFS: OptionFieldDef[] = [
|
|
37
40
|
{ id: "bid", label: "Bid", header: "BID", width: 7, description: "Best bid price." },
|
|
38
41
|
{ id: "ask", label: "Ask", header: "ASK", width: 7, description: "Best ask price." },
|
|
39
|
-
{ id: "spread", label: "Spread", header: "SPRD", width: 6, description: "Bid/ask width as a
|
|
42
|
+
{ id: "spread", label: "Spread %", header: "SPRD%", width: 6, description: "Bid/ask width as a percent of the midpoint, the comparable liquidity read across strikes." },
|
|
40
43
|
{ id: "last", label: "Last", header: "LAST", width: 7, description: "Last traded price." },
|
|
41
44
|
{ id: "delta", label: "Delta", header: "Δ", width: 6, description: "Price sensitivity to a $1 move in the underlying." },
|
|
42
45
|
{ id: "gamma", label: "Gamma", header: "Γ", width: 7, description: "Delta sensitivity to a $1 move in the underlying." },
|
|
@@ -46,13 +49,57 @@ export const OPTION_FIELD_DEFS: OptionFieldDef[] = [
|
|
|
46
49
|
{ id: "iv", label: "Implied volatility", header: "IV", width: 6, description: "Volatility implied by the contract price." },
|
|
47
50
|
{ id: "volume", label: "Volume", header: "VOL", width: 6, description: "Contracts traded in the current session." },
|
|
48
51
|
{ id: "openInterest", label: "Open interest", header: "OI", width: 6, description: "Outstanding open contracts." },
|
|
52
|
+
{
|
|
53
|
+
id: "extrinsicPerYear",
|
|
54
|
+
label: "Extrinsic per year",
|
|
55
|
+
header: "EXT/Y",
|
|
56
|
+
width: 7,
|
|
57
|
+
description: "Time value (midpoint less intrinsic) as a percent of spot, divided by years to expiry: the yearly cost of holding the contract instead of the stock.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "costOfSpot",
|
|
61
|
+
label: "Cost % of spot",
|
|
62
|
+
header: "COST%",
|
|
63
|
+
width: 8,
|
|
64
|
+
description: "Put side only. The put's midpoint as a percent of the underlying's price: what a protective put costs per share it covers.",
|
|
65
|
+
putOnly: true,
|
|
66
|
+
},
|
|
49
67
|
];
|
|
50
68
|
|
|
51
69
|
export const DEFAULT_OPTION_FIELD_IDS: OptionFieldId[] = ["bid", "ask", "last", "iv", "delta", "volume", "openInterest"];
|
|
52
70
|
|
|
53
71
|
const OPTION_FIELDS_BY_ID = new Map(OPTION_FIELD_DEFS.map((field) => [field.id, field]));
|
|
54
72
|
|
|
73
|
+
function optionFieldToken(value: string): string {
|
|
74
|
+
return value.trim().toLowerCase().replace(/[\s_%/-]+/g, "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Each field by its id, label and header as typed, any case: `openInterest`, `oi`, `sprd%`, `ext/y`. */
|
|
78
|
+
const OPTION_FIELDS_BY_TOKEN = new Map(OPTION_FIELD_DEFS.flatMap((field) => [
|
|
79
|
+
[optionFieldToken(field.id), field.id],
|
|
80
|
+
[optionFieldToken(field.label), field.id],
|
|
81
|
+
[optionFieldToken(field.header), field.id],
|
|
82
|
+
] as const));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `fn OMON --columns bid,ask,spread,extrinsicPerYear`: the fields mirrored
|
|
86
|
+
* around the strike, in order, as the pane setting stores them (a put-only
|
|
87
|
+
* field such as costOfSpot sits on the put side alone). An unknown field
|
|
88
|
+
* fails, naming the ones there are.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeOptionColumnsOption(raw: string): string {
|
|
91
|
+
const tokens = raw.split(/[\s,]+/).filter(Boolean);
|
|
92
|
+
const unknown = tokens.filter((token) => !OPTION_FIELDS_BY_TOKEN.has(optionFieldToken(token)));
|
|
93
|
+
if (tokens.length === 0 || unknown.length > 0) {
|
|
94
|
+
throw new Error(`${unknown.length > 0 ? `Unknown option column ${unknown.join(", ")}` : "No option columns given"}. `
|
|
95
|
+
+ `Use: ${OPTION_FIELD_DEFS.map((field) => field.id).join(", ")}.`);
|
|
96
|
+
}
|
|
97
|
+
return resolveOptionFieldIds(tokens.map((token) => OPTION_FIELDS_BY_TOKEN.get(optionFieldToken(token))!)).join(",");
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
export function resolveOptionFieldIds(value: unknown): OptionFieldId[] {
|
|
101
|
+
// A report's --columns arrives as the comma-separated text it was typed as.
|
|
102
|
+
if (typeof value === "string") value = value.split(",");
|
|
56
103
|
if (!Array.isArray(value)) return [...DEFAULT_OPTION_FIELD_IDS];
|
|
57
104
|
const seen = new Set<OptionFieldId>();
|
|
58
105
|
const fields = value.filter((id): id is OptionFieldId => {
|
|
@@ -80,7 +127,7 @@ function sideColumn(side: OptionSide, field: OptionFieldId): Omit<OptionColumn,
|
|
|
80
127
|
export function createOptionColumns(fieldIds: readonly OptionFieldId[]): Array<Omit<OptionColumn, "headerColor">> {
|
|
81
128
|
const fields = resolveOptionFieldIds(fieldIds);
|
|
82
129
|
return [
|
|
83
|
-
...fields.map((field) => sideColumn("call", field)),
|
|
130
|
+
...fields.filter((field) => !OPTION_FIELDS_BY_ID.get(field)!.putOnly).map((field) => sideColumn("call", field)),
|
|
84
131
|
{ id: "strike", field: "strike", side: null, label: "STRIKE", width: 9, align: "right" },
|
|
85
132
|
...[...fields].reverse().map((field) => sideColumn("put", field)),
|
|
86
133
|
];
|
|
@@ -165,7 +212,8 @@ function optionColumnRole(column: Pick<OptionColumn, "field" | "side">): OptionC
|
|
|
165
212
|
if (column.field === "strike") return "strike";
|
|
166
213
|
if (column.field === "iv") return "iv";
|
|
167
214
|
if (column.field === "volume" || column.field === "openInterest") return "activity";
|
|
168
|
-
if (column.field === "bid" || column.field === "ask" || column.field === "spread"
|
|
215
|
+
if (column.field === "bid" || column.field === "ask" || column.field === "spread" || column.field === "extrinsicPerYear"
|
|
216
|
+
|| column.field === "costOfSpot") return "price";
|
|
169
217
|
return column.side ?? "strike";
|
|
170
218
|
}
|
|
171
219
|
|
|
@@ -274,6 +322,10 @@ function formatOptionContractCell(
|
|
|
274
322
|
return formatGreek(greeks?.vegaPerPoint);
|
|
275
323
|
case "rho":
|
|
276
324
|
return formatGreek(greeks?.rhoPerPoint);
|
|
325
|
+
case "extrinsicPerYear":
|
|
326
|
+
return formatCarryPercent(column.side === "call" ? row.callExtrinsicPerYear : row.putExtrinsicPerYear);
|
|
327
|
+
case "costOfSpot":
|
|
328
|
+
return column.side === "put" ? formatCostOfSpot(row.putCostOfSpot) : "\u2014";
|
|
277
329
|
case "strike":
|
|
278
330
|
return formatStrikeLabel(contract.strike);
|
|
279
331
|
}
|
|
@@ -14,7 +14,9 @@ export type OptionFieldId =
|
|
|
14
14
|
| "rho"
|
|
15
15
|
| "iv"
|
|
16
16
|
| "volume"
|
|
17
|
-
| "openInterest"
|
|
17
|
+
| "openInterest"
|
|
18
|
+
| "extrinsicPerYear"
|
|
19
|
+
| "costOfSpot";
|
|
18
20
|
|
|
19
21
|
type OptionColumnId = "strike" | `${OptionSide}${Capitalize<OptionFieldId>}`;
|
|
20
22
|
|
|
@@ -32,6 +34,11 @@ export interface OptionTableRow {
|
|
|
32
34
|
impliedVolatility?: number;
|
|
33
35
|
callGreeks?: OptionValuation;
|
|
34
36
|
putGreeks?: OptionValuation;
|
|
37
|
+
/** Time value as a share of spot per year (see `carry.ts`); null when it cannot be read. */
|
|
38
|
+
callExtrinsicPerYear?: number | null;
|
|
39
|
+
putExtrinsicPerYear?: number | null;
|
|
40
|
+
/** The put's midpoint as a share of spot (see `carry.ts`); null without a two-sided quote or a current spot. */
|
|
41
|
+
putCostOfSpot?: number | null;
|
|
35
42
|
isPositionStrike: boolean;
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -6,9 +6,12 @@ import { isPlainKey } from "../../../utils/keyboard";
|
|
|
6
6
|
import { formatCompact, formatPercentRaw } from "../../../utils/format";
|
|
7
7
|
import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
|
|
8
8
|
import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
|
|
9
|
+
import { expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryText, readOptionExpiration } from "../../../utils/option-expiry";
|
|
9
10
|
import { canonicalTickerKey } from "../../../utils/exchanges";
|
|
11
|
+
import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../../market-data/options-alternatives";
|
|
10
12
|
import { useChartQueries, useOptionsQuery, useResolvedEntryValue, useTickerFinancials } from "../../../market-data/hooks";
|
|
11
13
|
import {
|
|
14
|
+
Button,
|
|
12
15
|
DataTableView,
|
|
13
16
|
EmptyState,
|
|
14
17
|
usePaneFooter,
|
|
@@ -67,6 +70,19 @@ import type { TickerFinancials } from "../../../types/financials";
|
|
|
67
70
|
import type { IvStats } from "../iv-history/client";
|
|
68
71
|
import { useIvRank } from "../iv-history/rank";
|
|
69
72
|
import { useOptionsSessionOpen, useThrottledValue } from "../shared/volatility/live-session";
|
|
73
|
+
import { useRemoteUiNode } from "../../../remote/semantic-tree";
|
|
74
|
+
import { optionCarry, optionCostOfSpot } from "./carry";
|
|
75
|
+
import {
|
|
76
|
+
ALL_STRIKES,
|
|
77
|
+
STRIKE_WINDOW_PRESETS,
|
|
78
|
+
STRIKE_WINDOW_SETTING,
|
|
79
|
+
applyStrikeWindow,
|
|
80
|
+
chainDeltasByStrike,
|
|
81
|
+
resolveStrikeWindow,
|
|
82
|
+
strikeWindowNotice,
|
|
83
|
+
strikeWindowShortLabel,
|
|
84
|
+
strikeWindowValue,
|
|
85
|
+
} from "./strike-window";
|
|
70
86
|
|
|
71
87
|
/** The summary strip and analytics recompute from live quotes at most this often. */
|
|
72
88
|
const OPTIONS_SUMMARY_THROTTLE_MS = 1_000;
|
|
@@ -76,6 +92,10 @@ const EXPIRY_NEXT_KEY = "]";
|
|
|
76
92
|
/** Switches the cursor row between its call and its put, as clicking either side does. */
|
|
77
93
|
const SIDE_KEY = "x";
|
|
78
94
|
|
|
95
|
+
function sentenceCase(text: string): string {
|
|
96
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
function formatRatio(value: number | null | undefined): string {
|
|
80
100
|
return value == null || !Number.isFinite(value) ? "--" : value.toFixed(2);
|
|
81
101
|
}
|
|
@@ -152,7 +172,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
152
172
|
[savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
|
|
153
173
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
154
174
|
const liveStreaming = useLiveStreamingSetting();
|
|
155
|
-
|
|
175
|
+
// A number from the app, or the date or seconds `fn OMON --expiration` took as text.
|
|
176
|
+
const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
|
|
156
177
|
const [expirationTargetKey] = usePaneSettingValue<string | null>("expirationTargetKey", null);
|
|
157
178
|
const updatePaneSettings = useUpdatePaneSettings();
|
|
158
179
|
const [calcSide, setCalcSide] = useState<OptionSide | null>(null);
|
|
@@ -207,11 +228,18 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
207
228
|
const [chainRefreshMinutes] = usePaneSettingValue<string>("chainRefreshMinutes", "");
|
|
208
229
|
const [storedOptionFieldIds] = usePaneSettingValue<OptionFieldId[]>("optionColumnIds", DEFAULT_OPTION_FIELD_IDS);
|
|
209
230
|
const optionFieldIds = useMemo(() => resolveOptionFieldIds(storedOptionFieldIds), [storedOptionFieldIds]);
|
|
231
|
+
const [storedStrikeWindow] = usePaneSettingValue<unknown>(STRIKE_WINDOW_SETTING, ALL_STRIKES);
|
|
232
|
+
const strikeWindow = useMemo(() => resolveStrikeWindow(storedStrikeWindow), [storedStrikeWindow]);
|
|
233
|
+
const strikeWindowText = strikeWindowValue(strikeWindow);
|
|
210
234
|
const initialChainEntry = useOptionsQuery(baseRequest);
|
|
211
235
|
const initialChain = useResolvedEntryValue(initialChainEntry);
|
|
212
236
|
const initialExpiration = initialChain?.expirationDates.reduce((best, expiration) => (
|
|
213
237
|
parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
|
|
214
238
|
), initialChain.expirationDates[0]!);
|
|
239
|
+
// A typed date names a day; the catalogue's stamp for that day is the expiry.
|
|
240
|
+
const seededDate = readOptionExpiration(seededSetting);
|
|
241
|
+
const seededExpiration = seededDate == null ? undefined
|
|
242
|
+
: findListedExpiry(seededDate, initialChain?.expirationDates ?? []) ?? seededDate;
|
|
215
243
|
const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
|
|
216
244
|
? seededExpiration ?? initialExpiration : initialExpiration;
|
|
217
245
|
const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
|
|
@@ -257,7 +285,24 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
257
285
|
const expirationPickRef = useRef<(value: string) => void>(() => {});
|
|
258
286
|
const { nativePaneChrome } = useUiCapabilities();
|
|
259
287
|
const selectedExpirationIndex = selectedExpiration == null ? -1 : expirationDates.indexOf(selectedExpiration);
|
|
260
|
-
const
|
|
288
|
+
const strikeWindowOptions = useMemo(() => {
|
|
289
|
+
const presets = STRIKE_WINDOW_PRESETS.map(({ value, label, short, description }) => ({ value, label, short, description }));
|
|
290
|
+
// A window typed on the command line that no preset matches still reads on the chip.
|
|
291
|
+
if (presets.some((preset) => preset.value === strikeWindowText)) return presets;
|
|
292
|
+
const label = strikeWindowShortLabel(resolveStrikeWindow(strikeWindowText));
|
|
293
|
+
return [...presets, { value: strikeWindowText, label, short: label, description: "Set from the command line." }];
|
|
294
|
+
}, [strikeWindowText]);
|
|
295
|
+
// The window chip leads: the expiry strip after it can run past the edge of the bar.
|
|
296
|
+
const queryFilters = useMemo<QueryBarFilter[]>(() => [{
|
|
297
|
+
id: "strikes",
|
|
298
|
+
label: "Strikes",
|
|
299
|
+
title: "Strikes",
|
|
300
|
+
value: strikeWindowText,
|
|
301
|
+
// Every strike is the default, so any window narrows the chain and the chip says so.
|
|
302
|
+
defaultValue: ALL_STRIKES,
|
|
303
|
+
options: strikeWindowOptions,
|
|
304
|
+
onChange: (value: string) => updatePaneSettings({ [STRIKE_WINDOW_SETTING]: value }),
|
|
305
|
+
}, {
|
|
261
306
|
id: "expiration",
|
|
262
307
|
label: "Exp",
|
|
263
308
|
inline: true,
|
|
@@ -272,7 +317,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
272
317
|
: index === selectedExpirationIndex + 1 ? EXPIRY_NEXT_KEY : undefined,
|
|
273
318
|
})),
|
|
274
319
|
onChange: (value: string) => expirationPickRef.current(value),
|
|
275
|
-
}], [expirationDates, selectedExpiration, selectedExpirationIndex]);
|
|
320
|
+
}], [expirationDates, selectedExpiration, selectedExpirationIndex, strikeWindowOptions, strikeWindowText, updatePaneSettings]);
|
|
276
321
|
// A scheduled refresh of a chain already on screen is quiet: the in-session
|
|
277
322
|
// cadence would otherwise blink the footer every few seconds.
|
|
278
323
|
const loading = (initialChainEntry?.phase === "loading" || initialChainEntry?.phase === "refreshing") && !chain
|
|
@@ -280,7 +325,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
280
325
|
|| (expirationChainEntry?.phase === "refreshing" && strikeChain === null);
|
|
281
326
|
// Refresh failures keep a ready entry with last-good data and an error.
|
|
282
327
|
// Surface that warning even when the cached chain is still usable.
|
|
283
|
-
|
|
328
|
+
// The body names the dates nearest the one asked for; the footer only flags it.
|
|
329
|
+
const unavailableExpiry = expirationUnavailable && selectedExpiration != null
|
|
330
|
+
? sentenceCase(missingExpiryText(selectedExpiration, availableExpirations)) : null;
|
|
331
|
+
const error = (expirationUnavailable ? `Expiry ${expiryIsoDate(selectedExpiration!)} unavailable` : null)
|
|
284
332
|
?? initialChainEntry?.error?.message ?? expirationChainEntry?.error?.message
|
|
285
333
|
?? (initialChainEntry?.phase === "error" || expirationChainEntry?.phase === "error"
|
|
286
334
|
? "Failed to load options" : null);
|
|
@@ -316,9 +364,76 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
316
364
|
|
|
317
365
|
useEffect(() => {
|
|
318
366
|
userSelectedStrikeRef.current = false;
|
|
319
|
-
}, [selectedExpiration]);
|
|
367
|
+
}, [selectedExpiration, strikeWindowText]);
|
|
320
368
|
|
|
321
|
-
const
|
|
369
|
+
const allStrikes = useMemo(() => strikeChain ? buildStrikeList(strikeChain) : [], [strikeChain]);
|
|
370
|
+
// The window is read once per chain snapshot, and again when a price first
|
|
371
|
+
// arrives, so streamed quotes never move strikes in or out under the cursor.
|
|
372
|
+
const underlyingPrice = underlying?.quote?.price;
|
|
373
|
+
const windowCenter = useMemo(
|
|
374
|
+
() => resolveDefaultStrikeTarget(parsed?.strike, underlyingPrice),
|
|
375
|
+
[strikeChain, parsed?.strike, underlyingPrice == null],
|
|
376
|
+
);
|
|
377
|
+
const windowSpot = useMemo(() => spot, [strikeChain, spot == null]);
|
|
378
|
+
const windowDeltas = useMemo(
|
|
379
|
+
() => strikeWindow.kind === "delta" && strikeChain ? chainDeltasByStrike(strikeChain, windowSpot, dividendYield) : null,
|
|
380
|
+
[dividendYield, strikeChain, strikeWindow.kind, windowSpot],
|
|
381
|
+
);
|
|
382
|
+
const strikeWindowResult = useMemo(
|
|
383
|
+
() => applyStrikeWindow(allStrikes, strikeWindow, { center: windowCenter, deltas: windowDeltas, keep: parsed?.strike ?? null }),
|
|
384
|
+
[allStrikes, parsed?.strike, strikeWindow, windowCenter, windowDeltas],
|
|
385
|
+
);
|
|
386
|
+
const strikes = strikeWindowResult.strikes;
|
|
387
|
+
usePaneNoticeFooter({ registrationId: "options-strike-window", focused,
|
|
388
|
+
notices: strikeWindowResult.fallback === "no-delta"
|
|
389
|
+
? ["The delta window needs a current underlying quote; showing every strike."]
|
|
390
|
+
: strikeWindowResult.fallback === "no-price" ? ["No price to centre the strike window on; showing every strike."] : [] });
|
|
391
|
+
// What the chain shows, in every renderer and in its reports: the expiry
|
|
392
|
+
// and its days left from the chain's as-of, beside the chain's own feed.
|
|
393
|
+
// Published ahead of the strike window, so a report names the expiry first.
|
|
394
|
+
const asOfChain = strikeChain ?? chain;
|
|
395
|
+
const chainAsOfMs = asOfChain?.asOf ? Date.parse(asOfChain.asOf) : Number.NaN;
|
|
396
|
+
const expiryAsOfMs = Number.isFinite(chainAsOfMs) ? chainAsOfMs : Date.now();
|
|
397
|
+
const expiryDate = selectedExpiration == null ? "" : expiryIsoDate(selectedExpiration);
|
|
398
|
+
const expiryDays = selectedExpiration == null ? "" : formatDaysToExpiry(selectedExpiration, expiryAsOfMs);
|
|
399
|
+
useRemoteUiNode(expiryDate && chain ? {
|
|
400
|
+
role: "report-notice",
|
|
401
|
+
label: "Expiry",
|
|
402
|
+
getMetadata: () => ({
|
|
403
|
+
text: `Expiry ${expiryDate} (${expiryDays})`,
|
|
404
|
+
key: "expiry",
|
|
405
|
+
value: { date: expiryDate, daysToExpiry: daysToExpiry(selectedExpiration!, expiryAsOfMs) },
|
|
406
|
+
}),
|
|
407
|
+
} : null);
|
|
408
|
+
// A refresh that failed keeps the last chain, which is then stale.
|
|
409
|
+
const asOfEntry = strikeChain == null ? null : strikeChain === expirationChain ? expirationChainEntry : initialChainEntry;
|
|
410
|
+
const chainDelay = asOfChain?.delayMinutes ?? 0;
|
|
411
|
+
const chainStatus = asOfEntry?.error != null ? "stale" as const
|
|
412
|
+
: asOfChain?.dataSource ?? (chainDelay > 0 ? "delayed" as const : undefined);
|
|
413
|
+
useRemoteUiNode(asOfChain ? {
|
|
414
|
+
role: "report-freshness",
|
|
415
|
+
label: "Chain freshness",
|
|
416
|
+
getMetadata: () => ({
|
|
417
|
+
...(asOfChain.asOf ? { asOf: asOfChain.asOf } : {}),
|
|
418
|
+
...(chainStatus ? { status: chainStatus } : {}),
|
|
419
|
+
...(chainDelay > 0 ? { delayMinutes: chainDelay } : {}),
|
|
420
|
+
}),
|
|
421
|
+
} : null);
|
|
422
|
+
// A report of the pane (`gloomberb fn OMON`) says how many strikes it lists of how many the expiry has.
|
|
423
|
+
useRemoteUiNode(strikeChain ? {
|
|
424
|
+
role: "report-notice",
|
|
425
|
+
label: "Strike window",
|
|
426
|
+
getMetadata: () => ({
|
|
427
|
+
text: strikeWindowNotice(strikeWindow, strikeWindowResult),
|
|
428
|
+
key: "strikes",
|
|
429
|
+
value: {
|
|
430
|
+
window: strikeWindowText,
|
|
431
|
+
shown: strikeWindowResult.strikes.length,
|
|
432
|
+
total: strikeWindowResult.total,
|
|
433
|
+
...(strikeWindowResult.fallback ? { fallback: strikeWindowResult.fallback } : {}),
|
|
434
|
+
},
|
|
435
|
+
}),
|
|
436
|
+
} : null);
|
|
322
437
|
const selectedStrikeIdx = selectedContract ? strikes.indexOf(selectedContract.strike) : strikeIdx;
|
|
323
438
|
// The snapshot's contracts decide which symbols stream; they do not change
|
|
324
439
|
// with the stream itself, so the subscription is stable between refreshes.
|
|
@@ -395,6 +510,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
395
510
|
impliedVolatility: volatilities?.byStrike.get(strike),
|
|
396
511
|
callGreeks: volatilities ? calculateOptionGreeks(call, "call", spot, dividendYield, volatilities) : undefined,
|
|
397
512
|
putGreeks: volatilities ? calculateOptionGreeks(put, "put", spot, dividendYield, volatilities) : undefined,
|
|
513
|
+
callExtrinsicPerYear: call && volatilities ? optionCarry(call, "call", spot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
|
|
514
|
+
putExtrinsicPerYear: put && volatilities ? optionCarry(put, "put", spot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
|
|
515
|
+
putCostOfSpot: put ? optionCostOfSpot(put, spot) : null,
|
|
398
516
|
isPositionStrike: !!parsed && strike === parsed.strike,
|
|
399
517
|
};
|
|
400
518
|
}), [callsByStrike, dividendYield, parsed, putsByStrike, spot, strikes, volatilities]);
|
|
@@ -643,9 +761,23 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
643
761
|
}, { enabled: focused, phase: "before" });
|
|
644
762
|
|
|
645
763
|
if (!ticker) {
|
|
646
|
-
return <EmptyState title="
|
|
764
|
+
return <EmptyState title="Select a ticker." />;
|
|
647
765
|
}
|
|
648
766
|
if (loading && !chain) return <Spinner label="Loading options chain..." />;
|
|
767
|
+
if (error && !chain && isOptionsUnavailableError(error)) {
|
|
768
|
+
// A future or commodity with no chain of its own: the listed fund whose options stand in, and OSA on it.
|
|
769
|
+
const alternative = listedOptionsAlternative(effectiveTicker);
|
|
770
|
+
return (
|
|
771
|
+
<EmptyState
|
|
772
|
+
title={optionsUnavailableTitle(effectiveTicker)}
|
|
773
|
+
hint={alternative ? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}).` : "OSA builds strategy payoffs on a listed underlying."}
|
|
774
|
+
actions={alternative ? <>
|
|
775
|
+
<Button label={`${alternative.symbol} options`} onPress={() => createPaneFromTemplate("options-pane", { symbol: alternative.symbol })} />
|
|
776
|
+
<Button label={`OSA ${alternative.symbol}`} onPress={() => createPaneFromTemplate("options-scenario-pane", { symbol: alternative.symbol })} />
|
|
777
|
+
</> : undefined}
|
|
778
|
+
/>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
649
781
|
if (error && !chain) return <EmptyState title="Options chain unavailable." message={error} />;
|
|
650
782
|
if (!chain || expirationDates.length === 0) {
|
|
651
783
|
return <EmptyState title={`No options available for ${effectiveTicker}.`} />;
|
|
@@ -656,7 +788,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
656
788
|
: 0;
|
|
657
789
|
// The root insets the terminal body, so the band gets the width inside it.
|
|
658
790
|
const statWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
|
|
659
|
-
const
|
|
791
|
+
const figures = optionsSummaryItems({
|
|
660
792
|
spot: spotItem(underlying, isOpt ? undefined : ticker.metadata.assetCategory),
|
|
661
793
|
summary, enrichment, currency: underlying?.quote?.currency || ticker.metadata.currency || "",
|
|
662
794
|
ivRank: showIvRank ? { stats: ivRank } : null,
|
|
@@ -665,8 +797,23 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
665
797
|
// Slope or IVR detail is the widest cell and would otherwise halve the
|
|
666
798
|
// columns and push the chain down; the detail is cut short instead.
|
|
667
799
|
const statRowBudget = width >= 110 ? 3 : width >= 65 ? 4 : 6;
|
|
668
|
-
const
|
|
800
|
+
const figureColumns = Math.max(statGridColumns(figures, statWidth), Math.ceil(figures.length / statRowBudget));
|
|
801
|
+
// The expiry leads the band, since the strip above can scroll it out of
|
|
802
|
+
// view. It joins the figures as one more cell where the row budget leaves a
|
|
803
|
+
// cell wide enough for all of it, and otherwise takes a row of its own so no
|
|
804
|
+
// figure is squeezed. Sized for four-digit days, so the band does not
|
|
805
|
+
// reflow as the chain's as-of arrives or the expiry steps.
|
|
806
|
+
const expiryItem: StatItem = { id: "expiry", label: "Expiry", value: expiryDate, detail: `(${expiryDays})` };
|
|
807
|
+
const cellColumns = Math.max(figureColumns, Math.ceil((figures.length + 1) / statRowBudget));
|
|
808
|
+
const expiryCells = Math.max(...figures.map((item) => item.label.length), expiryItem.label.length) + 1
|
|
809
|
+
+ expiryDate.length + 2 + "(9999d)".length;
|
|
810
|
+
const expiryInline = Math.floor((statWidth - 2 * (cellColumns - 1)) / cellColumns) >= expiryCells;
|
|
811
|
+
const statColumns = expiryInline ? cellColumns : figureColumns;
|
|
812
|
+
const statItems = [expiryInline ? expiryItem : { ...expiryItem, wide: true }, ...figures];
|
|
669
813
|
const summaryRowCount = Math.min(statGridRows(statItems, statWidth, statColumns), Math.max(0, height - 6));
|
|
814
|
+
// A short band gives up figures from the end; the expiry stays.
|
|
815
|
+
const shownStatItems = summaryRowCount === 0 ? []
|
|
816
|
+
: [statItems[0]!, ...figures.slice(0, summaryRowCount * statColumns - (expiryInline ? 1 : statColumns))];
|
|
670
817
|
// No term here follows the selection or the load, so an empty cold-expiry
|
|
671
818
|
// response cannot resize the table: growing it during loading would turn a
|
|
672
819
|
// clamped scroll into apparent user navigation.
|
|
@@ -677,10 +824,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
677
824
|
|
|
678
825
|
return (
|
|
679
826
|
<Box flexDirection="column" flexGrow={1} paddingX={nativePaneChrome ? 0 : 1}>
|
|
680
|
-
<QueryBar width={Math.max(1, width - 2)} filters={
|
|
827
|
+
<QueryBar width={Math.max(1, width - 2)} filters={queryFilters} />
|
|
681
828
|
|
|
682
829
|
{summaryRowCount > 0 && (
|
|
683
|
-
<StatGrid items={
|
|
830
|
+
<StatGrid items={shownStatItems} width={statWidth} columns={statColumns} />
|
|
684
831
|
)}
|
|
685
832
|
|
|
686
833
|
{isOpt && parsed && (
|
|
@@ -721,7 +868,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
721
868
|
getItemKey={(row) => String(row.strike)}
|
|
722
869
|
renderCell={renderCell}
|
|
723
870
|
selectedTextOverridesCellColor
|
|
724
|
-
emptyStateTitle={error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
|
|
871
|
+
emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
|
|
872
|
+
: strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
|
|
873
|
+
reportEveryRow
|
|
725
874
|
rootWidth={Math.max(1, width - 2 + inset * 2)}
|
|
726
875
|
rootHeight={tableHeight}
|
|
727
876
|
columnGap={0}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
|
+
import { expirationOptionSeconds, expiryIsoDate, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../../utils/option-expiry";
|
|
2
3
|
import { loadGamma, loadOpenInterest, positioningSymbol } from "./client";
|
|
3
4
|
import {
|
|
4
5
|
ALL_EXPIRIES,
|
|
@@ -39,8 +40,9 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
|
|
|
39
40
|
options: [
|
|
40
41
|
{ key: "tab", type: "enum", values: POSITIONING_TABS.map((tab) => ({ value: tab.value })), defaultValue: initialTab,
|
|
41
42
|
settingKey: "tab", description: "View the pane opens on: strikes, expiries or gex" },
|
|
42
|
-
{ key: "expiry", type: "string", settingKey: "expiry",
|
|
43
|
-
description:
|
|
43
|
+
{ key: "expiry", type: "string", settingKey: "expiry", placeholder: OPTION_EXPIRATION_PLACEHOLDER, example: "--expiry 2027-01-15",
|
|
44
|
+
description: `Expiry for the strikes and gamma, as ${OPTION_EXPIRATION_FORMAT}; the busiest near expiry when omitted (all expiries for gamma)`,
|
|
45
|
+
normalize: (value) => expiryIsoDate(expirationOptionSeconds(value, "expiry")) },
|
|
44
46
|
],
|
|
45
47
|
async load(args, ctx) {
|
|
46
48
|
const symbol = positioningSymbol(args.symbols[0] ?? "SPY");
|
|
@@ -3,7 +3,7 @@ import { StaticChartSurface, type StaticChartOverlay } from "../../../components
|
|
|
3
3
|
import { Box, Text } from "../../../ui";
|
|
4
4
|
import { resolveChartPalette } from "../../../components/chart/core/palette";
|
|
5
5
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
6
|
-
import type
|
|
6
|
+
import { currencyLabel, scenarioValueUnit, type buildScenario } from "./model";
|
|
7
7
|
|
|
8
8
|
export function ScenarioPayoffChart({ scenario, width, height, focused = false }: {
|
|
9
9
|
scenario: ReturnType<typeof buildScenario>; width: number; height: number; focused?: boolean;
|
|
@@ -28,7 +28,7 @@ export function ScenarioPayoffChart({ scenario, width, height, focused = false }
|
|
|
28
28
|
<Box height={1} paddingX={1} flexDirection="row" gap={3}>
|
|
29
29
|
<Text fg={colors.warning}>{`${scenario.expiryRisk.reason ? "First expiry" : "Expiry"} ${date(scenario.expiryDate)}`}</Text>
|
|
30
30
|
<Text fg={colors.borderFocused}>{`Selected ${date(scenario.controls.date)}`}</Text>
|
|
31
|
-
<Text fg={colors.textDim}>{`P&L ${scenario.position.currency}`}</Text>
|
|
31
|
+
<Text fg={colors.textDim}>{`P&L ${currencyLabel(scenario.position.currency)} ${scenarioValueUnit(scenario.position)}`}</Text>
|
|
32
32
|
</Box>
|
|
33
33
|
<StaticChartSurface points={points} overlays={overlays} calendarSpaced
|
|
34
34
|
width={width} height={Math.max(2, height - 1)} colors={{ ...resolveChartPalette(colors), lineColor: colors.warning }}
|