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
|
@@ -3,7 +3,8 @@ import { CompositeChart } from "../../../components";
|
|
|
3
3
|
import { resolveChartPalette } from "../../../components/chart/core/palette";
|
|
4
4
|
import { formatCompositeSeriesValue } from "../../../components/chart/composite/format";
|
|
5
5
|
import { staticSeries } from "../../../components/chart/static/series";
|
|
6
|
-
import { useThemeColors } from "../../../theme/theme-context";
|
|
6
|
+
import { useThemeColors, useThemeId } from "../../../theme/theme-context";
|
|
7
|
+
import { themeSeriesColors } from "../../../theme/series-colors";
|
|
7
8
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
8
9
|
import { formatPoints } from "./format";
|
|
9
10
|
import type { DatedValue, IvHistoryModel } from "./model";
|
|
@@ -11,8 +12,15 @@ import type { DatedValue, IvHistoryModel } from "./model";
|
|
|
11
12
|
const PANELS = [{ id: "vol", height: 3 }, { id: "spread", height: 1 }];
|
|
12
13
|
/** The chart drawing blue: IV90 must read apart from amber IV30 in every theme. */
|
|
13
14
|
const IV90_COLOR = "#4c9aff";
|
|
14
|
-
/** The
|
|
15
|
-
const
|
|
15
|
+
/** The quote capture point must stand apart from the amber IV30 line it continues. */
|
|
16
|
+
const QUOTE_COLOR = "#c084fc";
|
|
17
|
+
/** IV1Y: pink, apart from amber IV30, blue IV90, green HV and the violet quote points. */
|
|
18
|
+
const IV365_COLOR = "#f783ac";
|
|
19
|
+
/**
|
|
20
|
+
* Palette slots for a theme with its own series colours (the colour-blind
|
|
21
|
+
* ones), where the up colour HV takes elsewhere is the same blue as IV90.
|
|
22
|
+
*/
|
|
23
|
+
const THEMED_SLOTS = { iv30: 1, iv90: 0, hv: 2, quote: 5, iv365: 4 } as const;
|
|
16
24
|
|
|
17
25
|
function volSeries(id: string, label: string, color: string, points: readonly DatedValue[], panelId: string,
|
|
18
26
|
style: ResolvedSeries["style"] = "line"): ResolvedSeries {
|
|
@@ -27,22 +35,28 @@ function formatLegendValue(value: number, series: ResolvedSeries): string {
|
|
|
27
35
|
return series.id === "spread" ? `${formatPoints(value / 100)} pts` : formatCompositeSeriesValue(value, series);
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
function ivHistorySeries(model: IvHistoryModel, colors: { iv30: string; iv90: string; hv: string; quote: string; spread: string },
|
|
38
|
+
function ivHistorySeries(model: IvHistoryModel, colors: { iv30: string; iv90: string; iv365: string; hv: string; quote: string; spread: string },
|
|
31
39
|
hvLabel: string): ResolvedSeries[] {
|
|
32
40
|
return [
|
|
33
41
|
volSeries("iv30", "IV 30d", colors.iv30, model.iv30, "vol"),
|
|
34
42
|
volSeries("iv90", "IV 90d", colors.iv90, model.iv90, "vol"),
|
|
43
|
+
// Drawn only once there is a 1Y close, so an empty legend entry never suggests one.
|
|
44
|
+
...(model.iv365.length ? [volSeries("iv365", "IV 1Y", colors.iv365, model.iv365, "vol")] : []),
|
|
35
45
|
volSeries("hv", hvLabel, colors.hv, model.hv, "vol"),
|
|
36
|
-
...(model.quoteIv30.length ? [volSeries("iv30-quote", "IV 30d
|
|
46
|
+
...(model.quoteIv30.length ? [volSeries("iv30-quote", "IV 30d quote", colors.quote, model.quoteIv30, "vol", "points")] : []),
|
|
37
47
|
{ ...volSeries("spread", `IV 30d - ${hvLabel}`, colors.spread, model.spread, "spread", "columns"), ...SPREAD_UNIT },
|
|
38
48
|
];
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
export function IvHistoryChart({ model, width, height, hvLabel, focused = false }: { model: IvHistoryModel; width: number; height: number; hvLabel: string; focused?: boolean }) {
|
|
42
52
|
const colors = useThemeColors();
|
|
53
|
+
const themed = themeSeriesColors(useThemeId());
|
|
43
54
|
const palette = resolveChartPalette(colors);
|
|
44
|
-
const series = useMemo(() => ivHistorySeries(model,
|
|
45
|
-
|
|
55
|
+
const series = useMemo(() => ivHistorySeries(model, themed
|
|
56
|
+
? { iv30: themed[THEMED_SLOTS.iv30]!, iv90: themed[THEMED_SLOTS.iv90]!, iv365: themed[THEMED_SLOTS.iv365]!, hv: themed[THEMED_SLOTS.hv]!,
|
|
57
|
+
quote: themed[THEMED_SLOTS.quote]!, spread: colors.textDim }
|
|
58
|
+
: { iv30: colors.warning, iv90: IV90_COLOR, iv365: IV365_COLOR, hv: colors.positive, quote: QUOTE_COLOR, spread: colors.textDim },
|
|
59
|
+
hvLabel), [model, colors, themed, hvLabel]);
|
|
46
60
|
return <CompositeChart series={series} panels={PANELS} width={width} height={height} focused={focused} navigable={false} showLegend showTimeAxis formatValue={formatLegendValue}
|
|
47
61
|
remoteKind="implied-volatility-history"
|
|
48
62
|
colors={{ background: palette.bgColor, grid: palette.gridColor, crosshair: palette.crosshairColor, text: colors.text,
|
|
@@ -33,6 +33,10 @@ export interface IvStats {
|
|
|
33
33
|
high: number | null;
|
|
34
34
|
samples: number;
|
|
35
35
|
windowStart: string | null;
|
|
36
|
+
/** Prior sessions the rank needs: 120, or 60 for IV1Y. Absent from older servers. */
|
|
37
|
+
minSamples?: number;
|
|
38
|
+
/** Why rank or percentile is empty, when one is. */
|
|
39
|
+
rankNote?: string | null;
|
|
36
40
|
}
|
|
37
41
|
interface IvReading {
|
|
38
42
|
date: string;
|
|
@@ -51,8 +55,12 @@ export interface IvHistoryPayload {
|
|
|
51
55
|
symbol: string;
|
|
52
56
|
asOf: string;
|
|
53
57
|
status: IvCoverageStatus;
|
|
54
|
-
coverage: {
|
|
55
|
-
|
|
58
|
+
coverage: {
|
|
59
|
+
source: "seed" | "demand"; addedAt: string; backfilledThrough: string | null; since: string | null;
|
|
60
|
+
/** First trade close with a one-year reading; the 1Y series starts later and is sparser. */
|
|
61
|
+
iv365Since?: string | null;
|
|
62
|
+
} | null;
|
|
63
|
+
stats: { iv30: IvStats | null; iv90: IvStats | null; iv365?: IvStats | null };
|
|
56
64
|
latest: IvReading | null;
|
|
57
65
|
series: IvPoint[];
|
|
58
66
|
warnings: string[];
|
|
@@ -62,6 +70,8 @@ export interface IvScreenRow {
|
|
|
62
70
|
status: "ready" | "queued";
|
|
63
71
|
iv30: IvStats | null;
|
|
64
72
|
iv90: IvStats | null;
|
|
73
|
+
iv365?: IvStats | null;
|
|
74
|
+
iv365Since?: string | null;
|
|
65
75
|
latest: IvReading | null;
|
|
66
76
|
skew: { date: string; put25: number; call25: number; skew: number } | null;
|
|
67
77
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IvMethod } from "./client";
|
|
2
|
+
import type { IvStatRow, IvStatUnit, RichCheap, RichCheapDates } from "./model";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VCA's sign conventions, and what VIX is beside an at-the-money IV30, one
|
|
6
|
+
* line each: under the table, in the shot and in the report's notes. The skew
|
|
7
|
+
* is the stored 25-delta put and call IVs at a 30-day constant maturity, from
|
|
8
|
+
* the latest quote capture; both differences are decimal IV shown in points.
|
|
9
|
+
*/
|
|
10
|
+
export const VCA_DEFINITIONS = [
|
|
11
|
+
"25D skew: 30-day 25-delta put IV minus 25-delta call IV, in vol points; positive means puts are richer. 30-90: IV30 minus IV90.",
|
|
12
|
+
"VIX is Cboe's 30-day S&P 500 implied vol from the whole SPX strike strip, skew included; IV30 here is at the money, so VIX usually reads higher.",
|
|
13
|
+
] as const;
|
|
2
14
|
|
|
3
15
|
export const formatVol = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : `${(value * 100).toFixed(1)}%`;
|
|
4
16
|
export const formatPoints = (value: number | null | undefined) =>
|
|
@@ -13,6 +25,25 @@ export function formatStat(value: number | null | undefined, unit: IvStatUnit):
|
|
|
13
25
|
if (value == null || !Number.isFinite(value)) return "--";
|
|
14
26
|
return unit === "vol" ? formatVol(value) : unit === "points" ? formatPoints(value) : value.toFixed(2);
|
|
15
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* How a stored reading was taken. A quote capture is the option quote mids
|
|
30
|
+
* from the minutes before the close, taken once a session and never updated:
|
|
31
|
+
* it is not a live feed, so it is not called live.
|
|
32
|
+
*/
|
|
33
|
+
const readingSource = (method: IvMethod | null | undefined) => method === "quote-mid" ? "quote" : "close";
|
|
16
34
|
export function statSource(row: Pick<IvStatRow, "method">): string {
|
|
17
|
-
return row.method === "
|
|
35
|
+
return row.method === "prices" ? "closes" : row.method ? readingSource(row.method) : "";
|
|
36
|
+
}
|
|
37
|
+
/** "2026-10-09 quote": the session a reading belongs to and how it was taken. */
|
|
38
|
+
export const readingLabel = (date: string, method: IvMethod | null | undefined, short = false) =>
|
|
39
|
+
`${short ? shortDate(date) : date} ${readingSource(method)}`;
|
|
40
|
+
/**
|
|
41
|
+
* The dates every row agrees on, said once: the reading's, and the trade close
|
|
42
|
+
* its rank is measured on. `short` is for a footer with little room: month and
|
|
43
|
+
* day, and no window.
|
|
44
|
+
*/
|
|
45
|
+
export function sharedDates({ reading, rank }: Pick<RichCheapDates, "reading" | "rank">, short = false): string[] {
|
|
46
|
+
if (reading?.method === "trade-close" && reading.date === rank) return [readingLabel(reading.date, reading.method, short)];
|
|
47
|
+
return [reading && `IV30 ${readingLabel(reading.date, reading.method, short)}`,
|
|
48
|
+
rank && (short ? `IVR/IVP ${shortDate(rank)} close` : `IVR/IVP vs 52 weeks to ${rank} close`)].filter((part): part is string => !!part);
|
|
18
49
|
}
|
|
@@ -2,8 +2,8 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
|
2
2
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
3
3
|
import { createRealizedVolatilityDependencies, loadRealizedVolatilityHistory } from "../realized-vol/client";
|
|
4
4
|
import { createHvDependencies, loadIvHistory, loadIvScreen, loadRealizedVolatilities } from "./client";
|
|
5
|
-
import { formatPoints, formatRank, formatStat, formatVol, verdictLabel } from "./format";
|
|
6
|
-
import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap,
|
|
5
|
+
import { formatPoints, formatRank, formatStat, formatVol, readingLabel, sharedDates, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
6
|
+
import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, VCA_LIMIT, VCA_PRESETS } from "./model";
|
|
7
7
|
import { vcaUniverse } from "./universe";
|
|
8
8
|
|
|
9
9
|
const METHODOLOGY = "docs/research-data.md#implied-volatility-history";
|
|
@@ -14,7 +14,8 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
14
14
|
freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date", oldest: null },
|
|
15
15
|
describe: (args) => `HIVG ${args.symbols[0] ?? ""}`,
|
|
16
16
|
discovery: { aliases: ["HIVG"], screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
|
|
17
|
-
limitations: ["History starts February 2024 (
|
|
17
|
+
limitations: ["History starts February 2024 (daily option trade closes)", "Rank and percentile use trade-close readings only",
|
|
18
|
+
"IV1Y needs long-dated trades: it can start later, has gaps, and ranks from 60 prior sessions"] },
|
|
18
19
|
options: [
|
|
19
20
|
{ key: "lookback", type: "enum", values: [{ value: "1Y" }, { value: "2Y" }, { value: "ALL" }], defaultValue: "1Y", description: "Visible history" },
|
|
20
21
|
{ key: "hvWindow", type: "enum", values: HV_WINDOWS.map((window) => ({ value: String(window) })), defaultValue: "20", description: "Realized volatility window in sessions" },
|
|
@@ -28,8 +29,10 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
28
29
|
const model = projectIvHistory(payload, prices.history, { lookback: String(args.options.lookback) as IvLookback,
|
|
29
30
|
hvWindow: (Number(args.options.hvWindow) === 30 ? 30 : 20) as HvWindow });
|
|
30
31
|
const hvByDay = new Map(model.hv.map((point) => [point.date.getTime(), point.value]));
|
|
32
|
+
const iv365ByDay = new Map(model.iv365.map((point) => [point.date.getTime(), point.value]));
|
|
31
33
|
const rows = model.iv30.map((point, index) => ({ date: point.date.toISOString().slice(0, 10), iv30: point.value,
|
|
32
34
|
iv90: model.iv90.find((entry) => entry.date.getTime() === point.date.getTime())?.value ?? null,
|
|
35
|
+
iv365: iv365ByDay.get(point.date.getTime()) ?? null,
|
|
33
36
|
hv: hvByDay.get(point.date.getTime()) ?? null, spread: model.spread[index]?.value ?? null })).reverse();
|
|
34
37
|
const errors = [prices.error, ...model.warnings].filter((value): value is string => !!value);
|
|
35
38
|
return {
|
|
@@ -45,11 +48,12 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
45
48
|
{ key: "samples", header: "Sessions" },
|
|
46
49
|
], rows: model.stats.map((row) => ({ ...row })) },
|
|
47
50
|
{ title: "Daily history", columns: [{ key: "date", header: "Session" },
|
|
48
|
-
...["iv30", "iv90", "hv"].map((key) => ({ key
|
|
51
|
+
...[["iv30", "IV30"], ["iv90", "IV90"], ["iv365", "IV1Y"], ["hv", "HV"]].map(([key, header]) => ({ key: key!, header: header!, format: percent })),
|
|
49
52
|
{ key: "spread", header: "IV-HV", format: (value: unknown) => typeof value === "number" ? formatPoints(value) : "--" }], rows },
|
|
50
53
|
],
|
|
51
54
|
complete: payload.status === "ready" && errors.length === 0,
|
|
52
55
|
unavailableSymbols: model.iv30.length ? [] : [instrument.symbol], errors,
|
|
56
|
+
...(model.oneYearNote ? { notes: [model.oneYearNote] } : {}),
|
|
53
57
|
metadata: { status: payload.status, coverage: payload.coverage, latest: payload.latest, stats: payload.stats,
|
|
54
58
|
unit: "decimal annualized volatility", methodology: METHODOLOGY },
|
|
55
59
|
};
|
|
@@ -58,7 +62,8 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
58
62
|
|
|
59
63
|
export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
60
64
|
shape: "bundle",
|
|
61
|
-
|
|
65
|
+
// Dated by the readings themselves; the response's own date is the day it was fetched.
|
|
66
|
+
freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date" },
|
|
62
67
|
argument: { kind: "symbol-list", optional: true, maximum: VCA_LIMIT, description: "US option underlyings; defaults to index and sector ETFs." },
|
|
63
68
|
options: [{ key: "preset", type: "enum", values: [{ value: "etfs" }, { value: "megacaps" }], defaultValue: "etfs", description: "Preset when no symbols are given" }],
|
|
64
69
|
describe: "Volatility rich/cheap",
|
|
@@ -73,24 +78,31 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
73
78
|
]);
|
|
74
79
|
const rows = projectRichCheap(payload.rows, hv).sort((a, b) => (b.percentile ?? -1) - (a.percentile ?? -1));
|
|
75
80
|
const queued = rows.filter((row) => row.status === "queued").map((row) => row.symbol);
|
|
76
|
-
const
|
|
81
|
+
const dates = richCheapDates(rows);
|
|
77
82
|
const hasSkew = rows.some((row) => row.skew != null);
|
|
83
|
+
const hasIv1y = rows.some((row) => row.iv1y != null), hasIv1yRank = rows.some((row) => row.iv1yPercentile != null);
|
|
84
|
+
const dated = (value: unknown) => typeof value === "string" ? value : "--";
|
|
78
85
|
return {
|
|
79
|
-
sections: [{ title:
|
|
86
|
+
sections: [{ title: ["Rich/cheap", universe.label, ...sharedDates(dates)].join(" · "), columns: [
|
|
80
87
|
{ key: "symbol", header: "Symbol" }, { key: "iv30", header: "IV30", format: percent },
|
|
81
|
-
...(
|
|
88
|
+
...(dates.reading ? [] : [{ key: "date", header: "As of", format: (value: unknown, row?: unknown) =>
|
|
89
|
+
typeof value === "string" ? readingLabel(value, (row as RichCheapRow).method) : "--" }]),
|
|
90
|
+
...(dates.rankApart ? [{ key: "rankDate", header: "Rank as of", format: (value: unknown) => typeof value === "string" ? `${value} close` : "--" }] : []),
|
|
82
91
|
{ key: "rank", header: "IVR", format: (value: unknown) => formatRank(value as number | null) },
|
|
83
92
|
{ key: "percentile", header: "IVP", format: (value: unknown) => formatRank(value as number | null) },
|
|
84
93
|
{ key: "verdict", header: "Rich/Cheap", format: (value: unknown) => verdictLabel(value as never) },
|
|
94
|
+
...(hasIv1y ? [{ key: "iv1y", header: "IV1Y", format: percent }] : []),
|
|
95
|
+
...(hasIv1yRank ? [{ key: "iv1yPercentile", header: "IVP1Y", format: (value: unknown) => formatRank(value as number | null) }] : []),
|
|
85
96
|
{ key: "termSlope", header: "30-90", format: (value: unknown) => formatPoints(value as number | null) },
|
|
86
97
|
...(hasSkew ? [{ key: "skew", header: "25D skew", format: (value: unknown) => formatPoints(value as number | null) }] : []),
|
|
98
|
+
...(hasSkew && dates.skewApart ? [{ key: "skewDate", header: "Skew as of", format: dated }] : []),
|
|
87
99
|
{ key: "hv", header: "HV20", format: percent },
|
|
88
100
|
{ key: "ivHv", header: "IV/HV", format: (value: unknown) => typeof value === "number" ? value.toFixed(2) : "--" },
|
|
89
101
|
], rows: rows.map((row) => ({ ...row })) }],
|
|
90
102
|
complete: !queued.length && !universe.error, unavailableSymbols: queued,
|
|
91
103
|
errors: [universe.error].filter((value): value is string => !!value),
|
|
92
|
-
...
|
|
93
|
-
metadata: {
|
|
104
|
+
notes: [...queued.length ? [`Queued for backfill: ${queued.join(", ")}.`] : [], ...VCA_DEFINITIONS],
|
|
105
|
+
metadata: { presets: VCA_PRESETS, methodology: METHODOLOGY },
|
|
94
106
|
};
|
|
95
107
|
},
|
|
96
108
|
};
|
|
@@ -26,12 +26,12 @@ export const ivHistoryModule: PluginModule = {
|
|
|
26
26
|
paneTemplates: [
|
|
27
27
|
{ ...createTickerSurfacePaneTemplate({
|
|
28
28
|
id: "iv-history-pane", paneId: "iv-history", label: "Implied Volatility History",
|
|
29
|
-
description: "30
|
|
30
|
-
keywords: ["hivg", "implied", "volatility", "history", "iv rank", "ivr", "percentile", "vrp"], shortcut: "HIVG", publicShare: true,
|
|
29
|
+
description: "30-day, 90-day and one-year ATM implied volatility since 2024 against realized, with IV rank and percentile.",
|
|
30
|
+
keywords: ["hivg", "implied", "volatility", "history", "iv rank", "ivr", "percentile", "vrp", "iv1y", "leaps"], shortcut: "HIVG", publicShare: true,
|
|
31
31
|
}), headless: ivHistoryHeadless },
|
|
32
32
|
{
|
|
33
33
|
id: "iv-screen-pane", paneId: "iv-screen", label: "Volatility Rich/Cheap",
|
|
34
|
-
description: "IV rank, percentile, term slope, skew and IV/HV across a list, ranked rich to cheap.",
|
|
34
|
+
description: "IV rank, percentile, one-year IV, term slope, skew and IV/HV across a list, ranked rich to cheap.",
|
|
35
35
|
keywords: ["vca", "rich", "cheap", "iv rank", "implied volatility", "screen"],
|
|
36
36
|
shortcut: { prefix: "VCA", argKind: "ticker-list" as const, argOptional: true }, headless: ivScreenHeadless,
|
|
37
37
|
createInstance: (_context, options) => ({
|
|
@@ -31,12 +31,16 @@ export interface IvHistoryModel {
|
|
|
31
31
|
status: IvHistoryPayload["status"];
|
|
32
32
|
iv30: DatedValue[];
|
|
33
33
|
iv90: DatedValue[];
|
|
34
|
+
/** Trade-close one-year IV; it starts later than IV30 and has gaps where long-dated series did not trade. */
|
|
35
|
+
iv365: DatedValue[];
|
|
34
36
|
/** Same-session quote captures, drawn as points over the trade-close line. */
|
|
35
37
|
quoteIv30: DatedValue[];
|
|
36
38
|
hv: DatedValue[];
|
|
37
39
|
spread: DatedValue[];
|
|
38
40
|
stats: IvStatRow[];
|
|
39
41
|
since: string | null;
|
|
42
|
+
/** Where the 1Y series starts and how deep it is, when that differs from the rest or leaves it unranked. */
|
|
43
|
+
oneYearNote: string | null;
|
|
40
44
|
asOf: string | null;
|
|
41
45
|
warnings: string[];
|
|
42
46
|
}
|
|
@@ -76,7 +80,7 @@ function seriesStat(id: string, label: string, unit: IvStatUnit, points: readonl
|
|
|
76
80
|
percentile: midrankPercentile(window, value), samples: window.length };
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
function byMethod(series: readonly IvPoint[], method: IvMethod, field: "iv30" | "iv90") {
|
|
83
|
+
function byMethod(series: readonly IvPoint[], method: IvMethod, field: "iv30" | "iv90" | "iv365") {
|
|
80
84
|
return series.filter((point) => point.method === method && point[field] != null)
|
|
81
85
|
.map((point) => ({ date: utc(point.sessionDate), value: point[field] }))
|
|
82
86
|
.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
@@ -119,12 +123,18 @@ export function projectIvHistory(
|
|
|
119
123
|
value: stats?.value ?? null, date: stats?.date ?? null, method: stats?.method ?? null, low: stats?.low ?? null, high: stats?.high ?? null,
|
|
120
124
|
rank: stats?.rank ?? null, percentile: stats?.percentile ?? null, samples: stats?.samples ?? 0 });
|
|
121
125
|
const stats: IvStatRow[] = [fromServer("iv30", "IV 30d ATM", payload.stats.iv30)];
|
|
122
|
-
// A newer
|
|
126
|
+
// A newer quote capture is shown on its own row: its rank would mix methods.
|
|
123
127
|
if (latest?.method === "quote-mid" && latest.iv30 != null && (!payload.stats.iv30 || latest.date > payload.stats.iv30.date)) {
|
|
124
|
-
stats.push({ id: "iv30-
|
|
128
|
+
stats.push({ id: "iv30-quote", label: "IV 30d quote", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid",
|
|
125
129
|
low: null, high: null, rank: null, percentile: null, samples: 0 });
|
|
126
130
|
}
|
|
131
|
+
// IV1Y ranks trade closes only; until there is one, a stored quote gives its level, unranked.
|
|
132
|
+
const oneYear = payload.stats.iv365 ?? null;
|
|
127
133
|
stats.push(fromServer("iv90", "IV 90d ATM", payload.stats.iv90),
|
|
134
|
+
!oneYear && latest?.iv365 != null
|
|
135
|
+
? { id: "iv365", label: "IV 1Y ATM", unit: "vol", value: latest.iv365, date: latest.date, method: latest.method,
|
|
136
|
+
low: null, high: null, rank: null, percentile: null, samples: 0 }
|
|
137
|
+
: fromServer("iv365", "IV 1Y ATM", oneYear),
|
|
128
138
|
seriesStat("hv", `HV ${window}`, "vol", rolling, "prices"),
|
|
129
139
|
seriesStat("spread", `IV 30d - HV ${window}`, "points", spreadAll, "trade-close"),
|
|
130
140
|
seriesStat("term", "IV 30d / 90d", "ratio", termAll, "trade-close"));
|
|
@@ -134,12 +144,19 @@ export function projectIvHistory(
|
|
|
134
144
|
else if (payload.status === "unavailable" && !payload.warnings.length) warnings.push(`No listed options history found for ${payload.symbol}.`);
|
|
135
145
|
if (!prices.length) warnings.push("Realized volatility unavailable: daily price history is missing.");
|
|
136
146
|
|
|
147
|
+
const tradeIv365 = byMethod(payload.series, "trade-close", "iv365");
|
|
148
|
+
const since = payload.coverage?.since ?? earliest;
|
|
149
|
+
const oneYearSince = payload.coverage?.iv365Since ?? (tradeIv365[0] ? day(tradeIv365[0].date) : null);
|
|
150
|
+
const oneYearNote = !oneYearSince ? (since ? "no IV1Y history yet" : null)
|
|
151
|
+
: oneYear?.percentile == null ? `IV1Y from ${oneYearSince}, ${tradeIv365.length} session${tradeIv365.length === 1 ? "" : "s"}`
|
|
152
|
+
: since && oneYearSince > since ? `IV1Y from ${oneYearSince}` : null;
|
|
153
|
+
|
|
137
154
|
return {
|
|
138
155
|
symbol: payload.symbol, status: payload.status,
|
|
139
|
-
iv30: visible(tradeIv30), iv90: visible(byMethod(payload.series, "trade-close", "iv90")),
|
|
156
|
+
iv30: visible(tradeIv30), iv90: visible(byMethod(payload.series, "trade-close", "iv90")), iv365: visible(tradeIv365),
|
|
140
157
|
quoteIv30: visible(byMethod(payload.series, "quote-mid", "iv30")),
|
|
141
158
|
hv, spread: visible(spreadAll), stats,
|
|
142
|
-
since
|
|
159
|
+
since, oneYearNote, asOf: latestDate, warnings,
|
|
143
160
|
};
|
|
144
161
|
}
|
|
145
162
|
|
|
@@ -152,21 +169,50 @@ export interface RichCheapRow {
|
|
|
152
169
|
method: IvMethod | null;
|
|
153
170
|
rank: number | null;
|
|
154
171
|
percentile: number | null;
|
|
155
|
-
/**
|
|
172
|
+
/** Trade close the rank is measured on: the newest trade-close session, which a newer quote reading can be ahead of. */
|
|
156
173
|
rankDate: string | null;
|
|
157
174
|
termSlope: number | null;
|
|
175
|
+
/** The latest reading's one-year IV, dated with IV30. */
|
|
176
|
+
iv1y: number | null;
|
|
177
|
+
/**
|
|
178
|
+
* IV1Y percentile on the close IVR and IVP are ranked on. Empty when the 1Y
|
|
179
|
+
* series has no reading that session, or there is no IV1Y beside it to describe.
|
|
180
|
+
*/
|
|
181
|
+
iv1yPercentile: number | null;
|
|
158
182
|
skew: number | null;
|
|
183
|
+
/** Session of the quote capture the skew comes from. */
|
|
184
|
+
skewDate: string | null;
|
|
159
185
|
hv: number | null;
|
|
160
186
|
ivHv: number | null;
|
|
161
187
|
verdict: RichCheap;
|
|
162
188
|
}
|
|
163
189
|
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
/**
|
|
191
|
+
* The dates the screened rows agree on, so each is said once instead of per
|
|
192
|
+
* row. The reading (IV30, term slope, IV/HV) and the rank (IVR, IVP) are dated
|
|
193
|
+
* apart: a quote reading can be a session ahead of the trade close its rank
|
|
194
|
+
* is measured on. A group the rows disagree on gets its own column.
|
|
195
|
+
*/
|
|
196
|
+
export interface RichCheapDates {
|
|
197
|
+
reading: { date: string; method: IvMethod } | null;
|
|
198
|
+
/** The trade close every ranked row is ranked on. */
|
|
199
|
+
rank: string | null;
|
|
200
|
+
/** Ranked rows are ranked on different closes. */
|
|
201
|
+
rankApart: boolean;
|
|
202
|
+
/** Some skew comes from a different session than its row's reading. */
|
|
203
|
+
skewApart: boolean;
|
|
204
|
+
}
|
|
205
|
+
export function richCheapDates(rows: readonly RichCheapRow[]): RichCheapDates {
|
|
206
|
+
const read = rows.filter((row) => row.date && row.method);
|
|
207
|
+
const ranked = rows.filter((row) => row.rankDate);
|
|
208
|
+
const first = read[0], firstRanked = ranked[0];
|
|
209
|
+
return {
|
|
210
|
+
reading: first && read.every((row) => row.date === first.date && row.method === first.method)
|
|
211
|
+
? { date: first.date!, method: first.method! } : null,
|
|
212
|
+
rank: firstRanked && ranked.every((row) => row.rankDate === firstRanked.rankDate) ? firstRanked.rankDate : null,
|
|
213
|
+
rankApart: !!firstRanked && ranked.some((row) => row.rankDate !== firstRanked.rankDate),
|
|
214
|
+
skewApart: rows.some((row) => row.skew != null && row.skewDate !== row.date),
|
|
215
|
+
};
|
|
170
216
|
}
|
|
171
217
|
|
|
172
218
|
/** Rich or cheap against the symbol's own year: IV30 percentile at or above 80, or at or below 20. */
|
|
@@ -185,7 +231,9 @@ export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<s
|
|
|
185
231
|
symbol: row.symbol, status: row.status, iv30, date: latest?.date ?? null, method: latest?.method ?? null,
|
|
186
232
|
rank: row.iv30?.rank ?? null, percentile, rankDate: row.iv30?.date ?? null,
|
|
187
233
|
termSlope: iv30 != null && latest?.iv90 != null ? iv30 - latest.iv90 : null,
|
|
188
|
-
|
|
234
|
+
iv1y: latest?.iv365 ?? null,
|
|
235
|
+
iv1yPercentile: latest?.iv365 != null && row.iv365 && row.iv365.date === row.iv30?.date ? row.iv365.percentile : null,
|
|
236
|
+
skew: row.skew?.skew ?? null, skewDate: row.skew?.date ?? null, hv: realized,
|
|
189
237
|
ivHv: iv30 != null && realized != null && realized > 0 ? iv30 / realized : null,
|
|
190
238
|
verdict: verdictFor(percentile),
|
|
191
239
|
};
|
|
@@ -17,6 +17,8 @@ import { loadIvHistory } from "./client";
|
|
|
17
17
|
import { formatRank, formatStat, statSource } from "./format";
|
|
18
18
|
import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory } from "./model";
|
|
19
19
|
|
|
20
|
+
/** Pane borders, the footer's padding and its [s]urface hint. */
|
|
21
|
+
const FOOTER_RESERVE = 16;
|
|
20
22
|
const LOOKBACKS = [{ value: "1Y", label: "1Y" }, { value: "2Y", label: "2Y" }, { value: "ALL", label: "All" }];
|
|
21
23
|
const HV_OPTIONS = HV_WINDOWS.map((window) => ({ value: String(window), label: `HV ${window}` }));
|
|
22
24
|
const STAT_COLUMNS: DataTableColumn[] = [
|
|
@@ -80,14 +82,19 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
80
82
|
else return;
|
|
81
83
|
event.preventDefault(); event.stopPropagation();
|
|
82
84
|
}, { enabled: focused });
|
|
85
|
+
// In a pane too narrow for the whole footer, the history start drops its prefix.
|
|
86
|
+
const sinceText = model?.since ? `trade closes since ${model.since}` : null;
|
|
87
|
+
const footerText = [sinceText, model?.oneYearNote, model?.asOf].filter(Boolean).join(" ");
|
|
88
|
+
const since = sinceText && footerText.length + FOOTER_RESERVE > width ? `since ${model!.since}` : sinceText;
|
|
83
89
|
usePaneFooter("iv-history", () => ({ info: [
|
|
84
90
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
|
|
85
|
-
...(
|
|
91
|
+
...(since ? [{ id: "since", parts: [{ text: since, tone: "muted" as const }] }] : []),
|
|
92
|
+
...(model?.oneYearNote ? [{ id: "one-year", parts: [{ text: model.oneYearNote, tone: "muted" as const }] }] : []),
|
|
86
93
|
...(model?.asOf ? [{ id: "date", parts: [{ text: model.asOf, tone: "muted" as const }] }] : []),
|
|
87
94
|
], hints: [
|
|
88
95
|
// Lookback (y) and realized window (w) keep their keys; the query bar shows them.
|
|
89
96
|
...(symbol ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
|
|
90
|
-
] }), [resource.loading, model?.
|
|
97
|
+
] }), [resource.loading, since, model?.oneYearNote, model?.asOf, lookback, hvWindow, symbol]);
|
|
91
98
|
|
|
92
99
|
const statsHeight = model ? model.stats.length + 1 : 0;
|
|
93
100
|
const statSelection = statSelectionValue ?? model?.stats[0]?.id ?? "";
|
|
@@ -112,7 +119,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
112
119
|
{ id: "lookback", label: "Lookback", value: lookback, options: LOOKBACKS, onChange: setLookback },
|
|
113
120
|
{ id: "realized", label: "Realized", value: String(hvWindow), options: HV_OPTIONS, onChange: setHvWindow },
|
|
114
121
|
]} />
|
|
115
|
-
{!symbol ? <EmptyState title="
|
|
122
|
+
{!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
|
|
116
123
|
error={!model ? resource.error ?? identityError ?? null : null} empty={!!model && !model.iv30.length && !model.quoteIv30.length}
|
|
117
124
|
emptyTitle={model?.status === "queued" || model?.status === "backfilling" ? `Backfilling ${symbol} implied volatility history...` : undefined}>
|
|
118
125
|
{model ? <>
|
|
@@ -120,6 +127,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
120
127
|
getItemKey={(row) => row.id} emptyStateTitle="No statistics." sortColumnId={null} sortDirection="asc"
|
|
121
128
|
selection={{ kind: "id", selectedId: statSelection, getId: (row) => row.id, onChange: (id) => setStatSelection(id) }}
|
|
122
129
|
getExportMetadata={() => [["symbol", model.symbol], ["as of", model.asOf], ["history since", model.since],
|
|
130
|
+
...(model.oneYearNote ? [["IV1Y", model.oneYearNote]] : []),
|
|
123
131
|
["IV", "ATM, constant maturity, annualized"], ["HV", `${hvWindow}-session close-to-close`], ["warnings", ...notices]]}
|
|
124
132
|
renderCell={(row, column) => statCell(row, column.id)} />
|
|
125
133
|
<IvHistoryChart model={model} width={width} height={Math.max(6, height - 1 - statsHeight)} hvLabel={`HV ${hvWindow}`} focused={focused} />
|
|
@@ -4,25 +4,32 @@ import { useAsyncResource } from "../../../react/async-resource";
|
|
|
4
4
|
import { usePaneCollection, usePaneSettingValue, usePluginAppActions, useTickers } from "../../../public/react";
|
|
5
5
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
6
6
|
import type { PaneProps } from "../../../types/plugin";
|
|
7
|
-
import { Box } from "../../../ui";
|
|
7
|
+
import { Box, Text } from "../../../ui";
|
|
8
|
+
import { wrapTextLines } from "../../../utils/text-wrap";
|
|
8
9
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
9
10
|
import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
|
|
10
11
|
import { loadIvScreen, loadRealizedVolatilities } from "./client";
|
|
11
|
-
import { formatPoints, formatRank, formatVol, shortDate, verdictLabel } from "./format";
|
|
12
|
-
import { projectRichCheap, type RichCheapRow,
|
|
12
|
+
import { formatPoints, formatRank, formatVol, readingLabel, sharedDates, shortDate, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
13
|
+
import { projectRichCheap, type RichCheapRow, richCheapDates, type VcaPreset } from "./model";
|
|
13
14
|
import { vcaUniverse } from "./universe";
|
|
14
15
|
|
|
15
16
|
type SortId = keyof RichCheapRow;
|
|
16
17
|
const HV_WINDOW = 20;
|
|
18
|
+
/** Pane borders and the footer's own padding. */
|
|
19
|
+
const FOOTER_MARGIN = 6;
|
|
17
20
|
const COLUMNS: DataTableColumn[] = [
|
|
18
21
|
{ id: "symbol", label: "Symbol", width: 8, align: "left" },
|
|
19
22
|
{ id: "iv30", label: "IV30", width: 7, align: "right" },
|
|
20
23
|
{ id: "date", label: "As of", width: 11, align: "right" },
|
|
24
|
+
{ id: "rankDate", label: "Rank as of", width: 11, align: "right" },
|
|
21
25
|
{ id: "rank", label: "IVR", width: 5, align: "right" },
|
|
22
26
|
{ id: "percentile", label: "IVP", width: 5, align: "right" },
|
|
23
27
|
{ id: "verdict", label: "Rich/Cheap", width: 11, align: "left" },
|
|
28
|
+
{ id: "iv1y", label: "IV1Y", width: 7, align: "right" },
|
|
29
|
+
{ id: "iv1yPercentile", label: "IVP1Y", width: 6, align: "right" },
|
|
24
30
|
{ id: "termSlope", label: "30-90", width: 7, align: "right" },
|
|
25
31
|
{ id: "skew", label: "25D skew", width: 9, align: "right" },
|
|
32
|
+
{ id: "skewDate", label: "Skew as of", width: 11, align: "right" },
|
|
26
33
|
{ id: "hv", label: `HV${HV_WINDOW}`, width: 7, align: "right" },
|
|
27
34
|
{ id: "ivHv", label: "IV/HV", width: 6, align: "right" },
|
|
28
35
|
];
|
|
@@ -57,10 +64,19 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
|
|
|
57
64
|
const projected = projectRichCheap(resource.data?.rows ?? [], hv);
|
|
58
65
|
return projected.sort((left, right) => compareSortValues(left[sort.columnId], right[sort.columnId], sort.direction));
|
|
59
66
|
}, [resource.data, hv, sort]);
|
|
60
|
-
const
|
|
61
|
-
// A
|
|
62
|
-
const columns = useMemo(() => COLUMNS.filter((column) =>
|
|
63
|
-
|
|
67
|
+
const dates = useMemo(() => richCheapDates(rows), [rows]);
|
|
68
|
+
// A date every row shares moves to the footer; a column no row fills, or whose dates agree, is left out.
|
|
69
|
+
const columns = useMemo(() => COLUMNS.filter((column) => {
|
|
70
|
+
switch (column.id) {
|
|
71
|
+
case "date": return !dates.reading;
|
|
72
|
+
case "rankDate": return dates.rankApart;
|
|
73
|
+
case "iv1y": return rows.some((row) => row.iv1y != null);
|
|
74
|
+
case "iv1yPercentile": return rows.some((row) => row.iv1yPercentile != null);
|
|
75
|
+
case "skew": return rows.some((row) => row.skew != null);
|
|
76
|
+
case "skewDate": return dates.skewApart;
|
|
77
|
+
default: return true;
|
|
78
|
+
}
|
|
79
|
+
}), [rows, dates]);
|
|
64
80
|
const [selected, setSelected] = useState<string | null>(null);
|
|
65
81
|
const queued = rows.filter((row) => row.status === "queued").length;
|
|
66
82
|
const uncovered = rows.filter((row) => row.status !== "queued" && row.iv30 == null).length;
|
|
@@ -70,11 +86,13 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
|
|
|
70
86
|
.filter((value): value is string => !!value);
|
|
71
87
|
usePaneNoticeFooter({ registrationId: "iv-screen-notices", notices, focused });
|
|
72
88
|
const openHistory = (row: RichCheapRow) => createPaneFromTemplate("iv-history-pane", { symbol: row.symbol });
|
|
89
|
+
// The full wording gives way to the short form in a pane too narrow to show the rank date.
|
|
90
|
+
const fullDatesText = useMemo(() => sharedDates(dates).join(" · "), [dates]);
|
|
91
|
+
const datesText = fullDatesText.length + FOOTER_MARGIN > width ? sharedDates(dates, true).join(" · ") : fullDatesText;
|
|
73
92
|
usePaneFooter("iv-screen", () => ({ info: [
|
|
74
93
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
|
|
75
|
-
...(
|
|
76
|
-
|
|
77
|
-
] }), [resource.loading, shared, resource.data?.asOf]);
|
|
94
|
+
...(datesText ? [{ id: "dates", parts: [{ text: datesText, tone: "muted" as const }] }] : []),
|
|
95
|
+
] }), [resource.loading, datesText]);
|
|
78
96
|
const handleKey = (event: DataTableKeyEvent): boolean => {
|
|
79
97
|
if (event.ctrl || event.alt || event.meta || event.name !== "r") return false;
|
|
80
98
|
void resource.reload();
|
|
@@ -84,19 +102,25 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
|
|
|
84
102
|
switch (id) {
|
|
85
103
|
case "symbol": return { text: row.symbol, color: colors.textBright };
|
|
86
104
|
case "iv30": return { text: formatVol(row.iv30), color: colors.warning };
|
|
87
|
-
case "date": return { text: row.date ? readingLabel(row.date, row.method) : row.status === "queued" ? "queued" : "--",
|
|
105
|
+
case "date": return { text: row.date ? readingLabel(row.date, row.method, true) : row.status === "queued" ? "queued" : "--",
|
|
88
106
|
color: colors.textDim };
|
|
107
|
+
case "rankDate": return { text: row.rankDate ? `${shortDate(row.rankDate)} close` : "--", color: colors.textDim };
|
|
89
108
|
case "rank": return { text: formatRank(row.rank) };
|
|
90
109
|
case "percentile": return { text: formatRank(row.percentile) };
|
|
91
110
|
case "verdict": return { text: verdictLabel(row.verdict), color: row.verdict === "rich" ? colors.negative : row.verdict === "cheap" ? colors.positive : colors.textDim };
|
|
111
|
+
case "iv1y": return { text: formatVol(row.iv1y) };
|
|
112
|
+
case "iv1yPercentile": return { text: formatRank(row.iv1yPercentile) };
|
|
92
113
|
case "termSlope": return { text: formatPoints(row.termSlope), color: row.termSlope != null && row.termSlope > 0 ? colors.negative : colors.text };
|
|
93
114
|
case "skew": return { text: formatPoints(row.skew) };
|
|
115
|
+
case "skewDate": return { text: row.skewDate ? readingLabel(row.skewDate, "quote-mid", true) : "--", color: colors.textDim };
|
|
94
116
|
case "hv": return { text: hv.has(row.symbol) ? formatVol(row.hv) : "...", color: colors.positive };
|
|
95
117
|
case "ivHv": return { text: row.ivHv == null ? "--" : row.ivHv.toFixed(2) };
|
|
96
118
|
default: return { text: "" };
|
|
97
119
|
}
|
|
98
120
|
};
|
|
99
121
|
const custom = scope === "custom";
|
|
122
|
+
// The skew's sign and VIX beside IV30 are units of the figures above them, so they stay under the table.
|
|
123
|
+
const definitions = useMemo(() => VCA_DEFINITIONS.flatMap((line) => wrapTextLines(line, Math.max(1, width - 2))), [width]);
|
|
100
124
|
return <Box width={width} height={height} flexDirection="column" overflow="hidden">
|
|
101
125
|
<QueryBar width={width} filters={[
|
|
102
126
|
{ id: "universe", label: "Universe", value: VCA_SCOPE_OPTIONS.some((option) => option.value === scope) ? scope : "etfs",
|
|
@@ -106,22 +130,22 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
|
|
|
106
130
|
onChange: setSymbolsText }] : []),
|
|
107
131
|
]} />
|
|
108
132
|
<PaneStatusBody subject="volatility rich/cheap" loading={resource.loading && !resource.data} error={universe.error && !universe.instruments.length ? universe.error : !resource.data ? resource.error : null}>
|
|
109
|
-
<DataTableView<RichCheapRow> focused={focused && !symbolsActive} columns={columns} items={rows} rootWidth={width}
|
|
133
|
+
<DataTableView<RichCheapRow> focused={focused && !symbolsActive} columns={columns} items={rows} rootWidth={width}
|
|
134
|
+
rootHeight={Math.max(2, height - 1 - definitions.length)}
|
|
110
135
|
getItemKey={(row) => row.symbol} sortColumnId={sort.columnId} sortDirection={sort.direction} emptyStateTitle="No symbols to screen."
|
|
111
136
|
onHeaderClick={(id) => setSort((current) => nextHeaderSort(current, id as SortId, { firstDirection: "desc" }))}
|
|
112
137
|
selection={{ kind: "id", selectedId: selected ?? rows[0]?.symbol ?? "", getId: (row) => row.symbol, onChange: (id) => setSelected(id) }}
|
|
113
138
|
onActivate={openHistory} onRootKeyDown={handleKey}
|
|
114
|
-
getExportMetadata={() => [["universe", universe.label], ["as of",
|
|
115
|
-
["rank window", "prior 52 weeks of trade-close readings"], ["HV", `${HV_WINDOW}-session close-to-close`]]}
|
|
139
|
+
getExportMetadata={() => [["universe", universe.label], ["as of", fullDatesText || undefined],
|
|
140
|
+
["IV", "30-day ATM, annualized"], ["IV1Y", "one-year ATM, annualized"], ["rank window", "prior 52 weeks of trade-close readings"], ["HV", `${HV_WINDOW}-session close-to-close`]]}
|
|
116
141
|
renderCell={(row, column) => cell(row, column.id)} />
|
|
142
|
+
<Box flexDirection="column" paddingX={1} height={definitions.length} flexShrink={0}>
|
|
143
|
+
{definitions.map((line, index) => <Text key={index} fg={colors.textDim}>{line}</Text>)}
|
|
144
|
+
</Box>
|
|
117
145
|
</PaneStatusBody>
|
|
118
146
|
</Box>;
|
|
119
147
|
}
|
|
120
148
|
|
|
121
|
-
function readingLabel(date: string, method: RichCheapRow["method"]): string {
|
|
122
|
-
return `${shortDate(date)} ${method === "quote-mid" ? "live" : "close"}`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
149
|
export const VCA_SCOPE_OPTIONS = [
|
|
126
150
|
{ value: "etfs", label: "Index and sector ETFs" },
|
|
127
151
|
{ value: "megacaps", label: "US mega caps" },
|
|
@@ -750,7 +750,7 @@ function JobsView({ width, height, focused, companyOnly = false }: JobsViewProps
|
|
|
750
750
|
if (!access.hasProAccess) return <HiringProWall symbol={symbol} exchange={exchange} width={width} height={height} />;
|
|
751
751
|
|
|
752
752
|
if (!symbol) {
|
|
753
|
-
if (companyOnly) return <EmptyState title="
|
|
753
|
+
if (companyOnly) return <EmptyState title="Select a ticker." />;
|
|
754
754
|
return <HomeView width={width} height={height} focused={focused} registrationId={registrationId} />;
|
|
755
755
|
}
|
|
756
756
|
return (
|