gloomberb 0.15.0 → 0.15.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/bin/gloomberb +9 -1
- package/package.json +1 -1
- package/src/api-client/central-bank-rates.ts +3 -0
- package/src/api-client/types.ts +1 -1
- package/src/app/pane-runtime/index.tsx +7 -5
- package/src/app/runtime/broker-import.test.tsx +89 -0
- package/src/app/runtime/broker-import.ts +29 -8
- package/src/brokers/sync-broker-instance.ts +67 -0
- package/src/cli/commands/overview.ts +6 -2
- package/src/cli/native-smoke.ts +2 -0
- package/src/components/chart/composite/composite-chart.tsx +21 -1
- package/src/components/chart/composite/format.ts +71 -8
- package/src/components/chart/composite/fx-precision.test.ts +21 -3
- package/src/components/chart/composite/scene.ts +13 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/layout/market-summary.ts +4 -2
- package/src/components/ticker/badge/format.test.ts +3 -3
- package/src/components/ticker/badge/format.ts +11 -9
- package/src/components/ticker/badge/text.test.tsx +6 -6
- package/src/components/ui/index.ts +2 -1
- package/src/components/ui/multi-select/dialog.tsx +46 -4
- package/src/components/ui/number-prompt-dialog.tsx +71 -0
- package/src/core/state/app/state.ts +8 -2
- package/src/market-data/coordinator/events.ts +5 -0
- package/src/market-data/coordinator/index.ts +44 -10
- package/src/market-data/market/format.test.ts +54 -0
- package/src/market-data/market/format.ts +164 -5
- package/src/market-data/query-store.test.ts +87 -0
- package/src/market-data/query-store.ts +66 -9
- package/src/plugins/builtin/alerts/format.ts +4 -3
- package/src/plugins/builtin/analytics/display.ts +3 -3
- package/src/plugins/builtin/analytics/index.test.tsx +22 -22
- package/src/plugins/builtin/analytics/pane-model.test.ts +10 -10
- package/src/plugins/builtin/analytics/pane-model.ts +8 -8
- package/src/plugins/builtin/analytics/portfolio-identity.test.tsx +6 -6
- package/src/plugins/builtin/analytics/risk-client.ts +6 -8
- package/src/plugins/builtin/analytics/risk-model.test.ts +21 -2
- package/src/plugins/builtin/analytics/risk-model.ts +8 -0
- package/src/plugins/builtin/analytics/view.tsx +2 -2
- package/src/plugins/builtin/backtest/engine.test.ts +0 -1
- package/src/plugins/builtin/backtest/engine.ts +1 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/central-bank-rates/client.ts +2 -1
- package/src/plugins/builtin/central-bank-rates/headless.ts +2 -2
- package/src/plugins/builtin/central-bank-rates/model.test.ts +16 -1
- package/src/plugins/builtin/central-bank-rates/model.ts +4 -4
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +50 -3
- package/src/plugins/builtin/chart-composer/presets.test.ts +17 -0
- package/src/plugins/builtin/chart-composer/presets.ts +48 -0
- package/src/plugins/builtin/chart-composer/settings.ts +42 -11
- package/src/plugins/builtin/chat/chat.test.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/board-pane.test.tsx +25 -0
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +3 -2
- package/src/plugins/builtin/cloud/thesis/store.ts +2 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +1 -1
- package/src/plugins/builtin/congress-trades/table.tsx +1 -1
- package/src/plugins/builtin/cot/client.ts +2 -1
- package/src/plugins/builtin/cot/model.test.ts +9 -0
- package/src/plugins/builtin/cot/model.ts +30 -1
- package/src/plugins/builtin/cot/pane.tsx +2 -4
- package/src/plugins/builtin/credit-conditions/client.ts +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +2 -1
- package/src/plugins/builtin/crypto-board/model.test.ts +9 -0
- package/src/plugins/builtin/crypto-board/model.ts +11 -3
- package/src/plugins/builtin/custom-view/live-quotes.test.ts +6 -1
- package/src/plugins/builtin/custom-view/live-quotes.ts +26 -0
- package/src/plugins/builtin/custom-view/pane.tsx +18 -7
- package/src/plugins/builtin/custom-view/view-spec.test.ts +24 -6
- package/src/plugins/builtin/custom-view/view-spec.ts +53 -21
- package/src/plugins/builtin/debt-maturities/client.ts +16 -3
- package/src/plugins/builtin/debt-maturities/debt-maturities.test.ts +8 -0
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -2
- package/src/plugins/builtin/debt-maturities/model.ts +2 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +0 -8
- package/src/plugins/builtin/earnings-calls/pane.test.tsx +14 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +3 -1
- package/src/plugins/builtin/econ/fred-client.test.ts +2 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +0 -1
- package/src/plugins/builtin/econ/index.tsx +4 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +4 -2
- package/src/plugins/builtin/econ-statistics/stats.ts +29 -24
- package/src/plugins/builtin/econ-statistics/view.test.ts +16 -0
- package/src/plugins/builtin/equity-screener/headless.test.ts +22 -0
- package/src/plugins/builtin/equity-screener/headless.ts +18 -10
- package/src/plugins/builtin/estimate-revisions/client.ts +3 -1
- package/src/plugins/builtin/estimate-revisions/model.test.ts +7 -0
- package/src/plugins/builtin/estimate-revisions/model.ts +0 -5
- package/src/plugins/builtin/futures/table.tsx +4 -3
- package/src/plugins/builtin/futures-curve/client.ts +2 -1
- package/src/plugins/builtin/futures-curve/model.test.ts +6 -4
- package/src/plugins/builtin/futures-curve/model.ts +18 -14
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +21 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +7 -0
- package/src/plugins/builtin/fx-matrix/pairs.test.ts +5 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +9 -3
- package/src/plugins/builtin/insider/index.tsx +15 -12
- package/src/plugins/builtin/insider/pane.test.tsx +24 -0
- package/src/plugins/builtin/market-movers/headless.ts +2 -2
- package/src/plugins/builtin/market-movers/model.ts +23 -10
- package/src/plugins/builtin/market-movers/screener.test.ts +5 -1
- package/src/plugins/builtin/market-movers/screener.ts +3 -0
- package/src/plugins/builtin/market-movers/table.ts +2 -2
- package/src/plugins/builtin/market-valuation/client.test.ts +3 -0
- package/src/plugins/builtin/market-valuation/client.ts +6 -1
- package/src/plugins/builtin/money-markets/client.ts +2 -1
- package/src/plugins/builtin/money-markets/pane.tsx +0 -1
- package/src/plugins/builtin/options/expiry-selection.test.tsx +1 -1
- package/src/plugins/builtin/options/footer.test.ts +2 -2
- package/src/plugins/builtin/options/footer.ts +2 -2
- package/src/plugins/builtin/options/market-reference.test.ts +2 -2
- package/src/plugins/builtin/options/market-reference.ts +14 -2
- package/src/plugins/builtin/options/table.ts +5 -8
- package/src/plugins/builtin/options/view.test.tsx +2 -2
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/options-calculator/pane.test.tsx +6 -6
- package/src/plugins/builtin/options-calculator/pane.tsx +23 -8
- package/src/plugins/builtin/options-calculator/surface.test.ts +16 -1
- package/src/plugins/builtin/options-calculator/surface.ts +13 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +17 -9
- package/src/plugins/builtin/portfolio-list/index.test.ts +5 -5
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +8 -8
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +12 -12
- package/src/plugins/builtin/portfolio-list/position-cost-integrity.test.ts +2 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +7 -6
- package/src/plugins/builtin/rate-path/client.ts +2 -1
- package/src/plugins/builtin/rate-path/headless.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +0 -1
- package/src/plugins/builtin/realized-vol/client.test.ts +7 -0
- package/src/plugins/builtin/realized-vol/headless.ts +1 -1
- package/src/plugins/builtin/realized-vol/model.test.ts +16 -4
- package/src/plugins/builtin/realized-vol/model.ts +24 -9
- package/src/plugins/builtin/relative-rotation/client.ts +2 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -10
- package/src/plugins/builtin/risk-factors/data.test.ts +7 -0
- package/src/plugins/builtin/risk-factors/data.ts +8 -1
- package/src/plugins/builtin/risk-factors/pane.test.tsx +3 -0
- package/src/plugins/builtin/shared/screener-live-quotes.test.ts +3 -3
- package/src/plugins/builtin/shared/screener-live-quotes.ts +16 -1
- package/src/plugins/builtin/short-volume/client.ts +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +0 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -3
- package/src/plugins/builtin/thirteenf/model.ts +14 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +12 -5
- package/src/plugins/builtin/thirteenf/overlap.test.ts +13 -1
- package/src/plugins/builtin/thirteenf/overlap.ts +31 -3
- package/src/plugins/builtin/thirteenf/pane.test.tsx +2 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -9
- package/src/plugins/builtin/thirteenf/table.ts +9 -3
- package/src/plugins/builtin/ticker-detail/financials/model.ts +0 -12
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -1
- package/src/plugins/builtin/ticker-detail/index.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +9 -10
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +6 -9
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +3 -2
- package/src/plugins/builtin/time-sales/model.test.ts +17 -1
- package/src/plugins/builtin/time-sales/model.ts +14 -3
- package/src/plugins/builtin/time-sales/pane.tsx +8 -4
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +4 -1
- package/src/plugins/builtin/vol-surface/model.test.ts +4 -2
- package/src/plugins/builtin/vol-surface/model.ts +14 -2
- package/src/plugins/builtin/vol-surface/pane.tsx +9 -2
- package/src/plugins/builtin/volatility/client.test.ts +13 -8
- package/src/plugins/builtin/volatility/client.ts +3 -2
- package/src/plugins/builtin/volatility/headless.test.ts +2 -2
- package/src/plugins/builtin/volatility/model.test.ts +4 -0
- package/src/plugins/builtin/volatility/model.ts +6 -7
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -2
- package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +33 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +34 -1
- package/src/renderers/electrobun/bun/index.ts +2 -1
- package/src/renderers/opentui/render-timing-smoke.ts +20 -0
- package/src/sources/provider-router/profile-enrichment.test.ts +0 -3
- package/src/sources/yahoo-finance.test.ts +5 -1
- package/src/sources/yahoo-finance.ts +2 -1
- package/src/time-series/forward-valuation.ts +6 -0
- package/src/time-series/resolve.ts +5 -1
- package/src/time-series/valuation-currency.test.ts +2 -0
- package/src/time-series/valuation-currency.ts +3 -1
- package/src/utils/exchange-rate-snapshot.test.ts +12 -1
- package/src/utils/exchange-rate-snapshot.ts +3 -2
- package/src/utils/format.ts +55 -20
- package/src/utils/fx-market-hours.test.ts +28 -0
- package/src/utils/fx-market-hours.ts +42 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { fetchDebtMaturities } from "./client";
|
|
3
|
-
import { bucketShare } from "./model";
|
|
3
|
+
import { bucketShare, debtNotices } from "./model";
|
|
4
4
|
|
|
5
5
|
export const debtMaturitiesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
6
6
|
shape: "bundle",
|
|
@@ -14,6 +14,7 @@ export const debtMaturitiesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
14
14
|
dataRequirements: ["Gloom Cloud SEC principal maturity schedules"],
|
|
15
15
|
limitations: [
|
|
16
16
|
"Six relative fiscal maturity buckets; thereafter is open ended",
|
|
17
|
+
"Reported principal obligations, not carrying debt; scope can include short-term borrowing and hedges",
|
|
17
18
|
"Native currency; unsupported or missing facts remain null",
|
|
18
19
|
"No inferred weighted coupon or duration",
|
|
19
20
|
"Ten-year percentiles require five comparable annual observations",
|
|
@@ -38,7 +39,7 @@ export const debtMaturitiesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
38
39
|
const data = await fetchDebtMaturities(args.symbols[0]!, ctx.apiClient);
|
|
39
40
|
return {
|
|
40
41
|
complete: data.status === "available",
|
|
41
|
-
errors: data
|
|
42
|
+
errors: debtNotices(data),
|
|
42
43
|
sections: [
|
|
43
44
|
{
|
|
44
45
|
title: "Principal maturity schedule",
|
|
@@ -124,7 +124,8 @@ export function sortedDebtHistory(
|
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
export function debtNotices(data: DebtMaturitiesPayload): string[] {
|
|
127
|
-
// Standing scope and open-ended bucket methodology are documented, not repeated as
|
|
127
|
+
// Standing scope and open-ended bucket methodology are documented, not repeated as
|
|
128
|
+
// active failures. The backend stops sending them; this covers older servers and caches.
|
|
128
129
|
return data.warnings.filter(
|
|
129
130
|
(warning) =>
|
|
130
131
|
!warning.startsWith(
|
|
@@ -445,14 +445,6 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
445
445
|
},
|
|
446
446
|
]
|
|
447
447
|
: []),
|
|
448
|
-
...(data.status !== "available"
|
|
449
|
-
? [
|
|
450
|
-
{
|
|
451
|
-
id: "partial",
|
|
452
|
-
parts: [{ text: data.status, tone: "warning" as const }],
|
|
453
|
-
},
|
|
454
|
-
]
|
|
455
|
-
: []),
|
|
456
448
|
]
|
|
457
449
|
: [],
|
|
458
450
|
});
|
|
@@ -422,6 +422,20 @@ for (const alreadyOnShelf of [true, false]) {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
|
|
425
|
+
for (const answer of [{ calls: [], pending: false }, { calls: [], unknownTicker: true }]) {
|
|
426
|
+
test(`a company lookup with ${answer.unknownTicker ? "an unknown ticker" : "no calls"} says so in the footer`, async () => {
|
|
427
|
+
signIn();
|
|
428
|
+
setCloudApiFetchTransport(async url => Response.json(new URL(String(url)).searchParams.get("ticker") ? answer : { calls: [call("OTHER")] }));
|
|
429
|
+
await mount(80, null);
|
|
430
|
+
await findInPane("MSFT");
|
|
431
|
+
await act(async () => { await Bun.sleep(600); });
|
|
432
|
+
await frames();
|
|
433
|
+
const frame = setup!.captureCharFrame();
|
|
434
|
+
expect(frame).toContain(answer.unknownTicker ? "MSFT not found" : "no MSFT calls yet");
|
|
435
|
+
expect(frame).not.toContain(answer.unknownTicker ? "no MSFT calls yet" : "MSFT not found");
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
425
439
|
test("shelf refresh reloads the active company and a slower older lookup cannot override refreshed global rows", async () => {
|
|
426
440
|
signIn();
|
|
427
441
|
let refreshed = false;
|
|
@@ -700,8 +700,10 @@ export function EarningsCallsPane({ focused, width, height }: EarningsCallsViewP
|
|
|
700
700
|
if (lookupTicker && lookupState) {
|
|
701
701
|
if (lookupState === "loading" || lookupState === "pending") {
|
|
702
702
|
info.push({ id: "lookup", parts: [{ text: `looking up ${lookupTicker}`, tone: "muted" }] });
|
|
703
|
-
} else if (lookupState === "unknown"
|
|
703
|
+
} else if (lookupState === "unknown") {
|
|
704
704
|
info.push({ id: "lookup", parts: [{ text: `${lookupTicker} not found`, tone: "warning" }] });
|
|
705
|
+
} else if (lookupState === "none") {
|
|
706
|
+
info.push({ id: "lookup", parts: [{ text: `no ${lookupTicker} calls yet`, tone: "muted" }] });
|
|
705
707
|
} else if (lookupState === "error") {
|
|
706
708
|
info.push({ id: "lookup", parts: [{ text: `${lookupTicker}: ${lookup?.refreshError ?? "lookup failed"}`, tone: "warning" }] });
|
|
707
709
|
}
|
|
@@ -74,4 +74,6 @@ test("calendar history shows inflation rates, payroll job changes, and annualize
|
|
|
74
74
|
test("calendar headline PPI uses final demand and cannot alias Conference Board to OECD", () => {
|
|
75
75
|
expect(resolveFredMapping("PPI m/m", "US")?.seriesId).toBe("PPIFIS");
|
|
76
76
|
expect(resolveFredMapping("CB Consumer Confidence", "US")).toBeNull();
|
|
77
|
+
// ISM withdrew its PMI from FRED; NAPM no longer exists there.
|
|
78
|
+
expect(resolveFredMapping("ISM Manufacturing PMI", "US")).toBeNull();
|
|
77
79
|
});
|
|
@@ -28,7 +28,6 @@ const SERIES_MAP: Record<string, FredMapping> = {
|
|
|
28
28
|
"adp non-farm employment change": { seriesId: "NPPTTL", displayMode: "change", relatedTickers: ["SPY"] },
|
|
29
29
|
"retail sales m/m": { seriesId: "RSAFS", displayMode: "mom", relatedTickers: ["XRT", "SPY"] },
|
|
30
30
|
"core retail sales m/m": { seriesId: "RSFSXMV", displayMode: "mom", relatedTickers: ["XRT", "SPY"] },
|
|
31
|
-
"ism manufacturing pmi": { seriesId: "NAPM", displayMode: "level", relatedTickers: ["SPY", "XLI"] },
|
|
32
31
|
"prelim uom consumer sentiment": { seriesId: "UMCSENT", displayMode: "level", relatedTickers: ["SPY"] },
|
|
33
32
|
"revised uom consumer sentiment": { seriesId: "UMCSENT", displayMode: "level", relatedTickers: ["SPY"] },
|
|
34
33
|
"federal funds rate": { seriesId: "DFEDTARU", displayMode: "level", relatedTickers: ["^TNX", "TLT", "DX-Y.NYB"] },
|
|
@@ -243,11 +243,13 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
243
243
|
// The countdown changes every tick, so it is footer status, not query bar
|
|
244
244
|
// context; the footer ellipsizes a long event name.
|
|
245
245
|
const nextText = nextEvent && nextCountdown ? `next ${nextEvent.event} ${nextCountdown}` : null;
|
|
246
|
+
// The cached first paint is being replaced; its age only counts once that load settles.
|
|
247
|
+
const showStale = stale && (settled || !loading);
|
|
246
248
|
const calendarStatus = useMemo<PaneFooterSegment[]>(() => [
|
|
247
249
|
...(nextText && !detailEvent ? [{ id: "next", parts: [{ text: nextText, tone: "muted" as const }] }] : []),
|
|
248
|
-
...(
|
|
250
|
+
...(showStale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
|
|
249
251
|
...(staleness ? [{ id: "updated", parts: [{ text: staleness, tone: "muted" as const }] }] : []),
|
|
250
|
-
], [detailEvent, nextText,
|
|
252
|
+
], [detailEvent, nextText, showStale, staleness]);
|
|
251
253
|
usePaneStatusFooter({
|
|
252
254
|
registrationId: "econ-calendar",
|
|
253
255
|
loading,
|
|
@@ -212,11 +212,13 @@ export function EconStatisticsPane({ focused, width, height }: PaneProps) {
|
|
|
212
212
|
const info: PaneFooterSegment[] = [
|
|
213
213
|
{ id: "as-of", parts: [{ text: `as of ${selected.latest.date}`, tone: "muted" }] },
|
|
214
214
|
];
|
|
215
|
-
|
|
215
|
+
// The cached first paint is being replaced; its age only counts once that load fails.
|
|
216
|
+
const seeding = resource.loading && resource.updatedAt === null && !selected.refreshError;
|
|
217
|
+
if (selected.observationStale || (selected.cacheStale && !seeding)) {
|
|
216
218
|
info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
|
|
217
219
|
}
|
|
218
220
|
return info;
|
|
219
|
-
}, [selected]);
|
|
221
|
+
}, [resource.loading, resource.updatedAt, selected]);
|
|
220
222
|
|
|
221
223
|
usePaneStatusFooter({
|
|
222
224
|
registrationId: "econ-statistics",
|
|
@@ -4,8 +4,13 @@ import type { StatDef } from "./defs";
|
|
|
4
4
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
5
|
const DAILY = 6 * DAY_MS;
|
|
6
6
|
const WEEKLY = 21 * DAY_MS;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// FRED dates a print at the start of its period, so an on-time print is two
|
|
8
|
+
// periods plus the release lag old just before the next one lands. Each limit
|
|
9
|
+
// adds about two weeks to the slowest normal lag of its group.
|
|
10
|
+
const MONTHLY_EARLY = 90 * DAY_MS; // CPI, PPI, jobs, output, retail, housing starts: lag up to ~19d
|
|
11
|
+
const MONTHLY_LATE = 105 * DAY_MS; // PCE, income, new home sales, sentiment, M2: lag up to ~31d
|
|
12
|
+
const MONTHLY_LAGGED = 115 * DAY_MS; // JOLTS, durables, factory orders, trade: lag up to ~40d
|
|
13
|
+
const QUARTERLY = 235 * DAY_MS; // GDP, productivity: two quarters plus a lag up to ~38d
|
|
9
14
|
|
|
10
15
|
const pct = (value: number) => `${formatNumber(value, 1)}%`;
|
|
11
16
|
const pct2 = (value: number) => `${formatNumber(value, 2)}%`;
|
|
@@ -34,7 +39,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
34
39
|
axisUnit: "%",
|
|
35
40
|
formatValue: pct,
|
|
36
41
|
reference: { value: 2, label: "2%" },
|
|
37
|
-
staleAfterMs:
|
|
42
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
38
43
|
note: "Headline consumer prices against a year earlier, not seasonally adjusted, including food and energy.",
|
|
39
44
|
limit: 900,
|
|
40
45
|
}),
|
|
@@ -50,7 +55,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
50
55
|
axisUnit: "%",
|
|
51
56
|
formatValue: pct,
|
|
52
57
|
reference: { value: 2, label: "2%" },
|
|
53
|
-
staleAfterMs:
|
|
58
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
54
59
|
note: "Consumer prices excluding food and energy against a year earlier, not seasonally adjusted.",
|
|
55
60
|
limit: 900,
|
|
56
61
|
}),
|
|
@@ -65,7 +70,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
65
70
|
axisUnit: "%",
|
|
66
71
|
formatValue: pct,
|
|
67
72
|
reference: { value: 2, label: "2%" },
|
|
68
|
-
staleAfterMs:
|
|
73
|
+
staleAfterMs: MONTHLY_LATE,
|
|
69
74
|
note: "The price index for personal consumption, broader in scope than CPI.",
|
|
70
75
|
limit: 900,
|
|
71
76
|
}),
|
|
@@ -81,7 +86,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
81
86
|
axisUnit: "%",
|
|
82
87
|
formatValue: pct,
|
|
83
88
|
reference: { value: 2, label: "2% reference" },
|
|
84
|
-
staleAfterMs:
|
|
89
|
+
staleAfterMs: MONTHLY_LATE,
|
|
85
90
|
note: "Excludes food and energy to track underlying PCE inflation. The Fed target applies to overall PCE inflation.",
|
|
86
91
|
limit: 900,
|
|
87
92
|
}),
|
|
@@ -97,7 +102,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
97
102
|
axisUnit: "%",
|
|
98
103
|
formatValue: pct,
|
|
99
104
|
reference: null,
|
|
100
|
-
staleAfterMs:
|
|
105
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
101
106
|
note: "Final-demand producer prices against a year earlier, not seasonally adjusted.",
|
|
102
107
|
limit: 900,
|
|
103
108
|
}),
|
|
@@ -114,7 +119,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
114
119
|
axisUnit: "%",
|
|
115
120
|
formatValue: pct,
|
|
116
121
|
reference: null,
|
|
117
|
-
staleAfterMs:
|
|
122
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
118
123
|
note: "Share of the labour force without work and looking for it.",
|
|
119
124
|
limit: 900,
|
|
120
125
|
}),
|
|
@@ -129,7 +134,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
129
134
|
axisUnit: "",
|
|
130
135
|
formatValue: signedThousands,
|
|
131
136
|
reference: { value: 0, label: "flat" },
|
|
132
|
-
staleAfterMs:
|
|
137
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
133
138
|
note: "Jobs added or lost last month, the single most watched release.",
|
|
134
139
|
limit: 900,
|
|
135
140
|
}),
|
|
@@ -159,7 +164,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
159
164
|
axisUnit: "",
|
|
160
165
|
formatValue: thousands,
|
|
161
166
|
reference: null,
|
|
162
|
-
staleAfterMs:
|
|
167
|
+
staleAfterMs: MONTHLY_LAGGED,
|
|
163
168
|
note: "Unfilled positions employers report, a read on labour demand.",
|
|
164
169
|
limit: 400,
|
|
165
170
|
}),
|
|
@@ -193,7 +198,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
193
198
|
axisUnit: "%",
|
|
194
199
|
formatValue: pct,
|
|
195
200
|
reference: { value: 0, label: "flat" },
|
|
196
|
-
staleAfterMs:
|
|
201
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
197
202
|
note: "Output of factories, mines and utilities against a year earlier.",
|
|
198
203
|
limit: 900,
|
|
199
204
|
}),
|
|
@@ -208,7 +213,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
208
213
|
axisUnit: "%",
|
|
209
214
|
formatValue: pct,
|
|
210
215
|
reference: null,
|
|
211
|
-
staleAfterMs:
|
|
216
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
212
217
|
note: "How much of industry's capacity is in use, a slack and inflation gauge.",
|
|
213
218
|
limit: 900,
|
|
214
219
|
}),
|
|
@@ -223,7 +228,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
223
228
|
axisUnit: "%",
|
|
224
229
|
formatValue: pct,
|
|
225
230
|
reference: { value: 0, label: "flat" },
|
|
226
|
-
staleAfterMs:
|
|
231
|
+
staleAfterMs: MONTHLY_LAGGED,
|
|
227
232
|
note: "New orders for goods meant to last, a forward read on investment.",
|
|
228
233
|
limit: 500,
|
|
229
234
|
}),
|
|
@@ -238,7 +243,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
238
243
|
axisUnit: "%",
|
|
239
244
|
formatValue: pct,
|
|
240
245
|
reference: { value: 0, label: "flat" },
|
|
241
|
-
staleAfterMs:
|
|
246
|
+
staleAfterMs: MONTHLY_LAGGED,
|
|
242
247
|
note: "New orders across manufacturing, durable and not.",
|
|
243
248
|
limit: 500,
|
|
244
249
|
}),
|
|
@@ -256,7 +261,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
256
261
|
axisUnit: "%",
|
|
257
262
|
formatValue: pct,
|
|
258
263
|
reference: null,
|
|
259
|
-
staleAfterMs:
|
|
264
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
260
265
|
note: "What households actually spent at retailers, not adjusted for prices.",
|
|
261
266
|
limit: 500,
|
|
262
267
|
}),
|
|
@@ -272,7 +277,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
272
277
|
axisUnit: "%",
|
|
273
278
|
formatValue: pct,
|
|
274
279
|
reference: null,
|
|
275
|
-
staleAfterMs:
|
|
280
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
276
281
|
note: "Retail sales excluding motor vehicles, which swing the headline around.",
|
|
277
282
|
limit: 500,
|
|
278
283
|
}),
|
|
@@ -288,7 +293,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
288
293
|
axisUnit: "",
|
|
289
294
|
formatValue: plain,
|
|
290
295
|
reference: null,
|
|
291
|
-
staleAfterMs:
|
|
296
|
+
staleAfterMs: MONTHLY_LATE,
|
|
292
297
|
note: "Michigan's survey of how households feel about their finances.",
|
|
293
298
|
limit: 900,
|
|
294
299
|
}),
|
|
@@ -304,7 +309,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
304
309
|
axisUnit: "%",
|
|
305
310
|
formatValue: pct,
|
|
306
311
|
reference: { value: 0, label: "flat" },
|
|
307
|
-
staleAfterMs:
|
|
312
|
+
staleAfterMs: MONTHLY_LATE,
|
|
308
313
|
note: "What households received before tax, month on month.",
|
|
309
314
|
limit: 900,
|
|
310
315
|
}),
|
|
@@ -319,7 +324,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
319
324
|
axisUnit: "%",
|
|
320
325
|
formatValue: pct,
|
|
321
326
|
reference: { value: 0, label: "flat" },
|
|
322
|
-
staleAfterMs:
|
|
327
|
+
staleAfterMs: MONTHLY_LATE,
|
|
323
328
|
note: "What households spent, the largest single component of GDP.",
|
|
324
329
|
limit: 900,
|
|
325
330
|
}),
|
|
@@ -337,7 +342,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
337
342
|
axisUnit: "",
|
|
338
343
|
formatValue: thousands,
|
|
339
344
|
reference: null,
|
|
340
|
-
staleAfterMs:
|
|
345
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
341
346
|
note: "Homes broken ground on, at an annual rate.",
|
|
342
347
|
limit: 900,
|
|
343
348
|
}),
|
|
@@ -352,7 +357,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
352
357
|
axisUnit: "",
|
|
353
358
|
formatValue: thousands,
|
|
354
359
|
reference: null,
|
|
355
|
-
staleAfterMs:
|
|
360
|
+
staleAfterMs: MONTHLY_EARLY,
|
|
356
361
|
note: "Permits issued, which lead starts by a month or two.",
|
|
357
362
|
limit: 900,
|
|
358
363
|
}),
|
|
@@ -368,7 +373,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
368
373
|
axisUnit: "",
|
|
369
374
|
formatValue: thousands,
|
|
370
375
|
reference: null,
|
|
371
|
-
staleAfterMs:
|
|
376
|
+
staleAfterMs: MONTHLY_LATE,
|
|
372
377
|
note: "Newly built homes sold, at an annual rate.",
|
|
373
378
|
limit: 900,
|
|
374
379
|
}),
|
|
@@ -458,7 +463,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
458
463
|
axisUnit: "%",
|
|
459
464
|
formatValue: pct,
|
|
460
465
|
reference: { value: 0, label: "flat" },
|
|
461
|
-
staleAfterMs:
|
|
466
|
+
staleAfterMs: MONTHLY_LATE,
|
|
462
467
|
note: "Money supply growth, which contracted outright for the first time in 2022.",
|
|
463
468
|
limit: 900,
|
|
464
469
|
}),
|
|
@@ -477,7 +482,7 @@ export const STATS: readonly StatDef[] = [
|
|
|
477
482
|
axisUnit: "",
|
|
478
483
|
formatValue: billions,
|
|
479
484
|
reference: { value: 0, label: "balanced" },
|
|
480
|
-
staleAfterMs:
|
|
485
|
+
staleAfterMs: MONTHLY_LAGGED,
|
|
481
486
|
note: "Goods and services exported less imported, persistently negative.",
|
|
482
487
|
limit: 500,
|
|
483
488
|
}),
|
|
@@ -25,3 +25,19 @@ test("daily 1Y ago clamps February 29 to February 28 instead of looking forward
|
|
|
25
25
|
const view = projectStat({ stat: findStat("ten-year"), points, trend: fitTrend(points) }, "5Y");
|
|
26
26
|
expect(view.yearAgo).toEqual({ date: "2023-02-28", value: 4.2 });
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
test("an on-schedule print is not stale until its release is overdue", () => {
|
|
30
|
+
const stale = (id: string, latest: string, now: string, months = 1) => {
|
|
31
|
+
const points = Array.from({ length: 8 }, (_, index) => {
|
|
32
|
+
const date = new Date(`${latest}T00:00:00Z`);
|
|
33
|
+
date.setUTCMonth(date.getUTCMonth() - (7 - index) * months);
|
|
34
|
+
return { date: date.toISOString().slice(0, 10), value: index };
|
|
35
|
+
});
|
|
36
|
+
return projectStat({ stat: findStat(id), points, trend: fitTrend(points) }, "5Y", { nowMs: Date.parse(`${now}T12:00:00Z`) }).observationStale;
|
|
37
|
+
};
|
|
38
|
+
expect(stale("pce-yoy", "2026-07-01", "2026-09-24")).toBe(false);
|
|
39
|
+
expect(stale("pce-yoy", "2026-07-01", "2026-10-20")).toBe(true);
|
|
40
|
+
expect(stale("cpi-yoy", "2026-08-01", "2026-10-15")).toBe(false);
|
|
41
|
+
expect(stale("real-gdp", "2026-04-01", "2026-10-29", 3)).toBe(false);
|
|
42
|
+
expect(stale("real-gdp", "2026-04-01", "2026-11-30", 3)).toBe(true);
|
|
43
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { MAX_TOOL_RESULT_BYTES } from "../cloud/askg/protocol";
|
|
3
|
+
import { equityScreenerHeadless } from "./headless";
|
|
4
|
+
import { screenFixture } from "./test-fixture";
|
|
5
|
+
|
|
6
|
+
test("a full page with more matches is neither an error nor too large for an Ask Gloom tool result", async () => {
|
|
7
|
+
const data = screenFixture();
|
|
8
|
+
const row = data.rows[0]!;
|
|
9
|
+
data.rows = Array.from({ length: 100 }, (_, index) => ({ ...row, symbol: `S${index}` }));
|
|
10
|
+
// Sparse fields make every real payload "partial"; that is not a failure.
|
|
11
|
+
Object.assign(data, { status: "partial", nextCursor: "next", universe: { ...data.universe, covered: 700, matched: 680 } });
|
|
12
|
+
const apiClient = { equityScreener: async () => structuredClone(data) };
|
|
13
|
+
const result = await equityScreenerHeadless.load({ argument: undefined, options: {} } as never, { apiClient } as never);
|
|
14
|
+
expect(result.errors).toEqual([]);
|
|
15
|
+
expect(result.complete).toBe(false);
|
|
16
|
+
expect(result.metadata).toMatchObject({ matched: 680, returned: 100, hasMore: true, notices: ["Top 100 of 680 matches; narrow the criteria to see the rest."] });
|
|
17
|
+
expect(new TextEncoder().encode(JSON.stringify(result)).byteLength).toBeLessThan(MAX_TOOL_RESULT_BYTES);
|
|
18
|
+
|
|
19
|
+
data.warnings = ["Bounded scan: some listings were not evaluated."];
|
|
20
|
+
const bounded = await equityScreenerHeadless.load({ argument: undefined, options: {} } as never, { apiClient } as never);
|
|
21
|
+
expect(bounded.errors).toEqual(data.warnings);
|
|
22
|
+
});
|
|
@@ -46,16 +46,11 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
46
46
|
: DEFAULT_SCREEN;
|
|
47
47
|
const data = await fetchScreen(definition, null, ctx.signal, ctx.apiClient);
|
|
48
48
|
const metric = (args.options.metric ?? "marketCap") as NumericField;
|
|
49
|
+
// Payload status also counts ordinary per-field sparsity; only generation
|
|
50
|
+
// warnings are failures, and more pages are not an error.
|
|
49
51
|
return {
|
|
50
|
-
complete: data.
|
|
51
|
-
errors:
|
|
52
|
-
...data.warnings,
|
|
53
|
-
...(data.nextCursor
|
|
54
|
-
? [
|
|
55
|
-
"Additional matching rows require cursor pagination through the Cloud query API or pane.",
|
|
56
|
-
]
|
|
57
|
-
: []),
|
|
58
|
-
],
|
|
52
|
+
complete: !data.nextCursor && data.warnings.length === 0,
|
|
53
|
+
errors: data.warnings,
|
|
59
54
|
sections: [
|
|
60
55
|
{
|
|
61
56
|
title: "Matching equities",
|
|
@@ -88,7 +83,20 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
88
83
|
}),
|
|
89
84
|
},
|
|
90
85
|
],
|
|
91
|
-
|
|
86
|
+
// Per-row metric provenance stays out: 100 annotated rows overflow tool results.
|
|
87
|
+
metadata: {
|
|
88
|
+
snapshot: data.snapshot,
|
|
89
|
+
definition: data.definition,
|
|
90
|
+
matched: data.universe.matched,
|
|
91
|
+
covered: data.universe.covered,
|
|
92
|
+
returned: data.rows.length,
|
|
93
|
+
hasMore: !!data.nextCursor,
|
|
94
|
+
notices: data.nextCursor
|
|
95
|
+
? [
|
|
96
|
+
`Top ${data.rows.length} of ${data.universe.matched} matches; narrow the criteria to see the rest.`,
|
|
97
|
+
]
|
|
98
|
+
: [],
|
|
99
|
+
},
|
|
92
100
|
};
|
|
93
101
|
},
|
|
94
102
|
};
|
|
@@ -254,6 +254,8 @@ export async function fetchEstimates(
|
|
|
254
254
|
}
|
|
255
255
|
const key = (symbol: string, exchange: string) =>
|
|
256
256
|
`${canonicalExchange(exchange)}:${normalizeSymbol(symbol)}`;
|
|
257
|
+
// Seeds the first paint while the mount load runs; cache age is not staleness,
|
|
258
|
+
// and a failed refresh reports its own.
|
|
257
259
|
export function cachedEstimates(symbol: string, exchange: string) {
|
|
258
260
|
const result = estimateRevisionsCache.get(key(symbol, exchange), {
|
|
259
261
|
allowExpired: true,
|
|
@@ -266,7 +268,7 @@ export function cachedEstimates(symbol: string, exchange: string) {
|
|
|
266
268
|
symbol,
|
|
267
269
|
exchange || result.data.exchange,
|
|
268
270
|
),
|
|
269
|
-
stale:
|
|
271
|
+
stale: false,
|
|
270
272
|
refreshError: null as string | null,
|
|
271
273
|
};
|
|
272
274
|
} catch {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
estimateHistory,
|
|
12
12
|
sortPeriods,
|
|
13
13
|
pinnedEstimatePeriods,
|
|
14
|
+
revisionNotices,
|
|
14
15
|
} from "./model";
|
|
15
16
|
const stats = {
|
|
16
17
|
percentile: null,
|
|
@@ -116,6 +117,12 @@ test("period identity and actual-versus-lookback boundaries reject unsafe ranks
|
|
|
116
117
|
expect(() => validateEstimates(count, "AAPL", "NASDAQ")).toThrow();
|
|
117
118
|
expect(() => validateEstimates(fixture(), "AAPL", "NYSE")).toThrow();
|
|
118
119
|
});
|
|
120
|
+
test("a young collection history alone raises no notice", () => {
|
|
121
|
+
const data = fixture();
|
|
122
|
+
for (const state of Object.values(data.sources)) state.status = "available";
|
|
123
|
+
expect(data.historyCoverage.recordedDays).toBeLessThan(20);
|
|
124
|
+
expect(revisionNotices(data)).toEqual([]);
|
|
125
|
+
});
|
|
119
126
|
test("sorting keeps missing consensus last without treating it as zero", () => {
|
|
120
127
|
const first = fixture().periods[0]!;
|
|
121
128
|
const missing = {
|
|
@@ -21,11 +21,6 @@ export function revisionNotices(data: EstimateRevisionsPayload) {
|
|
|
21
21
|
? [`${source}: ${state.reason ?? "source unavailable"}`]
|
|
22
22
|
: [],
|
|
23
23
|
),
|
|
24
|
-
...(data.historyCoverage.recordedDays < 20
|
|
25
|
-
? [
|
|
26
|
-
"Fewer than 20 actual observation days; EPS percentiles remain unavailable.",
|
|
27
|
-
]
|
|
28
|
-
: []),
|
|
29
24
|
...(data.historyCoverage.truncated
|
|
30
25
|
? ["Stored history is truncated to the latest 4,000 rows."]
|
|
31
26
|
: []),
|
|
@@ -119,7 +119,8 @@ export function resolveFuturesColumnIds(visibleIds?: readonly string[]): Futures
|
|
|
119
119
|
*
|
|
120
120
|
* The contract's own tick wins when the catalog knows it, so silver keeps its
|
|
121
121
|
* $0.005 increments instead of being rounded into two decimals with gold. The
|
|
122
|
-
* magnitude fallback only covers contracts without a declared tick
|
|
122
|
+
* magnitude fallback only covers contracts without a declared tick, and reads
|
|
123
|
+
* the previous close so a live price crossing 10 keeps its decimals.
|
|
123
124
|
*
|
|
124
125
|
* ponytail: rates render as decimals, not the 32nds tick notation traders
|
|
125
126
|
* quote (108'17). Add a tick formatter if rates users ask for it.
|
|
@@ -139,7 +140,7 @@ function priceDecimals(price: number, contract: FuturesContract): number {
|
|
|
139
140
|
*/
|
|
140
141
|
function formatContractPrice(quote: Quote, contract: FuturesContract): string {
|
|
141
142
|
if (!Number.isFinite(quote.price)) return "—";
|
|
142
|
-
const text = formatNumber(quote.price, priceDecimals(quote.price, contract));
|
|
143
|
+
const text = formatNumber(quote.price, priceDecimals(quote.previousClose ?? quote.price, contract));
|
|
143
144
|
return quote.currency === "USX" ? `${text}c` : text;
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -151,7 +152,7 @@ function formatContractPrice(quote: Quote, contract: FuturesContract): string {
|
|
|
151
152
|
function formatContractChange(quote: Quote, contract: FuturesContract): string {
|
|
152
153
|
if (!Number.isFinite(quote.change)) return "—";
|
|
153
154
|
const decimals = contract.tick || Number.isFinite(quote.price)
|
|
154
|
-
? priceDecimals(quote.price, contract)
|
|
155
|
+
? priceDecimals(quote.previousClose ?? quote.price, contract)
|
|
155
156
|
: 2;
|
|
156
157
|
const text = formatNumber(Math.abs(quote.change), decimals);
|
|
157
158
|
return `${quote.change >= 0 ? "+" : "-"}${text}`;
|
|
@@ -60,7 +60,8 @@ export async function fetchFuturesCurve(root: string, client: Pick<typeof apiCli
|
|
|
60
60
|
|
|
61
61
|
export function getCachedFuturesCurve(root: string): FuturesCurvePayload | null {
|
|
62
62
|
const cached = futuresCurveCache.get(root, { allowExpired: true });
|
|
63
|
-
|
|
63
|
+
// The pane revalidates this copy on mount; only the source or a failed refresh makes it stale.
|
|
64
|
+
return cached?.data ?? null;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export async function loadFuturesCurve(root: string, force = false): Promise<FuturesCurvePayload> {
|
|
@@ -74,11 +74,13 @@ test("charts the strip within the horizon and dates it by its freshest quote, no
|
|
|
74
74
|
expect(newestQuote([])).toBeNull();
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
test("Treasury prices
|
|
77
|
+
test("Treasury prices keep one decimal count per root on their 32nd tick grid and contracts read by delivery month", () => {
|
|
78
78
|
expect(curvePrice(105.265625, "ZN")).toBe("105.265625");
|
|
79
|
-
expect(curvePrice(
|
|
80
|
-
expect(curvePrice(
|
|
81
|
-
expect(curvePrice(
|
|
79
|
+
expect(curvePrice(105.5, "ZN")).toBe("105.500000");
|
|
80
|
+
expect(curvePrice(101.6796875, "ZT")).toBe("101.67968750");
|
|
81
|
+
expect(curvePrice(108, "ZB")).toBe("108.00000");
|
|
82
|
+
expect(curvePrice(-0.1875, "ZN")).toBe("-0.187500");
|
|
83
|
+
expect(curvePrice(-0.001, "CL")).toBe("0.00");
|
|
82
84
|
expect(curvePrice(0.00635, "6J")).toBe("0.0063500");
|
|
83
85
|
// Crude's November contract expires in October.
|
|
84
86
|
expect(curveContractMonth("CLX26.NYM", "2026-10-20")).toBe("Nov 26");
|
|
@@ -15,22 +15,26 @@ export function normalizeCurveRoot(value: unknown): string | null {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Treasury futures trade in fractions of a 32nd (ZT to 1/256, ZF 1/128, ZN
|
|
18
|
-
* 1/64, ZB and UB 1/32)
|
|
19
|
-
*
|
|
18
|
+
* 1/64, ZB and UB 1/32), so the catalogue leaves their tick unset. A root
|
|
19
|
+
* shows every price at the decimals its tick needs to stay exact (ZT 8, ZN 6),
|
|
20
|
+
* so a column and the M2-M1 spread keep one width: 103.50000000 next to
|
|
21
|
+
* 103.51562500, never 103.50 next to 103.515625.
|
|
20
22
|
*/
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const RATE_TICKS: Readonly<Record<string, number>> = { ZT: 1 / 256, ZF: 1 / 128, ZN: 1 / 64, ZB: 1 / 32, UB: 1 / 32 };
|
|
24
|
+
|
|
25
|
+
function curvePriceDecimals(root: string): number {
|
|
26
|
+
const rateTick = RATE_TICKS[root];
|
|
27
|
+
if (rateTick != null) return tickDecimals(rateTick);
|
|
28
|
+
if (root === "VX") return 4;
|
|
29
|
+
const tick = FUTURES_CONTRACTS.find((row) => row.code === root)?.tick;
|
|
30
|
+
return tick == null ? 5 : Math.max(2, tickDecimals(tick));
|
|
31
|
+
}
|
|
27
32
|
|
|
28
33
|
export function curvePrice(value: number | null, root: string): string {
|
|
29
34
|
if (value == null) return "--";
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return value.toFixed(root === "VX" ? 4 : tick == null ? 5 : Math.max(2, tickDecimals(tick)));
|
|
35
|
+
const text = value.toFixed(curvePriceDecimals(root));
|
|
36
|
+
// A spread that rounds to zero is unsigned: 0.00, never -0.00.
|
|
37
|
+
return /[1-9]/.test(text) ? text : text.replace("-", "");
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -38,9 +42,9 @@ export function curvePrice(value: number | null, root: string): string {
|
|
|
38
42
|
* decimal count across the gutter, only as many as those values use.
|
|
39
43
|
*/
|
|
40
44
|
export function curveAxisPrice(value: number, domain: CompositeAxisDomain, root: string): string {
|
|
41
|
-
if (
|
|
45
|
+
if (RATE_TICKS[root] == null) return curvePrice(value, root);
|
|
42
46
|
const decimals = Math.max(0, ...compositeAxisTicks(domain, String)
|
|
43
|
-
.map((tick) =>
|
|
47
|
+
.map((tick) => tick.value.toFixed(4).replace(/\.?0+$/, "").split(".")[1]?.length ?? 0));
|
|
44
48
|
return value.toFixed(decimals);
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -7,6 +7,7 @@ import { usePaneInstance, usePaneTitle } from "../../../state/app/context";
|
|
|
7
7
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
8
8
|
import type { PaneProps } from "../../../types/plugin";
|
|
9
9
|
import { Box } from "../../../ui";
|
|
10
|
+
import { formatPercentRaw } from "../../../utils/format";
|
|
10
11
|
import { useAutoRefresh } from "../shared/auto-refresh";
|
|
11
12
|
import { futuresSessionRefreshInterval } from "../shared/futures-session";
|
|
12
13
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
@@ -24,7 +25,7 @@ const COLUMNS: DataTableColumn[] = [
|
|
|
24
25
|
{ id: "asOf", label: "AS OF UTC", width: 16, align: "left" },
|
|
25
26
|
];
|
|
26
27
|
const clearDenied = (error: unknown) => error instanceof ApiRequestError && [401, 403].includes(error.status ?? 0);
|
|
27
|
-
const signedPercent = (value: number | null) => value == null ? "--" :
|
|
28
|
+
const signedPercent = (value: number | null) => value == null ? "--" : formatPercentRaw(value);
|
|
28
29
|
const integer = (value: number | null) => value == null ? "--" : value.toLocaleString("en-US");
|
|
29
30
|
|
|
30
31
|
export function FuturesCurvePane(props: PaneProps) {
|
|
@@ -83,7 +84,6 @@ function FuturesCurveView({ width, height, focused, root }: PaneProps & { root:
|
|
|
83
84
|
info: data ? [
|
|
84
85
|
{ id: "source", parts: [{ text: `${data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${data.quoteUnit ?? data.currency ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] },
|
|
85
86
|
...(staleCount ? [{ id: "stale", parts: [{ text: `${staleCount} of ${data.contracts.length} stale`, tone: "warning" as const }] }] : []),
|
|
86
|
-
...(data.status !== "available" ? [{ id: "partial", parts: [{ text: data.status, tone: "warning" as const }] }] : []),
|
|
87
87
|
] : [],
|
|
88
88
|
});
|
|
89
89
|
const renderCell = useCallback((row: FuturesContract, column: DataTableColumn) => {
|