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.
Files changed (189) hide show
  1. package/bin/gloomberb +9 -1
  2. package/package.json +1 -1
  3. package/src/api-client/central-bank-rates.ts +3 -0
  4. package/src/api-client/types.ts +1 -1
  5. package/src/app/pane-runtime/index.tsx +7 -5
  6. package/src/app/runtime/broker-import.test.tsx +89 -0
  7. package/src/app/runtime/broker-import.ts +29 -8
  8. package/src/brokers/sync-broker-instance.ts +67 -0
  9. package/src/cli/commands/overview.ts +6 -2
  10. package/src/cli/native-smoke.ts +2 -0
  11. package/src/components/chart/composite/composite-chart.tsx +21 -1
  12. package/src/components/chart/composite/format.ts +71 -8
  13. package/src/components/chart/composite/fx-precision.test.ts +21 -3
  14. package/src/components/chart/composite/scene.ts +13 -1
  15. package/src/components/chart/composite/types.ts +4 -0
  16. package/src/components/layout/market-summary.ts +4 -2
  17. package/src/components/ticker/badge/format.test.ts +3 -3
  18. package/src/components/ticker/badge/format.ts +11 -9
  19. package/src/components/ticker/badge/text.test.tsx +6 -6
  20. package/src/components/ui/index.ts +2 -1
  21. package/src/components/ui/multi-select/dialog.tsx +46 -4
  22. package/src/components/ui/number-prompt-dialog.tsx +71 -0
  23. package/src/core/state/app/state.ts +8 -2
  24. package/src/market-data/coordinator/events.ts +5 -0
  25. package/src/market-data/coordinator/index.ts +44 -10
  26. package/src/market-data/market/format.test.ts +54 -0
  27. package/src/market-data/market/format.ts +164 -5
  28. package/src/market-data/query-store.test.ts +87 -0
  29. package/src/market-data/query-store.ts +66 -9
  30. package/src/plugins/builtin/alerts/format.ts +4 -3
  31. package/src/plugins/builtin/analytics/display.ts +3 -3
  32. package/src/plugins/builtin/analytics/index.test.tsx +22 -22
  33. package/src/plugins/builtin/analytics/pane-model.test.ts +10 -10
  34. package/src/plugins/builtin/analytics/pane-model.ts +8 -8
  35. package/src/plugins/builtin/analytics/portfolio-identity.test.tsx +6 -6
  36. package/src/plugins/builtin/analytics/risk-client.ts +6 -8
  37. package/src/plugins/builtin/analytics/risk-model.test.ts +21 -2
  38. package/src/plugins/builtin/analytics/risk-model.ts +8 -0
  39. package/src/plugins/builtin/analytics/view.tsx +2 -2
  40. package/src/plugins/builtin/backtest/engine.test.ts +0 -1
  41. package/src/plugins/builtin/backtest/engine.ts +1 -4
  42. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  43. package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
  44. package/src/plugins/builtin/central-bank-rates/client.ts +2 -1
  45. package/src/plugins/builtin/central-bank-rates/headless.ts +2 -2
  46. package/src/plugins/builtin/central-bank-rates/model.test.ts +16 -1
  47. package/src/plugins/builtin/central-bank-rates/model.ts +4 -4
  48. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -2
  49. package/src/plugins/builtin/chart-composer/pane.tsx +50 -3
  50. package/src/plugins/builtin/chart-composer/presets.test.ts +17 -0
  51. package/src/plugins/builtin/chart-composer/presets.ts +48 -0
  52. package/src/plugins/builtin/chart-composer/settings.ts +42 -11
  53. package/src/plugins/builtin/chat/chat.test.tsx +2 -2
  54. package/src/plugins/builtin/cloud/askg/pane.tsx +1 -1
  55. package/src/plugins/builtin/cloud/thesis/board-pane.test.tsx +25 -0
  56. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +3 -2
  57. package/src/plugins/builtin/cloud/thesis/store.ts +2 -2
  58. package/src/plugins/builtin/congress-trades/detail.tsx +1 -1
  59. package/src/plugins/builtin/congress-trades/table.tsx +1 -1
  60. package/src/plugins/builtin/cot/client.ts +2 -1
  61. package/src/plugins/builtin/cot/model.test.ts +9 -0
  62. package/src/plugins/builtin/cot/model.ts +30 -1
  63. package/src/plugins/builtin/cot/pane.tsx +2 -4
  64. package/src/plugins/builtin/credit-conditions/client.ts +2 -1
  65. package/src/plugins/builtin/crypto-board/client.ts +2 -1
  66. package/src/plugins/builtin/crypto-board/model.test.ts +9 -0
  67. package/src/plugins/builtin/crypto-board/model.ts +11 -3
  68. package/src/plugins/builtin/custom-view/live-quotes.test.ts +6 -1
  69. package/src/plugins/builtin/custom-view/live-quotes.ts +26 -0
  70. package/src/plugins/builtin/custom-view/pane.tsx +18 -7
  71. package/src/plugins/builtin/custom-view/view-spec.test.ts +24 -6
  72. package/src/plugins/builtin/custom-view/view-spec.ts +53 -21
  73. package/src/plugins/builtin/debt-maturities/client.ts +16 -3
  74. package/src/plugins/builtin/debt-maturities/debt-maturities.test.ts +8 -0
  75. package/src/plugins/builtin/debt-maturities/headless.ts +3 -2
  76. package/src/plugins/builtin/debt-maturities/model.ts +2 -1
  77. package/src/plugins/builtin/debt-maturities/pane.tsx +0 -8
  78. package/src/plugins/builtin/earnings-calls/pane.test.tsx +14 -0
  79. package/src/plugins/builtin/earnings-calls/pane.tsx +3 -1
  80. package/src/plugins/builtin/econ/fred-client.test.ts +2 -0
  81. package/src/plugins/builtin/econ/fred-series-map.ts +0 -1
  82. package/src/plugins/builtin/econ/index.tsx +4 -2
  83. package/src/plugins/builtin/econ-statistics/pane.tsx +4 -2
  84. package/src/plugins/builtin/econ-statistics/stats.ts +29 -24
  85. package/src/plugins/builtin/econ-statistics/view.test.ts +16 -0
  86. package/src/plugins/builtin/equity-screener/headless.test.ts +22 -0
  87. package/src/plugins/builtin/equity-screener/headless.ts +18 -10
  88. package/src/plugins/builtin/estimate-revisions/client.ts +3 -1
  89. package/src/plugins/builtin/estimate-revisions/model.test.ts +7 -0
  90. package/src/plugins/builtin/estimate-revisions/model.ts +0 -5
  91. package/src/plugins/builtin/futures/table.tsx +4 -3
  92. package/src/plugins/builtin/futures-curve/client.ts +2 -1
  93. package/src/plugins/builtin/futures-curve/model.test.ts +6 -4
  94. package/src/plugins/builtin/futures-curve/model.ts +18 -14
  95. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  96. package/src/plugins/builtin/fx-matrix/index.tsx +21 -4
  97. package/src/plugins/builtin/fx-matrix/live-legs.ts +7 -0
  98. package/src/plugins/builtin/fx-matrix/pairs.test.ts +5 -0
  99. package/src/plugins/builtin/fx-matrix/pairs.ts +9 -3
  100. package/src/plugins/builtin/insider/index.tsx +15 -12
  101. package/src/plugins/builtin/insider/pane.test.tsx +24 -0
  102. package/src/plugins/builtin/market-movers/headless.ts +2 -2
  103. package/src/plugins/builtin/market-movers/model.ts +23 -10
  104. package/src/plugins/builtin/market-movers/screener.test.ts +5 -1
  105. package/src/plugins/builtin/market-movers/screener.ts +3 -0
  106. package/src/plugins/builtin/market-movers/table.ts +2 -2
  107. package/src/plugins/builtin/market-valuation/client.test.ts +3 -0
  108. package/src/plugins/builtin/market-valuation/client.ts +6 -1
  109. package/src/plugins/builtin/money-markets/client.ts +2 -1
  110. package/src/plugins/builtin/money-markets/pane.tsx +0 -1
  111. package/src/plugins/builtin/options/expiry-selection.test.tsx +1 -1
  112. package/src/plugins/builtin/options/footer.test.ts +2 -2
  113. package/src/plugins/builtin/options/footer.ts +2 -2
  114. package/src/plugins/builtin/options/market-reference.test.ts +2 -2
  115. package/src/plugins/builtin/options/market-reference.ts +14 -2
  116. package/src/plugins/builtin/options/table.ts +5 -8
  117. package/src/plugins/builtin/options/view.test.tsx +2 -2
  118. package/src/plugins/builtin/options/view.tsx +1 -1
  119. package/src/plugins/builtin/options-calculator/pane.test.tsx +6 -6
  120. package/src/plugins/builtin/options-calculator/pane.tsx +23 -8
  121. package/src/plugins/builtin/options-calculator/surface.test.ts +16 -1
  122. package/src/plugins/builtin/options-calculator/surface.ts +13 -6
  123. package/src/plugins/builtin/portfolio-list/column-values.ts +17 -9
  124. package/src/plugins/builtin/portfolio-list/index.test.ts +5 -5
  125. package/src/plugins/builtin/portfolio-list/metrics.test.ts +8 -8
  126. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +12 -12
  127. package/src/plugins/builtin/portfolio-list/position-cost-integrity.test.ts +2 -2
  128. package/src/plugins/builtin/portfolio-list/summary/index.tsx +7 -6
  129. package/src/plugins/builtin/rate-path/client.ts +2 -1
  130. package/src/plugins/builtin/rate-path/headless.ts +1 -1
  131. package/src/plugins/builtin/rate-path/pane.tsx +0 -1
  132. package/src/plugins/builtin/realized-vol/client.test.ts +7 -0
  133. package/src/plugins/builtin/realized-vol/headless.ts +1 -1
  134. package/src/plugins/builtin/realized-vol/model.test.ts +16 -4
  135. package/src/plugins/builtin/realized-vol/model.ts +24 -9
  136. package/src/plugins/builtin/relative-rotation/client.ts +2 -1
  137. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -10
  138. package/src/plugins/builtin/risk-factors/data.test.ts +7 -0
  139. package/src/plugins/builtin/risk-factors/data.ts +8 -1
  140. package/src/plugins/builtin/risk-factors/pane.test.tsx +3 -0
  141. package/src/plugins/builtin/shared/screener-live-quotes.test.ts +3 -3
  142. package/src/plugins/builtin/shared/screener-live-quotes.ts +16 -1
  143. package/src/plugins/builtin/short-volume/client.ts +2 -1
  144. package/src/plugins/builtin/short-volume/pane.tsx +0 -1
  145. package/src/plugins/builtin/thirteenf/headless.ts +3 -3
  146. package/src/plugins/builtin/thirteenf/model.ts +14 -4
  147. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +12 -5
  148. package/src/plugins/builtin/thirteenf/overlap.test.ts +13 -1
  149. package/src/plugins/builtin/thirteenf/overlap.ts +31 -3
  150. package/src/plugins/builtin/thirteenf/pane.test.tsx +2 -1
  151. package/src/plugins/builtin/thirteenf/pane.tsx +12 -9
  152. package/src/plugins/builtin/thirteenf/table.ts +9 -3
  153. package/src/plugins/builtin/ticker-detail/financials/model.ts +0 -12
  154. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -1
  155. package/src/plugins/builtin/ticker-detail/index.test.tsx +1 -1
  156. package/src/plugins/builtin/ticker-detail/overview/components.tsx +9 -10
  157. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  158. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +6 -9
  159. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +3 -2
  160. package/src/plugins/builtin/time-sales/model.test.ts +17 -1
  161. package/src/plugins/builtin/time-sales/model.ts +14 -3
  162. package/src/plugins/builtin/time-sales/pane.tsx +8 -4
  163. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  164. package/src/plugins/builtin/vol-surface/client.ts +4 -1
  165. package/src/plugins/builtin/vol-surface/model.test.ts +4 -2
  166. package/src/plugins/builtin/vol-surface/model.ts +14 -2
  167. package/src/plugins/builtin/vol-surface/pane.tsx +9 -2
  168. package/src/plugins/builtin/volatility/client.test.ts +13 -8
  169. package/src/plugins/builtin/volatility/client.ts +3 -2
  170. package/src/plugins/builtin/volatility/headless.test.ts +2 -2
  171. package/src/plugins/builtin/volatility/model.test.ts +4 -0
  172. package/src/plugins/builtin/volatility/model.ts +6 -7
  173. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -2
  174. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +33 -1
  175. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +34 -1
  176. package/src/renderers/electrobun/bun/index.ts +2 -1
  177. package/src/renderers/opentui/render-timing-smoke.ts +20 -0
  178. package/src/sources/provider-router/profile-enrichment.test.ts +0 -3
  179. package/src/sources/yahoo-finance.test.ts +5 -1
  180. package/src/sources/yahoo-finance.ts +2 -1
  181. package/src/time-series/forward-valuation.ts +6 -0
  182. package/src/time-series/resolve.ts +5 -1
  183. package/src/time-series/valuation-currency.test.ts +2 -0
  184. package/src/time-series/valuation-currency.ts +3 -1
  185. package/src/utils/exchange-rate-snapshot.test.ts +12 -1
  186. package/src/utils/exchange-rate-snapshot.ts +3 -2
  187. package/src/utils/format.ts +55 -20
  188. package/src/utils/fx-market-hours.test.ts +28 -0
  189. 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.warnings,
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 active failures.
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" || lookupState === "none") {
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
- ...(stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
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, stale, staleness]);
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
- if (selected.observationStale || selected.cacheStale) {
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
- const MONTHLY = 75 * DAY_MS;
8
- const QUARTERLY = 200 * DAY_MS;
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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: MONTHLY,
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.status === "available" && !data.nextCursor,
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
- metadata: { ...data },
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: result.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
- return cached ? { ...cached.data, stale: cached.data.stale || cached.stale } : null;
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 stay on their 32nd tick grid and contracts read by delivery month", () => {
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(101.6796875, "ZT")).toBe("101.6796875");
80
- expect(curvePrice(108, "ZB")).toBe("108.00");
81
- expect(curvePrice(-0.1875, "ZN")).toBe("-0.1875");
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): the decimals that hold every tick exactly. A fixed
19
- * five decimals put 105.265625 off the tick grid at 105.26563.
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 RATE_TICK_DECIMALS: Readonly<Record<string, number>> = { ZT: 8, ZF: 7, ZN: 6, ZB: 5, UB: 5 };
22
- const trimZeros = (text: string, minimum: number) => {
23
- const [whole, fraction = ""] = text.split(".");
24
- const kept = fraction.replace(/0+$/, "").padEnd(minimum, "0");
25
- return kept ? `${whole}.${kept}` : whole!;
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 rateDecimals = RATE_TICK_DECIMALS[root];
31
- if (rateDecimals != null) return trimZeros(value.toFixed(rateDecimals), 2);
32
- const tick = FUTURES_CONTRACTS.find((row) => row.code === root)?.tick;
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 (RATE_TICK_DECIMALS[root] == null) return curvePrice(value, root);
45
+ if (RATE_TICKS[root] == null) return curvePrice(value, root);
42
46
  const decimals = Math.max(0, ...compositeAxisTicks(domain, String)
43
- .map((tick) => trimZeros(tick.value.toFixed(4), 0).split(".")[1]?.length ?? 0));
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 ? "--" : `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
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) => {