diff --git a/README.md b/README.md index 0ce7872..7ed6400 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Live: **[gridtilt.com](https://gridtilt.com)** | Module | What's inside | |---|---| -| **Tilt Overview** | Top movers, sector pulse, catalyst calendar, US electricity demand chart (2010 → 2030 projection), thesis-health KPIs. | +| **Tilt Overview** | The buildout scoreboard (signed nuclear GW, DC pipeline GW, interconnection queue, grid pulse), top movers, catalyst calendar, US electricity demand chart (2010 → 2030 projection). | | **The Stack** | 100 tickers across 13 supply-chain layers (compute, nuclear, uranium, power hardware, utilities, data-center REITs, construction, mining, natural gas, renewables, grid hardware, crypto/AI DC, ETF benchmarks). | | **Power Map** | US data center locations with power capacity and the utility / RTO they sit on. | | **Supply Chain** | D3 force graph of 24 nodes and 52 real supply relationships from raw materials to end-use compute. | @@ -32,23 +32,27 @@ Live: **[gridtilt.com](https://gridtilt.com)** | Physical electricity output | FRED [`IPG2211A2N`](https://fred.stlouisfed.org/series/IPG2211A2N) served live at `/api/physical/electricity-output`; EIA US48 hourly demand at `/api/physical/load-hourly` once `EIA_API_KEY` is set | Live (FRED daily cache; EIA 30-min cache) | | Data center locations | Public announcements (Microsoft, Google, Amazon, Meta, Apple, xAI, OpenAI, Oracle), curated through a reviewed RSS ingestion pipeline | Curated, refreshed as announcements land | | Industry news | Live RSS from 8 publications | Live, refreshed hourly | -| AI Demand, Grid Stress, NPI | Composite indices computed from constituent **equity moves** (NPI also uses uranium spot and a hand-derived policy score). They are market-based gauges, **not** physical grid measurements. See [Index methodology](#index-methodology). | Live, with labeled static fallback | +| Buildout scoreboard (nuclear deals, DC pipeline, queue, capex) | Curated datasets in `server/data/` summed in real units; every group carries its source and as-of date. See [The scoreboard](#the-scoreboard). | Curated, refreshed as deals and filings land | No proprietary data feeds and no scraped paywalled sources. All projections are clearly labeled as such. --- -## Index methodology +## The scoreboard -The three headline indices are deterministic functions of their published constituents. Exact formulas, so you can check the math: +The headline numbers are sums over curated datasets, in real units. No baselines, no clamps, no index anchors, nothing rebased to 100. -- **AI Demand** = clamp(52–94, `72 + 1.2 × (NVDA% × 0.40 + TSM% × 0.25 + EQIX% × 0.20 + MU% × 0.15)`) using today's intraday percent changes. It reads how the market is pricing the AI-buildout complex **today**; it does not measure data-center load. -- **Grid Stress** = clamp(52–92, `68 + (VST% × 0.40 + CEG% × 0.35 + EQIX% × 0.25)`). Same construction; EQIX appears in both baskets. It reads power-equity momentum, not reserve margins or LMPs. -- **NPI (Nuclear Power Index)** = `100 × (0.25·CEG + 0.20·VST + 0.15·CCJ + 0.20·NLR + 0.10·uranium spot + 0.10·policy)` as price relatives to Jan 1, 2024 bases, times a 0.9–1.1 policy multiplier from a hand-derived SMR policy score. Weights are judgment calls and labeled as such. +- **Nuclear-for-AI, signed** = sum of `capacityMW` over active, datacenter-relevant nuclear projects marked `firmness: "signed"` in `server/data/interconnection-queue.json`. Signed means executed contracts and restarts underway. Options, proposals, and aggregate LOI pipelines live in separate buckets and never inflate the headline. +- **DC pipeline** = sums by build status over `server/data/datacenters.json` (tracked US sites at 400 MW or more; a curated registry, not a census), plus disclosed FY2025 hyperscaler capex with per-company source links. +- **Interconnection queue** = LBNL "Queued Up" headline stats plus ISO filings, with as-of dates shipped in the data. +- **Grid pulse** = live US48 demand from EIA's hourly grid monitor (free key) and year-over-year US electric output from FRED `IPG2211A2N`. Measurements, not sentiment. +- The one market element left is a single line: an equal-weight mean of today's percent moves across the tracked tickers, with stale tickers excluded and the live count disclosed. A percent, never a level. -Baselines (72, 68) and clamps are presentation choices that keep the gauges readable; they are disclosed here so nobody mistakes them for measurements. Constituent values are exposed at `/api/kpis`. +Everything is served at `/api/metrics` with a source and as-of per group; daily snapshots append at `/api/metrics/history`. -**Validation:** the gauges are backtested against physical electricity output (FRED `IPG2211A2N`, 2019–2026, leads 0–3 months) in [docs/INDEX_VALIDATION.md](./docs/INDEX_VALIDATION.md). Result: neither AI Demand nor Grid Stress shows a physical signal, so both are labeled **market sentiment gauges** in the UI, not measurements. Reproduce it yourself: `npm run backtest:indices`. The reconstructed daily series lives in `server/data/index-history.json`. +### What happened to the indices + +GridTilt used to headline three composite indices (AI Demand, Grid Stress, NPI). We backtested them against physical electricity output (FRED, 2019–2026) and published the result: no physical signal at any lead, and NPI moved at r = 0.95 with a single constituent stock. So on 2026-06-10 we retired them and replaced them with the real numbers above. The study stays public in [docs/INDEX_VALIDATION.md](./docs/INDEX_VALIDATION.md), the formulas remain in `server/indices.ts`, the archived daily series is still served at `/api/index-history`, and `npm run backtest:indices` still reproduces it from public prices. --- diff --git a/client/src/components/home/Hero.tsx b/client/src/components/home/Hero.tsx index 8c65420..1baea75 100644 --- a/client/src/components/home/Hero.tsx +++ b/client/src/components/home/Hero.tsx @@ -1,7 +1,7 @@ import { Link } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { Wordmark } from "./Wordmark"; -import type { KpiData } from "@/lib/types"; +import type { MetricsSummary } from "@/lib/types"; import logoPath from "@assets/Image_[Vectorized]_(2)_1773890483514.png"; import powerMapSvg from "@assets/previews/power-map.svg"; @@ -19,14 +19,12 @@ function formatRefreshTimeFromIso(iso: string | undefined): string | null { } export function Hero() { - const { data } = useQuery({ - queryKey: ["/api/kpis"], + const { data } = useQuery({ + queryKey: ["/api/metrics"], refetchInterval: 5 * 60_000, refetchIntervalInBackground: false, }); const refresh = formatRefreshTimeFromIso(data?.asOf); - const sourceKnown = data?.source === "live" || data?.source === "static"; - const isLive = data?.source === "live"; return (
- {sourceKnown && refresh && ( + {data && refresh && ( - {isLive ? "data refreshed" : "static fallback"} {refresh} + {data.nuclear.signedGW} GW signed nuclear · {data.pipeline.constructionGW} GW dc construction · {data.backlog.queueOverallGW.toLocaleString()} GW queued +
+ scoreboard refreshed {refresh}
)} diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index 3b7653e..56c6582 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -37,18 +37,11 @@ export interface AllCatalystsResponse { items: MergedCatalystItem[]; } -export interface KpiData { - aiPowerIndex: number; - npiValue: number; - gridStress: number; - smrPolicyScore: number; - npiBaseDate: string; - source?: "live" | "static"; - asOf?: string; - constituents: { - nvdaChange: number; tsmChange: number; eqixChange: number; muChange: number; - cegPerf: number; vstPerf: number; ccjPerf: number; nlrPerf: number; - uPerf: number; policyPerf: number; npiPolicyMultiplier: number; npiMomentum: number; - vstChange: number; cegChange: number; - }; +// Minimal shape of GET /api/metrics for surfaces that only need the +// headline numbers (the dashboard keeps its richer local interface). +export interface MetricsSummary { + nuclear: { signedGW: number; announcedGW: number; signedDeals: number; totalDeals: number }; + pipeline: { operationalGW: number; constructionGW: number; announcedGW: number; siteCount: number }; + backlog: { queueOverallGW: number; medianWaitMonths: number }; + asOf: string; } diff --git a/client/src/pages/TheStack.tsx b/client/src/pages/TheStack.tsx index 8b77a31..0baa666 100644 --- a/client/src/pages/TheStack.tsx +++ b/client/src/pages/TheStack.tsx @@ -8,18 +8,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { - LineChart, - Line, - ResponsiveContainer, - ScatterChart, - Scatter, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, -} from "recharts"; +import { LineChart, Line, ResponsiveContainer } from "recharts"; import { Cpu, Server, Zap, TrendingUp, TrendingDown, Info, Clock } from "lucide-react"; interface StockData { @@ -37,11 +26,6 @@ interface StockData { stale?: boolean; } -interface CorrelationPoint { - uranium: number; - ccj: number; -} - interface StackData { compute: StockData[]; nuclear: StockData[]; @@ -56,9 +40,6 @@ interface StackData { transmissionGrid: StockData[]; cryptoAIDC: StockData[]; etfsBenchmarks: StockData[]; - correlation: CorrelationPoint[]; - correlationCoeff: number; - cegCorrelationCoeff: number; } function Sparkline({ data, color }: { data: number[] | undefined; color: string }) { @@ -182,47 +163,10 @@ function StockCardSkeleton() { ); } -const CustomScatterTooltip = ({ active, payload }: any) => { - if (active && payload && payload.length) { - return ( -
-

Uranium Spot: ${payload[0]?.value?.toFixed(2)}/lb

-

CCJ: ${payload[1]?.value?.toFixed(2)}

-
- ); - } - return null; -}; - -// Compute OLS regression line + confidence band from scatter data -function computeRegression(points: { uranium: number; ccj: number }[]) { - if (!points || points.length < 3) return { line: [], upper: [], lower: [] }; - const n = points.length; - const xs = points.map((p) => p.uranium); - const ys = points.map((p) => p.ccj); - const meanX = xs.reduce((s, v) => s + v, 0) / n; - const meanY = ys.reduce((s, v) => s + v, 0) / n; - const sxx = xs.reduce((s, x) => s + (x - meanX) ** 2, 0); - const sxy = xs.reduce((s, x, i) => s + (x - meanX) * (ys[i] - meanY), 0); - const slope = sxy / sxx; - const intercept = meanY - slope * meanX; - const residuals = xs.map((x, i) => ys[i] - (slope * x + intercept)); - const se = Math.sqrt(residuals.reduce((s, r) => s + r * r, 0) / (n - 2)); - const minX = Math.min(...xs); - const maxX = Math.max(...xs); - const steps = 30; - const line = []; - const upper = []; - const lower = []; - for (let i = 0; i <= steps; i++) { - const x = minX + ((maxX - minX) * i) / steps; - const fit = slope * x + intercept; - line.push({ uranium: parseFloat(x.toFixed(2)), ccj: parseFloat(fit.toFixed(2)) }); - upper.push({ uranium: parseFloat(x.toFixed(2)), ccj: parseFloat((fit + 1.5 * se).toFixed(2)) }); - lower.push({ uranium: parseFloat(x.toFixed(2)), ccj: parseFloat((fit - 1.5 * se).toFixed(2)) }); - } - return { line, upper, lower }; -} +// (The uranium-vs-CCJ/CEG correlation scatter that lived here was removed on +// 2026-06-10: the server generated its points with Math.random tuned to a +// target Pearson r. No honest free daily uranium series exists to draw the +// real chart, so it is gone rather than faked.) type Timeframe = "1D" | "5D" | "1M"; type SortBy = "change" | "marketcap" | "alpha"; @@ -258,11 +202,6 @@ export default function TheStack() { refetchInterval: 900000, }); - const regression = useMemo( - () => computeRegression(data?.correlation ?? []), - [data?.correlation] - ); - const layerConfig = [ { key: "compute", @@ -482,136 +421,6 @@ export default function TheStack() { ); })} - {/* Uranium vs CCJ Correlation scatter */} -
- -
-
-
-

Uranium Spot vs. CCJ Correlation

- - - - - -

CCJ is the largest public uranium miner with the highest direct spot price beta. CEG (utility) is influenced by electricity contracts and regulated returns. CCJ = commodity bet, CEG = infrastructure bet.

-
-
-
-

52-week uranium spot price ($/lb) vs. CCJ stock price. Each dot = one week.

-
-
- {data?.correlationCoeff !== undefined && ( -
-

CCJ Pearson r

-

{data.correlationCoeff.toFixed(3)}

-

- {data.correlationCoeff > 0.7 ? "Strong" : data.correlationCoeff > 0.4 ? "Moderate" : "Weak"} correlation -

-
- )} - {data?.cegCorrelationCoeff !== undefined && ( -
-

CEG Pearson r

-

{data.cegCorrelationCoeff.toFixed(3)}

-

Utility beta

-
- )} -
-
- - {isLoading ? ( - - ) : ( - <> - - - - - - } /> - {/* Upper confidence band */} - null as any} - legendType="none" - name="Upper Band" - /> - {/* Lower confidence band */} - null as any} - legendType="none" - name="Lower Band" - /> - {/* OLS regression line */} - null as any} - legendType="none" - name="OLS Fit" - /> - {/* Raw scatter dots */} - - - -
-
-
- Weekly observation -
-
-
- OLS trend line -
-
-
- ±1.5σ channel -
-
- - )} - -
-

- CCJ (pure miner) has higher uranium spot beta. Its P&L moves directly with U3O8 pricing. -

-

- CEG (nuclear utility) is influenced by electricity contracts and regulated returns. Smoother, less volatile nuclear exposure. -

-
- -
); diff --git a/client/src/pages/TiltOverview.tsx b/client/src/pages/TiltOverview.tsx index 7264f11..3bd0063 100644 --- a/client/src/pages/TiltOverview.tsx +++ b/client/src/pages/TiltOverview.tsx @@ -60,21 +60,30 @@ const annotations = [ { year: "2024", label: "TMI restart + SMR deal", color: "rgba(240,165,0,0.5)" }, ]; -interface KpiData { - aiPowerIndex: number; - npiValue: number; - gridStress: number; - smrPolicyScore: number; - npiBaseDate: string; - constituents: { - // AI Power Index constituents (intraday % change signals) - nvdaChange: number; tsmChange: number; eqixChange: number; muChange: number; - // NPI constituents (price performance since Jan 1, 2024 base) - cegPerf: number; vstPerf: number; ccjPerf: number; nlrPerf: number; - uPerf: number; policyPerf: number; npiPolicyMultiplier: number; npiMomentum: number; - // Grid Stress signals - vstChange: number; cegChange: number; +// Shape of GET /api/metrics: the buildout scoreboard that replaced the +// retired market-sentiment gauges on 2026-06-10. Real units, sourced. +interface MetricsData { + nuclear: { + signedGW: number; announcedGW: number; aggregateGW: number; + signedDeals: number; totalDeals: number; + uraniumSpot: { usdPerLb: number; asOf: string; source: string }; + source: string; asOf: string; }; + pipeline: { + operationalGW: number; constructionGW: number; announcedGW: number; + siteCount: number; + capex?: { totalUsdBillions: number; asOf?: string }; + source: string; + }; + backlog: { + queueOverallGW: number; queueOverallProjects: number; medianWaitMonths: number; + historicalWithdrawalPct: number; ercotLargeLoadGW: number; + ercotLargeLoadDataCenterPct: number; pjmReopenedGW: number; + asOf: string; sourceUrl: string; + }; + gridPulse: { currentGW?: number; atUtc?: string; outputYoYPct?: number; outputMonth?: string } | null; + market: { allPct: number; allCount: number; allTotal: number; nuclearPct: number | null; nuclearCount: number } | null; + asOf: string; } interface TopMover { @@ -259,69 +268,6 @@ function KpiCard({ ); } -function TiltStatusBar({ aiPower, gridStress, npi }: { aiPower: number | null; gridStress: number | null; npi: number | null }) { - if (aiPower === null || gridStress === null || npi === null) return null; - - const isElevated = aiPower > 78 && gridStress > 70 && npi > 130; - const isEasing = aiPower < 68 && gridStress < 55; - const status = isElevated ? "elevated" : isEasing ? "easing" : "tracking baseline"; - const statusColor = isElevated ? "#F07800" : isEasing ? "#6b7280" : "#F0A500"; - const statusBg = isElevated ? "bg-[#F07800]/10 border-[#F07800]/25" : isEasing ? "bg-muted/20 border-card-border" : "bg-[#F0A500]/10 border-[#F0A500]/20"; - const description = isElevated - ? `All three gauges elevated. The market is pricing the AI-power complex aggressively today; grid-equity momentum at ${gridStress.toFixed(0)}/100. These read market positioning, not physical grid conditions.` - : isEasing - ? `Gauges have pulled back from peaks. Could be sector rotation or cooling sentiment. Watch hyperscaler capex guidance.` - : `Tracking baseline. Market gauges near their fixed baselines. NPI at ${npi.toFixed(0)} reflects nuclear-complex performance since the Jan 2024 base.`; - - const numbers = [ - { label: "AI Demand", val: aiPower }, - { label: "NPI", val: npi }, - { label: "Grid Stress", val: gridStress }, - ] as const; - - return ( - -
- {/* Status + numbers row on mobile, status only on desktop */} -
-
-

Tilt Status

-
-
- {status} -
-
- {/* Mini numbers shown beside status on mobile */} -
- {numbers.map(({ label, val }) => ( -
-

{label}

-

{val.toFixed(0)}

-
- ))} -
-
- - {/* Vertical divider - desktop only */} -
- - {/* Description */} -

{description}

- - {/* Mini numbers - desktop only (shown inline on mobile) */} -
- {numbers.map(({ label, val }) => ( -
-

{label}

-

{val.toFixed(0)}

-
- ))} -
-
- - ); -} - const SECTOR_COLORS: Record = { compute: "#94a3b8", nuclear: "#F0A500", @@ -364,7 +310,7 @@ function TopMoversSection({ topMovers, pulse, isLoading, isError }: { topMovers: -

Top 5 stocks by absolute % change across all 8 stack layers. Refreshes every 10 min.

+

Top 5 stocks by absolute % change across all 13 stack layers. Refreshes every 10 min.

@@ -427,12 +373,13 @@ function TopMoversSection({ topMovers, pulse, isLoading, isError }: { topMovers: ); } -interface IndexHistoryDay { +interface MetricsHistoryDay { date: string; - aiDemand: number | null; - gridStress: number | null; - npiEquityLegs?: number | null; - npi?: number | null; + signedGW: number; + announcedGW: number; + constructionGW: number; + operationalGW: number; + queueOverallGW: number; } const MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; @@ -441,145 +388,91 @@ function gaugeTickLabel(date: string): string { return `${MONTH_ABBR[Number(m) - 1]} '${y.slice(2)}`; } -// Replaces the old Sector Pulse card (its sector averages now live as chips -// inside Top Movers). Plots the daily gauge history that the validation -// study reconstructed and the server now records: one consistent series, -// our own data, time depth instead of a second "what moved today" list. -function GaugeHistoryCard({ live }: { live: { npi: number; ai: number; gs: number } | null }) { - const { data, isLoading } = useQuery<{ days: IndexHistoryDay[] }>({ - queryKey: ["/api/index-history"], +// Replaced the retired gauge-history chart on 2026-06-10: plots the daily +// buildout-scoreboard series the server records. No backfill, no synthetic +// seed; depth builds one real snapshot per day. +function BuildoutHistoryCard() { + const { data, isLoading } = useQuery<{ days: MetricsHistoryDay[] }>({ + queryKey: ["/api/metrics/history"], refetchInterval: 30 * 60_000, }); - const series = (data?.days ?? []) - .filter((d) => d.npiEquityLegs != null || d.aiDemand != null) - .map((d) => ({ date: d.date, npi: d.npiEquityLegs ?? null, ai: d.aiDemand, gs: d.gridStress })); + const series = (data?.days ?? []).map((d) => ({ + date: d.date, + signed: d.signedGW, + construction: d.constructionGW, + queue: d.queueOverallGW, + })); return ( // flex-1: fills the left column so it ends flush with the right column. - +
-

Gauges Since Jan 2024

+

Buildout History

- Daily series reconstructed from public prices with the shipped formulas, then recorded live going - forward (one methodology end to end; reproduce with npm run backtest:indices). The NPI line is the - equity legs with uranium and policy at par; the headline number is the full live NPI including both. - Sparklines show the two sentiment gauges oscillating around their fixed baselines. + Daily snapshots of the scoreboard: signed nuclear GW and DC construction GW on the left axis, + total interconnection-queue GW on the right. Recording started 2026-06-10 when the + market-sentiment gauges were retired, so depth builds one day at a time. Raw series at + /api/metrics/history; the retired gauge series stays archived at /api/index-history.

- {live && ( - - NPI {live.npi.toFixed(1)} - - )}

- line: NPI equity legs · headline: full live NPI · /api/index-history + signed nuclear + dc construction (left, GW) · queue total (right, GW)

- {isLoading || series.length === 0 ? ( + {isLoading ? (
+ ) : series.length < 2 ? ( +
+

+ History starts now: one snapshot per day, no backfill, nothing synthetic. +
+ {series.length === 1 + ? `First snapshot recorded ${series[0].date}.` + : "The first snapshot lands with the next scoreboard refresh."} +

+
) : ( - <> -
- - - - - - - - - - - - value == null ? ["n/a", name] : [Number(value).toFixed(1), name] - } - /> - - - - -
- -
- {( - [ - { key: "ai", label: "AI Demand", color: "#F0A500", value: live?.ai ?? null }, - { key: "gs", label: "Grid Stress", color: "#ef4444", value: live?.gs ?? null }, - ] as const - ).map((g) => ( -
-
- {g.label} - - {g.value != null ? g.value.toFixed(1) : "–"} - -
-
- - - - - -
-
- ))} -
- +
+ + + + + + (value == null ? ["n/a", name] : [`${Number(value).toLocaleString()} GW`, name])} + /> + + + + + +
)}
); @@ -981,12 +874,16 @@ function ModuleGrid() { } export default function TiltOverview() { - const { data: kpiData, isLoading, isError: kpiError } = useQuery({ - queryKey: ["/api/kpis"], + const { + data: metricsData, + isLoading, + isError: metricsError, + dataUpdatedAt, + } = useQuery({ + queryKey: ["/api/metrics"], refetchInterval: 900000, }); - const { data: topMovers, isLoading: topMoversLoading, isError: topMoversError } = useQuery({ queryKey: ["/api/top-movers"], refetchInterval: 900000, @@ -997,9 +894,6 @@ export default function TiltOverview() { refetchInterval: 900000, }); - const { dataUpdatedAt: kpiUpdatedAt } = useQuery({ queryKey: ["/api/kpis"] }); - const c = kpiData?.constituents; - return (
@@ -1009,18 +903,130 @@ export default function TiltOverview() { Grid information, tilted in your favor

- Live equities, infrastructure, and power data across 60+ tickers tracking the AI power economy. + Live equities, infrastructure, and power data across 100 tickers tracking the AI power economy.

- Updated {relativeTime(kpiUpdatedAt)} + Updated {relativeTime(dataUpdatedAt)}
+ {/* The buildout scoreboard. Real units from curated, sourced + datasets; replaced the retired market-sentiment gauges on + 2026-06-10. The autopsy is public: docs/INDEX_VALIDATION.md. */} +
+
+
+ the buildout scoreboard · real units, sourced +
+ {metricsError && ( + + scoreboard unavailable; retrying + + )} +
+
+ {[ + { + label: "Nuclear-for-AI, signed", + value: metricsData ? `${metricsData.nuclear.signedGW}` : null, + unit: "GW", + color: "#F07800", + sub: metricsData + ? `${metricsData.nuclear.signedDeals} executed deals · ${metricsData.nuclear.announcedGW} GW more announced` + : "", + info: "Executed nuclear PPAs and restarts underway, summed from GridTilt's curated deal registry. Signed means contracts: options, proposals, and aggregate LOI pipelines are tracked separately and never inflate this number. Project list and sources on the Backlog page.", + }, + { + label: "DC under construction", + value: metricsData ? `${metricsData.pipeline.constructionGW}` : null, + unit: "GW", + color: "#F0A500", + sub: metricsData + ? `${metricsData.pipeline.operationalGW} GW operational · ${metricsData.pipeline.siteCount} tracked sites` + : "", + info: "Tracked US datacenter sites at 400 MW or more, summed by build status from public announcements. A curated registry, not a census. FY2025 hyperscaler capex rides alongside: disclosed guides total $340B.", + }, + { + label: "Interconnection queue", + value: metricsData ? metricsData.backlog.queueOverallGW.toLocaleString() : null, + unit: "GW", + color: "#ef4444", + sub: metricsData + ? `median wait ${metricsData.backlog.medianWaitMonths} mo · ${metricsData.backlog.historicalWithdrawalPct}% historically withdraw` + : "", + info: "Lawrence Berkeley National Lab's Queued Up dataset (emp.lbl.gov/queues) covering nearly the entire US generating queue, plus ISO filings for the large-load lines. As-of dates ship with the data on the Backlog page.", + }, + { + label: "Grid pulse", + value: + metricsData?.gridPulse?.currentGW != null + ? `${metricsData.gridPulse.currentGW}` + : metricsData?.gridPulse?.outputYoYPct != null + ? `${metricsData.gridPulse.outputYoYPct > 0 ? "+" : ""}${metricsData.gridPulse.outputYoYPct}` + : null, + unit: metricsData?.gridPulse?.currentGW != null ? "GW now" : "% YoY", + color: "#34d399", + sub: + metricsData?.gridPulse?.currentGW != null + ? "US48 demand right now (EIA hourly)" + : metricsData?.gridPulse?.outputYoYPct != null + ? `US electric output, ${metricsData.gridPulse.outputMonth} (FRED)` + : "", + info: "The physical side, measured: live lower-48 demand from EIA's Hourly Electric Grid Monitor when a key is configured, with year-over-year US electric output from FRED (IPG2211A2N) alongside. Measurements, not market sentiment.", + }, + ].map((c) => ( + +
+

{c.label}

+ + + + + +

{c.info}

+
+
+
+ {c.value == null ? ( + + ) : ( +

+ {c.value} {c.unit} +

+ )} +

{c.sub}

+
+ ))} +
+ {metricsData?.market && ( +
+ market + + ai infra, equal weight across {metricsData.market.allCount} names:{" "} + = 0 ? "text-green-400" : "text-red-400"}> + {metricsData.market.allPct >= 0 ? "+" : ""} + {metricsData.market.allPct.toFixed(2)}% today + + + {metricsData.market.nuclearPct != null && ( + + nuclear names:{" "} + = 0 ? "text-green-400" : "text-red-400"}> + {metricsData.market.nuclearPct >= 0 ? "+" : ""} + {metricsData.market.nuclearPct.toFixed(2)}% + + + )} + percent moves only · the composite indices are retired +
+ )} +
+ {/* Dashboard density - 2-col */}
@@ -1030,9 +1036,7 @@ export default function TiltOverview() { isLoading={topMoversLoading} isError={topMoversError} /> - +
@@ -1204,96 +1208,19 @@ export default function TiltOverview() {
- {/* Market gauges — research depth, intentionally demoted from headline. - Labels follow the published backtest (docs/INDEX_VALIDATION.md): - the momentum gauges showed no correlation with physical output, - so they are presented as market sentiment, not measurements. */} -
-
- market gauges · methodology and validation in each card -
-
- - - - - - - ) : undefined} - isLoading={isLoading} - /> - - - - - - - - ) : undefined} - isLoading={isLoading} - /> - - - - - - ) : undefined} - isLoading={isLoading} - /> -
- - {!isLoading && kpiData && ( -
- -
- )} - - {kpiError && ( - -
- - Live index data unavailable. Showing last known values. -
-
- )} -
- - {/* 4-column stat strip */} + {/* Context stat strip. The nuclear figure reads live from the same + registry the scoreboard uses, so the two can never disagree. */}
{[ { label: "DC Share of US Demand", value: "~6.4%", sub: "EIA 2025: ~288 TWh, up from 4.4% in 2023. DOE projects 12%+ by 2028.", color: "#a855f7" }, - { label: "Nuclear Power Committed", value: "12+ GW", sub: "Big Tech nuclear PPAs as of Q1 2026. Meta 6.6 GW, Microsoft 1.2 GW, Amazon 2.5+ GW.", color: "#F0A500" }, + { + label: "Nuclear-for-AI Committed", + value: metricsData ? `${(metricsData.nuclear.signedGW + metricsData.nuclear.announcedGW).toFixed(1)} GW` : "…", + sub: metricsData + ? `${metricsData.nuclear.signedDeals} signed deals (${metricsData.nuclear.signedGW} GW) plus announced and optioned projects. LOI pipelines excluded.` + : "Tracked deal registry.", + color: "#F0A500", + }, { label: "Grid Reserve Margins", value: "Tightening", sub: "MISO 13.4%, ERCOT 15.8% per NERC 2026. Capacity warnings through 2028.", color: "#94a3b8" }, ].map((s) => ( @@ -1354,7 +1281,7 @@ export default function TiltOverview() {