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
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
type DataTableRootKeyContext,
|
|
14
14
|
} from "../../../components";
|
|
15
15
|
import { loadingErrorFooterInfo, usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
16
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
17
|
+
import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
|
|
16
18
|
import { usePaneInstance } from "../../../state/app/context";
|
|
17
19
|
import { colors } from "../../../theme/colors";
|
|
18
20
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -105,6 +107,9 @@ function secTypeColor(secType: string, selected: boolean): string {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/** An announced auction's Rate in the table, the word other panes use for a figure not out yet (KPI's Later actual). */
|
|
111
|
+
const RESULTS_PENDING = "Pending";
|
|
112
|
+
|
|
108
113
|
// Stable table adapter so memoized rows survive pane re-renders.
|
|
109
114
|
const auctionKey = (auction: TreasuryAuction) => auction.id;
|
|
110
115
|
|
|
@@ -119,8 +124,8 @@ function renderAuctionCell(
|
|
|
119
124
|
|
|
120
125
|
switch (column.id) {
|
|
121
126
|
case "date":
|
|
122
|
-
// Announced auctions have no results yet
|
|
123
|
-
//
|
|
127
|
+
// Announced auctions have no results yet: Rate reads Pending, the other
|
|
128
|
+
// metric cells "—", and the date is bright.
|
|
124
129
|
return {
|
|
125
130
|
text: showYear ? auction.auctionDate : formatShortDate(auction.auctionDate, { year: false, utc: true, fallback: "\u2014" }),
|
|
126
131
|
color: rowState.selected ? colors.selectedText : isPendingAuction(auction) ? colors.textBright : colors.textDim,
|
|
@@ -134,7 +139,10 @@ function renderAuctionCell(
|
|
|
134
139
|
case "term":
|
|
135
140
|
return { text: auction.securityTerm, color: selected ?? colors.text };
|
|
136
141
|
case "rate":
|
|
137
|
-
|
|
142
|
+
// An announced auction says its results are pending, as its detail does; other metric cells stay "—".
|
|
143
|
+
return isPendingAuction(auction)
|
|
144
|
+
? { text: RESULTS_PENDING, color: dimmed }
|
|
145
|
+
: { text: formatAuctionRate(auction, rateValue(auction), "—"), color: selected ?? colors.textBright };
|
|
138
146
|
case "stopOut":
|
|
139
147
|
return { text: formatBp(stopOutVsAverageBp(auction)), color: selected ?? colors.text };
|
|
140
148
|
case "btc":
|
|
@@ -202,8 +210,10 @@ export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
|
|
|
202
210
|
const auctions = resource.data?.auctions ?? NO_AUCTIONS;
|
|
203
211
|
const stale = resource.data?.stale ?? false;
|
|
204
212
|
const fetchedAt = resource.data?.fetchedAt ?? null;
|
|
213
|
+
// A refresh that failed shows the auctions an earlier read saved, and says when that copy is from.
|
|
214
|
+
const savedAt = resource.data?.savedAt ?? null;
|
|
205
215
|
// A cache that could not be refreshed arrives as a result, not a failure.
|
|
206
|
-
const error = resource.error ?? (resource.loading ? null : resource.data?.refreshError ?? null);
|
|
216
|
+
const error = resource.error ?? (resource.loading || savedAt != null ? null : resource.data?.refreshError ?? null);
|
|
207
217
|
// Once auctions have loaded, a refresh runs without a loading state.
|
|
208
218
|
const loading = resource.loading && !resource.data;
|
|
209
219
|
const [filter, setFilter] = usePluginPaneState<AuctionFilter>("activeTab", "all");
|
|
@@ -296,8 +306,9 @@ export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
|
|
|
296
306
|
|
|
297
307
|
usePaneFooter(TREASURY_AUCTIONS_PANE_ID, () => {
|
|
298
308
|
const info = loadingErrorFooterInfo(loading, error);
|
|
299
|
-
if (
|
|
300
|
-
if (
|
|
309
|
+
if (savedAt != null) info.push(...savedCopyFooterInfo([savedAt]));
|
|
310
|
+
else if (stale) info.push(STALE_SEGMENT);
|
|
311
|
+
if (fetchedAt && savedAt == null) {
|
|
301
312
|
info.push({ id: "updated", parts: [{ text: formatRelativeAge(fetchedAt), tone: "muted" }] });
|
|
302
313
|
}
|
|
303
314
|
return {
|
|
@@ -317,6 +328,7 @@ export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) {
|
|
|
317
328
|
fetchedAt,
|
|
318
329
|
focusSearch,
|
|
319
330
|
loading,
|
|
331
|
+
savedAt,
|
|
320
332
|
stale,
|
|
321
333
|
]);
|
|
322
334
|
|
|
@@ -14,6 +14,7 @@ import { buildVolatilityData, buildVolatilityTermHistory, IMPLIED_CORRELATION_RO
|
|
|
14
14
|
type VolatilitySeriesId, type VolatilitySeriesInput, type VolatilityTermHistory } from "./model";
|
|
15
15
|
import { abortable, abortError } from "../../../utils/async-deadline";
|
|
16
16
|
import { errorMessage } from "../../../utils/errors";
|
|
17
|
+
import { savedCopyAt } from "../../../data/saved-copies";
|
|
17
18
|
|
|
18
19
|
const VOLATILITY_LOAD_CONCURRENCY = 4;
|
|
19
20
|
const VOLATILITY_HISTORY_LIMIT = 400;
|
|
@@ -22,6 +23,8 @@ export interface VolatilityLoadResult {
|
|
|
22
23
|
/** The daily inputs the data was built from, so live index levels can be laid over them. */
|
|
23
24
|
inputs: VolatilityInputs;
|
|
24
25
|
stale: boolean;
|
|
26
|
+
/** The oldest copy a failed refresh left answering, by when it was saved (ms); its failure is not in `errors`. */
|
|
27
|
+
savedAt?: number;
|
|
25
28
|
errors: string[];
|
|
26
29
|
phase: "loading" | "partial" | "ready" | "error";
|
|
27
30
|
loaded: number;
|
|
@@ -76,14 +79,16 @@ const legError = (legs: ReadonlyArray<{ error?: string | null } | undefined>) =>
|
|
|
76
79
|
* The 3M/30D ratio's history as the board builds it: the board's own VIX and
|
|
77
80
|
* VIX 3M daily closes (one year, the same request and cache), FRED's
|
|
78
81
|
* republished closes from `start` on the dates those lack. When Cboe's closes
|
|
79
|
-
* fail FRED stands in, a day behind, and the warning says so
|
|
80
|
-
* from
|
|
81
|
-
*
|
|
82
|
+
* fail FRED stands in, a day behind, and the warning says so. A ratio read
|
|
83
|
+
* from closes a failed refresh left in place carries when they were saved
|
|
84
|
+
* (`savedAt`), which the chart dates as every pane does; other cached closes
|
|
85
|
+
* name whose. With neither publisher's pair it rejects with both reasons
|
|
86
|
+
* rather than return an empty history.
|
|
82
87
|
*/
|
|
83
88
|
export async function loadVolatilityTermHistory(
|
|
84
89
|
{ start, signal }: { start?: Date; signal?: AbortSignal } = {},
|
|
85
90
|
dependencies: VolatilityLoaderDependencies & { loadFredPoints?: typeof loadFredPoints } = termDependencies(),
|
|
86
|
-
): Promise<{ term: VolatilityTermHistory; warning?: string }> {
|
|
91
|
+
): Promise<{ term: VolatilityTermHistory; warning?: string; savedAt?: number }> {
|
|
87
92
|
const loadFred = dependencies.loadFredPoints ?? loadFredPoints;
|
|
88
93
|
const now = dependencies.now?.() ?? Date.now();
|
|
89
94
|
const history: NonNullable<VolatilityInputs["history"]> = {};
|
|
@@ -101,7 +106,7 @@ export async function loadVolatilityTermHistory(
|
|
|
101
106
|
try {
|
|
102
107
|
const loaded = await loadFred(seriesId, start);
|
|
103
108
|
fred[seriesId] = { observations: loaded.points, info: null, stale: loaded.points.length > 0 && loaded.stale,
|
|
104
|
-
error: loaded.refreshError ?? null };
|
|
109
|
+
error: loaded.refreshError ?? null, ...(loaded.savedAt != null && loaded.points.length ? { savedAt: loaded.savedAt } : {}) };
|
|
105
110
|
} catch (error) {
|
|
106
111
|
fred[seriesId] = { observations: [], info: null, error: errorMessage(error) };
|
|
107
112
|
}
|
|
@@ -119,12 +124,14 @@ export async function loadVolatilityTermHistory(
|
|
|
119
124
|
} else if (fredError && !term.ratioHistory.some((point) => point.publisher === "FRED")) {
|
|
120
125
|
notes.push(`FRED closes unavailable (${fredError}); showing Cboe's from ${term.ratioHistory[0]!.date}.`);
|
|
121
126
|
}
|
|
122
|
-
|
|
127
|
+
const legs = term.publisher === "FRED" ? VOLATILITY_SERIES.map(({ seriesId }) => fred[seriesId]) : [history.vix, history.vix3m];
|
|
128
|
+
const savedAt = term.stale ? oldestSavedAt(legs) : undefined;
|
|
129
|
+
if (term.stale && savedAt == null) {
|
|
123
130
|
const error = term.publisher === "FRED" ? fredError : cboeError;
|
|
124
131
|
notes.push(error ? `Refresh failed (${error}); showing cached ${term.publisher} closes.`
|
|
125
132
|
: `Showing cached ${term.publisher} closes; the source has not updated since.`);
|
|
126
133
|
}
|
|
127
|
-
return notes.length ? {
|
|
134
|
+
return { term, ...(notes.length ? { warning: notes.join(" ") } : {}), ...(savedAt != null ? { savedAt } : {}) };
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
/** A CBOE daily close as a daily bar dated at the 16:15 New York settlement. */
|
|
@@ -141,23 +148,39 @@ function validateFredData(seriesId: VolatilitySeriesId, data: FredSeriesData): F
|
|
|
141
148
|
}
|
|
142
149
|
return data;
|
|
143
150
|
}
|
|
144
|
-
function fredInput(result: Pick<FredSeriesLoadResult, "data" | "stale" | "fetchedAt"> &
|
|
151
|
+
function fredInput(result: Pick<FredSeriesLoadResult, "data" | "stale" | "fetchedAt"> & Partial<Pick<FredSeriesLoadResult, "source">>
|
|
152
|
+
& { refreshError?: string }): VolatilitySeriesInput {
|
|
153
|
+
const saved = result.source === "stale-fallback" && result.data.observations.length > 0;
|
|
145
154
|
return { observations: result.data.observations, info: result.data.info, fetchedAt: result.fetchedAt,
|
|
146
155
|
stale: result.data.observations.length > 0 && result.stale,
|
|
147
|
-
error: result.refreshError ?? (result.data.observations.length ? null : "Daily FRED observations unavailable")
|
|
156
|
+
error: result.refreshError ?? (result.data.observations.length ? null : "Daily FRED observations unavailable"),
|
|
157
|
+
...(saved ? { savedAt: result.fetchedAt } : {}) };
|
|
148
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* A history answered from the copy saved before its refetch failed is one only
|
|
161
|
+
* when the router says so (its saved-copy mark, which a command's answers
|
|
162
|
+
* carry): closes kept from an earlier answer, or a fresh answer with no data,
|
|
163
|
+
* say their error instead.
|
|
164
|
+
*/
|
|
149
165
|
function historyInput(entry: QueryEntry<PricePoint[]>, now: number): VolatilityHistoryInput {
|
|
150
166
|
const history = resolveEntryValue(entry) ?? [];
|
|
167
|
+
const savedAt = history.length > 0 ? savedCopyAt(entry.history) : null;
|
|
151
168
|
return { history, source: entry.source, fetchedAt: entry.fetchedAt,
|
|
152
|
-
stale: history.length > 0 && (!!entry.error || (entry.staleAt != null && entry.staleAt <= now)),
|
|
153
|
-
error: entry.error?.message ?? (history.length ? null : "Daily index history unavailable")
|
|
169
|
+
stale: history.length > 0 && (savedAt != null || !!entry.error || (entry.staleAt != null && entry.staleAt <= now)),
|
|
170
|
+
error: entry.error?.message ?? (history.length ? null : "Daily index history unavailable"),
|
|
171
|
+
...(savedAt != null ? { savedAt } : {}) };
|
|
172
|
+
}
|
|
173
|
+
/** The oldest of `inputs`' saved copies, by when each was saved. */
|
|
174
|
+
function oldestSavedAt(inputs: ReadonlyArray<{ savedAt?: number | null } | undefined>): number | undefined {
|
|
175
|
+
const saved = inputs.flatMap((input) => input?.savedAt != null ? [input.savedAt] : []);
|
|
176
|
+
return saved.length ? Math.min(...saved) : undefined;
|
|
154
177
|
}
|
|
155
178
|
/** Seeds the first paint; the load that replaces each input settles its freshness, so cache age is not stale. */
|
|
156
179
|
function cachedInputs(dependencies: VolatilityLoaderDependencies, now: number): VolatilityInputs {
|
|
157
180
|
const inputs: VolatilityInputs = { history: {}, fred: {} };
|
|
158
181
|
for (const definition of VOLATILITY_INDICES) {
|
|
159
182
|
const entry = dependencies.getChartEntry?.(volatilityHistoryRequest(definition.symbol));
|
|
160
|
-
if (entry && resolveEntryValue(entry) != null) inputs.history![definition.id] = { ...historyInput(entry, now), stale: false };
|
|
183
|
+
if (entry && resolveEntryValue(entry) != null) inputs.history![definition.id] = { ...historyInput(entry, now), stale: false, savedAt: null };
|
|
161
184
|
}
|
|
162
185
|
for (const { seriesId } of VOLATILITY_SERIES) {
|
|
163
186
|
const cached = getCachedFredSeries(requestFor(seriesId), { allowExpired: true });
|
|
@@ -176,12 +199,16 @@ function historyError(id: string, error: string): string {
|
|
|
176
199
|
}
|
|
177
200
|
function project(inputs: VolatilityInputs, loaded: number, total: number, pending: boolean): VolatilityLoadResult {
|
|
178
201
|
const data = buildVolatilityData(inputs);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
// A saved copy answering a failed refresh says when it was saved (`savedAt`) instead of the failure.
|
|
203
|
+
const errors = [...Object.entries(inputs.history ?? {}).flatMap(([id, value]) => value.error && value.savedAt == null ? [historyError(id, value.error)] : []),
|
|
204
|
+
...Object.entries(inputs.fred ?? {}).flatMap(([id, value]) => value.error && value.savedAt == null ? [`${id}: ${value.error}`] : [])];
|
|
205
|
+
const all = [...Object.values(inputs.history ?? {}), ...Object.values(inputs.fred ?? {})];
|
|
206
|
+
const stale = all.some((value) => value.stale);
|
|
207
|
+
const savedAt = oldestSavedAt(all);
|
|
182
208
|
const phase = pending ? hasValues(data) ? "partial" : "loading" : !hasValues(data) ? "error"
|
|
183
209
|
: errors.length || stale || data.warnings.length || data.board.some((row) => row.status !== "available") ? "partial" : "ready";
|
|
184
|
-
return { data, inputs: { history: { ...inputs.history }, fred: { ...inputs.fred } }, stale,
|
|
210
|
+
return { data, inputs: { history: { ...inputs.history }, fred: { ...inputs.fred } }, stale, ...(savedAt != null ? { savedAt } : {}),
|
|
211
|
+
errors, phase, loaded, total };
|
|
185
212
|
}
|
|
186
213
|
export function getCachedVolatilityData(dependencies: VolatilityLoaderDependencies = createVolatilityDependencies()): VolatilityLoadResult | null {
|
|
187
214
|
const inputs = cachedInputs(dependencies, dependencies.now?.() ?? Date.now());
|
|
@@ -2,6 +2,7 @@ import type { HeadlessBundleResult, HeadlessPaneContext, HeadlessPaneDefinition,
|
|
|
2
2
|
import { createVolatilityDependencies, loadVolatilityData, type VolatilityLoadResult } from "./client";
|
|
3
3
|
import { termStateLabel, type VolatilityData } from "./model";
|
|
4
4
|
import { newestReportTime, oldestReportTime } from "../../../utils/utc-time";
|
|
5
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
5
6
|
|
|
6
7
|
function formattedValue(value: unknown): string { return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "--"; }
|
|
7
8
|
function formattedPercentile(value: unknown): string { return typeof value === "number" && Number.isFinite(value) ? value.toFixed(0) : "--"; }
|
|
@@ -60,6 +61,8 @@ export function createVolatilityHeadless(dependencies: VolatilityHeadlessDepende
|
|
|
60
61
|
async load(args, context) {
|
|
61
62
|
const result = await dependencies.load(args, context);
|
|
62
63
|
context.signal.throwIfAborted();
|
|
64
|
+
// A saved copy answering a failed refresh ends the report with when it was saved.
|
|
65
|
+
const savedCopy = <T>(value: T) => result.savedAt != null ? servedFromSavedCopy(value, result.savedAt) : value;
|
|
63
66
|
// Daily index closes, dated by the newest index on the board; a sparse index shows as the oldest.
|
|
64
67
|
const boardDates = result.data.board.filter((row) => row.value != null).map((row) => row.date);
|
|
65
68
|
const freshness = { source: "Cboe and FRED", status: "not-a-feed" as const, basis: "daily index closes", cadence: "daily" as const,
|
|
@@ -67,10 +70,10 @@ export function createVolatilityHeadless(dependencies: VolatilityHeadlessDepende
|
|
|
67
70
|
return { ...projectVolatilityHeadless(result.data), freshness, errors: result.errors,
|
|
68
71
|
complete: result.phase === "ready" && !result.stale && result.data.warnings.length === 0,
|
|
69
72
|
unavailableSymbols: result.data.board.filter((row) => row.value == null).map((row) => row.symbol),
|
|
70
|
-
metadata: { stale: result.stale, phase: result.phase, loaded: result.loaded, total: result.total,
|
|
73
|
+
metadata: savedCopy({ stale: result.stale, phase: result.phase, loaded: result.loaded, total: result.total,
|
|
71
74
|
data: result.data, unit: "index points", observations: "daily history or isolated index observation",
|
|
72
75
|
percentile: "Midrank within last calendar year; at least 200 observations spanning 300 days",
|
|
73
|
-
vixFuturesAvailable: false, methodology: "docs/research-data.md#vix-curve-and-cross-asset-volatility-board" },
|
|
76
|
+
vixFuturesAvailable: false, methodology: "docs/research-data.md#vix-curve-and-cross-asset-volatility-board" }),
|
|
74
77
|
};
|
|
75
78
|
},
|
|
76
79
|
};
|
|
@@ -30,6 +30,11 @@ export interface VolatilityHistoryInput {
|
|
|
30
30
|
stale?: boolean;
|
|
31
31
|
error?: string | null;
|
|
32
32
|
fetchedAt?: number | null;
|
|
33
|
+
/**
|
|
34
|
+
* When the copy shown was saved, when a failed refresh left it answering:
|
|
35
|
+
* the pane's footer says "saved copy from …" instead of the failure.
|
|
36
|
+
*/
|
|
37
|
+
savedAt?: number | null;
|
|
33
38
|
}
|
|
34
39
|
export interface VolatilitySeriesInput {
|
|
35
40
|
observations: CloudFredObservationPayload[];
|
|
@@ -37,6 +42,8 @@ export interface VolatilitySeriesInput {
|
|
|
37
42
|
stale?: boolean;
|
|
38
43
|
error?: string | null;
|
|
39
44
|
fetchedAt?: number | null;
|
|
45
|
+
/** As `VolatilityHistoryInput.savedAt`. */
|
|
46
|
+
savedAt?: number | null;
|
|
40
47
|
}
|
|
41
48
|
export interface VolatilityInputs {
|
|
42
49
|
history?: Partial<Record<VolatilityIndexId, VolatilityHistoryInput>>;
|
|
@@ -131,6 +138,8 @@ export interface VolatilityBoardRow {
|
|
|
131
138
|
status: "available" | "limited" | "unavailable";
|
|
132
139
|
stale: boolean;
|
|
133
140
|
error: string | null;
|
|
141
|
+
/** When the copy shown was saved, when a failed refresh left it answering. */
|
|
142
|
+
savedAt?: number;
|
|
134
143
|
warnings: string[];
|
|
135
144
|
}
|
|
136
145
|
export interface VolatilityData {
|
|
@@ -233,8 +242,9 @@ function fredHistory(inputs: VolatilityInputs["fred"]): FredVolatilityHistory {
|
|
|
233
242
|
const input = inputs?.[definition.seriesId];
|
|
234
243
|
const normalized = normalizeHistory(input?.observations ?? [], true);
|
|
235
244
|
warnings.push(...normalized.warnings.map((warning) => `${definition.seriesId}: ${warning}`));
|
|
236
|
-
|
|
237
|
-
if (input?.
|
|
245
|
+
// A saved copy is dated in the footer; its failed refresh is not repeated here.
|
|
246
|
+
if (input?.error && input.savedAt == null) warnings.push(`${definition.seriesId}: ${input.error}`);
|
|
247
|
+
if (input?.stale && input.savedAt == null) warnings.push(`${definition.seriesId}: cached source is stale`);
|
|
238
248
|
const latest = normalized.history.at(-1);
|
|
239
249
|
return { ...definition, title: input?.info?.title ?? definition.label, value: latest?.value ?? null,
|
|
240
250
|
date: latest?.date ?? null, history: normalized.history,
|
|
@@ -300,15 +310,16 @@ function boardRow(definition: typeof VOLATILITY_INDICES[number], input: Volatili
|
|
|
300
310
|
if (latest && latest.date !== normalized.rows.at(-1)?.date) warnings.push("Latest supplied close unavailable; showing last valid observation");
|
|
301
311
|
if (latest && !adjacent) warnings.push("1D change unavailable: previous daily close missing or too far apart");
|
|
302
312
|
if (latest && !broadCoverage) warnings.push(`1Y percentile unavailable: ${history.length} observations across ${coverageDays} days`);
|
|
303
|
-
|
|
304
|
-
if (input?.
|
|
313
|
+
// A saved copy is dated in the footer; its failed refresh is not repeated here.
|
|
314
|
+
if (input?.stale && input.savedAt == null) warnings.push("Cached daily history is stale");
|
|
315
|
+
if (input?.error && input.savedAt == null) warnings.push(input.error);
|
|
305
316
|
return { ...definition, unit: "index points", value: latest?.value ?? null, date: latest?.date ?? null,
|
|
306
317
|
source: input?.source ?? null, previousDate: previous?.date ?? null, change1d,
|
|
307
318
|
change1dPercent: change1d != null ? change1d / previous!.value! * 100 : null,
|
|
308
319
|
percentile1y, sampleSize: history.length, coverageDays, history,
|
|
309
320
|
missingDates: normalized.rows.filter((row) => row.value == null && Date.parse(row.date) > cutoff).map((row) => row.date),
|
|
310
321
|
status: !latest ? "unavailable" : warnings.length > 0 ? "limited" : "available",
|
|
311
|
-
stale: input?.stale ?? false, error: input?.error ?? null, warnings };
|
|
322
|
+
stale: input?.stale ?? false, error: input?.error ?? null, ...(input?.savedAt != null ? { savedAt: input.savedAt } : {}), warnings };
|
|
312
323
|
}
|
|
313
324
|
function alignedCurve(board: readonly VolatilityBoardRow[], fred: FredVolatilityHistory, term: VolatilityTermHistory): VolatilityCurve {
|
|
314
325
|
const rows = VOLATILITY_CURVE_INDICES.map((definition) => board.find((row) => row.id === definition.id)!);
|
|
@@ -337,7 +348,7 @@ function alignedCurve(board: readonly VolatilityBoardRow[], fred: FredVolatility
|
|
|
337
348
|
const spot = points[1]!.value, threeMonth = points[2]!.value;
|
|
338
349
|
const warnings = points.flatMap((point, index) => [
|
|
339
350
|
...(point.value == null ? [`${point.label}: no close aligned to ${date ?? "a common date"}`] : []),
|
|
340
|
-
...(point.value != null && rows[index]!.stale ? [`${point.label}: aligned close is from stale cached history`] : []),
|
|
351
|
+
...(point.value != null && rows[index]!.stale && rows[index]!.savedAt == null ? [`${point.label}: aligned close is from stale cached history`] : []),
|
|
341
352
|
]);
|
|
342
353
|
const ratio = spot != null && threeMonth != null ? threeMonth / spot : null;
|
|
343
354
|
const context = ratioPercentile(term, ratio, date);
|
|
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, formatPercentAxis, PaneStatusBody, useChartTableSelection,
|
|
3
3
|
usePaneFooter, usePaneNoticeFooter, usePaneTabs, type DataTableColumn, type StatItem } from "../../../components";
|
|
4
4
|
import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
|
|
5
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
6
|
+
import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
|
|
5
7
|
import { dropColumnsToFit } from "../../../components/ui/table-layout";
|
|
6
8
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
7
9
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
@@ -170,10 +172,15 @@ export function VolatilityPane({ focused, width, height }: PaneProps) {
|
|
|
170
172
|
const observationBasis = intraday ? "intraday" : sparse ? "observation" : settled ? "close" : DAY_SO_FAR_TEXT;
|
|
171
173
|
usePaneFooter("volatility", () => ({ info: [
|
|
172
174
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading volatility", tone: "muted" as const }] }] : []),
|
|
173
|
-
|
|
174
|
-
...(
|
|
175
|
-
...(
|
|
176
|
-
|
|
175
|
+
// A saved copy answering a failed refresh says when it was saved; other stale closes say only that.
|
|
176
|
+
...savedCopyFooterInfo([result?.savedAt]),
|
|
177
|
+
...(result?.stale && result.savedAt == null ? [STALE_SEGMENT] : []),
|
|
178
|
+
// One fact, "close 2026-09-25": the basis and its date read as one segment.
|
|
179
|
+
...(data || asOf ? [{ id: "basis", parts: [
|
|
180
|
+
...(data ? [{ text: observationBasis, tone: "muted" as const }] : []),
|
|
181
|
+
...(asOf ? [{ text: observationTime ? `${observationTime.slice(0, 16).replace("T", " ")} UTC` : asOf, tone: "muted" as const }] : []),
|
|
182
|
+
] }] : []),
|
|
183
|
+
], hints: [{ id: "view", key: "v", label: "iew", onPress: cycleTab }] }), [resource.loading, result?.stale, result?.savedAt, data, asOf, observationTime, observationBasis, cycleTab]);
|
|
177
184
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused, dense: true });
|
|
178
185
|
const contentHeight = Math.max(5, height - tabRows);
|
|
179
186
|
// The board needs only its rows; the selected index history takes the rest.
|
|
@@ -105,11 +105,12 @@ export const VOLATILITY_SERIES: readonly SeriesDefinition[] = [
|
|
|
105
105
|
tags: ["term structure", "contango", "backwardation", "VIX"],
|
|
106
106
|
description: "VIX 3M over VIX on each date both closed, from Cboe's daily closes and FRED's before them; above 1 is contango.",
|
|
107
107
|
load: async ({ start, signal }) => {
|
|
108
|
-
const { term, warning } = await loadVolatilityTermHistory({ ...(start ? { start } : {}), ...(signal ? { signal } : {}) });
|
|
108
|
+
const { term, warning, savedAt } = await loadVolatilityTermHistory({ ...(start ? { start } : {}), ...(signal ? { signal } : {}) });
|
|
109
109
|
const attribution = termAttribution(term);
|
|
110
110
|
return {
|
|
111
111
|
points: term.ratioHistory.map((point) => ({ date: point.date, value: point.value })),
|
|
112
112
|
...(warning ? { warning } : {}),
|
|
113
|
+
...(savedAt != null ? { savedAt } : {}),
|
|
113
114
|
...(attribution ? { attribution } : {}),
|
|
114
115
|
};
|
|
115
116
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import type { SwapCurvePayload, SwapTenor } from "../../../api-client/swap-curve";
|
|
3
|
-
import { createPluginCache } from "../../../data/plugin-cache";
|
|
3
|
+
import { createPluginCache, savedCopyForTransientFailure, type PluginCacheResult } from "../../../data/plugin-cache";
|
|
4
4
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
5
5
|
import { validateSwapCurve } from "./swaps-model";
|
|
6
|
+
import { loadCurveData, type CurveData, type CurveId } from "./curves";
|
|
7
|
+
import { loadWorldRows, type WorldRow } from "./world";
|
|
6
8
|
|
|
7
9
|
export const swapCurveCache = createPluginCache<SwapCurvePayload>({
|
|
8
10
|
kind: "sofr-swap-curve", source: "gloom-cloud", schemaVersion: 1,
|
|
@@ -23,3 +25,59 @@ export async function fetchSwapCurve(tenor: SwapTenor,
|
|
|
23
25
|
export const getCachedSwapCurve = (tenor: SwapTenor) => cachedCloudResource(swapCurveCache, tenor, validateSwapCurve);
|
|
24
26
|
export const loadSwapCurve = (tenor: SwapTenor, force = false) =>
|
|
25
27
|
loadCloudResource(swapCurveCache, tenor, () => fetchSwapCurve(tenor), { force, validate: validateSwapCurve });
|
|
28
|
+
|
|
29
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The Curve and World tabs' curves as last read, so a refresh that fails
|
|
33
|
+
* (offline, a service error) shows the copy an earlier read saved instead of
|
|
34
|
+
* an empty tab. Every load still asks the server; the copy is the fallback.
|
|
35
|
+
*/
|
|
36
|
+
export const curveViewCache = createPluginCache<CurveData>({
|
|
37
|
+
kind: "yield-curve-view", source: "gloom-cloud", schemaVersion: 1,
|
|
38
|
+
policy: { staleMs: 15 * 60_000, expireMs: 30 * DAY_MS },
|
|
39
|
+
});
|
|
40
|
+
export const worldCurvesCache = createPluginCache<WorldRow[]>({
|
|
41
|
+
kind: "world-curves", source: "gloom-cloud", schemaVersion: 1,
|
|
42
|
+
policy: { staleMs: 15 * 60_000, expireMs: 30 * DAY_MS },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** What a tab shows, and when the copy shown was saved when it is one, its refresh having failed. */
|
|
46
|
+
export interface SavedCopyAnswer<T> {
|
|
47
|
+
data: T;
|
|
48
|
+
savedAt: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const curveKey = (curve: CurveId, requestedDate: string) => `${curve}:${requestedDate || "latest"}`;
|
|
52
|
+
/**
|
|
53
|
+
* A copy is the saved one by where it came from, whatever its failure said. A
|
|
54
|
+
* refusal (access denied, an unknown curve) or a reply that fails validation
|
|
55
|
+
* is said rather than covered by the copy, as `loadCloudResource` does.
|
|
56
|
+
*/
|
|
57
|
+
function answer<T>(result: PluginCacheResult<T>): SavedCopyAnswer<T> {
|
|
58
|
+
const saved = savedCopyForTransientFailure(result).source === "stale-fallback";
|
|
59
|
+
return { data: result.data, savedAt: saved ? result.fetchedAt : null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The curve on the date (its latest without one), or the copy an earlier read saved when it cannot be read. */
|
|
63
|
+
export async function loadSavedCurve(curve: CurveId, requestedDate: string,
|
|
64
|
+
client?: Parameters<typeof loadCurveData>[2]): Promise<SavedCopyAnswer<CurveData>> {
|
|
65
|
+
return answer(await curveViewCache.load(curveKey(curve, requestedDate), () => loadCurveData(curve, requestedDate, client), { force: true }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The saved curve, for the tab's first paint while it is read again. */
|
|
69
|
+
export function cachedCurve(curve: CurveId, requestedDate: string): SavedCopyAnswer<CurveData> | null {
|
|
70
|
+
const cached = curveViewCache.get(curveKey(curve, requestedDate), { allowExpired: true });
|
|
71
|
+
return cached ? { data: cached.data, savedAt: null } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Each market's latest curve, or the copy an earlier read saved when they cannot be read. */
|
|
75
|
+
export async function loadSavedWorldRows(client?: Parameters<typeof loadWorldRows>[0]): Promise<SavedCopyAnswer<WorldRow[]>> {
|
|
76
|
+
return answer(await worldCurvesCache.load("latest", () => loadWorldRows(client), { force: true }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The saved world board, for the tab's first paint while it is read again. */
|
|
80
|
+
export function cachedWorldRows(): SavedCopyAnswer<WorldRow[]> | null {
|
|
81
|
+
const cached = worldCurvesCache.get("latest", { allowExpired: true });
|
|
82
|
+
return cached ? { data: cached.data, savedAt: null } : null;
|
|
83
|
+
}
|
|
@@ -4,16 +4,17 @@ import type {
|
|
|
4
4
|
} from "../../../types/plugin";
|
|
5
5
|
import {
|
|
6
6
|
compareDate, curveCutoff, CURVE_OPTIONS, curveIdOf, curveOption, curveSpreadFigures, curveTenorMove, loadComparePoints,
|
|
7
|
-
|
|
7
|
+
parseCompareInput, parseCurveArgument, type CurveData, type CurveId,
|
|
8
8
|
} from "./curves";
|
|
9
9
|
import { yieldTenorRows, type YieldLookbackCurves } from "./chart";
|
|
10
10
|
import { curveAsOf, type YieldPoint, yieldCurveErrors } from "./treasury-data";
|
|
11
11
|
import { loadYieldCurveLookbacks, yieldCurveDate } from "./history";
|
|
12
|
-
import {
|
|
12
|
+
import type { WorldRow } from "./world";
|
|
13
13
|
import { formatBasisPoints, toBasisPoints } from "../../../utils/basis-points";
|
|
14
14
|
import { DATE_OPTION_EXAMPLES } from "../../../utils/calendar-date";
|
|
15
15
|
import { SWAP_TENORS, type SwapCurvePayload, type SwapTenor } from "../../../api-client/swap-curve";
|
|
16
|
-
import { fetchSwapCurve } from "./client";
|
|
16
|
+
import { fetchSwapCurve, loadSavedCurve, loadSavedWorldRows } from "./client";
|
|
17
|
+
import { savedCopyMark } from "../../../data/saved-copies";
|
|
17
18
|
import { formatSwapBp, selectedSwapTenor } from "./swaps-model";
|
|
18
19
|
|
|
19
20
|
const percent = (value: unknown) => value == null ? "-" : `${Number(value).toFixed(2)}%`;
|
|
@@ -87,13 +88,14 @@ export interface YieldCurveHeadlessDependencies {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const defaultDependencies: Required<YieldCurveHeadlessDependencies> = {
|
|
90
|
-
|
|
91
|
+
// A curve that cannot be read is the copy an earlier read saved, marked so the report says so.
|
|
92
|
+
load: async (curve, requestedDate, context) => (await loadSavedCurve(curve, requestedDate, context.apiClient)).data,
|
|
91
93
|
compare: (curve, date, context) => loadComparePoints(curve, date, context.apiClient),
|
|
92
94
|
lookbacks: async (session, context) => Object.fromEntries(
|
|
93
95
|
(await loadYieldCurveLookbacks(session, (id, options) => context.apiClient.getCloudFredSeries(id, options)))
|
|
94
96
|
.map((lookback) => [lookback.id, lookback.points]),
|
|
95
97
|
),
|
|
96
|
-
world: (context) =>
|
|
98
|
+
world: async (context) => (await loadSavedWorldRows(context.apiClient)).data,
|
|
97
99
|
swaps: (tenor, context) => fetchSwapCurve(tenor, context.apiClient),
|
|
98
100
|
};
|
|
99
101
|
|
|
@@ -184,7 +186,7 @@ export function createYieldCurveHeadless(
|
|
|
184
186
|
return {
|
|
185
187
|
freshness: { source: "Government bond publishers", status: "not-a-feed", basis: "daily curves" },
|
|
186
188
|
columns: WORLD_COLUMNS,
|
|
187
|
-
rows: rows.map((row) => ({ market: row.market, twoYear: row.twoYear, tenYear: row.tenYear, thirtyYear: row.thirtyYear,
|
|
189
|
+
rows: rows.map((row) => ({ ...savedCopyMark(row), market: row.market, twoYear: row.twoYear, tenYear: row.tenYear, thirtyYear: row.thirtyYear,
|
|
188
190
|
twosTensBasisPoints: bp(row.twosTens), tenYearChangeBasisPoints: bp(row.tenYearChange), asOf: row.asOf })),
|
|
189
191
|
metadata: { units: "Percent" },
|
|
190
192
|
};
|
|
@@ -244,6 +246,7 @@ export function createYieldCurveHeadless(
|
|
|
244
246
|
const day = ranked ? curveTenorMove(data, point.maturity, "1D") : null;
|
|
245
247
|
const week = ranked ? curveTenorMove(data, point.maturity, "1W") : null;
|
|
246
248
|
return {
|
|
249
|
+
...savedCopyMark(data),
|
|
247
250
|
...point,
|
|
248
251
|
change1dBasisPoints: change == null ? null : toBasisPoints(change, 1),
|
|
249
252
|
...(ranked ? {
|
|
@@ -8,6 +8,7 @@ import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve
|
|
|
8
8
|
import { curveLookbackLabel } from "../../../components/chart/curve/model";
|
|
9
9
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
10
10
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
11
|
+
import { isPermanentClientError } from "../../../api-client/errors";
|
|
11
12
|
import { usePaneSettingValue } from "../../../state/app/context";
|
|
12
13
|
import { usePluginPaneState } from "../../../public/react";
|
|
13
14
|
import { priceColor } from "../../../theme/colors";
|
|
@@ -18,6 +19,7 @@ import type { PluginModule } from "../plugin-module";
|
|
|
18
19
|
import { GOVERNMENT_BOND_SERIES, TREASURY_SERIES } from "./series";
|
|
19
20
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
20
21
|
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
22
|
+
import { savedCopyFooterSegment, type PaneFooterSegment } from "../../../components/layout/pane/footer";
|
|
21
23
|
import { formatPercentileRank } from "../../../utils/format";
|
|
22
24
|
import { SERIES_COLORS } from "../../../theme/series-colors";
|
|
23
25
|
import type { CurveSeries } from "../../../components/chart/curve";
|
|
@@ -27,20 +29,28 @@ import {
|
|
|
27
29
|
type YieldTenorRow,
|
|
28
30
|
} from "./chart";
|
|
29
31
|
import {
|
|
30
|
-
compareDate, curveCutoff, curveIdOf, CURVE_OPTIONS, curveSpreadFigures, curveTenorMove, loadComparePoints,
|
|
32
|
+
compareDate, curveCutoff, curveIdOf, CURVE_OPTIONS, curveSpreadFigures, curveTenorMove, loadComparePoints,
|
|
31
33
|
parseCompareInput, parseCurveArgument, type CurveId,
|
|
32
34
|
} from "./curves";
|
|
33
35
|
import { formatMoveRank, MOVE_HORIZONS } from "../shared/move-rank";
|
|
34
36
|
import { forwardCurve, type CurveBasis } from "./forward";
|
|
35
37
|
import { loadYieldCurveLookbacks, yieldCurveDate } from "./history";
|
|
36
38
|
import { curveAsOf, isYieldObservationDate, type YieldPoint, yieldCurveErrors } from "./treasury-data";
|
|
37
|
-
import {
|
|
39
|
+
import type { WorldRow } from "./world";
|
|
38
40
|
import { SwapsTab } from "./swaps";
|
|
39
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
cachedCurve, cachedWorldRows, curveViewCache, loadSavedCurve, loadSavedWorldRows, swapCurveCache, worldCurvesCache,
|
|
43
|
+
} from "./client";
|
|
40
44
|
|
|
41
45
|
type GcTab = "curve" | "world" | "swaps";
|
|
42
46
|
const TABS = [{ label: "Curve", value: "curve" }, { label: "World", value: "world" }, { label: "Swaps", value: "swaps" }];
|
|
43
47
|
const EMPTY_POINTS: YieldPoint[] = [];
|
|
48
|
+
|
|
49
|
+
/** A tab showing the copy an earlier read saved, its refresh having failed, says when that copy is from. */
|
|
50
|
+
function useSavedCopyInfo(savedAt: number | null | undefined): PaneFooterSegment[] | undefined {
|
|
51
|
+
return useMemo(() => savedAt != null ? [savedCopyFooterSegment("saved-copy", savedAt)] : undefined, [savedAt]);
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/** Axis word for the loaded curve's basis. */
|
|
45
55
|
const BASIS_LABEL: Record<CurveBasis, string> = {
|
|
46
56
|
par: "Par yield",
|
|
@@ -165,8 +175,14 @@ function CurveTab({ curve, onCurveChange, focused, width, height }: {
|
|
|
165
175
|
const compareField = useDraftField(compareInput);
|
|
166
176
|
const editing = dateField.active || compareField.active;
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
// The saved copy paints first and answers when a refresh fails; a failed refresh never empties the tab, unless
|
|
179
|
+
// asking again cannot help (access denied), which is said instead.
|
|
180
|
+
const loadCurve = useCallback(async () => loadSavedCurve(curve, requestedDate), [curve, requestedDate]);
|
|
181
|
+
const { data: answer, loading, error, updatedAt: lastUpdated, load } = useAsyncResource(loadCurve, {
|
|
182
|
+
initialData: () => cachedCurve(curve, requestedDate), clearOnError: isPermanentClientError,
|
|
183
|
+
});
|
|
184
|
+
const data = answer?.data ?? null;
|
|
185
|
+
const savedInfo = useSavedCopyInfo(answer?.savedAt);
|
|
170
186
|
// A pending curve or date change must never relabel the previous curve as the new one.
|
|
171
187
|
const current = data?.curve === curve && data.requestedDate === requestedDate ? data : null;
|
|
172
188
|
const points = current?.points ?? EMPTY_POINTS;
|
|
@@ -260,6 +276,7 @@ function CurveTab({ curve, onCurveChange, focused, width, height }: {
|
|
|
260
276
|
registrationId: "yield-curve",
|
|
261
277
|
loading: loading || (compareKey != null && compareResource.loading && !comparePoints),
|
|
262
278
|
error: error || sourceError || null,
|
|
279
|
+
info: savedInfo,
|
|
263
280
|
hints: [
|
|
264
281
|
{ id: "date", key: "d", label: "ate", onPress: dateField.focus },
|
|
265
282
|
{ id: "compare", key: "v", label: "s", title: "Compare with a date", onPress: compareField.focus },
|
|
@@ -413,11 +430,14 @@ function WorldTab({ onOpen, focused, width, height }: {
|
|
|
413
430
|
}) {
|
|
414
431
|
const colors = useThemeColors();
|
|
415
432
|
const [selected, setSelected] = usePluginPaneState<string | null>("yield-curve:market", null);
|
|
416
|
-
const loadWorld = useCallback(() =>
|
|
417
|
-
const { data, loading, error, updatedAt, load } = useAsyncResource(loadWorld
|
|
433
|
+
const loadWorld = useCallback(() => loadSavedWorldRows(), []);
|
|
434
|
+
const { data: answer, loading, error, updatedAt, load } = useAsyncResource(loadWorld, {
|
|
435
|
+
initialData: cachedWorldRows, clearOnError: isPermanentClientError,
|
|
436
|
+
});
|
|
437
|
+
const data = answer?.data ?? null;
|
|
418
438
|
useAutoRefresh(updatedAt, load);
|
|
419
439
|
usePaneRefreshKey(() => void load(), { focused });
|
|
420
|
-
usePaneStatusFooter({ registrationId: "yield-curve:world", loading, error: error || null });
|
|
440
|
+
usePaneStatusFooter({ registrationId: "yield-curve:world", loading, error: error || null, info: useSavedCopyInfo(answer?.savedAt) });
|
|
421
441
|
const rows = data ?? [];
|
|
422
442
|
const selectedId = rows.some((row) => row.id === selected) ? selected! : rows[0]?.id ?? null;
|
|
423
443
|
// Every market's latest curve, each in its own colour; the selected row is
|
|
@@ -465,8 +485,12 @@ function WorldTab({ onOpen, focused, width, height }: {
|
|
|
465
485
|
}
|
|
466
486
|
|
|
467
487
|
export const yieldCurveModule: PluginModule = {
|
|
468
|
-
setup(ctx) {
|
|
469
|
-
|
|
488
|
+
setup(ctx) {
|
|
489
|
+
for (const cache of [swapCurveCache, curveViewCache, worldCurvesCache]) cache.attach(ctx.persistence);
|
|
490
|
+
},
|
|
491
|
+
dispose() {
|
|
492
|
+
for (const cache of [swapCurveCache, curveViewCache, worldCurvesCache]) cache.reset();
|
|
493
|
+
},
|
|
470
494
|
series: [...TREASURY_SERIES, ...GOVERNMENT_BOND_SERIES],
|
|
471
495
|
panes: [{
|
|
472
496
|
id: "yield-curve",
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
|
|
8
8
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
9
9
|
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
10
|
+
import { savedCopyFooterSegment } from "../../../components/layout/pane/footer";
|
|
10
11
|
import { FILL_REST } from "../../../components/ui/fill";
|
|
11
|
-
import {
|
|
12
|
+
import { isPermanentClientError } from "../../../api-client/errors";
|
|
12
13
|
import { usePluginPaneState } from "../../../public/react";
|
|
13
14
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
14
15
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
@@ -37,17 +38,20 @@ export function SwapsTab({ focused, width, height }: { focused: boolean; width:
|
|
|
37
38
|
const tenor = selectedSwapTenor(selected);
|
|
38
39
|
const loader = useCallback((force: boolean) => loadSwapCurve(tenor, force), [tenor]);
|
|
39
40
|
const resource = useAsyncResource(loader, {
|
|
40
|
-
initialData: () => getCachedSwapCurve(tenor), keepPreviousData: true, clearOnError:
|
|
41
|
+
initialData: () => getCachedSwapCurve(tenor), keepPreviousData: true, clearOnError: isPermanentClientError,
|
|
41
42
|
});
|
|
42
43
|
const payload = resource.data?.payload;
|
|
43
44
|
const rows = payload?.asOf ? payload.points : EMPTY_ROWS;
|
|
44
45
|
// A new tenor keeps the curve/table in place, but never relabels the old tenor's history.
|
|
45
46
|
const history = payload?.history.tenor === tenor ? payload.history : null;
|
|
46
|
-
|
|
47
|
-
const
|
|
47
|
+
// A refresh that failed shows the copy an earlier read saved, and says when that copy is from.
|
|
48
|
+
const savedAt = resource.data?.savedAt ?? null;
|
|
49
|
+
const savedInfo = useMemo(() => savedAt != null ? [savedCopyFooterSegment("saved-copy", savedAt)] : undefined, [savedAt]);
|
|
50
|
+
const stale = !!payload?.asOf && (payload.stale || (savedAt == null && resource.data?.stale) || false);
|
|
51
|
+
const error = resource.error || (savedAt == null ? resource.data?.refreshError : null) || null;
|
|
48
52
|
useAutoRefresh(stale ? null : resource.updatedAt, resource.load);
|
|
49
53
|
usePaneRefreshKey(resource.reload, { focused });
|
|
50
|
-
usePaneStatusFooter({ registrationId: "yield-curve:swaps", loading: resource.loading, error, stale });
|
|
54
|
+
usePaneStatusFooter({ registrationId: "yield-curve:swaps", loading: resource.loading, error, stale, info: savedInfo });
|
|
51
55
|
usePaneNoticeFooter({
|
|
52
56
|
registrationId: "yield-curve:swaps:notices", focused,
|
|
53
57
|
notices: payload?.asOf ? [
|