@gloom-sh/terminal 0.19.0 → 0.19.1
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 +1 -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/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/architecture/core-builtin-allowlist.json +2 -2
- package/src/cli/commands/market.ts +35 -12
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +86 -20
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +6 -0
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/listing-arg.ts +57 -10
- 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 +1 -1
- 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 +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- 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/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- 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/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/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
- 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 +2 -2
- 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/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 +3 -0
- 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/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 +2 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/screener.ts +8 -4
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- 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/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 +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- 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/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- 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/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/pane.tsx +5 -2
- package/src/plugins/builtin/yield-curve/client.ts +61 -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/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- 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/create.ts +43 -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/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +6 -1
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/index.ts +6 -2
- 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/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +27 -0
- package/src/types/headless.ts +6 -0
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/fundamentals.ts +36 -5
- package/src/version.ts +2 -2
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
HeadlessShotState,
|
|
7
7
|
} from "../../../types/plugin";
|
|
8
8
|
import { formatShortDate } from "../../../utils/datetime-format";
|
|
9
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
9
10
|
import { normalizeCik, searchThirteenFFunds, TOP_FUNDS_MAX_LIMIT } from "./api";
|
|
10
11
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
11
12
|
import { REPORT_PAGE, REPORT_ROWS } from "../shared/headless-options";
|
|
@@ -229,15 +230,40 @@ export interface ThirteenFHeadlessDependencies {
|
|
|
229
230
|
loadEtfHoldings?(ticker: string, ctx: HeadlessPaneContext): Promise<FundMembersPayload>;
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
/** The oldest copy a load's 13F reads answered from because their refresh failed, by the load's context. */
|
|
234
|
+
const savedCopies = new WeakMap<HeadlessPaneContext, { savedAt: number | null }>();
|
|
235
|
+
|
|
236
|
+
/** How a 13F read for the load `ctx` says it answered from the copy saved at `savedAt`. */
|
|
237
|
+
const noteSavedCopy = (ctx: HeadlessPaneContext) => (savedAt: number) => {
|
|
238
|
+
const entry = savedCopies.get(ctx);
|
|
239
|
+
if (entry) entry.savedAt = Math.min(entry.savedAt ?? savedAt, savedAt);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* One load, whose report ends with when the oldest saved copy it answered
|
|
244
|
+
* from was saved (`couldn't refresh; saved copy from <time>`), as every
|
|
245
|
+
* command says it, instead of listing each failed refresh as an error.
|
|
246
|
+
*/
|
|
247
|
+
async function readingSavedCopies<T extends object>(ctx: HeadlessPaneContext, run: () => Promise<T>): Promise<T> {
|
|
248
|
+
const entry: { savedAt: number | null } = { savedAt: null };
|
|
249
|
+
savedCopies.set(ctx, entry);
|
|
250
|
+
try {
|
|
251
|
+
const result = await run();
|
|
252
|
+
return entry.savedAt == null ? result : servedFromSavedCopy(result, entry.savedAt);
|
|
253
|
+
} finally {
|
|
254
|
+
if (savedCopies.get(ctx) === entry) savedCopies.delete(ctx);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
232
258
|
const defaultDependencies: Required<ThirteenFHeadlessDependencies> = {
|
|
233
259
|
loadBrowser: (tab, query, limit, _args, ctx) => loadBrowserRows(
|
|
234
260
|
tab,
|
|
235
261
|
query,
|
|
236
262
|
ctx.signal,
|
|
237
|
-
{ limit },
|
|
263
|
+
{ limit, onSavedCopy: noteSavedCopy(ctx) },
|
|
238
264
|
),
|
|
239
265
|
// An overlap never shows a fund's missed quarter, so it does not ask EDGAR.
|
|
240
|
-
loadDetail: (cik, name, args, ctx) => loadFundDetail(cik, name, ctx.signal, { filingGap: !isOverlapLoad(args) }),
|
|
266
|
+
loadDetail: (cik, name, args, ctx) => loadFundDetail(cik, name, ctx.signal, { filingGap: !isOverlapLoad(args), onSavedCopy: noteSavedCopy(ctx) }),
|
|
241
267
|
loadSplitFactors: (detail, ctx) => loadSplitFactors(
|
|
242
268
|
detail,
|
|
243
269
|
(ticker, after, through) => corporateSplitFactor(ctx.marketData, ticker, after, through),
|
|
@@ -247,6 +273,7 @@ const defaultDependencies: Required<ThirteenFHeadlessDependencies> = {
|
|
|
247
273
|
searchFunds: (query, ctx) => searchThirteenFFunds(query, THIRTEENF_MAX_LIMIT, ctx.signal),
|
|
248
274
|
loadPerformance: (cik, name, ctx) => loadFundPerformance(cik, name, ctx.signal, {
|
|
249
275
|
splitFactor: (ticker, after, through) => corporateSplitFactor(ctx.marketData, ticker, after, through),
|
|
276
|
+
onSavedCopy: noteSavedCopy(ctx),
|
|
250
277
|
}),
|
|
251
278
|
loadEtfFunds: async (ctx) => (await fetchFunds(ctx.apiClient)).funds,
|
|
252
279
|
loadEtfHoldings: (ticker, ctx) => fetchMembers(ticker, ctx.apiClient),
|
|
@@ -541,7 +568,7 @@ export function createThirteenFHeadless(
|
|
|
541
568
|
],
|
|
542
569
|
describe: (args, result) => thirteenFTitle(args, result?.metadata),
|
|
543
570
|
shotState: async (args, load) => thirteenFShotState(args, (await load()).metadata ?? {}),
|
|
544
|
-
|
|
571
|
+
load: (args, ctx) => readingSavedCopies(ctx, async () => {
|
|
545
572
|
const query = typeof args.argument === "string" ? args.argument.trim() : "";
|
|
546
573
|
const requestedView = String(args.options.view) as HeadlessThirteenFView;
|
|
547
574
|
const compare = typeof args.options.compare === "string" ? args.options.compare.trim() : "";
|
|
@@ -920,7 +947,7 @@ export function createThirteenFHeadless(
|
|
|
920
947
|
],
|
|
921
948
|
},
|
|
922
949
|
};
|
|
923
|
-
},
|
|
950
|
+
}),
|
|
924
951
|
};
|
|
925
952
|
}
|
|
926
953
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { formatPriceEarnings, NOT_MEANINGFUL } from "../../../../utils/price-earnings";
|
|
2
2
|
import { convertMarketCapitalization } from "../../../../utils/market-capitalization";
|
|
3
3
|
import { priceColor } from "../../../../theme/colors";
|
|
4
|
-
import type { CompanyProfile, DepositaryPair, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
|
|
4
|
+
import type { CedearRatio, CompanyProfile, DepositaryPair, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
5
|
import { adrRatioText } from "../../../../utils/depositary-receipt";
|
|
6
|
-
import { ratioCount } from "../../../../market-data/depositary-pair";
|
|
6
|
+
import { cedearRatioText, ratioCount } from "../../../../market-data/depositary-pair";
|
|
7
7
|
import { ADR_PAIR_TAB_ID, ADR_PAIR_TEMPLATE_ID } from "../adr-pair/model";
|
|
8
8
|
import type { Portfolio, TickerPosition, TickerRecord } from "../../../../types/ticker";
|
|
9
9
|
import type { BrokerInstanceConfig } from "../../../../types/config";
|
|
@@ -151,6 +151,7 @@ export function buildOverviewStats({
|
|
|
151
151
|
nextEarnings,
|
|
152
152
|
depositaryReceipt = false,
|
|
153
153
|
depositaryPair = null,
|
|
154
|
+
cedearRatio = null,
|
|
154
155
|
today,
|
|
155
156
|
dividend,
|
|
156
157
|
bond = null,
|
|
@@ -169,6 +170,8 @@ export function buildOverviewStats({
|
|
|
169
170
|
depositaryReceipt?: boolean;
|
|
170
171
|
/** The receipt pair the listing is a side of, as Gloom Cloud serves it; `receiptSide` when the listing is the receipt. */
|
|
171
172
|
depositaryPair?: { pair: DepositaryPair; receiptSide: boolean } | null;
|
|
173
|
+
/** A Buenos Aires CEDEAR's ratio in its company's shares, as Gloom Cloud serves it. */
|
|
174
|
+
cedearRatio?: CedearRatio | null;
|
|
172
175
|
/** The listing's calendar day, YYYY-MM-DD; dates before it are left out. */
|
|
173
176
|
today?: string;
|
|
174
177
|
/** The history the dividend yield reads (useOverviewDividend): a trailing one is measured from its cash, a forward one stops with it; without it the yield stands. */
|
|
@@ -223,6 +226,11 @@ export function buildOverviewStats({
|
|
|
223
226
|
? { label: "ADR Ratio", value: `1:${count}`, detail: [adrRatioText(pair.ratio)!, `${count} sh per ADR`], link: ADR_PAIR }
|
|
224
227
|
: { label: "ADR", value: pair.receipt, detail: [`1 ADR = ${count} of these shares`, `1:${count}`], link: ADR_PAIR });
|
|
225
228
|
}
|
|
229
|
+
if (cedearRatio) {
|
|
230
|
+
const receipts = ratioCount(cedearRatio.receipts);
|
|
231
|
+
const shares = ratioCount(cedearRatio.shares);
|
|
232
|
+
stats.push({ label: "CEDEAR Ratio", value: `${receipts}:${shares}`, detail: [cedearRatioText(cedearRatio), `${receipts} per ${shares} sh`] });
|
|
233
|
+
}
|
|
226
234
|
const trailingPE = liveTrailingPE(quote, fundamentals);
|
|
227
235
|
if (trailingPE != null) {
|
|
228
236
|
stats.push({ label: "P/E (TTM)", value: formatPriceEarnings(trailingPE) });
|
|
@@ -42,7 +42,7 @@ import { appTtmRate, enterpriseValueReconciliationNote, ttmAtAppRate, ttmConvers
|
|
|
42
42
|
import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
43
43
|
import { zonedDateKey } from "../../../utils/zoned-date-time";
|
|
44
44
|
import { sharesOutstandingInReceipts } from "../../../utils/depositary-receipt";
|
|
45
|
-
import { isReceiptSide, listingDepositaryPair } from "../../../market-data/depositary-pair";
|
|
45
|
+
import { isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../../market-data/depositary-pair";
|
|
46
46
|
import {
|
|
47
47
|
CompactRangeBar,
|
|
48
48
|
FundamentalsGrid,
|
|
@@ -335,6 +335,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
335
335
|
nextEarnings: financials?.nextEarnings,
|
|
336
336
|
depositaryReceipt: sharesOutstandingInReceipts(quote, fundamentals, description),
|
|
337
337
|
depositaryPair: depositaryPair ? { pair: depositaryPair, receiptSide: isReceiptSide(financials) } : null,
|
|
338
|
+
cedearRatio: listingCedearRatio(financials),
|
|
338
339
|
// Report and ex-dividend dates are the listing's calendar days.
|
|
339
340
|
today: zonedDateKey(Date.now(), chartTimeZone ?? "America/New_York"),
|
|
340
341
|
dividend,
|
|
@@ -326,16 +326,21 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
326
326
|
selectResearchTab(tabId);
|
|
327
327
|
return true;
|
|
328
328
|
}, [selectResearchTab, visibleTabIds]);
|
|
329
|
+
// Without a ticker the pane draws only its empty state, and the tab list has
|
|
330
|
+
// no Overview, so the fallback above lands on the first tab that needs no
|
|
331
|
+
// ticker. Nobody opened it, and keeping it mounted would load its data on
|
|
332
|
+
// every ticker switch.
|
|
333
|
+
const shownTabId = ticker && visibleTabIds.has(resolvedTabId) ? resolvedTabId : null;
|
|
329
334
|
const renderedTabIds = useMemo(() => {
|
|
330
335
|
const next = new Set<string>();
|
|
331
336
|
for (const tabId of mountedTabIds) {
|
|
332
337
|
if (visibleTabIds.has(tabId)) next.add(tabId);
|
|
333
338
|
}
|
|
334
|
-
if (
|
|
335
|
-
next.add(
|
|
339
|
+
if (shownTabId) {
|
|
340
|
+
next.add(shownTabId);
|
|
336
341
|
}
|
|
337
342
|
return next;
|
|
338
|
-
}, [mountedTabIds,
|
|
343
|
+
}, [mountedTabIds, shownTabId, visibleTabIds]);
|
|
339
344
|
|
|
340
345
|
const handlePluginCapture = useCallback((capturing: boolean) => {
|
|
341
346
|
setPluginCaptured(capturing);
|
|
@@ -354,12 +359,12 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
354
359
|
for (const tabId of current) {
|
|
355
360
|
if (visibleTabIds.has(tabId)) next.add(tabId);
|
|
356
361
|
}
|
|
357
|
-
if (
|
|
358
|
-
next.add(
|
|
362
|
+
if (shownTabId) {
|
|
363
|
+
next.add(shownTabId);
|
|
359
364
|
}
|
|
360
365
|
return sameStringSet(current, next) ? current : next;
|
|
361
366
|
});
|
|
362
|
-
}, [
|
|
367
|
+
}, [shownTabId, visibleTabIds]);
|
|
363
368
|
|
|
364
369
|
if (!ticker) {
|
|
365
370
|
const isEmptyFollowCollection = paneInstance?.binding?.kind === "follow" && !!collectionId && collectionTickerCount === 0;
|
|
@@ -28,27 +28,34 @@ import {
|
|
|
28
28
|
} from "./model";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* A
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* A missing bid-to-cover or bidder split is a blank cell: the source has
|
|
32
|
+
* bid-to-cover regularly from late 1999 and the split only from 2008-04-07,
|
|
33
|
+
* and an announced auction has no results to fill them; its Rate says so.
|
|
34
34
|
*/
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const formatPercent = (value: unknown) => value == null ? "" : `${Number(value).toFixed(1)}%`;
|
|
36
|
+
|
|
37
|
+
/** What an announced auction's Rate reads until the source publishes its results. */
|
|
38
|
+
const RESULTS_NOT_PUBLISHED = "results not published";
|
|
39
|
+
const pending = (row: Record<string, unknown>) => isPendingAuction(row as unknown as TreasuryAuction);
|
|
37
40
|
|
|
38
41
|
/** Why the stop-out column is not a tail; the docs page says the same at length. */
|
|
39
42
|
const STOP_OUT_NOTE = "Stop-out vs avg is the high yield minus the average/median yield, in bp (bills: discount rates). "
|
|
40
43
|
+ "A true tail, the stop-out against the 1 pm when-issued yield, needs the when-issued yield, which the source does not carry.";
|
|
41
44
|
|
|
42
45
|
const MISSING_METRICS_NOTE = "A blank B/C or bidder share is one the source does not have for that auction: "
|
|
43
|
-
+ "bid-to-cover is regular from late 1999, the indirect, direct and dealer split from 2008-04-07.
|
|
46
|
+
+ "bid-to-cover is regular from late 1999, the indirect, direct and dealer split from 2008-04-07.";
|
|
44
47
|
|
|
45
48
|
/** A completed auction the source gives no bid-to-cover or no bidder split for. */
|
|
46
49
|
const lacksMetrics = (auction: TreasuryAuction) => !isPendingAuction(auction)
|
|
47
50
|
&& (auction.bidToCoverRatio == null || indirectPct(auction) == null || directPct(auction) == null || dealerPct(auction) == null);
|
|
48
51
|
|
|
49
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The rate as the table prints it; a TIPS or FRN row says what its number is
|
|
54
|
+
* (`2.653% real`, `4.0bp spread`), and an announced auction that its results
|
|
55
|
+
* are not published yet.
|
|
56
|
+
*/
|
|
50
57
|
const rateCell = (value: unknown, row: Record<string, unknown>) => {
|
|
51
|
-
const text = formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), "-");
|
|
58
|
+
const text = formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), pending(row) ? RESULTS_NOT_PUBLISHED : "-");
|
|
52
59
|
const qualifier = value == null ? null : rateQualifier(row as unknown as TreasuryAuction);
|
|
53
60
|
return qualifier ? `${text} ${qualifier}` : text;
|
|
54
61
|
};
|
|
@@ -61,14 +68,16 @@ const COLUMNS = [
|
|
|
61
68
|
key: "rate",
|
|
62
69
|
header: "Rate",
|
|
63
70
|
align: "right" as const,
|
|
64
|
-
format: (value: unknown, row: Record<string, unknown>) => formatAuctionRate(
|
|
71
|
+
format: (value: unknown, row: Record<string, unknown>) => formatAuctionRate(
|
|
72
|
+
row as unknown as TreasuryAuction, value == null ? null : Number(value), pending(row) ? RESULTS_NOT_PUBLISHED : "-",
|
|
73
|
+
),
|
|
65
74
|
textFormat: rateCell,
|
|
66
75
|
},
|
|
67
|
-
{ key: "stopOutVsAvgBp", header: "Stop-out vs avg (bp)", align: "right" as const, format: (value: unknown) => value == null ? "-" : Number(value).toFixed(1) },
|
|
68
76
|
{
|
|
69
|
-
key: "
|
|
70
|
-
format: (value: unknown, row: Record<string, unknown>) => value == null ?
|
|
77
|
+
key: "stopOutVsAvgBp", header: "Stop-out vs avg (bp)", align: "right" as const,
|
|
78
|
+
format: (value: unknown, row: Record<string, unknown>) => value == null ? pending(row) ? "" : "-" : Number(value).toFixed(1),
|
|
71
79
|
},
|
|
80
|
+
{ key: "bidToCoverRatio", header: "B/C", align: "right" as const, format: (value: unknown) => value == null ? "" : Number(value).toFixed(2) },
|
|
72
81
|
{ key: "indirectPercent", header: "Indirect", align: "right" as const, format: formatPercent },
|
|
73
82
|
{ key: "directPercent", header: "Direct", align: "right" as const, format: formatPercent },
|
|
74
83
|
{ key: "dealerPercent", header: "Dealer", align: "right" as const, format: formatPercent },
|
|
@@ -105,6 +105,9 @@ function secTypeColor(secType: string, selected: boolean): string {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** An announced auction's Rate in the table, the word other panes use for a figure not out yet (KPI's Later actual). */
|
|
109
|
+
const RESULTS_PENDING = "Pending";
|
|
110
|
+
|
|
108
111
|
// Stable table adapter so memoized rows survive pane re-renders.
|
|
109
112
|
const auctionKey = (auction: TreasuryAuction) => auction.id;
|
|
110
113
|
|
|
@@ -119,8 +122,8 @@ function renderAuctionCell(
|
|
|
119
122
|
|
|
120
123
|
switch (column.id) {
|
|
121
124
|
case "date":
|
|
122
|
-
// Announced auctions have no results yet
|
|
123
|
-
//
|
|
125
|
+
// Announced auctions have no results yet: Rate reads Pending, the other
|
|
126
|
+
// metric cells "—", and the date is bright.
|
|
124
127
|
return {
|
|
125
128
|
text: showYear ? auction.auctionDate : formatShortDate(auction.auctionDate, { year: false, utc: true, fallback: "\u2014" }),
|
|
126
129
|
color: rowState.selected ? colors.selectedText : isPendingAuction(auction) ? colors.textBright : colors.textDim,
|
|
@@ -134,7 +137,10 @@ function renderAuctionCell(
|
|
|
134
137
|
case "term":
|
|
135
138
|
return { text: auction.securityTerm, color: selected ?? colors.text };
|
|
136
139
|
case "rate":
|
|
137
|
-
|
|
140
|
+
// An announced auction says its results are pending, as its detail does; other metric cells stay "—".
|
|
141
|
+
return isPendingAuction(auction)
|
|
142
|
+
? { text: RESULTS_PENDING, color: dimmed }
|
|
143
|
+
: { text: formatAuctionRate(auction, rateValue(auction), "—"), color: selected ?? colors.textBright };
|
|
138
144
|
case "stopOut":
|
|
139
145
|
return { text: formatBp(stopOutVsAverageBp(auction)), color: selected ?? colors.text };
|
|
140
146
|
case "btc":
|
|
@@ -171,8 +171,11 @@ export function VolatilityPane({ focused, width, height }: PaneProps) {
|
|
|
171
171
|
usePaneFooter("volatility", () => ({ info: [
|
|
172
172
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading volatility", tone: "muted" as const }] }] : []),
|
|
173
173
|
...(result?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
|
|
174
|
-
|
|
175
|
-
...(asOf ? [{ id: "
|
|
174
|
+
// One fact, "close 2026-09-25": the basis and its date read as one segment.
|
|
175
|
+
...(data || asOf ? [{ id: "basis", parts: [
|
|
176
|
+
...(data ? [{ text: observationBasis, tone: "muted" as const }] : []),
|
|
177
|
+
...(asOf ? [{ text: observationTime ? `${observationTime.slice(0, 16).replace("T", " ")} UTC` : asOf, tone: "muted" as const }] : []),
|
|
178
|
+
] }] : []),
|
|
176
179
|
], hints: [{ id: "view", key: "v", label: "iew", onPress: cycleTab }] }), [resource.loading, result?.stale, data, asOf, observationTime, observationBasis, cycleTab]);
|
|
177
180
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: setTab, focused, dense: true });
|
|
178
181
|
const contentHeight = Math.max(5, height - tabRows);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import type { SwapCurvePayload, SwapTenor } from "../../../api-client/swap-curve";
|
|
3
|
-
import {
|
|
3
|
+
import { isPermanentClientError } from "../../../api-client/errors";
|
|
4
|
+
import { createPluginCache, type PluginCacheResult } from "../../../data/plugin-cache";
|
|
4
5
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
5
6
|
import { validateSwapCurve } from "./swaps-model";
|
|
7
|
+
import { loadCurveData, type CurveData, type CurveId } from "./curves";
|
|
8
|
+
import { loadWorldRows, type WorldRow } from "./world";
|
|
6
9
|
|
|
7
10
|
export const swapCurveCache = createPluginCache<SwapCurvePayload>({
|
|
8
11
|
kind: "sofr-swap-curve", source: "gloom-cloud", schemaVersion: 1,
|
|
@@ -23,3 +26,60 @@ export async function fetchSwapCurve(tenor: SwapTenor,
|
|
|
23
26
|
export const getCachedSwapCurve = (tenor: SwapTenor) => cachedCloudResource(swapCurveCache, tenor, validateSwapCurve);
|
|
24
27
|
export const loadSwapCurve = (tenor: SwapTenor, force = false) =>
|
|
25
28
|
loadCloudResource(swapCurveCache, tenor, () => fetchSwapCurve(tenor), { force, validate: validateSwapCurve });
|
|
29
|
+
|
|
30
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Curve and World tabs' curves as last read, so a refresh that fails
|
|
34
|
+
* (offline, a service error) shows the copy an earlier read saved instead of
|
|
35
|
+
* an empty tab. Every load still asks the server; the copy is the fallback.
|
|
36
|
+
*/
|
|
37
|
+
export const curveViewCache = createPluginCache<CurveData>({
|
|
38
|
+
kind: "yield-curve-view", source: "gloom-cloud", schemaVersion: 1,
|
|
39
|
+
policy: { staleMs: 15 * 60_000, expireMs: 30 * DAY_MS },
|
|
40
|
+
});
|
|
41
|
+
export const worldCurvesCache = createPluginCache<WorldRow[]>({
|
|
42
|
+
kind: "world-curves", source: "gloom-cloud", schemaVersion: 1,
|
|
43
|
+
policy: { staleMs: 15 * 60_000, expireMs: 30 * DAY_MS },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** What a tab shows, and when the copy shown was saved when it is one, its refresh having failed. */
|
|
47
|
+
export interface SavedCopyAnswer<T> {
|
|
48
|
+
data: T;
|
|
49
|
+
savedAt: number | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const curveKey = (curve: CurveId, requestedDate: string) => `${curve}:${requestedDate || "latest"}`;
|
|
53
|
+
/**
|
|
54
|
+
* A copy is the saved one by where it came from, whatever its failure said. A
|
|
55
|
+
* refusal asking again will not change (access denied, an unknown curve) is
|
|
56
|
+
* said rather than covered by the copy, as `loadCloudResource` does.
|
|
57
|
+
*/
|
|
58
|
+
function answer<T>(result: PluginCacheResult<T>): SavedCopyAnswer<T> {
|
|
59
|
+
const saved = result.source === "stale-fallback";
|
|
60
|
+
if (saved && isPermanentClientError(result.error)) throw result.error;
|
|
61
|
+
return { data: result.data, savedAt: saved ? result.fetchedAt : null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The curve on the date (its latest without one), or the copy an earlier read saved when it cannot be read. */
|
|
65
|
+
export async function loadSavedCurve(curve: CurveId, requestedDate: string,
|
|
66
|
+
client?: Parameters<typeof loadCurveData>[2]): Promise<SavedCopyAnswer<CurveData>> {
|
|
67
|
+
return answer(await curveViewCache.load(curveKey(curve, requestedDate), () => loadCurveData(curve, requestedDate, client), { force: true }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The saved curve, for the tab's first paint while it is read again. */
|
|
71
|
+
export function cachedCurve(curve: CurveId, requestedDate: string): SavedCopyAnswer<CurveData> | null {
|
|
72
|
+
const cached = curveViewCache.get(curveKey(curve, requestedDate), { allowExpired: true });
|
|
73
|
+
return cached ? { data: cached.data, savedAt: null } : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Each market's latest curve, or the copy an earlier read saved when they cannot be read. */
|
|
77
|
+
export async function loadSavedWorldRows(client?: Parameters<typeof loadWorldRows>[0]): Promise<SavedCopyAnswer<WorldRow[]>> {
|
|
78
|
+
return answer(await worldCurvesCache.load("latest", () => loadWorldRows(client), { force: true }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The saved world board, for the tab's first paint while it is read again. */
|
|
82
|
+
export function cachedWorldRows(): SavedCopyAnswer<WorldRow[]> | null {
|
|
83
|
+
const cached = worldCurvesCache.get("latest", { allowExpired: true });
|
|
84
|
+
return cached ? { data: cached.data, savedAt: null } : null;
|
|
85
|
+
}
|
|
@@ -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 ? [
|
|
@@ -45,6 +45,7 @@ import { commentLettersPlugin } from "./builtin/comment-letters";
|
|
|
45
45
|
import { openFdaPlugin } from "./builtin/openfda";
|
|
46
46
|
import { browserGloomberbCloudPlugin } from "./builtin/cloud/browser";
|
|
47
47
|
import { changelogModule } from "./builtin/changelog";
|
|
48
|
+
import { roadmapModule } from "./builtin/roadmap";
|
|
48
49
|
import { chartComposerModule } from "./builtin/chart-composer";
|
|
49
50
|
import { connectionsModule } from "./builtin/connections";
|
|
50
51
|
import { relativeRotationModule } from "./builtin/relative-rotation";
|
|
@@ -126,7 +127,7 @@ import { FIXED_INCOME_HOSTS } from "./builtin/fixed-income/model";
|
|
|
126
127
|
|
|
127
128
|
const browserApplicationPlugin = composeBuiltinPlugin({
|
|
128
129
|
...applicationPluginMeta,
|
|
129
|
-
modules: [layoutManagerModule, helpModule, changelogModule, connectionsModule, docsModule],
|
|
130
|
+
modules: [layoutManagerModule, helpModule, changelogModule, connectionsModule, docsModule, roadmapModule],
|
|
130
131
|
});
|
|
131
132
|
|
|
132
133
|
const runtimeOnly = (module: PluginModule): PluginModule => {
|
package/src/plugins/host-link.ts
CHANGED
|
@@ -32,7 +32,7 @@ const LINKED_PACKAGES = ["gloomberb", "react"] as const;
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* The names the host is published under: `gloomberb`, which plugins import
|
|
35
|
-
* as, and `@gloom-sh/terminal
|
|
35
|
+
* as, and `@gloom-sh/terminal`.
|
|
36
36
|
*/
|
|
37
37
|
const HOST_PACKAGE_NAMES: ReadonlySet<string> = new Set(["gloomberb", "@gloom-sh/terminal"]);
|
|
38
38
|
|
|
@@ -185,15 +185,19 @@ function positionSideLabel(ticker: TickerRecord, activeTab: string | undefined):
|
|
|
185
185
|
return "MIX";
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* The currency a row is listed in: its position's, else its quote's or the one saved with it. An index's level is in
|
|
190
|
+
* points and has none, and a row whose currency no source gave (a coin pair before its quote) shows none, never the
|
|
191
|
+
* dollars it is valued in meanwhile: a row not held has no position currency, only that fallback.
|
|
192
|
+
*/
|
|
189
193
|
function listedCurrency(
|
|
190
|
-
|
|
191
|
-
quoteCurrency: string,
|
|
194
|
+
metrics: Pick<PortfolioPositionMetrics, "positionCount" | "positionCurrency">,
|
|
192
195
|
ticker: TickerRecord,
|
|
193
196
|
quote: TickerFinancials["quote"],
|
|
194
197
|
): string {
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
const held = metrics.positionCount > 0 ? metrics.positionCurrency : "";
|
|
199
|
+
if (!held && isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return "";
|
|
200
|
+
return (held || quote?.currency || ticker.metadata.currency || "").toUpperCase();
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
function compactText(value: string | null | undefined): string {
|
|
@@ -458,7 +462,7 @@ export function getColumnValue(
|
|
|
458
462
|
),
|
|
459
463
|
};
|
|
460
464
|
case "currency":
|
|
461
|
-
return { text: compactText(currencyCodeLabel(listedCurrency(
|
|
465
|
+
return { text: compactText(currencyCodeLabel(listedCurrency(positionMetrics, ticker, quote))) };
|
|
462
466
|
case "sector":
|
|
463
467
|
return { text: compactText(ticker.metadata.sector || financials?.profile?.sector) };
|
|
464
468
|
case "industry":
|
|
@@ -713,7 +717,7 @@ export function getSortValue(
|
|
|
713
717
|
?? ticker.metadata.exchange
|
|
714
718
|
?? null;
|
|
715
719
|
case "currency":
|
|
716
|
-
return listedCurrency(
|
|
720
|
+
return listedCurrency(positionMetrics, ticker, quote) || null;
|
|
717
721
|
case "sector":
|
|
718
722
|
return ticker.metadata.sector || financials?.profile?.sector || null;
|
|
719
723
|
case "industry":
|