gloomberb 0.19.0 → 0.19.2
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
removeTickerFromCollection,
|
|
80
80
|
tickerRemovalSummary,
|
|
81
81
|
} from "./remove-ticker";
|
|
82
|
-
import { QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
|
|
82
|
+
import { QUICK_ADD_KEY, QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
|
|
83
83
|
import { delistingLabel } from "../../../../market-data/quotes/delisting";
|
|
84
84
|
import {
|
|
85
85
|
buildTrackedCurrencies,
|
|
@@ -143,8 +143,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
143
143
|
|
|
144
144
|
const [streamWindow, setStreamWindow] = useState({ start: 0, end: 24 });
|
|
145
145
|
const [quickAddFocused, setQuickAddFocused] = useState(false);
|
|
146
|
-
// Bumped by the empty table's "+ ticker" button to open the add row.
|
|
147
|
-
const [quickAddRequest, setQuickAddRequest] = useState(0);
|
|
148
146
|
|
|
149
147
|
const {
|
|
150
148
|
cursorSymbol,
|
|
@@ -737,7 +735,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
737
735
|
return null;
|
|
738
736
|
}, [activeCollectionId, config, currentPortfolio]);
|
|
739
737
|
const showQuickAdd = !!(activeCollectionId && activeCollectionEntry && quickAddCollectionKind);
|
|
740
|
-
const requestQuickAdd = useCallback(() => setQuickAddRequest((count) => count + 1), []);
|
|
741
738
|
const quickAddHeight = showQuickAdd ? 1 : 0;
|
|
742
739
|
const selectedFinancials = cursorSymbol ? financialsMap.get(cursorSymbol) : undefined;
|
|
743
740
|
const selectedCap = liveMarketCapitalization(selectedFinancials?.quote, selectedFinancials?.fundamentals);
|
|
@@ -765,7 +762,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
765
762
|
width={width}
|
|
766
763
|
onAdded={handleTickerAdded}
|
|
767
764
|
onFocusChange={setQuickAddFocused}
|
|
768
|
-
focusRequest={quickAddRequest}
|
|
769
765
|
/>
|
|
770
766
|
) : null;
|
|
771
767
|
|
|
@@ -807,7 +803,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
807
803
|
visibleRangeBuffer={3}
|
|
808
804
|
resetScrollKey={activeCollectionId}
|
|
809
805
|
onRowActivate={handleRowActivate}
|
|
810
|
-
|
|
806
|
+
addTickerShortcut={showQuickAdd ? QUICK_ADD_KEY : undefined}
|
|
811
807
|
onOpenPerpetual={showsPerpetuals ? openPerpMarket : undefined}
|
|
812
808
|
rootHeight={contentHeight}
|
|
813
809
|
/>
|
|
@@ -112,7 +112,6 @@ export function QuickAddTickerInput({
|
|
|
112
112
|
width,
|
|
113
113
|
onAdded,
|
|
114
114
|
onFocusChange,
|
|
115
|
-
focusRequest = 0,
|
|
116
115
|
}: {
|
|
117
116
|
collectionId: string;
|
|
118
117
|
collectionKind: QuickAddCollectionKind;
|
|
@@ -121,12 +120,6 @@ export function QuickAddTickerInput({
|
|
|
121
120
|
width: number;
|
|
122
121
|
onAdded: (symbol: string) => void;
|
|
123
122
|
onFocusChange?: (focused: boolean) => void;
|
|
124
|
-
/**
|
|
125
|
-
* Bumped by a control outside the row, such as the empty table's
|
|
126
|
-
* "+ ticker" button, to open it. A click there focuses the pane in the same
|
|
127
|
-
* press, so the row opens once the pane is focused.
|
|
128
|
-
*/
|
|
129
|
-
focusRequest?: number;
|
|
130
123
|
}) {
|
|
131
124
|
const dispatch = useAppDispatch();
|
|
132
125
|
const { notify } = usePluginAppActions();
|
|
@@ -150,13 +143,6 @@ export function QuickAddTickerInput({
|
|
|
150
143
|
setInputFocused(false);
|
|
151
144
|
}, []);
|
|
152
145
|
|
|
153
|
-
const handledFocusRequestRef = useRef(focusRequest);
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (focusRequest === handledFocusRequestRef.current || !focused) return;
|
|
156
|
-
handledFocusRequestRef.current = focusRequest;
|
|
157
|
-
focusInput();
|
|
158
|
-
}, [focusInput, focusRequest, focused]);
|
|
159
|
-
|
|
160
146
|
// A symbol with no listing leaves the field, so the next one typed replaces
|
|
161
147
|
// it instead of running on (OMV.VIREP.MC); the line under it still says what
|
|
162
148
|
// failed until something is typed.
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
} from "../../../../tickers/ticker-changes";
|
|
23
23
|
|
|
24
24
|
const QUICK_ADD_MAX_QUERY_LENGTH = 32;
|
|
25
|
-
|
|
25
|
+
// A slash writes a pair as crypto exchanges and FX desks do: BTC/GBP, EUR/USD.
|
|
26
|
+
const QUICK_ADD_SYMBOL_RE = /^[A-Z0-9][A-Z0-9.\-/\s]*$/;
|
|
26
27
|
|
|
27
28
|
export type QuickAddCollectionKind = "portfolio" | "watchlist";
|
|
28
29
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
-
import { QUICK_ADD_KEY } from "./quick-add";
|
|
3
2
|
import {
|
|
4
3
|
TickerListTableView,
|
|
5
4
|
type DataTableKeyEvent,
|
|
@@ -120,7 +119,7 @@ export function PortfolioTickerTable({
|
|
|
120
119
|
visibleRangeBuffer,
|
|
121
120
|
resetScrollKey,
|
|
122
121
|
onRowActivate,
|
|
123
|
-
|
|
122
|
+
addTickerShortcut,
|
|
124
123
|
onOpenPerpetual,
|
|
125
124
|
rootHeight,
|
|
126
125
|
}: {
|
|
@@ -146,8 +145,8 @@ export function PortfolioTickerTable({
|
|
|
146
145
|
visibleRangeBuffer?: number;
|
|
147
146
|
resetScrollKey?: unknown;
|
|
148
147
|
onRowActivate?: (ticker: TickerRecord) => void;
|
|
149
|
-
/**
|
|
150
|
-
|
|
148
|
+
/** The key of the add row, which the empty table names. The pane shows the row itself, once. */
|
|
149
|
+
addTickerShortcut?: string;
|
|
151
150
|
/** Opens PERP on a row's stock perpetual: its PERP% and FUND 8H cells link to it. */
|
|
152
151
|
onOpenPerpetual?: (marketId: string) => void;
|
|
153
152
|
rootHeight?: number;
|
|
@@ -243,8 +242,7 @@ export function PortfolioTickerTable({
|
|
|
243
242
|
resetScrollKey={resetScrollKey}
|
|
244
243
|
onRowActivate={onRowActivate}
|
|
245
244
|
rowMenuAccelerators={PORTFOLIO_ROW_MENU_KEYS}
|
|
246
|
-
|
|
247
|
-
addTickerShortcut={onAddTicker ? QUICK_ADD_KEY : undefined}
|
|
245
|
+
addTickerShortcut={addTickerShortcut}
|
|
248
246
|
rootHeight={rootHeight}
|
|
249
247
|
/>
|
|
250
248
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { sourceDidNotAnswer } from "../../../api-client/errors";
|
|
2
3
|
import type { GasStorageBoard, GasStorageHistory, GasStorageRow, GasStorageSeasonal } from "../../../api-client/europe-energy";
|
|
3
4
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
4
5
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
@@ -77,7 +78,7 @@ export async function fetchGasStorage(client: Pick<GasApi, "getCloudEuropeGas">
|
|
|
77
78
|
} catch (error) {
|
|
78
79
|
throw unavailableOnServer(error, GAS_STORAGE_NOT_AVAILABLE, [404, 503]);
|
|
79
80
|
}
|
|
80
|
-
if (board.status === "unavailable") throw
|
|
81
|
+
if (board.status === "unavailable") throw sourceDidNotAnswer(GAS_STORAGE_NOT_AVAILABLE);
|
|
81
82
|
return board;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { sourceDidNotAnswer } from "../../../api-client/errors";
|
|
2
3
|
import type { CarbonBoard, CarbonHistory, EuropePowerBoard, EuropePowerHistory, PowerPriceRow } from "../../../api-client/europe-energy";
|
|
3
4
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
4
5
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
@@ -87,7 +88,7 @@ export async function fetchPowerPrices(client: Pick<PricesApi, "getCloudEuropePo
|
|
|
87
88
|
} catch (error) {
|
|
88
89
|
throw unavailableOnServer(error, POWER_PRICES_NOT_AVAILABLE, [404, 503]);
|
|
89
90
|
}
|
|
90
|
-
if (board.status === "unavailable") throw
|
|
91
|
+
if (board.status === "unavailable") throw sourceDidNotAnswer(POWER_PRICES_NOT_AVAILABLE);
|
|
91
92
|
return board;
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -110,7 +111,7 @@ export async function fetchCarbonPrice(client: Pick<PricesApi, "getCloudEuropeCa
|
|
|
110
111
|
} catch (error) {
|
|
111
112
|
throw unavailableOnServer(error, CARBON_PRICE_NOT_AVAILABLE, [404, 503]);
|
|
112
113
|
}
|
|
113
|
-
if (board.status === "unavailable") throw
|
|
114
|
+
if (board.status === "unavailable") throw sourceDidNotAnswer(CARBON_PRICE_NOT_AVAILABLE);
|
|
114
115
|
return board;
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -4,6 +4,7 @@ import type { QueryEntry } from "../../../market-data/result-types";
|
|
|
4
4
|
import type { DataProvider } from "../../../types/data-provider";
|
|
5
5
|
import type { PricePoint } from "../../../types/financials";
|
|
6
6
|
import { createSurfaceDependencies, loadVolatilitySurface, type SurfaceLoaderDependencies } from "../vol-surface/client";
|
|
7
|
+
import type { SurfaceSnapshot } from "../vol-surface/model";
|
|
7
8
|
import { projectCurrentAtmIv, type CurrentAtmIvSnapshot } from "./model";
|
|
8
9
|
import { abortable, abortError } from "../../../utils/async-deadline";
|
|
9
10
|
import { errorMessage } from "../../../utils/errors";
|
|
@@ -70,7 +71,9 @@ export interface CurrentAtmIvRequest extends RealizedHistoryRequest {
|
|
|
70
71
|
* Re-read the current reference's own expiry at the live spot: that one chain
|
|
71
72
|
* is refetched and refitted, the catalogue comes from cache, and the other
|
|
72
73
|
* slices of the full load are not touched. When the refreshed slice cannot
|
|
73
|
-
* price an ATM level, the
|
|
74
|
+
* price an ATM level, the refresh rejects with why ("ATM IV 31d not
|
|
75
|
+
* refreshed: ..."): the dated reference already shown stays, and the pane
|
|
76
|
+
* warns that it is not moving.
|
|
74
77
|
*/
|
|
75
78
|
export async function refreshCurrentAtmIv(
|
|
76
79
|
request: CurrentAtmIvRequest,
|
|
@@ -80,14 +83,19 @@ export async function refreshCurrentAtmIv(
|
|
|
80
83
|
const reference = previous.reference;
|
|
81
84
|
if (!reference || !(request.spot > 0) || !Number.isFinite(request.spot)) return previous;
|
|
82
85
|
if (request.signal?.aborted) throw abortError(CANCELLED);
|
|
86
|
+
const notRefreshed = (reason: string) => new Error(`${reference.label} not refreshed: ${reason}`);
|
|
87
|
+
let surface: SurfaceSnapshot;
|
|
83
88
|
try {
|
|
84
|
-
|
|
85
|
-
settings: { ivSource: reference.ivSource } }, dependencies)
|
|
86
|
-
return next.reference?.expiration === reference.expiration ? { ...previous, reference: next.reference } : previous;
|
|
89
|
+
surface = await loadVolatilitySurface({ ...request, forceRefresh: true, expiries: [reference.expiration],
|
|
90
|
+
settings: { ivSource: reference.ivSource } }, dependencies);
|
|
87
91
|
} catch (error) {
|
|
88
92
|
if (request.signal?.aborted || (error instanceof Error && error.name === "AbortError")) throw abortError(CANCELLED);
|
|
89
|
-
|
|
93
|
+
throw notRefreshed(errorMessage(error));
|
|
90
94
|
}
|
|
95
|
+
const next = projectCurrentAtmIv(surface);
|
|
96
|
+
if (next.reference?.expiration === reference.expiration) return { ...previous, reference: next.reference };
|
|
97
|
+
const reasons = [...new Set(surface.failures.map((failure) => failure.message))];
|
|
98
|
+
throw notRefreshed(reasons.join("; ") || "no clean quote at the money");
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/** Options are independent of daily history and use the existing surface chain cache. */
|
|
@@ -19,9 +19,18 @@ export interface RealizedVolEvidenceStatus {
|
|
|
19
19
|
source: string | null;
|
|
20
20
|
asOf: string | null;
|
|
21
21
|
errors: string[];
|
|
22
|
+
/** The errors that are the current IV's alone: a chart missing only its IV point names that, not the symbol. */
|
|
23
|
+
ivErrors?: string[];
|
|
24
|
+
/** The windows the history is too short for: their series have no values, and the capture is whole without them. */
|
|
25
|
+
shortWindows?: number[];
|
|
22
26
|
currentIv: { value: number; date: string; label: string; source: string | null; expiration: number } | null;
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
/** A series a short history leaves without values: a selected window it is too short for. */
|
|
30
|
+
function shortSeries(id: string, shortWindows: readonly number[] | undefined): boolean {
|
|
31
|
+
return /^hv-\d+$/.test(id) && (shortWindows ?? []).includes(Number(id.slice(3)));
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
export interface RealizedVolEvidence extends RealizedVolEvidenceStatus {
|
|
26
35
|
kind: "realized-volatility";
|
|
27
36
|
version: 1;
|
|
@@ -38,7 +47,8 @@ export function realizedVolSemanticEvidence(series: readonly ResolvedSeries[], s
|
|
|
38
47
|
const plottedValueCount = plotted.reduce((sum, entry) => sum + entry.points.filter((point) => isFiniteNumber(point.value)).length, 0);
|
|
39
48
|
return { ...status, kind: "realized-volatility", version: 1, series: plotted, plottedValueCount,
|
|
40
49
|
complete: !status.loading && !status.stale && status.errors.length === 0 && !!status.asOf
|
|
41
|
-
&& plotted.length > 0 && plotted.
|
|
50
|
+
&& plotted.length > 0 && plotted.some((entry) => entry.points.some((point) => isFiniteNumber(point.value)))
|
|
51
|
+
&& plotted.every((entry) => shortSeries(entry.id, status.shortWindows) || entry.points.some((point) => isFiniteNumber(point.value)))
|
|
42
52
|
&& (!status.showIv || status.currentIv != null),
|
|
43
53
|
};
|
|
44
54
|
}
|
|
@@ -56,12 +66,15 @@ function readRealizedVolEvidence(value: unknown): RealizedVolEvidence | null {
|
|
|
56
66
|
|| !isFiniteNumber(value.lookbackYears) || typeof value.showIv !== "boolean" || typeof value.complete !== "boolean"
|
|
57
67
|
|| typeof value.loading !== "boolean" || typeof value.stale !== "boolean" || !isDateString(value.asOf)
|
|
58
68
|
|| !isStringArray(value.errors)
|
|
69
|
+
|| (value.shortWindows !== undefined && (!Array.isArray(value.shortWindows) || !value.shortWindows.every(isFiniteNumber)))
|
|
70
|
+
|| (value.ivErrors !== undefined && !isStringArray(value.ivErrors))
|
|
59
71
|
|| !Array.isArray(value.series) || !value.series.every((entry) => isRecord(entry) && typeof entry.id === "string"
|
|
60
72
|
&& typeof entry.unit === "string" && Array.isArray(entry.points) && entry.points.every((point) => isRecord(point)
|
|
61
73
|
&& isDateString(point.date) && (point.value === null || isFiniteNumber(point.value))))) return null;
|
|
62
74
|
const evidence = value as unknown as RealizedVolEvidence;
|
|
75
|
+
// The IV point is drawn only when there is one: a chart without it matches its data, and is incomplete for it.
|
|
63
76
|
const required = evidence.view === "graph" ? [...evidence.windows.map((window) => `hv-${window}`), "price",
|
|
64
|
-
...(evidence.showIv ? ["current-iv"] : [])] : ["min", "max", "mean", "current"];
|
|
77
|
+
...(evidence.showIv && evidence.currentIv != null ? ["current-iv"] : [])] : ["min", "max", "mean", "current"];
|
|
65
78
|
if (!required.length || (evidence.view === "graph" && !evidence.windows.length)
|
|
66
79
|
|| evidence.series.length !== required.length || new Set(evidence.series.map((entry) => entry.id)).size !== required.length
|
|
67
80
|
|| required.some((id) => !evidence.series.some((entry) => entry.id === id))) return null;
|
|
@@ -75,10 +88,19 @@ function readRealizedVolEvidence(value: unknown): RealizedVolEvidence | null {
|
|
|
75
88
|
}
|
|
76
89
|
if (evidence.complete && (evidence.loading || evidence.stale || evidence.errors.length > 0
|
|
77
90
|
|| (evidence.showIv && !evidence.currentIv)
|
|
78
|
-
|| evidence.series.some((entry) => !entry.points.some((point) => isFiniteNumber(point.value))))) return null;
|
|
91
|
+
|| evidence.series.some((entry) => !shortSeries(entry.id, evidence.shortWindows) && !entry.points.some((point) => isFiniteNumber(point.value))))) return null;
|
|
79
92
|
return evidence;
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
/** Drawn, current and whole but for the current IV, whose errors are the only ones. */
|
|
96
|
+
function missingOnlyIv(evidence: RealizedVolEvidence): boolean {
|
|
97
|
+
const ivErrors = new Set(evidence.ivErrors ?? []);
|
|
98
|
+
return evidence.view === "graph" && evidence.showIv && evidence.currentIv == null
|
|
99
|
+
&& !evidence.loading && !evidence.stale && evidence.plottedValueCount > 0
|
|
100
|
+
&& evidence.errors.every((error) => ivErrors.has(error))
|
|
101
|
+
&& evidence.series.every((entry) => shortSeries(entry.id, evidence.shortWindows) || entry.points.some((point) => isFiniteNumber(point.value)));
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
export const realizedVolScreenshotEvidence: PaneScreenshotEvidenceHook<RealizedVolEvidence> = {
|
|
83
105
|
paneId: "realized-vol",
|
|
84
106
|
kind: "realized-volatility",
|
|
@@ -98,6 +120,9 @@ export const realizedVolScreenshotEvidence: PaneScreenshotEvidenceHook<RealizedV
|
|
|
98
120
|
},
|
|
99
121
|
unavailable(evidence, { resolved, payload }) {
|
|
100
122
|
const symbol = payload.financials[0]?.[0] ?? resolved.createOptions?.symbol;
|
|
101
|
-
|
|
123
|
+
if (evidence?.complete && !evidence.loading) return [];
|
|
124
|
+
// The volatility drawn whole and only the IV point missing: the capture names the IV, not the symbol.
|
|
125
|
+
if (evidence && missingOnlyIv(evidence)) return [`${symbol ?? evidence.symbol} current ATM IV`];
|
|
126
|
+
return [symbol ?? "realized volatility"];
|
|
102
127
|
},
|
|
103
128
|
};
|
|
@@ -3,7 +3,7 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
|
3
3
|
import { createSurfaceDependencies } from "../vol-surface/client";
|
|
4
4
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
5
5
|
import { createRealizedVolatilityDependencies, loadCurrentAtmIv, loadRealizedVolatilityHistory } from "./client";
|
|
6
|
-
import { projectRealizedVolatility, type CurrentAtmIvSnapshot } from "./model";
|
|
6
|
+
import { atmIvObservedLabel, projectRealizedVolatility, type CurrentAtmIvSnapshot } from "./model";
|
|
7
7
|
import { ESTIMATOR_OPTIONS, selectedWindows } from "./settings";
|
|
8
8
|
import { DAILY_CLOSES } from "../shared/report-freshness";
|
|
9
9
|
import { listingAbroadVenue } from "../../../utils/sec";
|
|
@@ -69,13 +69,17 @@ export function realizedVolHeadless(initialView: "graph" | "cone"): HeadlessPane
|
|
|
69
69
|
formatted: iv?.reference ? new Date(iv.reference.expiration * 1000).toISOString().slice(0, 10) : "--" },
|
|
70
70
|
{ label: "Days", value: iv?.reference?.daysToExpiry ?? null,
|
|
71
71
|
formatted: iv?.reference ? String(Math.round(iv.reference.daysToExpiry)) : "--" },
|
|
72
|
-
|
|
72
|
+
// To the minute, as the pane reads it; the JSON keeps the chain's read time whole.
|
|
73
|
+
{ label: "Observed", value: iv?.reference?.date.toISOString() ?? null,
|
|
74
|
+
formatted: iv?.reference ? atmIvObservedLabel(iv.reference.date) : "--" },
|
|
73
75
|
] },
|
|
74
76
|
],
|
|
75
|
-
|
|
77
|
+
// A short history is whole: the windows it is too short for have no estimate yet, and the note says when it starts.
|
|
78
|
+
complete: model.cone.every((row) => row.current != null || model.shortWindows.includes(row.window))
|
|
79
|
+
&& model.warnings.length === 0 && !history.stale && errors.length === 0
|
|
76
80
|
&& (!args.options.showIv || iv?.reference != null || iv?.noOptionChain === true),
|
|
77
81
|
unavailableSymbols: model.history.length ? [] : [instrument.symbol], errors,
|
|
78
|
-
notes: [barsPerYearLabel(model.barsPerYear)],
|
|
82
|
+
notes: [barsPerYearLabel(model.barsPerYear), ...model.historyNote ? [model.historyNote] : []],
|
|
79
83
|
metadata: { unit: "decimal annualized volatility", annualization: model.barsPerYear, model, currentIv: iv,
|
|
80
84
|
...(abroad && args.options.showIv ? { notices: [`Options data covers US listings only, so ${args.symbols[0]} has no current IV.`] } : {}),
|
|
81
85
|
source: history.source, stale: history.stale, fetchedAt: history.fetchedAt,
|
|
@@ -19,6 +19,24 @@ export interface RealizedVolatilityModel {
|
|
|
19
19
|
/** What the volatility is annualised at, read from the series' own dates. */
|
|
20
20
|
barsPerYear: BarsPerYear;
|
|
21
21
|
warnings: string[];
|
|
22
|
+
/**
|
|
23
|
+
* The windows the history is too short for, longest last: a stock that
|
|
24
|
+
* listed in May has no 180- or 260-bar estimate yet, and nothing failed.
|
|
25
|
+
*/
|
|
26
|
+
shortWindows: number[];
|
|
27
|
+
/**
|
|
28
|
+
* What a short history leaves out, said once: "History starts 2026-05-07: 121
|
|
29
|
+
* daily bars so far; the 180- and 260-bar windows need more". Null when the
|
|
30
|
+
* history covers the lookback and every window.
|
|
31
|
+
*/
|
|
32
|
+
historyNote: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** "the 180-bar window", "the 180- and 260-bar windows", "the 90-, 180- and 260-bar windows". */
|
|
36
|
+
function windowsText(windows: readonly number[]): string {
|
|
37
|
+
if (windows.length === 1) return `the ${windows[0]}-bar window`;
|
|
38
|
+
const head = windows.slice(0, -1).map((window) => `${window}-`);
|
|
39
|
+
return `the ${head.join(", ")} and ${windows.at(-1)}-bar windows`;
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
export interface RealizedVolatilityProjectionOptions {
|
|
@@ -45,6 +63,10 @@ export function projectRealizedVolatility(
|
|
|
45
63
|
const windows = [...new Set(options.windows ?? REALIZED_VOLATILITY_WINDOWS)]
|
|
46
64
|
.filter((window) => REALIZED_VOLATILITY_WINDOWS.some((standard) => standard === window));
|
|
47
65
|
const warnings: string[] = [];
|
|
66
|
+
// What follows from a history shorter than the windows and lookback asked for: notes, not failures.
|
|
67
|
+
let shortLookback = false;
|
|
68
|
+
let shortWarmup = false;
|
|
69
|
+
const shortWindows: number[] = [];
|
|
48
70
|
const dates = new Map<number, PricePoint>();
|
|
49
71
|
for (const point of points) {
|
|
50
72
|
const date = new Date(point.date);
|
|
@@ -66,24 +88,28 @@ export function projectRealizedVolatility(
|
|
|
66
88
|
const cone = volatilityCone(cadenceIssue ? [] : history, { estimator, lookbackYears });
|
|
67
89
|
const visible = history.filter((point) => point.date.getTime() > cutoff);
|
|
68
90
|
const visibleSeries = series.filter((point) => point.date.getTime() > cutoff);
|
|
69
|
-
if (history.length > 0 && history[0]!.date.getTime() > cutoff)
|
|
70
|
-
warnings.push(`History covers less than the requested ${lookbackYears}Y lookback`);
|
|
71
|
-
}
|
|
91
|
+
if (history.length > 0 && history[0]!.date.getTime() > cutoff) shortLookback = true;
|
|
72
92
|
for (const window of history.length > 0 && !cadenceIssue ? REALIZED_VOLATILITY_WINDOWS : []) {
|
|
73
93
|
const result = realizedVolatilityResult(history, window, estimator);
|
|
74
|
-
if (result.reason === "insufficient-history")
|
|
94
|
+
if (result.reason === "insufficient-history") shortWindows.push(window);
|
|
75
95
|
else if (result.reason) warnings.push(`HV${window} unavailable: ${result.reason.replaceAll("-", " ")}`);
|
|
76
96
|
}
|
|
77
97
|
const priorWarmup = history.filter((point) => point.date.getTime() <= cutoff).length;
|
|
78
98
|
const requiredWarmup = estimator === "close-to-close" || estimator === "yang-zhang" ? 260 : 259;
|
|
79
99
|
if (history.length > 0 && !cadenceIssue && priorWarmup < requiredWarmup) {
|
|
80
|
-
|
|
100
|
+
shortWarmup = true;
|
|
81
101
|
} else if (!cadenceIssue && cone.some((row) => row.sampleSize < visible.length)) {
|
|
82
102
|
warnings.push("Unavailable historical estimates reduce volatility-cone sample coverage");
|
|
83
103
|
}
|
|
104
|
+
const first = history[0]?.date.toISOString().slice(0, 10);
|
|
105
|
+
// Where the history starts, never read as the listing date: a source's first bar is not proof of when it listed.
|
|
106
|
+
const historyNote = !first || (!shortLookback && !shortWarmup && !shortWindows.length) ? null
|
|
107
|
+
: `History starts ${first}: ${history.length} daily bars so far; ${shortWindows.length
|
|
108
|
+
? `${windowsText(shortWindows)} need${shortWindows.length === 1 ? "s" : ""} more`
|
|
109
|
+
: "the long windows' cone covers part of the lookback"}`;
|
|
84
110
|
return { symbol: options.symbol.trim().toUpperCase(), estimator, lookbackYears, windows,
|
|
85
111
|
history: visible, series: visibleSeries, cone, asOf, barsPerYear: barsPerYear(history.map((point) => point.date)),
|
|
86
|
-
warnings: [...new Set(warnings)] };
|
|
112
|
+
warnings: [...new Set(warnings)], shortWindows, historyNote };
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
interface CurrentAtmIvReference {
|
|
@@ -107,6 +133,11 @@ export interface CurrentAtmIvSnapshot {
|
|
|
107
133
|
noOptionChain?: boolean;
|
|
108
134
|
}
|
|
109
135
|
|
|
136
|
+
/** When an IV was observed, to the minute in UTC (`2026-10-27 20:14 UTC`), in the pane and the report alike. */
|
|
137
|
+
export function atmIvObservedLabel(date: Date): string {
|
|
138
|
+
return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
139
|
+
}
|
|
140
|
+
|
|
110
141
|
/** A LEAPS ATM IV is not comparable with a 10 to 260 session realized cone. */
|
|
111
142
|
const MAX_CURRENT_ATM_IV_DAYS = 90;
|
|
112
143
|
|
|
@@ -18,7 +18,7 @@ import { useLiveSessionRefresh } from "../shared/volatility/live-session";
|
|
|
18
18
|
import type { RealizedVolatilityEstimator, VolatilityConeStatistics } from "../shared/volatility";
|
|
19
19
|
import { loadCurrentAtmIv, loadRealizedVolatilityHistory, refreshCurrentAtmIv } from "./client";
|
|
20
20
|
import type { CurrentAtmIvSnapshot } from "./model";
|
|
21
|
-
import { projectRealizedVolatility } from "./model";
|
|
21
|
+
import { atmIvObservedLabel, projectRealizedVolatility } from "./model";
|
|
22
22
|
import { RealizedVolGraph, VolatilityConeChart } from "./charts";
|
|
23
23
|
import { coneWindowDate } from "./chart-model";
|
|
24
24
|
import type { RealizedVolEvidenceStatus } from "./evidence";
|
|
@@ -118,7 +118,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
|
|
|
118
118
|
priceUnit: priceSeriesUnit(quote?.currency ?? "", quote?.instrumentType), iv: showIv ? iv.data?.reference ?? null : null } : null,
|
|
119
119
|
[model, quote?.currency, quote?.instrumentType, showIv, iv.data]);
|
|
120
120
|
const notices = [identityError, history.error, history.data?.error, iv.error, ivRefresh.error, iv.data?.error,
|
|
121
|
-
...(model?.warnings ?? []), ...(showIv ? iv.data?.warnings ?? [] : []),
|
|
121
|
+
...(model?.warnings ?? []), model?.historyNote, ...(showIv ? iv.data?.warnings ?? [] : []),
|
|
122
122
|
...(showIv && symbol && !spotAvailable ? ["Current ATM IV unavailable: underlying quote is missing or stale"] : []),
|
|
123
123
|
].filter((value): value is string => !!value);
|
|
124
124
|
usePaneNoticeFooter({ registrationId: "realized-vol-notices", notices: [...new Set(notices)], focused });
|
|
@@ -154,15 +154,17 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
|
|
|
154
154
|
role: "report-notice", label: "Chart window",
|
|
155
155
|
getMetadata: () => ({ text: `Chart legend: ${legendWindow}-bar window`, key: "legendWindow", value: legendWindow }),
|
|
156
156
|
} : null);
|
|
157
|
+
const ivErrors = showIv ? [...new Set([iv.error, ivRefresh.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null]
|
|
158
|
+
.filter((value): value is string => !!value))] : [];
|
|
157
159
|
const evidence: RealizedVolEvidenceStatus = {
|
|
158
160
|
symbol: model?.symbol ?? symbol ?? "", view: view === "cone" ? "cone" : "graph", estimator, legendWindow,
|
|
159
|
-
windows, lookbackYears: Number(lookback) === 2 ? 2 : 1, showIv,
|
|
161
|
+
windows, lookbackYears: Number(lookback) === 2 ? 2 : 1, showIv, shortWindows: model?.shortWindows ?? [],
|
|
160
162
|
loading: history.loading || (showIv && iv.loading),
|
|
161
163
|
stale: history.data?.stale ?? false, source: history.data?.source ?? null,
|
|
162
164
|
asOf: model?.asOf?.toISOString() ?? null,
|
|
163
|
-
errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []),
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []), ...ivErrors]
|
|
166
|
+
.filter((value): value is string => !!value))],
|
|
167
|
+
ivErrors,
|
|
166
168
|
currentIv: showIv && iv.data?.reference ? { value: iv.data.reference.value * 100,
|
|
167
169
|
date: iv.data.reference.date.toISOString(), label: iv.data.reference.label,
|
|
168
170
|
source: iv.data.reference.source, expiration: iv.data.reference.expiration } : null,
|
|
@@ -183,7 +185,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
|
|
|
183
185
|
<QueryBar width={width} filters={[
|
|
184
186
|
{ id: "estimator", label: "Estimator", value: estimator, options: ESTIMATOR_OPTIONS, onChange: (value: string) => setEstimator(value as RealizedVolatilityEstimator) },
|
|
185
187
|
{ id: "lookback", label: "Lookback", value: String(lookback), options: [{ value: "1", label: "1Y" }, { value: "2", label: "2Y" }], onChange: setLookback },
|
|
186
|
-
]} meta={reference ? `ATM IV observed ${reference.date
|
|
188
|
+
]} meta={reference ? `ATM IV observed ${atmIvObservedLabel(reference.date)}` : "Annualized %"} />
|
|
187
189
|
{!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="realized volatility" loading={history.loading && !model}
|
|
188
190
|
error={!model ? history.error ?? identityError ?? null : null} empty={!!model && !model.history.length}>
|
|
189
191
|
{model && view === "cone" ? <>
|
|
@@ -422,7 +422,8 @@ export function buildEventRows(
|
|
|
422
422
|
date: dividend.exDate,
|
|
423
423
|
status: "Dividend",
|
|
424
424
|
period: "-",
|
|
425
|
-
|
|
425
|
+
// A one-off beside the regular payments says so: "Declared special, payable 2026-11-25".
|
|
426
|
+
detail: `Declared${dividend.special ? " special" : ""}${dividend.payDate ? `, payable ${dividend.payDate}` : ""}`,
|
|
426
427
|
value: formatDistributionAmount(dividend.amount, data?.currency ?? currency),
|
|
427
428
|
tone: "positive",
|
|
428
429
|
});
|
|
@@ -480,9 +481,14 @@ export function eventSourceNotice(state: EventSourceState): EventSourceNotice |
|
|
|
480
481
|
const unavailableSections = Object.entries(state.actions?.coverage ?? {})
|
|
481
482
|
.filter(([section, status]) => status === "unavailable" && (state.variant !== "earnings-estimates" || section === "earnings"))
|
|
482
483
|
.map(([section]) => section);
|
|
484
|
+
// Sections and estimates that a source failed to give share one line: "Unavailable: earnings, analyst estimates (the source did not answer)".
|
|
485
|
+
const estimatesCode = !!state.estimatesError && REASON_CODE.test(state.estimatesError.trim());
|
|
486
|
+
const sharedLine = !state.actionsError && unavailableSections.length > 0 && estimatesCode;
|
|
483
487
|
|
|
484
488
|
if (state.actionsError) {
|
|
485
|
-
notices.push(`${actionsLabel} unavailable: ${state.actionsError}`);
|
|
489
|
+
notices.push(`${actionsLabel} unavailable: ${sourceErrorText(state.actionsError)}`);
|
|
490
|
+
} else if (sharedLine) {
|
|
491
|
+
notices.push(`Unavailable: ${unavailableSections.join(", ")}, analyst estimates (${sourceErrorText(state.estimatesError!)})`);
|
|
486
492
|
} else if (unavailableSections.length > 0) {
|
|
487
493
|
notices.push(`Unavailable: ${unavailableSections.join(", ")}`);
|
|
488
494
|
} else if (state.actions && !(state.variant === "earnings-estimates"
|
|
@@ -497,7 +503,7 @@ export function eventSourceNotice(state: EventSourceState): EventSourceNotice |
|
|
|
497
503
|
if (state.estimates?.stale) notices.push(`Analyst estimates stale${state.estimates.fetchedAt ? ` (fetched ${state.estimates.fetchedAt})` : ""}`);
|
|
498
504
|
|
|
499
505
|
if (state.estimatesError) {
|
|
500
|
-
notices.push(`Analyst estimates unavailable: ${state.estimatesError}`);
|
|
506
|
+
if (!sharedLine) notices.push(`Analyst estimates unavailable: ${sourceErrorText(state.estimatesError)}`);
|
|
501
507
|
} else if (state.estimates && !hasAnalystEstimates(state.estimates)) {
|
|
502
508
|
notices.push(`No analyst estimates for ${state.symbol}`);
|
|
503
509
|
}
|
|
@@ -509,8 +515,28 @@ export function eventSourceNotice(state: EventSourceState): EventSourceNotice |
|
|
|
509
515
|
};
|
|
510
516
|
}
|
|
511
517
|
|
|
518
|
+
/** A data service's reason code (UPSTREAM_ERROR) rather than words. */
|
|
519
|
+
const REASON_CODE = /^[A-Z][A-Z0-9_]+$/;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A source's failure in words: a reason code reads as what happened
|
|
523
|
+
* ("UPSTREAM_ERROR" is "the source did not answer"); a sentence stands.
|
|
524
|
+
*/
|
|
525
|
+
export function sourceErrorText(error: string): string {
|
|
526
|
+
const text = error.trim();
|
|
527
|
+
if (!REASON_CODE.test(text)) return text;
|
|
528
|
+
switch (text) {
|
|
529
|
+
case "TIMEOUT": return "the source timed out";
|
|
530
|
+
case "NOT_FOUND":
|
|
531
|
+
case "NO_DATA": return "the source has none for this symbol";
|
|
532
|
+
case "RATE_LIMITED": return "the source is busy, try again shortly";
|
|
533
|
+
default: return "the source did not answer";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
512
537
|
function hasCorporateActionRows(data: CorporateActionsData): boolean {
|
|
513
|
-
|
|
538
|
+
// A declared dividend is a row too: AIOK's $3.50 special with nothing paid yet.
|
|
539
|
+
return data.dividends.length > 0 || data.splits.length > 0 || data.earnings.length > 0 || declaredDividends(data).length > 0;
|
|
514
540
|
}
|
|
515
541
|
|
|
516
542
|
function hasAnalystEstimates(data: AnalystResearchData): boolean {
|
|
@@ -6,6 +6,8 @@ import { CorporateActionsView } from "./corporate-actions-pane";
|
|
|
6
6
|
import { EquityDiagnosticView } from "./equity-diagnostic-pane";
|
|
7
7
|
import { RelativeValuationPane } from "./relative-valuation-pane";
|
|
8
8
|
import { relativeValuationHeadless } from "./relative-valuation-headless";
|
|
9
|
+
import { AUTO_PEERS_SETTING, relativeValuationTitle } from "./relative-valuation-peers";
|
|
10
|
+
import { buildRelativeValuationSettingsDef, MAX_RELATIVE_VALUATION_TICKERS } from "./relative-valuation-settings";
|
|
9
11
|
import { analystResearchHeadless } from "./analyst-headless";
|
|
10
12
|
import { eventsHeadless } from "./events-headless";
|
|
11
13
|
import { earningsEstimatesHeadless } from "./headless";
|
|
@@ -95,6 +97,7 @@ export const researchModule: PluginModule = {
|
|
|
95
97
|
defaultMode: "floating",
|
|
96
98
|
defaultFloatingSize: { width: 104, height: 24 },
|
|
97
99
|
tableExport: true,
|
|
100
|
+
settings: buildRelativeValuationSettingsDef,
|
|
98
101
|
},
|
|
99
102
|
{
|
|
100
103
|
id: "earnings-estimates",
|
|
@@ -168,14 +171,15 @@ export const researchModule: PluginModule = {
|
|
|
168
171
|
try {
|
|
169
172
|
symbols = options?.symbols?.length
|
|
170
173
|
? options.symbols
|
|
171
|
-
: parseTickerListInput(options?.arg ?? context.activeTicker ?? "",
|
|
174
|
+
: parseTickerListInput(options?.arg ?? context.activeTicker ?? "", MAX_RELATIVE_VALUATION_TICKERS);
|
|
172
175
|
} catch {
|
|
173
176
|
return null;
|
|
174
177
|
}
|
|
175
178
|
return {
|
|
176
|
-
title:
|
|
179
|
+
title: relativeValuationTitle(symbols, null),
|
|
177
180
|
placement: "floating",
|
|
178
|
-
|
|
181
|
+
// One ticker is compared with the peers `--peers` picks, which the pane finds once it opens.
|
|
182
|
+
settings: { symbols, symbolsText: formatTickerListInput(symbols), ...(symbols.length === 1 ? { [AUTO_PEERS_SETTING]: true } : {}) },
|
|
179
183
|
};
|
|
180
184
|
},
|
|
181
185
|
},
|
|
@@ -8,11 +8,10 @@ import {
|
|
|
8
8
|
RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE, RELATIVE_VALUATION_STALE_QUOTE_NOTICE, relativeValuationValues, type RelativeValuationGapKey,
|
|
9
9
|
} from "./relative-valuation-model";
|
|
10
10
|
import { paneSchemas } from "./headless-schema";
|
|
11
|
-
import { isRecord } from "../../../utils/guards";
|
|
12
|
-
import { errorMessage } from "../../../utils/errors";
|
|
13
|
-
import { notATickerSymbol } from "../../../sources/provider-errors";
|
|
14
11
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
15
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
AUTO_PEERS_SETTING, findPeers, peerMethodTitle, peerSelectionOf, PEER_SELECTION_SETTING, relativeValuationTitle, type PeerSelection,
|
|
14
|
+
} from "./relative-valuation-peers";
|
|
16
15
|
import { extraColumns, extraColumnsBasis, loadExtraColumns, parseExtraColumns } from "./relative-valuation-columns";
|
|
17
16
|
import { fundamentalsBasisParts } from "./relative-valuation-basis";
|
|
18
17
|
import { conversionNote, convertMarketCaps, loadFxLegs } from "./relative-valuation-currency";
|
|
@@ -103,33 +102,14 @@ const VALUATION_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
103
102
|
})),
|
|
104
103
|
];
|
|
105
104
|
|
|
106
|
-
/** Peers `--peers` chose, as the expansion left them in the settings or the load picked them. */
|
|
107
|
-
function peerSelectionOf(value: unknown, symbols: readonly string[]): PeerSelection | null {
|
|
108
|
-
if (!isRecord(value) || typeof value.subject !== "string" || !Array.isArray(value.peers)) return null;
|
|
109
|
-
const selection = value as unknown as PeerSelection;
|
|
110
|
-
const expected = [selection.subject, ...selection.peers];
|
|
111
|
-
return expected.length === symbols.length && expected.every((symbol, index) => symbol.toUpperCase() === symbols[index]?.toUpperCase())
|
|
112
|
-
? selection : null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
105
|
/** The peers of the one symbol `--peers` was given; several symbols are already a peer list. */
|
|
116
106
|
async function choosePeers(symbols: readonly string[], ctx: HeadlessPaneContext): Promise<PeerSelection> {
|
|
117
107
|
if (symbols.length !== 1) {
|
|
118
108
|
throw new Error(`--peers picks the peers of one symbol, such as RV ${symbols[0] ?? "DUOL"} --peers; with several symbols, leave it out.`);
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
const financials = await loadHeadlessFinancials(ctx, subject).catch((error: unknown) => {
|
|
122
|
-
ctx.signal.throwIfAborted();
|
|
123
|
-
// A word that is no ticker is said as `fn RV duolingo lrn` says it, which the CLI follows with the search to run.
|
|
124
|
-
if (notATickerSymbol(errorMessage(error))) throw error;
|
|
125
|
-
// Any other failure leaves the peers to what the symbol alone places (a curated core's member).
|
|
126
|
-
return null;
|
|
127
|
-
});
|
|
128
|
-
return pickPeers(subject, financials, ctx);
|
|
110
|
+
return findPeers(symbols[0]!, ctx);
|
|
129
111
|
}
|
|
130
112
|
|
|
131
|
-
const PEER_SELECTION_SETTING = "peerSelection";
|
|
132
|
-
|
|
133
113
|
export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
134
114
|
...paneSchemas["relative-valuation-pane"],
|
|
135
115
|
// What Ask Gloom reads. The template's "across peers" never said this is where a list of holdings' P/E is.
|
|
@@ -149,9 +129,12 @@ export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
149
129
|
return {
|
|
150
130
|
argument: [selection.subject, ...selection.peers].join(", "),
|
|
151
131
|
settings: { [PEER_SELECTION_SETTING]: selection },
|
|
152
|
-
title:
|
|
132
|
+
title: relativeValuationTitle([], selection),
|
|
153
133
|
};
|
|
154
134
|
},
|
|
135
|
+
// The app adds the peers of a pane opened on one ticker; a capture draws the tickers given, as the report does,
|
|
136
|
+
// unless --peers widened them.
|
|
137
|
+
shotState: (args) => args.options.peers === true ? {} : { settings: { [AUTO_PEERS_SETTING]: false } },
|
|
155
138
|
async load(args, ctx) {
|
|
156
139
|
// --peers picked in advance (fn and shot) reads the same peers; any other caller picks them here.
|
|
157
140
|
const preset = args.options.peers === true ? peerSelectionOf(ctx.settings?.[PEER_SELECTION_SETTING], args.symbols) : null;
|