From 7e0c7ecf102ae518b39693bc12c132ff6f61970a Mon Sep 17 00:00:00 2001 From: aurph Date: Wed, 10 Jun 2026 13:17:39 -0400 Subject: [PATCH 1/4] Spec: replace the three indices with the real buildout scoreboard Headlines become physical units (signed nuclear GW, DC pipeline GW, queue backlog, live grid pulse) plus one equal-weight market line. Social posts become event-driven with a Friday digest. Synthetic correlation scatter dies as a rider. Old series archived as the public autopsy. --- .../specs/2026-06-10-real-metrics-design.md | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-10-real-metrics-design.md diff --git a/docs/superpowers/specs/2026-06-10-real-metrics-design.md b/docs/superpowers/specs/2026-06-10-real-metrics-design.md new file mode 100644 index 0000000..facf592 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-real-metrics-design.md @@ -0,0 +1,127 @@ +# Real Metrics: replace the three indices with the buildout scoreboard + +Date: 2026-06-10. Owner: Jack. Status: approved direction, pending spec sign-off. +Branch: `feat/real-metrics` (stacked on `fix/m0-m1-truth-security`, PR #1). + +## Why + +The three headline indices (AI Demand, Grid Stress, NPI) are market-sentiment composites with invented scaffolding: arbitrary baselines (72, 68), clamps, a Jan-2024 = 100 anchor, and a hand-tuned policy multiplier. Our own backtest (`docs/INDEX_VALIDATION.md`) shows AI Demand and Grid Stress carry no physical signal, and NPI is r = 0.95 with VST alone. The honest labeling was a band-aid. Meanwhile the repo already curates the real thing: named nuclear deals with offtakers and MW, a datacenter pipeline in GW by status, the LBNL interconnection backlog, and sourced hyperscaler capex. + +Decisions made with Jack (2026-06-10): +1. Headlines become real units. One thin, clearly labeled market line survives for daily pulse. No index levels, no base years, no clamps, anywhere. +2. Social posts become event-driven plus a Friday digest. Quiet day = no post. +3. The headline nuclear number counts signed deals only. Options, LOIs, and aggregates display in a separate bucket. + +## The scoreboard + +Four metric groups. Every group carries `asOf` and `source` (label + URL). Every number is traceable to a curated dataset or a public feed. Nothing is normalized, indexed, or clamped. + +### 1. Nuclear-for-AI (replaces NPI) + +Source: `server/data/interconnection-queue.json` projects where `type === "nuclear"` and `status === "active"` and `dcRelevant`, partitioned by a new curated `firmness` field. + +- `signedGW` (headline): sum of `capacityMW` where `firmness === "signed"`, in GW. +- `announcedGW`: sum where `firmness` is `optioned` or `proposed`. +- `aggregateGW`: sum where `firmness === "aggregate"` (LOI pipelines). Footnote only, never in either headline bucket. +- `signedDeals` / `totalDeals`: counts. +- `uraniumSpot`: passthrough of `market-constants.json` (value, asOf, source). Commodity price, not a stock. + +`firmness: "signed" | "optioned" | "proposed" | "aggregate"` is added to the project schema, the admin add/update endpoint allowlist, and validation. Initial classification (Jack reviews; flip any of these via the admin endpoint at any time): + +| Project | MW | Proposed firmness | Reason | +|---|---|---|---| +| Crane (TMI-1 restart) | 835 | signed | 20-yr Microsoft PPA, restart underway | +| Palisades restart | 800 | signed | contracted restart, DOE loan, underway | +| Talen-AWS Susquehanna | 1,920 | signed | definitive 17-yr, $18B | +| Clinton extension + uprate | 1,121 | signed | 20-yr Meta VPPA, full output | +| Talen-AWS 300 MW FoM | 300 | signed | executed transition agreement | +| Vistra-Meta existing PPAs | 1,500 | signed | executed | +| Vistra-Meta SMR option site | 300 | optioned | option agreement | +| Meta-Oklo Pike County | 1,200 | proposed | prepayments, pre-construction | +| Kairos Hermes 2 | 500 | proposed | demo-scale; Jack may upgrade (TVA/Google PPA exists) | +| AWS-Oklo arrangement | 750 | proposed | framework | +| Standard Power NuScale fleet | 2,000 | proposed | announcements | +| Comanche Peak Unit 3 | 1,200 | proposed | no offtaker | +| Oklo customer pipeline | 2,100 | aggregate | LOIs, not projects | + +With these defaults the headline reads roughly "6.5 GW signed across 6 deals, 6.0 GW announced" (exact values computed from data at runtime, never hand-summed). + +### 2. AI load pipeline (replaces AI Demand) + +Sources: `server/data/datacenters.json` (curated registry, >= 400 MW sites only) and `server/data/hyperscaler-capex.json`. + +- `operationalGW`, `constructionGW`, `announcedGW`: sum of `powerMW` by `status`, in GW. Today: ~9.4 / ~15.6 / ~5.1. +- `siteCount`: registry size (58). +- `capexUsdBillions`: FY2025 total (340) + per-company components with source links, passthrough. +- Labeling rule: always "tracked", never "US total". The registry is curated, not a census. +- Data change: add a top-level `lastRefreshed` to `datacenters.json` (bumped by the admin write path) so the group has a real `asOf`. + +### 3. The backlog (replaces Grid Stress) + +Source: `interconnection-queue.json` headline, passthrough promotion. `queueOverallGW` (2,290), `queueOverallProjects`, `medianWaitMonths` (55), `historicalWithdrawalPct` (77), `ercotLargeLoadGW` + `ercotLargeLoadDataCenterPct`, `pjmReopenedGW`. All fields already carry asOf strings and the LBNL source URL. No new data work. + +### 4. Grid pulse (new; the live, physical element) + +- Primary: EIA US48 hourly demand via existing `physical.ts` (free key, 30-min cache): current load GW and delta vs the same hour one year ago. When `EIA_API_KEY` is absent the group returns `configured: false` and the UI omits it honestly. +- Companion: FRED `IPG2211A2N` latest month year-over-year %, no key, already cached 24h. + +### Market line (the one stock element) + +From `stackCache["1D"]` (zero new fetches): equal-weight mean of `changePercent` across all stack tickers with live data, plus the nuclear-layer subset. Display: "ai infra, equal weight across N names: +0.8% today · nuclear names: +1.4%". Rules: percent-change only, never a level; equal weight (the no-judgment choice); stale tickers excluded with the live count shown; `source: "live" | "static"` honesty flag retained. + +## API + +- `GET /api/metrics` (new): `{ nuclear, pipeline, backlog, gridPulse, market, asOf }` per the shapes above. Assembly lives in `server/metrics.ts`: pure, typed functions over plain inputs (same discipline as `indices.ts`), unit-tested; `routes.ts` only gathers file/cache IO. +- `GET /api/metrics/history` (new): served from `server/data/metrics-history.json`. A daily snapshot of the scoreboard scalars (signedGW, announcedGW, constructionGW, operationalGW, queueOverallGW, capexUsdBillions, uraniumSpot) appended once per calendar day on `/api/metrics` hits, using the same once-a-day guard pattern as `index-history.ts` (no weekday/market-hours gate; physical data has no trading hours). Sparse early history is expected and honest. +- `GET /api/kpis`: returns `410 Gone` with `{ moved: "/api/metrics" }` for one release, then removed. Its side-effect write to `index-history.json` stops with it. +- `GET /api/index-history`: stays, serving the frozen file, with `{ discontinued: true, reason, replacedBy: "/api/metrics/history" }` merged into the response. The file is the archived evidence. + +## UI + +- `Hero.tsx` (home): swap `/api/kpis` for `/api/metrics`; show signed nuclear GW, construction GW, queue GW, and grid pulse when configured. +- `TiltOverview.tsx`: KPI strip becomes four metric cards (nuclear / pipeline / backlog / pulse), each with unit, asOf, and a source link. Market line renders as a slim strip beneath them. The gauge-history chart becomes a buildout-history chart reading `/api/metrics/history` (lines: signedGW, constructionGW, queueOverallGW). +- OG images: `liveIndicesStats()` becomes scoreboard stats ("6.5 GW signed nuclear" style). Template cards updated. +- `/api/export/daily`: `indices` block replaced by a `metrics` block; `tilt_status` dies with the gauges (it was derived from them). Only consumer is Jack's own automation. +- Grep-verified consumer list to migrate: `Hero.tsx`, `TiltOverview.tsx` (client); `liveIndicesStats` (routes.ts:1067), `ogCardForTemplate` (1101), `composeTiltStatusTweet` (1594), `composeNpiUpdateTweet` (1613), `/api/kpis` (1728), `/api/export/daily` (2910) (server). + +## Social: event-driven + Friday digest + +State: `server/data/social-state.json` stores the last-posted scoreboard snapshot. The daily cron URL does not change (external cron-job.org keeps firing); the handler becomes an evaluator: + +1. Compute current scoreboard; diff against last-posted snapshot. +2. If a meaningful event exists, post the highest-priority one: `new_deal` (project added or firmness upgraded to signed) > `pipeline_move` (a site changed status or GW totals moved) > `backlog_update` (queue headline fields changed) > `capex_update`. +3. Else if Friday: post the weekly digest: scoreboard values + deltas vs 7 days ago from metrics-history, plus next week's top catalyst (absorbs the Friday catalyst-preview slot). The digest always includes the week date ("week of jun 8"), so it is never byte-identical even in a flat week, and a flat week says so plainly. +4. Else: respond `{ skipped: true, reason: "no change" }` and log it. Honest silence. +5. Dedupe guard: never post text identical to any of the last 20 `social-log.json` entries. + +Composer changes in `social-format.ts` (all pure + unit-tested): add `buildNewDealTweet`, `buildPipelineMoveTweet`, `buildBacklogUpdateTweet`, `buildCapexUpdateTweet`, `buildWeeklyDigestTweet`. Delete `buildTiltStatusTweet`, `buildNpiUpdateTweet`, `npiHistoryContext`, `effectiveNpiWeights`, and the day-of-week `ROTATING_TEMPLATES` keying. `buildTopMoversTweet` and `buildCatalystTweet` survive as manual-only templates via `/api/social/generate` and `post-now` (real data, honestly labeled, just not scheduled). Voice rules unchanged (lowercase prose, no padded columns, vary from real numbers). + +## Rider: kill the synthetic correlation scatter (same disease) + +`/api/stack` currently ships `generateCCJCorrelationData`/`generateCEGCorrelationData`: Math.random scatter tuned to target r values, plus a correlation coefficient computed from the fake points (audit finding CORR-1). No free daily uranium spot series exists to compute the real thing, so the honest fix is removal: delete the generators and the `correlation*` fields from `/api/stack`, and remove the scatter display from `TheStack.tsx`. No fabricated data survives this change. + +## What stays, what dies + +Stays: `server/indices.ts`, the backtest script, `indices.test.ts`, `docs/INDEX_VALIDATION.md`, and the frozen `index-history.json`. They are the public autopsy and keep `npm run backtest:indices` reproducible against the archive. +Dies from live code: `computeKpis` gauge assembly, `deriveSmrPolicyScore` (the 0-10 score becomes the raw deal count/GW it was derived from), NPI plumbing in routes, the gauge UI, the gauge/NPI tweet composers, the synthetic scatter. + +## Docs and the public story + +- README: "Index methodology" section becomes "The scoreboard: every number and where it comes from" (per-metric source table), plus a short "What happened to the indices" pointing at the validation study and archived series. +- Blog post + announcement tweet, drafted with the change, owner-approved before publishing: we built indices, backtested them against physical data, they failed, we replaced them with the real numbers. +- CLAUDE.md and SECURITY.md touch-ups where they reference gauges (no security-posture change; the admin schema gains `firmness`). + +## Testing + +- `metrics.test.ts` (new): firmness partition sums, aggregate exclusion, GW conversion/rounding, missing-file and missing-key degradation, asOf/source passthrough, market-line stale exclusion. +- `social-format.test.ts`: rewritten for the five new composers, including the flat-week digest, the date-inclusion guarantee, and length limits. +- Cron evaluator test: no-change -> skipped; synthetic diff -> correct event priority; dedupe against log. +- Existing suites stay green: indices tests (archive math), throttle test, auth-boundary test (`/api/metrics` is public read-only; no new admin surface beyond the `firmness` field on an existing gated endpoint). + +## Out of scope + +Durability of the JSON files (audit M2), the routes.ts decomposition (M4), and the AI feature track (M7). This change reads and writes the same files the app uses today. + +## Definition of done + +`check`/`test`/`build` green; every consumer migrated (grep for `/api/kpis` returns only the 410 stub); UI screenshots of home + overview reviewed; no index levels, base years, clamps, or random data anywhere in the live app; spec, README, and code agree. From b518d674dac0ba0a3e2b0cbef2849a06308a617c Mon Sep 17 00:00:00 2001 From: aurph Date: Wed, 10 Jun 2026 13:46:24 -0400 Subject: [PATCH 2/4] Replace the three indices with the real buildout scoreboard The market-sentiment gauges (AI Demand, Grid Stress, NPI) are retired. Our own backtest (docs/INDEX_VALIDATION.md) showed no physical signal and NPI at r=0.95 with VST alone. Headlines are now real units summed from the curated datasets: no baselines, no clamps, no jan-2024=100. Scoreboard (/api/metrics, pure math in server/metrics.ts, unit-tested): - Nuclear-for-AI: 6.5 GW signed across 6 executed deals (new curated firmness field; options/proposals 6.0 GW separate; LOI aggregates footnoted only, never headlined) - DC pipeline: 9.4 GW operational / 15.6 construction / 5.1 announced across 58 tracked sites + USD 340B FY2025 disclosed capex - Interconnection queue: 2,290 GW / 55 mo median wait / 77% withdrawal (LBNL + ISO filings, as-of dates in the data) - Grid pulse: live US48 demand (EIA, keyed) + FRED output YoY - One market line: equal-weight percent move, never a level - Daily snapshots at /api/metrics/history (server/metrics-history.ts) Social: day-of-week rotation is gone. The cron now diffs the scoreboard against the last-posted snapshot and posts at most one event (new signed deal > pipeline move > backlog update > capex revision), or the Friday digest, or honest silence with the reason. Dedupe guard against the last 20 posts; first run initializes state without tweeting the backlog. Verified end to end: flipping a project to signed composes the deal tweet in dry-run; identical state skips. Also removed (same disease): the Math.random CCJ/CEG correlation scatter on The Stack and its server generators. No fabricated data survives. Archived, not erased: /api/kpis returns 410 with a pointer for one release; /api/index-history serves the frozen gauge series marked discontinued; indices.ts, the backtest, and its tests stay green as the public autopsy. README methodology section rewritten accordingly. check clean, 50/50 tests, build clean. --- README.md | 22 +- client/src/components/home/Hero.tsx | 16 +- client/src/lib/types.ts | 21 +- client/src/pages/TheStack.tsx | 201 +-- client/src/pages/TiltOverview.tsx | 519 ++++---- .../specs/2026-06-10-real-metrics-design.md | 2 +- server/__tests__/metrics.test.ts | 165 +++ server/__tests__/social-format.test.ts | 147 +- server/data/interconnection-queue.json | 1185 ++++++++++++++++- server/data/metrics-history.json | 16 + server/data/social-state.json | 3 + server/metrics-history.ts | 76 ++ server/metrics.ts | 240 ++++ server/routes.ts | 736 +++++----- server/social-format.ts | 279 ++-- 15 files changed, 2538 insertions(+), 1090 deletions(-) create mode 100644 server/__tests__/metrics.test.ts create mode 100644 server/data/metrics-history.json create mode 100644 server/data/social-state.json create mode 100644 server/metrics-history.ts create mode 100644 server/metrics.ts 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() {