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
|
@@ -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 (
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/headless";
|
|
2
|
+
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
3
|
+
import { MarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
|
|
4
|
+
import { parseHedgeAmount } from "../options-scenario/hedge";
|
|
5
|
+
import { clipPriceHistoryToRange } from "../../../time-series/history-window";
|
|
6
|
+
import { formatNumber } from "../../../utils/format";
|
|
7
|
+
import { buildBinaryOutcomes, solveKellyFraction } from "./model";
|
|
8
|
+
import {
|
|
9
|
+
clampKellyHistoryYears,
|
|
10
|
+
estimateKellyHistoryInputs,
|
|
11
|
+
KELLY_HISTORY_DEFAULT_YEARS,
|
|
12
|
+
KELLY_HISTORY_MAX_YEARS,
|
|
13
|
+
KELLY_HISTORY_MIN_PERIODS,
|
|
14
|
+
kellyHistoryRange,
|
|
15
|
+
kellyHistoryRequest,
|
|
16
|
+
type KellyHistoryInputs,
|
|
17
|
+
} from "./history";
|
|
18
|
+
import { formatPct, formatSignedPct } from "./view";
|
|
19
|
+
|
|
20
|
+
/** The fractions of full Kelly a report sizes, largest first. */
|
|
21
|
+
const KELLY_FRACTIONS = [
|
|
22
|
+
{ id: "full", label: "Full Kelly", fraction: 1 },
|
|
23
|
+
{ id: "half", label: "Half Kelly", fraction: 0.5 },
|
|
24
|
+
{ id: "quarter", label: "Quarter Kelly", fraction: 0.25 },
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/** The inputs a ticker's monthly returns give, as the History tab shows them. */
|
|
28
|
+
function inputEntries(inputs: KellyHistoryInputs, edge: number) {
|
|
29
|
+
const payoff = inputs.downsideReturn < 0 ? inputs.upsideReturn / -inputs.downsideReturn : null;
|
|
30
|
+
return [
|
|
31
|
+
{ key: "window", label: "Window", value: { start: inputs.start, end: inputs.end, months: inputs.periods },
|
|
32
|
+
formatted: `${inputs.start} to ${inputs.end}, ${inputs.periods} monthly returns` },
|
|
33
|
+
{ key: "winRate", label: "Win rate", value: inputs.winProbability,
|
|
34
|
+
formatted: `${formatPct(inputs.winProbability)} (${inputs.wins} of ${inputs.periods} months up)` },
|
|
35
|
+
{ key: "averageGain", label: "Average gain", value: inputs.upsideReturn, formatted: `${formatSignedPct(inputs.upsideReturn)} a month` },
|
|
36
|
+
{ key: "averageLoss", label: "Average loss", value: inputs.downsideReturn, formatted: `${formatSignedPct(inputs.downsideReturn)} a month` },
|
|
37
|
+
{ key: "payoffRatio", label: "Payoff ratio", value: payoff, formatted: payoff == null ? "no down month" : `${formatNumber(payoff, 2)}x` },
|
|
38
|
+
{ key: "edge", label: "Edge", value: edge, formatted: `${formatSignedPct(edge)} a month` },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const kellyHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
43
|
+
shape: "bundle",
|
|
44
|
+
argument: { kind: "ticker", description: "Ticker to size" },
|
|
45
|
+
freshness: { source: "Your inputs and Gloom Cloud", status: "not-a-feed", basis: "monthly returns from daily closes", cadence: "daily", oldest: null },
|
|
46
|
+
description: "Kelly sizing for a ticker from its own monthly returns: win rate, payoff ratio and edge over a lookback, then full, half and quarter Kelly as a share of a bankroll.",
|
|
47
|
+
describe: (args) => `Kelly sizing | ${args.symbols[0] ?? ""}`,
|
|
48
|
+
discovery: {
|
|
49
|
+
dataRequirements: ["Gloom Cloud daily closes over the lookback, and the latest quote for share counts"],
|
|
50
|
+
limitations: ["Win rate, gain and loss are the ticker's own non-overlapping 21-session returns; past months are not a forecast."],
|
|
51
|
+
},
|
|
52
|
+
options: [
|
|
53
|
+
{
|
|
54
|
+
key: "bankroll",
|
|
55
|
+
type: "string",
|
|
56
|
+
settingKey: "bankroll",
|
|
57
|
+
placeholder: "amount",
|
|
58
|
+
example: "--bankroll 250000",
|
|
59
|
+
description: "Bankroll in your base currency, such as 250000, 250k or 1.5m; sizes each Kelly fraction in money.",
|
|
60
|
+
normalize: (value) => parseHedgeAmount(value, "bankroll"),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "years",
|
|
64
|
+
type: "integer",
|
|
65
|
+
settingKey: "historyYears",
|
|
66
|
+
minimum: 1,
|
|
67
|
+
maximum: KELLY_HISTORY_MAX_YEARS,
|
|
68
|
+
defaultValue: KELLY_HISTORY_DEFAULT_YEARS,
|
|
69
|
+
description: "Lookback in years for the monthly returns.",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
async load(args, ctx) {
|
|
73
|
+
const { symbol, exchange = "" } = await resolveHeadlessInstrument(ctx, args.symbols[0]!);
|
|
74
|
+
const years = clampKellyHistoryYears(args.options.years);
|
|
75
|
+
const range = kellyHistoryRange(years);
|
|
76
|
+
// The quote and the one history window the lookback needs load together, through the
|
|
77
|
+
// coordinator every chart reads daily closes from (the History tab's request, cache included).
|
|
78
|
+
const [entry, quote] = await Promise.all([
|
|
79
|
+
new MarketDataCoordinator(ctx.marketData).loadChart(kellyHistoryRequest({ symbol, exchange }, years), { forceRefresh: ctx.refresh }),
|
|
80
|
+
ctx.marketData.getQuote(symbol, exchange, ctx.refresh ? { cacheMode: "refresh" } : undefined).catch(() => null),
|
|
81
|
+
]);
|
|
82
|
+
const history = clipPriceHistoryToRange(resolveEntryValue(entry) ?? [], range);
|
|
83
|
+
ctx.signal.throwIfAborted();
|
|
84
|
+
const inputs = estimateKellyHistoryInputs(history, years);
|
|
85
|
+
if (!inputs) {
|
|
86
|
+
return {
|
|
87
|
+
complete: false,
|
|
88
|
+
unavailableSymbols: [symbol],
|
|
89
|
+
errors: [`${symbol}: fewer than ${KELLY_HISTORY_MIN_PERIODS} monthly returns in the last ${years} year${years === 1 ? "" : "s"}.`],
|
|
90
|
+
sections: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const solve = solveKellyFraction(buildBinaryOutcomes(inputs.winProbability, inputs.upsideReturn, inputs.downsideReturn));
|
|
94
|
+
const bankrollValue = args.options.bankroll;
|
|
95
|
+
const bankroll = typeof bankrollValue === "number" && bankrollValue > 0 ? bankrollValue : null;
|
|
96
|
+
const baseCurrency = ctx.config.baseCurrency || "USD";
|
|
97
|
+
const price = quote && quote.price > 0 && Number.isFinite(quote.price) ? quote.price : null;
|
|
98
|
+
// Shares only when the bankroll and the price are in one currency.
|
|
99
|
+
const sharePrice = price != null && quote?.currency === baseCurrency ? price : null;
|
|
100
|
+
const rows: HeadlessPaneRow[] = KELLY_FRACTIONS.map(({ id, label, fraction }) => {
|
|
101
|
+
const share = solve.fraction * fraction;
|
|
102
|
+
const position = bankroll == null ? null : share * bankroll;
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
kelly: label,
|
|
106
|
+
fraction: share,
|
|
107
|
+
position,
|
|
108
|
+
shares: position != null && sharePrice != null ? Math.floor(position / sharePrice) : null,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const notes = [
|
|
112
|
+
...(solve.warning ? [solve.warning] : []),
|
|
113
|
+
...(solve.fraction === 0 ? ["No positive edge over this window: every Kelly fraction sizes nothing."] : []),
|
|
114
|
+
...(bankroll == null ? [`Add --bankroll <amount> to size each fraction in ${baseCurrency}.`] : []),
|
|
115
|
+
];
|
|
116
|
+
return {
|
|
117
|
+
complete: true,
|
|
118
|
+
freshness: { asOf: inputs.end },
|
|
119
|
+
notes,
|
|
120
|
+
sections: [
|
|
121
|
+
{ title: "Inputs", entries: inputEntries(inputs, solve.expectedReturn) },
|
|
122
|
+
{
|
|
123
|
+
title: bankroll == null ? "Sizing" : `Sizing on ${formatNumber(bankroll, 0)} ${baseCurrency}`,
|
|
124
|
+
columns: [
|
|
125
|
+
{ key: "kelly", header: "Kelly" },
|
|
126
|
+
{ key: "fraction", header: "% of bankroll", align: "right", format: (value) => typeof value === "number" ? formatPct(value, 2) : "--" },
|
|
127
|
+
...(bankroll == null ? [] : [
|
|
128
|
+
{ key: "position", header: `Position ${baseCurrency}`, align: "right" as const,
|
|
129
|
+
format: (value: unknown) => typeof value === "number" ? formatNumber(value, 0) : "--" },
|
|
130
|
+
...(sharePrice == null ? [] : [{ key: "shares", header: "Shares", align: "right" as const,
|
|
131
|
+
format: (value: unknown) => typeof value === "number" ? formatNumber(value, 0) : "--" }]),
|
|
132
|
+
]),
|
|
133
|
+
],
|
|
134
|
+
rows,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
metadata: {
|
|
138
|
+
symbol,
|
|
139
|
+
inputs,
|
|
140
|
+
expectedReturn: solve.expectedReturn,
|
|
141
|
+
expectedLogGrowth: solve.expectedLogGrowth,
|
|
142
|
+
bankroll,
|
|
143
|
+
currency: baseCurrency,
|
|
144
|
+
price: price == null ? null : { value: price, currency: quote?.currency ?? null, asOf: quote?.lastUpdated ?? null },
|
|
145
|
+
method: "Binary Kelly on non-overlapping 21-session returns: win rate, mean gain and mean loss.",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PricePoint } from "../../../types/financials";
|
|
2
|
+
import type { TimeRange } from "../../../time-series/range";
|
|
3
|
+
import type { ChartRequest, InstrumentRef } from "../../../market-data/request-types";
|
|
4
|
+
import { getPricePointTimestamp } from "../../../utils/price-history";
|
|
5
|
+
|
|
6
|
+
/** Sessions in one holding period: a month of trading days. */
|
|
7
|
+
export const KELLY_HISTORY_PERIOD_SESSIONS = 21;
|
|
8
|
+
export const KELLY_HISTORY_DEFAULT_YEARS = 5;
|
|
9
|
+
export const KELLY_HISTORY_MAX_YEARS = 5;
|
|
10
|
+
/** Fewer monthly returns than this say too little about a win rate to size on. */
|
|
11
|
+
export const KELLY_HISTORY_MIN_PERIODS = 6;
|
|
12
|
+
|
|
13
|
+
const YEAR_MS = 365.25 * 24 * 60 * 60_000;
|
|
14
|
+
|
|
15
|
+
/** The binary inputs a ticker's own monthly returns imply over a lookback window. */
|
|
16
|
+
export interface KellyHistoryInputs {
|
|
17
|
+
winProbability: number;
|
|
18
|
+
/** Mean return of the up months, as a fraction. */
|
|
19
|
+
upsideReturn: number;
|
|
20
|
+
/** Mean return of the down months, as a negative fraction (0 when none fell). */
|
|
21
|
+
downsideReturn: number;
|
|
22
|
+
/** Up months over all months. */
|
|
23
|
+
wins: number;
|
|
24
|
+
periods: number;
|
|
25
|
+
/** Close the first return starts from, and the last bar, as YYYY-MM-DD. */
|
|
26
|
+
start: string;
|
|
27
|
+
end: string;
|
|
28
|
+
years: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function clampKellyHistoryYears(value: unknown): number {
|
|
32
|
+
const years = Math.round(Number(value));
|
|
33
|
+
return Number.isFinite(years) ? Math.min(KELLY_HISTORY_MAX_YEARS, Math.max(1, years)) : KELLY_HISTORY_DEFAULT_YEARS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The shortest history range that covers the lookback, so a one-year window never loads five. */
|
|
37
|
+
export function kellyHistoryRange(years: number): TimeRange {
|
|
38
|
+
return clampKellyHistoryYears(years) <= 1 ? "1Y" : "5Y";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The daily closes the History tab and the KELLY report both read. */
|
|
42
|
+
export function kellyHistoryRequest(instrument: InstrumentRef, years: number): ChartRequest {
|
|
43
|
+
return { instrument, bufferRange: kellyHistoryRange(years), granularity: "resolution", resolution: "1d" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const dateKey = (time: number) => new Date(time).toISOString().slice(0, 10);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Win rate, mean gain and mean loss of non-overlapping 21-session returns,
|
|
50
|
+
* counted back from the last daily close so the newest month is complete.
|
|
51
|
+
* Null when the window holds fewer than `KELLY_HISTORY_MIN_PERIODS` of them.
|
|
52
|
+
*/
|
|
53
|
+
export function estimateKellyHistoryInputs(points: readonly PricePoint[], yearsInput: number): KellyHistoryInputs | null {
|
|
54
|
+
const years = clampKellyHistoryYears(yearsInput);
|
|
55
|
+
const closes = points
|
|
56
|
+
.map((point) => ({ time: getPricePointTimestamp(point), close: point.close }))
|
|
57
|
+
.filter((point) => Number.isFinite(point.time) && Number.isFinite(point.close) && point.close > 0)
|
|
58
|
+
.sort((left, right) => left.time - right.time);
|
|
59
|
+
const last = closes.at(-1);
|
|
60
|
+
if (!last) return null;
|
|
61
|
+
const cutoff = last.time - years * YEAR_MS;
|
|
62
|
+
const window = closes.filter((point) => point.time >= cutoff);
|
|
63
|
+
const picks: typeof window = [];
|
|
64
|
+
for (let index = window.length - 1; index >= 0; index -= KELLY_HISTORY_PERIOD_SESSIONS) picks.unshift(window[index]!);
|
|
65
|
+
const returns = picks.slice(1).map((point, index) => point.close / picks[index]!.close - 1);
|
|
66
|
+
if (returns.length < KELLY_HISTORY_MIN_PERIODS) return null;
|
|
67
|
+
const gains = returns.filter((value) => value > 0);
|
|
68
|
+
const losses = returns.filter((value) => value < 0);
|
|
69
|
+
const mean = (values: number[]) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
70
|
+
return {
|
|
71
|
+
winProbability: gains.length / returns.length,
|
|
72
|
+
upsideReturn: mean(gains),
|
|
73
|
+
downsideReturn: mean(losses),
|
|
74
|
+
wins: gains.length,
|
|
75
|
+
periods: returns.length,
|
|
76
|
+
start: dateKey(picks[0]!.time),
|
|
77
|
+
end: dateKey(last.time),
|
|
78
|
+
years,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { PaneTemplateContext, PaneTemplateCreateOptions } from "../../../ty
|
|
|
2
2
|
import type { PluginModule } from "../plugin-module";
|
|
3
3
|
import { DEFAULT_FLOATING_SIZE, KELLY_PANE_ID } from "./constants";
|
|
4
4
|
import { KellySizerPane } from "./pane";
|
|
5
|
+
import { kellyHeadless } from "./headless";
|
|
5
6
|
|
|
6
7
|
function resolveTemplateSymbol(context: PaneTemplateContext, options?: PaneTemplateCreateOptions): string | null {
|
|
7
8
|
return options?.symbol
|
|
@@ -40,6 +41,7 @@ export const positionSizerModule: PluginModule = {
|
|
|
40
41
|
const symbol = resolveTemplateSymbol(context, options);
|
|
41
42
|
return symbol ? { params: { symbol }, placement: "floating" } : null;
|
|
42
43
|
},
|
|
44
|
+
headless: kellyHeadless,
|
|
43
45
|
},
|
|
44
46
|
],
|
|
45
47
|
};
|