diff --git a/.gitignore b/.gitignore index 23e0c05..5c251ea 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,5 @@ reports/ Claude.md .claude/ .playwright-mcp/ -docs/ \ No newline at end of file +docs/ +.codex-audits/ \ No newline at end of file diff --git a/examples/rest/massive-heatmap/.env.example b/examples/rest/massive-heatmap/.env.example new file mode 100644 index 0000000..4e86601 --- /dev/null +++ b/examples/rest/massive-heatmap/.env.example @@ -0,0 +1,3 @@ +# A Massive API key with REST snapshot access for at least one asset class. +# A real-time tier gives live colors; a delayed tier works too, just on a delay. +MASSIVE_API_KEY=your_key_here diff --git a/examples/rest/massive-heatmap/.gitignore b/examples/rest/massive-heatmap/.gitignore new file mode 100644 index 0000000..75df5cf --- /dev/null +++ b/examples/rest/massive-heatmap/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.vite/ +package-lock.json diff --git a/examples/rest/massive-heatmap/README.md b/examples/rest/massive-heatmap/README.md new file mode 100644 index 0000000..6ad9571 --- /dev/null +++ b/examples/rest/massive-heatmap/README.md @@ -0,0 +1,142 @@ +# Massive Heatmap + +A live, cross-asset market heatmap built on Massive's REST API. Pick an asset class and a universe, and the screen fills with tiles sized by market cap and colored by percent change. Prices recolor in place as each REST snapshot refresh lands. Switch the lookback to see change over a day, a quarter, or five years, and export a branded PNG of whatever is on screen. + +![S&P 500 heatmap](screenshots/hero-sp500.png) + +## What it does + +- **Cross-asset coverage.** Stocks (S&P 500, Nasdaq 100, Dow 30), ETFs, crypto, forex, indices, and futures, across 18 prebuilt universes. +- **Live recoloring.** A diverging red-to-green color scale that updates every few seconds as the backend re-polls the snapshot. The refresh interval is configurable in Settings (2s to 60s). Equities move during US market hours including pre-market and after-hours; crypto and forex move around the clock. +- **Market cap sizing.** Tile area is proportional to market cap, so the names that move the index get the most screen. +- **Sector and category grouping.** GICS sectors for equities, thematic categories (Layer 1s, DeFi, Exchange, Energy, Metals, and so on) for everything else. +- **Selectable lookback.** Compare against the prior close (1D) or against the close 7, 30, 90, or 180 days ago, 1 year, or 5 years back. The color scale rescales with the window so a 5-year move and a one-day move both read clearly. +- **Session awareness.** A badge shows pre-market, regular session, after-hours, or closed for equities, and 24/7 for crypto and forex. +- **Zoom and pan.** Scroll to zoom toward the cursor, drag to pan, double-click to reset. Useful on the 500-tile S&P view. +- **Hover details.** Hovering a tile shows the ticker, name, current price, the period reference price, and the percent change. +- **PNG export.** One click renders a branded 16:9 snapshot of the current view and downloads it. + +## How it works + +The project is a Node + TypeScript backend paired with a React + TypeScript (Vite) frontend. The frontend never talks to Massive directly. It calls the backend over plain HTTP, and the backend holds your API key, brokers all Massive traffic. + +``` +browser ──GET /api/snapshot──> Node backend ──REST seed───────> api.massive.com +browser ──GET /api/prices ───> Node backend ──REST snapshot───> api.massive.com + (every refresh interval) +``` + +**Seed, then poll.** When you select a universe, the backend first fetches a REST snapshot to establish a reference price for every ticker. The seed matters: it gives the frontend a baseline to color against from the moment the page loads, so tiles are correct before any update arrives. Then the browser polls for current prices on the interval you set. + +- For **1D**, the reference is the prior official close, read straight from the snapshot. +- For **longer lookbacks**, the backend fetches the historical close at the start of the window (grouped daily aggregates for stocks, ETFs, crypto, and forex in a single call; per-ticker daily aggregates for indices; session aggregates for futures) and colors today's price against that. + +**Live updates.** After the seed, the browser polls `/api/prices` on the interval you choose in Settings. Each poll asks the backend for the current price of every ticker in the universe; the backend fetches a fresh REST snapshot (cached for a second so multiple tabs share one outbound call) and returns the new price and percent change per tile. The canvas recolors in place as each refresh lands. + +**Loading state.** Switching universe, asset class, or lookback triggers a fresh seed on the backend. While that is in flight the heatmap dims and a spinner names what is loading, then clears the moment the new snapshot lands. (Changing the refresh interval does not re-seed; it just re-times the poll in place.) + +![Loading a new universe](screenshots/loading-overlay.png) + +### Project layout + +- `backend/`: the Node REST proxy. Built on the official `@massive.com/client-js` client. Handles seeding (`seed.ts`), current-price snapshots (`prices.ts`), session phase (`session.ts`), and the HTTP API (`server.ts`). +- `frontend/`: the Vite app. Renders the canvas treemap, recalculates the color scale on each update, and owns tooltips, the session badge, settings, and the export flow. +- `universes/`: JSON files defining ticker membership, sector or category groupings, and market cap weights for each universe. +- `shared/`: the wire protocol and universe types shared by both ends. + +## Massive subscriptions you need + +On Massive each asset class is its own subscription. This app needs **REST snapshot access** (the snapshot endpoints that return the latest price per ticker) and, ideally, **real-time data** so the colors reflect now rather than a delayed tape. The free Basic tier of every class is end-of-day only, so it will not drive the heatmap. + +The prices below are individual (non-professional) monthly plans. Annual billing saves 20%. Business and enterprise use is covered separately, see [Business use](#business-use). + +| App asset class | Massive product | Real-time | Lowest tier with snapshot (delayed) | +|---|---|---|---| +| Stocks and ETFs | Stocks | Stocks Advanced, $199/mo | Stocks Starter, $29/mo (15-min delayed) | +| Crypto and Forex | Currencies | Currencies Starter, $49/mo (real-time included) | same plan | +| Indices | Indices | Indices Advanced, $99/mo | Indices Starter, $49/mo (15-min delayed) | +| Futures | Futures | Futures Advanced, $199/mo | Futures Starter, $29/mo (10-min delayed) | + +A few things worth knowing: + +- **Currencies is a single subscription that covers both crypto and forex**, and its entry Starter tier is already real-time, so it is the best value for the most live motion. +- **For stocks, futures, and indices, real-time lives only in the top tier.** The cheaper Starter and Developer tiers still include the snapshot endpoint, so the heatmap works and recolors, just on a 10 to 15 minute delay. That is a fine way to demo the app without paying for real-time. (Set a longer refresh interval on a delayed tier; there is no point polling every 2s for data that updates every 15 minutes.) +- **ETFs ride on the Stocks subscription**, since they are equities. There is no separate ETF plan. +- **Real-time individual plans are non-professional use only.** Commercial use needs a Business plan. + +**You do not need all four.** The default S&P 500 view needs only a Stocks plan. Open Settings and uncheck any asset class your key cannot reach, and the app runs on the subset you have. + +### Business use + +Individual real-time plans are restricted to non-professional use. For commercial or company use, Massive offers Business plans (for example, Stocks Business at $1,999/mo with real-time data and full history) and a custom Enterprise tier with dedicated support and tailored exchange feeds. Startups can ask about up to 50% off the first year. + +See [massive.com/pricing](https://massive.com/pricing) for individual plans and [massive.com/business](https://massive.com/business) for business and enterprise pricing. Prices here were current at the time of writing; check the site for the latest. + +![Settings: hide asset classes your key cannot reach](screenshots/settings.png) + +## Getting started + +**Requirements:** Node.js 18 or newer, and a Massive API key on a paid tier with snapshot access for at least one asset class (see [Massive subscriptions](#massive-subscriptions-you-need)). A real-time tier gives live colors; a delayed tier works too, just on a delay. + +```bash +cp .env.example .env +# open .env and set MASSIVE_API_KEY +``` + +```bash +npm install +npm run dev +``` + +`npm run dev` starts the backend (on `http://localhost:8787`) and the Vite dev server together. Vite proxies `/api` to the backend, so open the URL Vite prints, usually `http://localhost:5173`, and the heatmap loads against the S&P 500. + +## Configuration + +Open **Settings** in the top bar to: + +- **Set the refresh interval.** How often the heatmap polls for fresh prices, from 2s to 60s (default 5s). Lower is more live; higher is lighter on the API, which is the right call on a delayed tier. +- **Hide asset classes and universes** your key cannot reach, so the dropdowns only show what you can actually load. Hidden segments fall back to the first visible one. +- **Tune the color scale per lookback.** Each lookback has a clamp, the percent move that fully saturates red or green. Defaults scale with the window (±6% for 1D up to ±300% for 5Y); override any of them to suit your universe. + +Settings persist in the browser's local storage. There is nothing to configure on the backend beyond the API key. + +## Universes + +Eighteen universes ship across six asset classes, grouped in the two dropdowns by class and then by universe. + +| Asset class | Universes | +|---|---| +| Stocks | S&P 500, Nasdaq 100, Dow 30 | +| ETFs | Sector SPDRs, Broad Market, Thematic | +| Crypto | Top Coins, Layer 1s, DeFi | +| Forex | FX Majors, Crosses, USD Exotics | +| Futures | Equity Index, Energy, Metals | +| Indices | Major Indices, Sectors, Global | + +![Crypto top coins, grouped by category](screenshots/crypto-top-coins.png) + +Constituent lists and groupings are a shipped snapshot in `universes/`. Market cap weights come from Massive reference data and may need a periodic refresh as index composition changes. To add a universe, drop a new JSON file in `universes/` following the `Constituent` and `Universe` shape in `shared/universe.ts`, then add it to the dropdown lists in `frontend/src/Controls.tsx`. + +## Export + +The **Export** button opens a preview of a branded 16:9 composition (the heatmap, a header with the universe title and the MASSIVE wordmark, and a footer with the session, date, and legend), then downloads it as a PNG. It renders at 2x for a crisp image, and lays the treemap out at the export's own dimensions so tiles keep their true proportions. + +![Branded PNG export preview](screenshots/export-preview.png) + +## Running locally + +This example runs locally with one command. There is nothing to deploy: `npm run dev` starts the backend and the Vite dev server together, and Vite proxies `/api` to the backend. See [Getting started](#getting-started) for the full steps. The backend holds your `MASSIVE_API_KEY` and brokers every Massive call, so the key never reaches the browser. + +## Notes + +- **Lookback applies to non-futures classes.** Futures roll across contracts, so a fixed lookback is not meaningful. The futures view is forced to intraday (open to close on the front-month contract) and the lookback selector is hidden while a futures universe is shown. +- **Long windows can leave a few tiles neutral.** For 1Y and 5Y lookbacks, a ticker that did not trade at the window start (a recent listing, or a futures contract that did not exist yet) has no historical close to compare against. Those tiles stay neutral rather than show a faked move. +- **Off-hours stillness is expected.** Equity prices only move during US market hours. Crypto and forex move 24/7, so those universes show live motion at any time of day. + +## Disclaimer + +**Warning:** The examples, demos, and outputs produced with this project are generated by artificial intelligence and large language models. You acknowledge that this project and any outputs are provided "AS IS", may not always be accurate and may contain material inaccuracies even if they appear accurate because of their level of detail or specificity, outputs may not be error free, accurate, current, complete, or operate as you intended, you should not rely on any outputs or actions without independently confirming their accuracy, and any outputs should not be treated as financial or legal advice. You remain responsible for verifying the accuracy, suitability, and legality of any output before relying on it. + +## License + +This project is licensed under the [MIT License](../../../LICENSE). diff --git a/examples/rest/massive-heatmap/backend/package.json b/examples/rest/massive-heatmap/backend/package.json new file mode 100644 index 0000000..93db561 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "backend", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "test": "vitest run" + }, + "dependencies": { + "@massive.com/client-js": "^10.7.0", + "dotenv": "^16.4.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.16.0" + } +} diff --git a/examples/rest/massive-heatmap/backend/src/env.ts b/examples/rest/massive-heatmap/backend/src/env.ts new file mode 100644 index 0000000..8853a1d --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/env.ts @@ -0,0 +1,19 @@ +import { config } from "dotenv"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +// Load the demo-root .env regardless of the process cwd. `npm run dev` runs the +// backend with its cwd in backend/, but the .env (and .env.example) live at the +// demo root, one level up from backend/. +const HERE = dirname(fileURLToPath(import.meta.url)); // backend/src +config({ path: join(HERE, "..", "..", ".env") }); + +export function apiKey(): string { + const k = process.env.MASSIVE_API_KEY; + if (!k) { + throw new Error( + "MASSIVE_API_KEY not set. Copy .env.example to .env (in the massive-heatmap root) and add your key.", + ); + } + return k; +} diff --git a/examples/rest/massive-heatmap/backend/src/index.ts b/examples/rest/massive-heatmap/backend/src/index.ts new file mode 100644 index 0000000..430f5a3 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/index.ts @@ -0,0 +1,4 @@ +import { startServer } from "./server.js"; +const PORT = Number(process.env.PORT ?? 8787); +await startServer({ port: PORT }); +console.log(`[massive-heatmap] backend listening on http://localhost:${PORT}`); diff --git a/examples/rest/massive-heatmap/backend/src/lookback.ts b/examples/rest/massive-heatmap/backend/src/lookback.ts new file mode 100644 index 0000000..3a708ec --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/lookback.ts @@ -0,0 +1,20 @@ +export const LOOKBACKS = [1, 7, 30, 90, 180, 365, 1825] as const; +export type Lookback = (typeof LOOKBACKS)[number]; + +export function isLookback(n: unknown): n is Lookback { + return typeof n === "number" && (LOOKBACKS as readonly number[]).includes(n); +} + +// UTC YYYY-MM-DD for an epoch. +function ymd(ms: number): string { + return new Date(ms).toISOString().slice(0, 10); +} + +// Candidate window-start dates to try, newest first: (now - days), then walk back +// up to `back` more calendar days to skip weekends/holidays (grouped returns empty). +export function windowStartCandidates(nowMs: number, days: number, back = 6): string[] { + const start = nowMs - days * 86_400_000; + const out: string[] = []; + for (let i = 0; i <= back; i++) out.push(ymd(start - i * 86_400_000)); + return out; +} diff --git a/examples/rest/massive-heatmap/backend/src/prices.ts b/examples/rest/massive-heatmap/backend/src/prices.ts new file mode 100644 index 0000000..7080fe7 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/prices.ts @@ -0,0 +1,21 @@ +import { restClient } from "@massive.com/client-js"; +import type { Universe } from "../../shared/universe.js"; +import { groupBySegment } from "./seed.js"; +import { fetchSnapshot } from "./snapshot.js"; +import { apiKey } from "./env.js"; + +// Current price per display ticker across every asset class in the universe, via REST +// snapshots. No historical work: the prior-close baseline was set once at seed time. +// One REST snapshot call per asset class (per product code for futures) returns +// every current price. +export async function fetchCurrentPrices( + universe: Universe, + rest = restClient(apiKey(), "https://api.massive.com"), +): Promise> { + const out = new Map(); + for (const [segment, cons] of groupBySegment(universe)) { + const snap = await fetchSnapshot(rest, segment, cons); + for (const [ticker, row] of snap) out.set(ticker, row.price); + } + return out; +} diff --git a/examples/rest/massive-heatmap/backend/src/seed.ts b/examples/rest/massive-heatmap/backend/src/seed.ts new file mode 100644 index 0000000..36d036e --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/seed.ts @@ -0,0 +1,196 @@ +import { restClient } from "@massive.com/client-js"; +import type { Universe, Constituent, Segment } from "../../shared/universe.js"; +import { constituentSegment } from "../../shared/universe.js"; +import { apiKey } from "./env.js"; +import { windowStartCandidates } from "./lookback.js"; +import { fetchSnapshot, snapshotSymbol } from "./snapshot.js"; + +export interface Baseline { priorClose: number; price: number; } + +export function groupBySegment(universe: Universe): Map { + const groups = new Map(); + for (const c of universe.constituents) { + const seg = constituentSegment(universe, c); + const arr = groups.get(seg); + if (arr) arr.push(c); else groups.set(seg, [c]); + } + return groups; +} + +// Fetch grouped daily bars for one date, returns ticker -> close map. +// locale: "us" for stocks/etfs, "global" for crypto/forex. +// market: "stocks", "crypto", or "fx". +async function fetchGroupedBaselines( + locale: string, + market: string, + date: string, +): Promise> { + const url = `https://api.massive.com/v2/aggs/grouped/locale/${locale}/market/${market}/${date}?adjusted=true`; + const r = await fetch(url, { headers: { Authorization: `Bearer ${apiKey()}` } }); + const d: any = await r.json(); + const map = new Map(); + for (const row of d?.results ?? []) { + if (row?.T && typeof row.c === "number") map.set(row.T as string, row.c as number); + } + return map; +} + +// Walk candidates until we find a non-empty grouped result. +async function findGroupedBaselines( + locale: string, + market: string, + candidates: string[], +): Promise> { + for (const date of candidates) { + try { + const map = await fetchGroupedBaselines(locale, market, date); + if (map.size > 0) return map; + } catch (e) { + console.error(`[seed] grouped ${locale}/${market}/${date} failed:`, e); + } + } + return new Map(); +} + +async function seedSegment( + rest: ReturnType, + segment: Segment, + constituents: Constituent[], + lookback: number, + now: () => number, +): Promise> { + const out: Record = {}; + + if (segment === "futures") { + // Front-month contract + current session open/close per display ticker. + const snap = await fetchSnapshot(rest, "futures", constituents); + + if (lookback === 1) { + // Intraday open->close (futures lack a reliable prior settlement). priorClose is + // the session open, already gated to >0 by the snapshot helper. + for (const [disp, row] of snap) { + if (row.priorClose > 0) out[disp] = { priorClose: row.priorClose, price: row.price }; + } + return out; + } + + // lookback > 1: session close at the window start for the front-month contract. + // Long windows (1Y/5Y) often return nothing because the current contract did not + // trade then; those tiles are left unseeded (neutral) rather than faked. + const candidates = windowStartCandidates(now(), lookback); + const from = candidates[candidates.length - 1]; + const to = candidates[0]; + for (const [disp, row] of snap) { + const wsSym = row.wsSymbol; + if (!wsSym) continue; + try { + const url = `https://api.massive.com/futures/v1/aggs/${encodeURIComponent(wsSym)}` + + `?resolution=1session&window_start.gte=${from}&window_start.lte=${to}` + + `&sort=window_start.desc&limit=1`; + const r = await fetch(url, { headers: { Authorization: `Bearer ${apiKey()}` } }); + const d: any = await r.json(); + const bar = d?.results?.[0]; + if (bar && typeof bar.close === "number" && bar.close > 0) { + out[disp] = { priorClose: bar.close, price: row.price }; + } + } catch (e) { console.error("[seed] futures aggs failed for", wsSym, e); } + } + return out; + } + + // Current snapshot: price for every lookback, prior close for the lookback=1 baseline. + const snap = await fetchSnapshot(rest, segment, constituents); + const currentPrice = new Map(); + const priorCloseFromSnapshot = new Map(); + for (const [ticker, row] of snap) { + currentPrice.set(ticker, row.price); + if (row.priorClose > 0) priorCloseFromSnapshot.set(ticker, row.priorClose); + } + + if (lookback === 1) { + // Use prior close from snapshot as the baseline. + for (const ticker of currentPrice.keys()) { + const priorClose = priorCloseFromSnapshot.get(ticker); + const price = currentPrice.get(ticker)!; + if (priorClose && priorClose > 0) out[ticker] = { priorClose, price }; + } + return out; + } + + // lookback > 1: fetch historical grouped bars for the window-start date. + const candidates = windowStartCandidates(now(), lookback); + + if (segment === "indices") { + // Per-ticker daily agg for small universes. + for (const c of constituents) { + const ticker = c.ticker; + const price = currentPrice.get(ticker); + if (price === undefined) continue; + const from = candidates[0]; + // to = from + 5 days to ensure a bar is returned even for weekend starts. + const toMs = new Date(from).getTime() + 5 * 86_400_000; + const to = new Date(toMs).toISOString().slice(0, 10); + try { + const url = `https://api.massive.com/v2/aggs/ticker/${encodeURIComponent(c.wsSymbol)}/range/1/day/${from}/${to}?adjusted=true&sort=asc&limit=1`; + const r = await fetch(url, { headers: { Authorization: `Bearer ${apiKey()}` } }); + const d: any = await r.json(); + const bar = d?.results?.[0]; + if (bar && typeof bar.c === "number") { + out[ticker] = { priorClose: bar.c, price }; + } + } catch (e) { + console.error(`[seed] per-ticker agg failed for ${c.wsSymbol}:`, e); + } + } + return out; + } + + // stocks, etfs, crypto, forex: use grouped daily (one call, all tickers). + let locale: string; + let market: string; + + if (segment === "stocks" || segment === "etfs") { + locale = "us"; + market = "stocks"; + } else if (segment === "crypto") { + locale = "global"; + market = "crypto"; + } else { + // forex + locale = "global"; + market = "fx"; + } + + const groupedMap = await findGroupedBaselines(locale, market, candidates); + + // Map each grouped bar (keyed by snapshot symbol form) back to its display ticker. + const symToTicker = new Map(); + for (const c of constituents) symToTicker.set(snapshotSymbol(segment, c), c.ticker); + + for (const [symKey, ticker] of symToTicker) { + const price = currentPrice.get(ticker); + if (price === undefined) continue; + // grouped bar T field uses the same snapshot symbol form (plain ticker for stocks/etfs, + // X:... for crypto, C:... for forex). + const historicalClose = groupedMap.get(symKey); + if (historicalClose !== undefined && historicalClose > 0) { + out[ticker] = { priorClose: historicalClose, price }; + } + // Tickers with no historical close are left unseeded (they start neutral). + } + + return out; +} + +export async function seedBaselines( + universe: Universe, + lookback = 1, + now = () => Date.now(), +): Promise> { + const rest = restClient(apiKey(), "https://api.massive.com"); + const out: Record = {}; + for (const [segment, constituents] of groupBySegment(universe)) { + Object.assign(out, await seedSegment(rest, segment, constituents, lookback, now)); + } + return out; +} diff --git a/examples/rest/massive-heatmap/backend/src/server.ts b/examples/rest/massive-heatmap/backend/src/server.ts new file mode 100644 index 0000000..23b41c2 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/server.ts @@ -0,0 +1,118 @@ +import http from "node:http"; +import { loadUniverse } from "./universeLoader.js"; +import { UniverseState } from "./state.js"; +import { sessionPhase } from "./session.js"; +import { seedBaselines, type Baseline } from "./seed.js"; +import { fetchCurrentPrices } from "./prices.js"; +import { isLookback } from "./lookback.js"; +import { constituentSegment, type Universe, type Segment } from "../../shared/universe.js"; + +export interface ServerDeps { + port: number; + now?: () => number; + // Seed baselines for a universe+lookback. Defaults to the real REST seed; tests inject a fake. + seed?: (u: Universe, lookback: number) => Promise>; + // Current price per ticker. Defaults to the real REST snapshot fetch; tests inject a fake. + fetchPrices?: (u: Universe) => Promise>; + baselineTtlMs?: number; // prior-close cache lifetime (default 10 min) + priceTtlMs?: number; // current-price cache lifetime (default 1 s) +} + +// Effective session for a (possibly mixed) universe: equity phase if any equities are +// present, else 24/7 for crypto/forex/etc. +function universeSession(universe: Universe, nowMs: number): ReturnType { + const segs = new Set(universe.constituents.map((c) => constituentSegment(universe, c))); + if (segs.has("stocks") || segs.has("etfs")) return sessionPhase("stocks", nowMs); + if (segs.size === 1) return sessionPhase([...segs][0], nowMs); + return "open24"; +} + +export async function startServer(deps: ServerDeps) { + const now = deps.now ?? (() => Date.now()); + const seed = deps.seed ?? seedBaselines; + const fetchPrices = deps.fetchPrices ?? fetchCurrentPrices; + const baselineTtlMs = deps.baselineTtlMs ?? 10 * 60_000; + const priceTtlMs = deps.priceTtlMs ?? 1_000; + + // Cache baselines per universe+lookback: prior-close is stable through the trading + // day, and re-seeding on every prices poll would be wasteful. The TTL refreshes it + // across a day boundary. The price cache collapses bursts/multiple tabs into at most + // one upstream call per universe per second. + // A rare double-fetch on a cold-cache miss is acceptable here; the 1s price cache and the client's interval keep real duplication negligible. + const baselineCache = new Map; at: number }>(); + const priceCache = new Map; at: number }>(); + + const getBaselines = async (u: Universe, lookback: number) => { + const key = `${u.id}:${lookback}`; + const hit = baselineCache.get(key); + if (hit && now() - hit.at < baselineTtlMs) return hit.baselines; + const baselines = await seed(u, lookback); + baselineCache.set(key, { baselines, at: now() }); + return baselines; + }; + const getPrices = async (u: Universe) => { + const hit = priceCache.get(u.id); + if (hit && now() - hit.at < priceTtlMs) return hit.prices; + const prices = await fetchPrices(u); + priceCache.set(u.id, { prices, at: now() }); + return prices; + }; + + const json = (res: http.ServerResponse, status: number, body: unknown) => { + res.writeHead(status, { "Content-Type": "application/json" }); + res.end(JSON.stringify(body)); + }; + + // Resolve and validate the universe + lookback from the query string. + const parse = (url: URL): { u: Universe; lb: number } | { error: number; message: string } => { + const id = url.searchParams.get("universe"); + if (!id) return { error: 400, message: "universe is required" }; + let u: Universe; + try { u = loadUniverse(id); } + catch (e) { return { error: 404, message: String(e) }; } + const raw = Number(url.searchParams.get("lookback") ?? "1"); + // Futures roll across contracts, so a fixed lookback is meaningless: force intraday. + // universes are single-segment, so the universe segment is authoritative here. + const lb = u.segment === "futures" ? 1 : isLookback(raw) ? raw : 1; + return { u, lb }; + }; + + const server = http.createServer(async (req, res) => { + try { + const url = new URL(req.url ?? "/", "http://localhost"); + + if (req.method === "GET" && url.pathname === "/api/snapshot") { + const p = parse(url); + if ("error" in p) return json(res, p.error, { error: p.message }); + const baselines = await getBaselines(p.u, p.lb); + const state = new UniverseState(p.u.constituents); + state.seed(baselines); + return json(res, 200, { + universeId: p.u.id, label: p.u.label, segment: p.u.segment, + session: universeSession(p.u, now()), tiles: state.tiles(), + }); + } + + if (req.method === "GET" && url.pathname === "/api/prices") { + const p = parse(url); + if ("error" in p) return json(res, p.error, { error: p.message }); + const baselines = await getBaselines(p.u, p.lb); // uses the cached baseline; seeds only on a miss + const prices = await getPrices(p.u); + const state = new UniverseState(p.u.constituents); + state.seed(baselines); + for (const [t, price] of prices) state.applyPrice(t, price); + return json(res, 200, { session: universeSession(p.u, now()), updates: state.updates() }); + } + + return json(res, 404, { error: "not found" }); + } catch (e) { + return json(res, 500, { error: String(e) }); + } + }); + + await new Promise((r) => server.listen(deps.port, () => r())); + return { + address: () => server.address() as { port: number }, + close: () => new Promise((resolve) => server.close(() => resolve())), + }; +} diff --git a/examples/rest/massive-heatmap/backend/src/session.ts b/examples/rest/massive-heatmap/backend/src/session.ts new file mode 100644 index 0000000..4b641fd --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/session.ts @@ -0,0 +1,27 @@ +import type { Segment, SessionPhase } from "../../shared/protocol.js"; + +const ET_FMT = new Intl.DateTimeFormat("en-US", { + timeZone: "America/New_York", + hour: "2-digit", minute: "2-digit", weekday: "short", hour12: false, +}); + +function etParts(epochMs: number): { hours: number; weekday: number } { + const parts = ET_FMT.formatToParts(new Date(epochMs)); + const get = (t: string) => parts.find((p) => p.type === t)?.value ?? ""; + const hour = parseInt(get("hour"), 10) % 24; + const minute = parseInt(get("minute"), 10); + const wdMap: Record = { Sun:0, Mon:1, Tue:2, Wed:3, Thu:4, Fri:5, Sat:6 }; + return { hours: hour + minute / 60, weekday: wdMap[get("weekday")] ?? 0 }; +} + +export function sessionPhase(segment: Segment, epochMs: number): SessionPhase { + if (segment === "crypto") return "open24"; + const { hours, weekday } = etParts(epochMs); + const weekend = weekday === 0 || weekday === 6; + if (segment === "forex" || segment === "futures") return weekend ? "closed" : "regular"; + if (weekend) return "closed"; + if (hours >= 4 && hours < 9.5) return "premarket"; + if (hours >= 9.5 && hours < 16) return "regular"; + if (hours >= 16 && hours < 20) return "afterhours"; + return "closed"; +} diff --git a/examples/rest/massive-heatmap/backend/src/snapshot.ts b/examples/rest/massive-heatmap/backend/src/snapshot.ts new file mode 100644 index 0000000..372c66f --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/snapshot.ts @@ -0,0 +1,93 @@ +import { restClient } from "@massive.com/client-js"; +import type { Constituent, Segment } from "../../shared/universe.js"; +import { apiKey } from "./env.js"; + +export interface SnapRow { + price: number; + priorClose: number; + wsSymbol?: string; // front-month contract symbol (futures only), for historical aggs +} + +// Snapshot symbol form per segment (matches how the snapshot endpoints key rows). +export function snapshotSymbol(segment: Segment, c: Constituent): string { + switch (segment) { + case "crypto": return "X:" + c.wsSymbol.replace(/-/g, ""); + case "forex": return "C:" + c.wsSymbol.replace(/\//g, ""); + case "indices": return c.wsSymbol; + default: return c.ticker; // stocks, etfs, futures + } +} + +function chunk(arr: T[], n: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n)); + return out; +} + +// Current price + prior close per display ticker, from one asset class's REST snapshot. +// Shared by seed (which also reads priorClose) and the poller (which reads price). For +// futures it resolves the front-month contract and returns its symbol for historical aggs. +export async function fetchSnapshot( + rest: ReturnType, + segment: Segment, + constituents: Constituent[], +): Promise> { + const out = new Map(); + + if (segment === "futures") { + const symToDisplay = new Map(constituents.map((c) => [c.wsSymbol, c.ticker] as const)); + for (const product of [...new Set(constituents.map((c) => c.ticker))]) { + try { + const r = await fetch( + `https://api.massive.com/futures/v1/snapshot?product_code=${encodeURIComponent(product)}&limit=1000`, + { headers: { Authorization: `Bearer ${apiKey()}` } }, + ); + const d: any = await r.json(); + for (const row of d?.results ?? []) { + const wsSymbol: string | undefined = row?.details?.ticker; + const disp = wsSymbol && symToDisplay.get(wsSymbol); + if (!disp || !wsSymbol) continue; + const open = row?.session?.open, close = row?.session?.close; + if (typeof close === "number") { + out.set(disp, { price: close, priorClose: open > 0 ? open : 0, wsSymbol }); + } + } + } catch (e) { console.error("[snapshot] futures failed for", product, e); } + } + return out; + } + + const symToTicker = new Map(); + for (const c of constituents) symToTicker.set(snapshotSymbol(segment, c), c.ticker); + const record = (rows: any[] | undefined, getSym: (r: any) => string) => { + for (const r of rows ?? []) { + const ticker = symToTicker.get(getSym(r)); + if (!ticker) continue; + const priorClose = r?.prevDay?.c ?? r?.session?.previous_close ?? 0; + const price = r?.lastTrade?.p ?? r?.day?.c ?? r?.value ?? r?.session?.close ?? priorClose; + out.set(ticker, { price, priorClose }); + } + }; + + for (const part of chunk([...symToTicker.keys()], 100)) { + try { + if (segment === "stocks" || segment === "etfs") { + const res: any = await rest.getStocksSnapshotTickers({ tickers: part.join(",") } as any); + record(res?.tickers, (r) => r.ticker); + } else if (segment === "crypto") { + const res: any = await rest.getCryptoSnapshotTickers({ tickers: part.join(",") } as any); + record(res?.tickers, (r) => r.ticker); + } else if (segment === "forex") { + const res: any = await rest.getForexSnapshotTickers({ tickers: part.join(",") } as any); + record(res?.tickers, (r) => r.ticker); + } else if (segment === "indices") { + const res: any = await rest.getIndicesSnapshot({ tickerAnyOf: part.join(",") }); + record(res?.results, (r) => r.ticker); + } + } catch (e) { + console.error(`[snapshot] failed for ${segment} chunk`, e); + } + } + + return out; +} diff --git a/examples/rest/massive-heatmap/backend/src/state.ts b/examples/rest/massive-heatmap/backend/src/state.ts new file mode 100644 index 0000000..3a0572c --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/state.ts @@ -0,0 +1,35 @@ +import type { Constituent } from "../../shared/universe.js"; +import type { Tile, TileUpdate } from "../../shared/protocol.js"; + +export class UniverseState { + private rows = new Map(); + + constructor(constituents: Constituent[]) { + for (const c of constituents) { + this.rows.set(c.ticker, { + ticker: c.ticker, name: c.name, group: c.group, marketCap: c.marketCap, + priorClose: 0, price: 0, pct: 0, + }); + } + } + seed(baselines: Record): void { + for (const [ticker, b] of Object.entries(baselines)) { + const row = this.rows.get(ticker); + if (!row) continue; + row.priorClose = b.priorClose; row.price = b.price; + row.pct = b.priorClose > 0 ? (b.price - b.priorClose) / b.priorClose : 0; + } + } + applyPrice(ticker: string, price: number): void { + const row = this.rows.get(ticker); + if (!row) return; + row.price = price; + row.pct = row.priorClose > 0 ? (price - row.priorClose) / row.priorClose : 0; + } + // Current price/pct for every tile. /api/prices returns the full set each poll, so + // there is no per-call diff tracking to maintain. + updates(): TileUpdate[] { + return [...this.rows.values()].map((r) => ({ ticker: r.ticker, price: r.price, pct: r.pct })); + } + tiles(): Tile[] { return [...this.rows.values()].map((r) => ({ ...r })); } +} diff --git a/examples/rest/massive-heatmap/backend/src/universeLoader.ts b/examples/rest/massive-heatmap/backend/src/universeLoader.ts new file mode 100644 index 0000000..5e2f128 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/src/universeLoader.ts @@ -0,0 +1,24 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import type { Universe, Constituent, Segment } from "../../shared/universe.js"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const UNIVERSE_DIR = join(HERE, "..", "..", "universes"); +const SEGMENTS: Segment[] = ["stocks", "etfs", "crypto", "forex", "futures", "indices"]; + +export function listUniverseIds(): string[] { + return readdirSync(UNIVERSE_DIR).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, "")); +} +function isConstituent(x: unknown): x is Constituent { + const c = x as Constituent; + return !!c && typeof c.ticker === "string" && typeof c.wsSymbol === "string" + && typeof c.name === "string" && typeof c.group === "string" && typeof c.marketCap === "number"; +} +export function loadUniverse(id: string): Universe { + if (!listUniverseIds().includes(id)) throw new Error(`Unknown universe: ${id}`); + const raw = JSON.parse(readFileSync(join(UNIVERSE_DIR, `${id}.json`), "utf8")) as Universe; + if (!SEGMENTS.includes(raw.segment)) throw new Error(`Bad segment in ${id}: ${raw.segment}`); + if (!Array.isArray(raw.constituents) || !raw.constituents.every(isConstituent)) throw new Error(`Invalid constituents in ${id}`); + return raw; +} diff --git a/examples/rest/massive-heatmap/backend/test/lookback.test.ts b/examples/rest/massive-heatmap/backend/test/lookback.test.ts new file mode 100644 index 0000000..fa7a03e --- /dev/null +++ b/examples/rest/massive-heatmap/backend/test/lookback.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { isLookback, windowStartCandidates } from "../src/lookback.js"; + +describe("isLookback", () => { + it("accepts valid lookback values", () => { + expect(isLookback(1)).toBe(true); + expect(isLookback(30)).toBe(true); + expect(isLookback(1825)).toBe(true); + }); + it("rejects invalid lookback values", () => { + expect(isLookback(2)).toBe(false); + expect(isLookback("30")).toBe(false); + expect(isLookback(null)).toBe(false); + expect(isLookback(undefined)).toBe(false); + expect(isLookback(0)).toBe(false); + }); +}); + +describe("windowStartCandidates", () => { + // Date.UTC(2026, 4, 26) = 2026-05-26T00:00:00Z + const now = Date.UTC(2026, 4, 26); // 2026-05-26 + + it("returns 7 descending consecutive dates starting at (now - 1 day)", () => { + const candidates = windowStartCandidates(now, 1); + expect(candidates).toHaveLength(7); + expect(candidates[0]).toBe("2026-05-25"); + // Each subsequent date should be one day earlier + for (let i = 1; i < candidates.length; i++) { + const prev = new Date(candidates[i - 1]).getTime(); + const curr = new Date(candidates[i]).getTime(); + expect(prev - curr).toBe(86_400_000); + } + }); + + it("the 7 dates are descending from 2026-05-25", () => { + const candidates = windowStartCandidates(now, 1); + expect(candidates).toEqual([ + "2026-05-25", + "2026-05-24", + "2026-05-23", + "2026-05-22", + "2026-05-21", + "2026-05-20", + "2026-05-19", + ]); + }); + + it("365-day lookback yields a date ~1 year earlier", () => { + const candidates = windowStartCandidates(now, 365); + // now is 2026-05-26, minus 365 days = 2025-05-26 + expect(candidates[0]).toBe("2025-05-26"); + }); + + it("respects custom back parameter", () => { + const candidates = windowStartCandidates(now, 1, 2); + expect(candidates).toHaveLength(3); // 0..back inclusive + }); +}); diff --git a/examples/rest/massive-heatmap/backend/test/seed.multiSegment.test.ts b/examples/rest/massive-heatmap/backend/test/seed.multiSegment.test.ts new file mode 100644 index 0000000..bb3f7c9 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/test/seed.multiSegment.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import { groupBySegment } from "../src/seed.js"; +import type { Universe } from "../../shared/universe.js"; + +const mixed: Universe = { + id: "custom", segment: "stocks", label: "Mix", + constituents: [ + { ticker: "AAPL", wsSymbol: "AAPL", name: "Apple", group: "Stocks", marketCap: 1, segment: "stocks" }, + { ticker: "BTC", wsSymbol: "BTC-USD", name: "Bitcoin", group: "Crypto", marketCap: 1, segment: "crypto" }, + { ticker: "MSFT", wsSymbol: "MSFT", name: "Microsoft", group: "Stocks", marketCap: 1, segment: "stocks" }, + ], +}; + +describe("groupBySegment", () => { + it("buckets constituents by effective segment", () => { + const g = groupBySegment(mixed); + expect([...g.keys()].sort()).toEqual(["crypto", "stocks"]); + expect(g.get("stocks")!.map(c => c.ticker)).toEqual(["AAPL", "MSFT"]); + expect(g.get("crypto")!.map(c => c.ticker)).toEqual(["BTC"]); + }); + it("falls back to universe.segment when constituent omits it", () => { + const single: Universe = { id: "sp500", segment: "stocks", label: "S&P", + constituents: [{ ticker: "AAPL", wsSymbol: "AAPL", name: "Apple", group: "Tech", marketCap: 1 }] }; + expect([...groupBySegment(single).keys()]).toEqual(["stocks"]); + }); +}); diff --git a/examples/rest/massive-heatmap/backend/test/server.integration.test.ts b/examples/rest/massive-heatmap/backend/test/server.integration.test.ts new file mode 100644 index 0000000..45ea0b1 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/test/server.integration.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, vi } from "vitest"; +import { startServer } from "../src/server.js"; +import type { ServerDeps } from "../src/server.js"; +import type { Universe } from "../../shared/universe.js"; + +const baselines = { BTC: { priorClose: 100, price: 100 }, ETH: { priorClose: 50, price: 50 } }; + +function deps(extra: Partial = {}) { + return { + port: 0, + seed: vi.fn(async (_u: Universe, _lb: number) => ({ ...baselines })), + fetchPrices: vi.fn(async (_u: Universe) => new Map([["BTC", 110]])), + ...extra, + }; +} + +async function start(extra?: Partial) { + const d = deps(extra); + const srv = await startServer(d as any); + const base = `http://localhost:${srv.address().port}`; + return { srv, base, d }; +} + +describe("http server", () => { + it("GET /api/snapshot returns seeded tiles", async () => { + const { srv, base, d } = await start(); + const body = await (await fetch(`${base}/api/snapshot?universe=crypto&lookback=1`)).json(); + expect(d.seed).toHaveBeenCalledWith(expect.anything(), 1); + expect(body.universeId).toBe("crypto"); + expect(body.tiles).toContainEqual(expect.objectContaining({ ticker: "BTC", price: 100, pct: 0 })); + await srv.close(); + }); + + it("passes lookback to seed", async () => { + const { srv, base, d } = await start(); + await fetch(`${base}/api/snapshot?universe=crypto&lookback=30`); + expect(d.seed).toHaveBeenCalledWith(expect.anything(), 30); + await srv.close(); + }); + + it("GET /api/prices returns current price/pct updates", async () => { + const { srv, base } = await start(); + const body = await (await fetch(`${base}/api/prices?universe=crypto&lookback=1`)).json(); + expect(body.updates).toContainEqual({ ticker: "BTC", price: 110, pct: 0.1 }); + await srv.close(); + }); + + it("re-seeds on a prices cache miss when no snapshot ran first", async () => { + const { srv, base, d } = await start(); + await fetch(`${base}/api/prices?universe=crypto&lookback=1`); + expect(d.seed).toHaveBeenCalled(); + await srv.close(); + }); + + it("returns 404 for an unknown universe", async () => { + const { srv, base } = await start(); + const res = await fetch(`${base}/api/snapshot?universe=nope&lookback=1`); + expect(res.status).toBe(404); + await srv.close(); + }); + + it("404s an unknown path", async () => { + const { srv, base } = await start(); + const res = await fetch(`${base}/api/nope`); + expect(res.status).toBe(404); + await srv.close(); + }); + + it("returns 500 when seed throws", async () => { + const { srv, base } = await start({ + seed: vi.fn(async () => { throw new Error("upstream failed"); }), + }); + const res = await fetch(`${base}/api/snapshot?universe=crypto&lookback=1`); + expect(res.status).toBe(500); + await srv.close(); + }); +}); diff --git a/examples/rest/massive-heatmap/backend/test/session.test.ts b/examples/rest/massive-heatmap/backend/test/session.test.ts new file mode 100644 index 0000000..b70d2b1 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/test/session.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from "vitest"; +import { sessionPhase } from "../src/session.js"; + +const et = (h: number, m = 0) => Date.parse(`2026-06-23T${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:00-04:00`); + +describe("sessionPhase", () => { + it("crypto is always open24", () => { expect(sessionPhase("crypto", et(3, 0))).toBe("open24"); }); + it("stocks pre-market at 05:00 ET", () => { expect(sessionPhase("stocks", et(5, 0))).toBe("premarket"); }); + it("stocks regular at 10:00 ET", () => { expect(sessionPhase("stocks", et(10, 0))).toBe("regular"); }); + it("stocks after-hours at 17:00 ET", () => { expect(sessionPhase("stocks", et(17, 0))).toBe("afterhours"); }); + it("stocks closed at 22:00 ET", () => { expect(sessionPhase("stocks", et(22, 0))).toBe("closed"); }); + it("stocks closed on weekend", () => { const sunday = Date.parse("2026-06-21T10:00:00-04:00"); expect(sessionPhase("stocks", sunday)).toBe("closed"); }); +}); diff --git a/examples/rest/massive-heatmap/backend/test/state.test.ts b/examples/rest/massive-heatmap/backend/test/state.test.ts new file mode 100644 index 0000000..48da9d0 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/test/state.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { UniverseState } from "../src/state.js"; +import type { Constituent } from "../../shared/universe.js"; + +const cons: Constituent[] = [ + { ticker: "AAPL", wsSymbol: "AAPL", name: "Apple", group: "Tech", marketCap: 3000 }, + { ticker: "XOM", wsSymbol: "XOM", name: "Exxon", group: "Energy", marketCap: 500 }, +]; + +describe("UniverseState", () => { + it("seeds baselines and computes pct", () => { + const s = new UniverseState(cons); + s.seed({ AAPL: { priorClose: 100, price: 100 }, XOM: { priorClose: 50, price: 50 } }); + expect(s.tiles().find((t) => t.ticker === "AAPL")!.pct).toBe(0); + }); + + it("applyPrice updates price and pct", () => { + const s = new UniverseState(cons); + s.seed({ AAPL: { priorClose: 100, price: 100 }, XOM: { priorClose: 50, price: 50 } }); + s.applyPrice("AAPL", 110); + expect(s.updates()).toContainEqual({ ticker: "AAPL", price: 110, pct: 0.1 }); + }); + + it("updates() returns one entry per tile", () => { + const s = new UniverseState(cons); + s.seed({ AAPL: { priorClose: 100, price: 100 }, XOM: { priorClose: 50, price: 50 } }); + expect(s.updates()).toHaveLength(2); + }); + + it("ignores unknown tickers and never grows", () => { + const s = new UniverseState(cons); + s.seed({ AAPL: { priorClose: 100, price: 100 }, XOM: { priorClose: 50, price: 50 } }); + s.applyPrice("ZZZZ", 5); + expect(s.tiles()).toHaveLength(2); + }); +}); diff --git a/examples/rest/massive-heatmap/backend/test/universeLoader.test.ts b/examples/rest/massive-heatmap/backend/test/universeLoader.test.ts new file mode 100644 index 0000000..17a5d19 --- /dev/null +++ b/examples/rest/massive-heatmap/backend/test/universeLoader.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from "vitest"; +import { loadUniverse, listUniverseIds } from "../src/universeLoader.js"; + +describe("universeLoader", () => { + it("lists shipped universes", () => { expect(listUniverseIds()).toContain("crypto"); }); + it("loads and validates crypto", () => { + const u = loadUniverse("crypto"); + expect(u.segment).toBe("crypto"); + expect(u.constituents.length).toBeGreaterThan(0); + expect(u.constituents[0]).toHaveProperty("wsSymbol"); + }); + it("throws on unknown id", () => { expect(() => loadUniverse("nope")).toThrow(); }); +}); diff --git a/examples/rest/massive-heatmap/backend/tsconfig.json b/examples/rest/massive-heatmap/backend/tsconfig.json new file mode 100644 index 0000000..7c9bb3f --- /dev/null +++ b/examples/rest/massive-heatmap/backend/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "types": ["node"] + }, + "include": ["src", "test", "../shared"] +} diff --git a/examples/rest/massive-heatmap/frontend/index.html b/examples/rest/massive-heatmap/frontend/index.html new file mode 100644 index 0000000..f78e00b --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/index.html @@ -0,0 +1,5 @@ + + + Massive Heatmap +
+ diff --git a/examples/rest/massive-heatmap/frontend/package.json b/examples/rest/massive-heatmap/frontend/package.json new file mode 100644 index 0000000..ff2ee4b --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "test": "vitest run" + }, + "dependencies": { + "d3-hierarchy": "^3.1.2", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/d3-hierarchy": "^3.1.7", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "jsdom": "^25.0.0", + "vite": "^5.4.0", + "vitest": "^2.1.8" + } +} diff --git a/examples/rest/massive-heatmap/frontend/public/logo.png b/examples/rest/massive-heatmap/frontend/public/logo.png new file mode 100644 index 0000000..eeae1c9 Binary files /dev/null and b/examples/rest/massive-heatmap/frontend/public/logo.png differ diff --git a/examples/rest/massive-heatmap/frontend/public/massive-logo-white.svg b/examples/rest/massive-heatmap/frontend/public/massive-logo-white.svg new file mode 100644 index 0000000..3fa3b3e --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/public/massive-logo-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/rest/massive-heatmap/frontend/src/App.tsx b/examples/rest/massive-heatmap/frontend/src/App.tsx new file mode 100644 index 0000000..5d65340 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/App.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from "react"; +import { connect } from "./api.js"; +import { TileStore } from "./store.js"; +import { Heatmap } from "./Heatmap.js"; +import { Controls } from "./Controls.js"; +import { SessionBadge } from "./SessionBadge.js"; +import { Footer } from "./Footer.js"; +import { Tooltip } from "./Tooltip.js"; +import { ExportPreview } from "./ExportPreview.js"; +import { SettingsModal } from "./SettingsModal.js"; +import { LoadingOverlay } from "./LoadingOverlay.js"; +import { useSettings } from "./settings.js"; +import { clampForLookback } from "./color.js"; +import type { Tile, Segment, SessionPhase } from "../../shared/protocol.js"; + +const SESSION_LABEL: Record = { + regular: "Market open", premarket: "Pre-market", afterhours: "After hours", closed: "Market closed", open24: "24/7", +}; +const SEGMENT_TITLE: Record = { + stocks: "Stocks", etfs: "ETFs", crypto: "Crypto", forex: "Forex", futures: "Futures", indices: "Indices", +}; +const LOOKBACKS = [1, 7, 30, 90, 180, 365, 1825] as const; +const LOOKBACK_LABEL: Record = { 1: "1D", 7: "7D", 30: "30D", 90: "90D", 180: "180D", 365: "1Y", 1825: "5Y" }; +const LOOKBACK_EXPORT: Record = { 1: "1 Day", 7: "7 Day", 30: "30 Day", 90: "90 Day", 180: "180 Day", 365: "1 Year", 1825: "5 Year" }; +const clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v)); + +export function App() { + const store = useRef(new TileStore()); + const [, force] = useState(0); + const [label, setLabel] = useState("S&P 500"); + const [segment, setSegment] = useState("stocks"); + const [hover, setHover] = useState<{ tile: Tile; x: number; y: number } | null>(null); + const [showExport, setShowExport] = useState(false); + const [showSettings, setShowSettings] = useState(false); + const [zoom, setZoom] = useState(1); + const [lookback, setLookback] = useState(1); + // Covers the heatmap from when a fetch is requested until its snapshot (or an + // error) arrives. loadingName is what's being fetched, shown in the overlay. + const [loading, setLoading] = useState(true); + const [loadingName, setLoadingName] = useState("S&P 500"); + const wrapRef = useRef(null); + const dragRef = useRef<{ x: number; y: number; sl: number; st: number } | null>(null); + const [vp, setVp] = useState({ w: window.innerWidth, h: window.innerHeight - 108 }); + const [conn] = useState(() => + connect((m) => { + if (m.type === "snapshot") { store.current.applySnapshot(m); setLabel(m.label); setSegment(m.segment); setLoading(false); } + else if (m.type === "diff") store.current.applyDiff(m); + else if (m.type === "error") setLoading(false); + force((n) => n + 1); + }) + ); + // Futures roll across contracts, so lookback is not meaningful: force 1D (intraday) + // and hide the lookback control while a futures universe is shown. + const isFutures = segment === "futures"; + const effLookback = isFutures ? 1 : lookback; + const { settings } = useSettings(); + const colorClamp = settings.clamps[effLookback] ?? clampForLookback(effLookback); + useEffect(() => { conn.select("sp500", 1); }, []); + // Re-time the polling interval on load and whenever the setting changes. + useEffect(() => { conn.setRefresh(settings.refreshMs); }, [settings.refreshMs]); + useEffect(() => { + const onResize = () => setVp({ w: window.innerWidth, h: window.innerHeight - 108 }); + window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); + }, []); + + // Mouse wheel zooms (anchored to the cursor); no modifier needed. + useEffect(() => { + const el = wrapRef.current; if (!el) return; + const onWheel = (e: WheelEvent) => { + e.preventDefault(); + const rect = el.getBoundingClientRect(); + const cx = e.clientX - rect.left, cy = e.clientY - rect.top; + setZoom((z) => { + const nz = clamp(+(z * (e.deltaY < 0 ? 1.12 : 1 / 1.12)).toFixed(3), 1, 8); + if (nz === z) return z; + const ratio = nz / z; + requestAnimationFrame(() => { + el.scrollLeft = (el.scrollLeft + cx) * ratio - cx; + el.scrollTop = (el.scrollTop + cy) * ratio - cy; + }); + return nz; + }); + }; + el.addEventListener("wheel", onWheel, { passive: false }); + return () => el.removeEventListener("wheel", onWheel); + }, []); + + // Click and drag to pan. + useEffect(() => { + const onMove = (e: MouseEvent) => { + const el = wrapRef.current, d = dragRef.current; if (!el || !d) return; + el.scrollLeft = d.sl - (e.clientX - d.x); + el.scrollTop = d.st - (e.clientY - d.y); + }; + const onUp = () => { dragRef.current = null; }; + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; + }, []); + + const tiles = store.current.tiles(); + const session = store.current.session() as SessionPhase; + const dateStr = new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); + const title = `${SEGMENT_TITLE[segment]} ${label}`; + + return ( +
+
+
+
Real-time · Sized by market cap
+
{title} Heatmap
+
+ { setLoading(true); setLoadingName(name); conn.select(id, seg === "futures" ? 1 : lookback); setZoom(1); }} /> + {!isFutures && ( + + )} +
+ + + + MASSIVE +
+
+
+
setZoom(1)} + onMouseDown={(e) => { + const el = wrapRef.current; if (!el) return; + dragRef.current = { x: e.clientX, y: e.clientY, sl: el.scrollLeft, st: el.scrollTop }; + setHover(null); + }}> + { if (dragRef.current) return; setHover(t ? { tile: t, x: x ?? 0, y: y ?? 0 } : null); }} /> +
+ {loading && } +
+
+ {hover && } + {showSettings && setShowSettings(false)} />} + {showExport && ( + setShowExport(false)} /> + )} +
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/Controls.tsx b/examples/rest/massive-heatmap/frontend/src/Controls.tsx new file mode 100644 index 0000000..082455c --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/Controls.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { useSettings } from "./settings.js"; + +const SEGMENT_LABEL: Record = { stocks: "Stocks", etfs: "ETFs", crypto: "Crypto", forex: "Forex", futures: "Futures", indices: "Indices" }; +const UNIVERSES: Record = { + stocks: [{ id: "sp500", label: "S&P 500" }, { id: "nasdaq100", label: "Nasdaq 100" }, { id: "dow30", label: "Dow 30" }], + etfs: [{ id: "etf-sectors", label: "Sector SPDRs" }, { id: "etf-broad", label: "Broad Market" }, { id: "etf-thematic", label: "Thematic" }], + crypto: [{ id: "crypto", label: "Top Coins" }, { id: "crypto-l1", label: "Layer 1s" }, { id: "crypto-defi", label: "DeFi" }], + forex: [{ id: "forex", label: "FX Majors" }, { id: "forex-crosses", label: "Crosses" }, { id: "forex-exotic", label: "USD Exotics" }], + futures: [{ id: "futures-equity", label: "Equity Index" }, { id: "futures-energy", label: "Energy" }, { id: "futures-metals", label: "Metals" }], + indices: [{ id: "indices", label: "Major Indices" }, { id: "indices-sectors", label: "Sectors" }, { id: "indices-global", label: "Global" }], +}; + +export function Controls({ onSelectUniverse }: { + onSelectUniverse: (id: string, segment: string, label: string) => void; +}) { + const { settings } = useSettings(); + const hiddenSeg = new Set(settings.hiddenSegments); + const hiddenUni = new Set(settings.hiddenUniverses); + + const segments = Object.keys(UNIVERSES).filter((s) => !hiddenSeg.has(s as any)); + const [segment, setSegment] = useState(segments[0] ?? "stocks"); + + const visibleUniverses = (seg: string) => + (UNIVERSES[seg] ?? []).filter((u) => !hiddenUni.has(u.id)); + + const onSegmentChange = (s: string) => { + setSegment(s); + const first = visibleUniverses(s)[0]; + if (first) onSelectUniverse(first.id, s, first.label); + }; + + // If the active segment gets hidden in settings, fall back to the first visible one. + useEffect(() => { + if (segments.includes(segment)) return; + const next = segments[0] ?? "stocks"; + setSegment(next); + const first = visibleUniverses(next)[0]; + if (first) onSelectUniverse(first.id, next, first.label); + }, [segments.join(","), segment]); + + return ( +
+ + +
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/ExportPreview.tsx b/examples/rest/massive-heatmap/frontend/src/ExportPreview.tsx new file mode 100644 index 0000000..74e0f2f --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/ExportPreview.tsx @@ -0,0 +1,29 @@ +import { useEffect, useRef } from "react"; +import type { Tile } from "../../shared/protocol.js"; +import { renderExport, downloadCanvasPng, type ExportMeta } from "./capture.js"; + +export function ExportPreview({ tiles, meta, clamp = 0.06, layoutW, layoutH, onClose }: + { tiles: Tile[]; meta: ExportMeta; clamp?: number; layoutW: number; layoutH: number; onClose: () => void }) { + const ref = useRef(null); + useEffect(() => { + const canvas = ref.current; if (!canvas) return; + const logo = new Image(); + logo.onload = () => renderExport(canvas, tiles, meta, logo, clamp, layoutW, layoutH); + logo.onerror = () => renderExport(canvas, tiles, meta, undefined, clamp, layoutW, layoutH); + logo.src = "/massive-logo-white.svg"; + }, [tiles, meta, clamp, layoutW, layoutH]); + return ( +
+
e.stopPropagation()}> + +
+ + +
+
+
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/Footer.tsx b/examples/rest/massive-heatmap/frontend/src/Footer.tsx new file mode 100644 index 0000000..a155b94 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/Footer.tsx @@ -0,0 +1,16 @@ +import type { SessionPhase } from "../../shared/protocol.js"; +const SESSION_LABEL: Record = { + regular: "Market open", premarket: "Pre-market", afterhours: "After hours", closed: "Market closed", open24: "24/7", +}; +export function Footer({ session, dateStr, clamp = 0.06 }: { session: SessionPhase; dateStr: string; clamp?: number }) { + const pct = Math.round(clamp * 100); + return ( +
+
Data via Massive.com  ·  {SESSION_LABEL[session] ?? "—"}
+
+
-{pct}%
+{pct}%
+
{dateStr}
+
+
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/Heatmap.tsx b/examples/rest/massive-heatmap/frontend/src/Heatmap.tsx new file mode 100644 index 0000000..ff0cd4c --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/Heatmap.tsx @@ -0,0 +1,59 @@ +import { useEffect, useMemo, useRef } from "react"; +import type { Tile } from "../../shared/protocol.js"; +import { layout, type Layout, type Rect } from "./treemap.js"; +import { drawHeatmap, sectorAggregates } from "./render.js"; + +export function Heatmap({ tiles, width, height, clamp = 0.06, onHover }: + { tiles: Tile[]; width: number; height: number; clamp?: number; + onHover: (t: Tile | null, x?: number, y?: number) => void; }) { + const canvasRef = useRef(null); + // Render the backing store at device pixels so text/edges stay sharp on HiDPI + // displays; the canvas is still laid out (and hit-tested) in CSS pixels. + const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1; + const tilesRef = useRef(tiles); tilesRef.current = tiles; + const clampRef = useRef(clamp); clampRef.current = clamp; + const key = tiles.map((t) => t.ticker).join(","); + const lay = useMemo(() => layout(tiles, width, height), [key, width, height]); + const layRef = useRef(lay); layRef.current = lay; + const byTickerRef = useRef>(new Map()); + const aggRef = useRef>(new Map()); + const lastTilesRef = useRef(null); + const dirtyRef = useRef(true); + + // Mark dirty whenever the layout (zoom/size/universe) or clamp changes. + useEffect(() => { dirtyRef.current = true; }, [lay, width, height, clamp, dpr]); + + useEffect(() => { + const ctx = canvasRef.current!.getContext("2d")!; + let raf = 0; + const draw = () => { + if (tilesRef.current !== lastTilesRef.current) { + byTickerRef.current = new Map(tilesRef.current.map((t) => [t.ticker, t])); + aggRef.current = sectorAggregates(tilesRef.current); + lastTilesRef.current = tilesRef.current; + dirtyRef.current = true; + } + if (dirtyRef.current) { + // Map CSS pixels to device pixels for this frame, then draw in CSS units. + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, width, height); + drawHeatmap(ctx, layRef.current, byTickerRef.current, aggRef.current, clampRef.current); + dirtyRef.current = false; + } + raf = requestAnimationFrame(draw); + }; + raf = requestAnimationFrame(draw); + return () => cancelAnimationFrame(raf); + }, [width, height, dpr]); + + return { + const b = canvasRef.current!.getBoundingClientRect(); + const x = e.clientX - b.left, y = e.clientY - b.top; + const hit = layRef.current.tiles.find((r: Rect) => x >= r.x0 && x <= r.x1 && y >= r.y0 && y <= r.y1); + onHover(hit ? tilesRef.current.find((t) => t.ticker === hit.ticker) ?? null : null, e.clientX, e.clientY); + }} + onMouseLeave={() => onHover(null)} />; +} diff --git a/examples/rest/massive-heatmap/frontend/src/Legend.tsx b/examples/rest/massive-heatmap/frontend/src/Legend.tsx new file mode 100644 index 0000000..c32b9e8 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/Legend.tsx @@ -0,0 +1,3 @@ +export function Legend() { + return
-3%
+3%
; +} diff --git a/examples/rest/massive-heatmap/frontend/src/LoadingOverlay.tsx b/examples/rest/massive-heatmap/frontend/src/LoadingOverlay.tsx new file mode 100644 index 0000000..730fe37 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/LoadingOverlay.tsx @@ -0,0 +1,11 @@ +// Shown over the heatmap while a new snapshot is being fetched. The canvas behind +// it is dimmed and blurred (via the `.is-loading` class on .canvas-wrap) so the +// previous view stays as context instead of flashing to black. +export function LoadingOverlay({ name }: { name?: string }) { + return ( +
+
+
{name ? `Loading ${name}…` : "Loading…"}
+
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/SessionBadge.tsx b/examples/rest/massive-heatmap/frontend/src/SessionBadge.tsx new file mode 100644 index 0000000..7d924bc --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/SessionBadge.tsx @@ -0,0 +1,14 @@ +import type { SessionPhase } from "../../shared/protocol.js"; +const MAP: Record = { + regular: { label: "MARKET OPEN", color: "#16A34A" }, + premarket: { label: "PRE-MARKET", color: "#F59E0B" }, + afterhours: { label: "AFTER HOURS", color: "#3B82F6" }, + closed: { label: "CLOSED", color: "#7D8794" }, + open24: { label: "24/7", color: "#16A34A" }, +}; +export function SessionBadge({ phase }: { phase: SessionPhase }) { + const m = MAP[phase] ?? MAP.closed; + return + {m.label} + ; +} diff --git a/examples/rest/massive-heatmap/frontend/src/SettingsModal.tsx b/examples/rest/massive-heatmap/frontend/src/SettingsModal.tsx new file mode 100644 index 0000000..ed1fe7c --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/SettingsModal.tsx @@ -0,0 +1,103 @@ +import { useSettings } from "./settings.js"; +import { clampForLookback } from "./color.js"; +import { REFRESH_OPTIONS } from "./settingsStore.js"; +import type { Segment } from "../../shared/universe.js"; + +const LOOKBACKS = [1, 7, 30, 90, 180, 365, 1825]; +const LOOKBACK_LABEL: Record = { 1: "1D", 7: "7D", 30: "30D", 90: "90D", 180: "180D", 365: "1Y", 1825: "5Y" }; +const round1 = (x: number) => Math.round(x * 10) / 10; +const refreshLabel = (ms: number) => `${ms / 1000}s`; + +const SEGMENTS: { id: Segment; label: string }[] = [ + { id: "stocks", label: "Stocks" }, { id: "etfs", label: "ETFs" }, { id: "crypto", label: "Crypto" }, + { id: "forex", label: "Forex" }, { id: "futures", label: "Futures" }, { id: "indices", label: "Indices" }, +]; +const UNIVERSES: Record = { + stocks: [{ id: "sp500", label: "S&P 500" }, { id: "nasdaq100", label: "Nasdaq 100" }, { id: "dow30", label: "Dow 30" }], + etfs: [{ id: "etf-sectors", label: "Sector SPDRs" }, { id: "etf-broad", label: "Broad Market" }, { id: "etf-thematic", label: "Thematic" }], + crypto: [{ id: "crypto", label: "Top Coins" }, { id: "crypto-l1", label: "Layer 1s" }, { id: "crypto-defi", label: "DeFi" }], + forex: [{ id: "forex", label: "FX Majors" }, { id: "forex-crosses", label: "Crosses" }, { id: "forex-exotic", label: "USD Exotics" }], + futures: [{ id: "futures-equity", label: "Equity Index" }, { id: "futures-energy", label: "Energy" }, { id: "futures-metals", label: "Metals" }], + indices: [{ id: "indices", label: "Major Indices" }, { id: "indices-sectors", label: "Sectors" }, { id: "indices-global", label: "Global" }], +}; + +export function SettingsModal({ onClose }: { onClose: () => void }) { + const { settings, setHiddenSegments, setHiddenUniverses, setClamps, setRefreshMs } = useSettings(); + const hiddenSeg = new Set(settings.hiddenSegments); + const hiddenUni = new Set(settings.hiddenUniverses); + + const clampPct = (lb: number) => round1((settings.clamps[lb] ?? clampForLookback(lb)) * 100); + const setClampPct = (lb: number, raw: string) => { + const v = Number(raw); + if (!Number.isFinite(v) || v <= 0) return; + setClamps({ ...settings.clamps, [lb]: v / 100 }); + }; + + const toggleSeg = (id: Segment) => { + const next = new Set(hiddenSeg); + next.has(id) ? next.delete(id) : next.add(id); + setHiddenSegments([...next] as Segment[]); + }; + const toggleUni = (id: string) => { + const next = new Set(hiddenUni); + next.has(id) ? next.delete(id) : next.add(id); + setHiddenUniverses([...next]); + }; + + return ( +
+
e.stopPropagation()}> +
+

Settings

+ +
+
+
Visible asset classes
+

Hide classes your Massive key cannot reach.

+ {SEGMENTS.map((s) => ( +
+ + {!hiddenSeg.has(s.id) && ( +
+ {(UNIVERSES[s.id] ?? []).map((u) => ( + + ))} +
+ )} +
+ ))} +
+
+
Updates
+

How often the heatmap re-polls Massive for fresh prices.

+ +
+
+
Color scale
+

The percent move that fully saturates green/red, per lookback. Drives the tiles and the footer legend.

+ {LOOKBACKS.map((lb) => ( + + ))} +
+
+
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/Tooltip.tsx b/examples/rest/massive-heatmap/frontend/src/Tooltip.tsx new file mode 100644 index 0000000..7ae639d --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/Tooltip.tsx @@ -0,0 +1,47 @@ +import { useLayoutEffect, useRef, useState } from "react"; +import type { Tile } from "../../shared/protocol.js"; +import { pctTextColor } from "./color.js"; + +const fmt = (n: number) => `$${n.toLocaleString(undefined, { maximumFractionDigits: 2 })}`; + +// Keep the tooltip fully on screen: offset from the cursor, but flip to the other +// side when it would overflow the right/bottom edge, then clamp within a margin so +// it can never run off the viewport. Pure for testing. +export function placeTooltip( + x: number, y: number, w: number, h: number, vw: number, vh: number, gap = 14, margin = 8, +): { left: number; top: number } { + let left = x + gap; + if (left + w > vw - margin) left = x - gap - w; + left = Math.max(margin, Math.min(left, vw - w - margin)); + let top = y + gap; + if (top + h > vh - margin) top = y - gap - h; + top = Math.max(margin, Math.min(top, vh - h - margin)); + return { left, top }; +} + +export function Tooltip( + { tile, x, y, periodLabel }: + { tile: Tile; x: number; y: number; periodLabel: string }, +) { + const ref = useRef(null); + const [pos, setPos] = useState({ left: x + 14, top: y + 14 }); + // Measure the rendered tooltip and reposition before paint (no flash) so it stays + // on screen near the right/bottom edges. Recomputes as the cursor or content moves. + useLayoutEffect(() => { + const el = ref.current; if (!el) return; + const { width, height } = el.getBoundingClientRect(); + setPos(placeTooltip(x, y, width, height, window.innerWidth, window.innerHeight)); + }, [x, y, tile.ticker]); + + return ( +
+
{tile.ticker}
+
{tile.name}
+
Price{tile.price ? fmt(tile.price) : "—"}
+
{periodLabel}{tile.priorClose ? fmt(tile.priorClose) : "—"}
+
+ Change{(tile.pct * 100).toFixed(2)}% +
+
+ ); +} diff --git a/examples/rest/massive-heatmap/frontend/src/api.ts b/examples/rest/massive-heatmap/frontend/src/api.ts new file mode 100644 index 0000000..a829ff8 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/api.ts @@ -0,0 +1,59 @@ +import type { ServerMsg } from "../../shared/protocol.js"; + +// Polling REST client. Mirrors the old ws.ts surface (select / setLookback / setRefresh) +// so App.tsx is unchanged apart from the import. select/setLookback fetch a fresh +// snapshot; a timer polls /api/prices every refreshMs. No socket, no reconnect. +export function connect(onMsg: (m: ServerMsg) => void): { + select: (id: string, lookback: number) => void; + setLookback: (lookback: number) => void; + setRefresh: (intervalMs: number) => void; +} { + let universeId: string | null = null; + let lookback = 1; + let intervalMs = 5000; + let timer: ReturnType | null = null; + let snapshotInFlight = false; + let polling = false; + + const query = () => `universe=${encodeURIComponent(universeId!)}&lookback=${lookback}`; + + const fetchSnapshot = async () => { + if (!universeId) return; + snapshotInFlight = true; + try { + const r = await fetch(`/api/snapshot?${query()}`); + if (!r.ok) { onMsg({ type: "error", message: `snapshot ${r.status}` }); return; } + onMsg({ type: "snapshot", ...(await r.json()) }); + } catch (e) { + onMsg({ type: "error", message: String(e) }); + } finally { + snapshotInFlight = false; + } + }; + + // A failed poll keeps the last view and retries next tick. Skip while a snapshot is in + // flight or a prior poll has not returned, so requests never pile up. + const poll = async () => { + if (!universeId || snapshotInFlight || polling) return; + polling = true; + try { + const r = await fetch(`/api/prices?${query()}`); + if (r.ok) onMsg({ type: "diff", ...(await r.json()) }); + } catch { + /* keep last view */ + } finally { + polling = false; + } + }; + + const restart = () => { + if (timer) clearInterval(timer); + timer = setInterval(poll, intervalMs); + }; + + return { + select: (id, lb) => { universeId = id; lookback = lb; fetchSnapshot(); restart(); }, + setLookback: (lb) => { lookback = lb; fetchSnapshot(); }, + setRefresh: (ms) => { intervalMs = ms; restart(); }, + }; +} diff --git a/examples/rest/massive-heatmap/frontend/src/capture.ts b/examples/rest/massive-heatmap/frontend/src/capture.ts new file mode 100644 index 0000000..ca14673 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/capture.ts @@ -0,0 +1,85 @@ +import type { Tile } from "../../shared/protocol.js"; +import { renderRegion, FONT } from "./render.js"; + +export interface ExportMeta { title: string; sessionLabel: string; dateStr: string; lookbackLabel: string; } + +// Largest sub-rect of (boxW x boxH) with the given aspect (w/h), centered inside the box. +// Used to letterbox the heatmap so it keeps the live viewport's shape (no skew) within +// the fixed 16:9 frame. Exported for tests. +export function fitRect(boxW: number, boxH: number, aspect: number): { x: number; y: number; w: number; h: number } { + if (!(aspect > 0) || !(boxW > 0) || !(boxH > 0)) return { x: 0, y: 0, w: boxW, h: boxH }; + let w = boxW, h = boxW / aspect; + if (h > boxH) { h = boxH; w = boxH * aspect; } + return { x: (boxW - w) / 2, y: (boxH - h) / 2, w, h }; +} + +// Render the branded 16:9 composition (logical 1600x900, 2x device pixels) into `canvas`. +// `logo` (the white MASSIVE wordmark) is drawn top-right when provided; falls back to text. +// `layoutW`/`layoutH` are the live viewport dimensions: passing them lays the treemap out at +// the on-screen aspect (so the arrangement and which labels show are identical to the screen) +// and letterboxes it centered in the frame. Without them the heatmap fills the region. +export function renderExport( + canvas: HTMLCanvasElement, tiles: Tile[], meta: ExportMeta, logo?: HTMLImageElement, + clamp = 0.06, layoutW?: number, layoutH?: number, +): void { + const W = 1600, H = 900, dpr = 2; + canvas.width = W * dpr; canvas.height = H * dpr; + const ctx = canvas.getContext("2d")!; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.fillStyle = "#0A0D11"; ctx.fillRect(0, 0, W, H); + const padX = 44; + + // header: eyebrow + headline (left), MASSIVE wordmark (right) + ctx.textBaseline = "alphabetic"; + ctx.fillStyle = "#7D8794"; ctx.font = `600 13px ${FONT}`; + try { (ctx as any).letterSpacing = "0.16em"; } catch { /* ignore */ } + ctx.fillText(`REAL-TIME · ${meta.lookbackLabel.toUpperCase()} · SIZED BY MARKET CAP`, padX, 50); + try { (ctx as any).letterSpacing = "0px"; } catch { /* ignore */ } + ctx.fillStyle = "#fff"; ctx.font = `800 32px ${FONT}`; + ctx.fillText(`${meta.title} Heatmap`, padX, 88); + if (logo && logo.complete && logo.naturalWidth > 0) { + const lh = 26, lw = (lh * logo.naturalWidth) / logo.naturalHeight; + ctx.drawImage(logo, W - padX - lw, 58, lw, lh); + } else { + ctx.font = `800 34px ${FONT}`; + const wm = "MASSIVE"; const ww = ctx.measureText(wm).width; + ctx.fillStyle = "#fff"; ctx.fillText(wm, W - padX - ww, 82); + } + + // heatmap region. Lay the treemap out at the live viewport aspect and letterbox it + // (centered, uniform scale) so the arrangement matches the screen and tiles are not + // skewed. Without viewport dims, fall back to filling the region. + const top = 120, bottom = 64; + const regionW = W - 2 * padX, regionH = H - top - bottom; + if (layoutW && layoutH) { + const fit = fitRect(regionW, regionH, layoutW / layoutH); + renderRegion(ctx, tiles, padX + fit.x, top + fit.y, fit.w, fit.h, clamp, layoutW, layoutH); + } else { + renderRegion(ctx, tiles, padX, top, regionW, regionH, clamp); + } + + // footer + const pct = Math.round(clamp * 100); + const fy = H - 32; + ctx.textBaseline = "middle"; + ctx.fillStyle = "#7D8794"; ctx.font = `500 13px ${FONT}`; + ctx.fillText(`Data via Massive.com · ${meta.sessionLabel}`, padX, fy); + const lgW = 160, dateReserve = 240; + const lgX = W - padX - dateReserve - lgW; + ctx.textAlign = "right"; ctx.fillStyle = "#7D8794"; ctx.fillText(`-${pct}%`, lgX - 8, fy); ctx.textAlign = "left"; + const grad = ctx.createLinearGradient(lgX, 0, lgX + lgW, 0); + grad.addColorStop(0, "#DC2626"); grad.addColorStop(0.5, "#2A313A"); grad.addColorStop(1, "#16A34A"); + ctx.fillStyle = grad; ctx.fillRect(lgX, fy - 5, lgW, 10); + ctx.fillStyle = "#7D8794"; ctx.fillText(`+${pct}%`, lgX + lgW + 8, fy); + ctx.textAlign = "right"; ctx.fillStyle = "#fff"; ctx.font = `600 14px ${FONT}`; + ctx.fillText(meta.dateStr, W - padX, fy); ctx.textAlign = "left"; +} + +export function downloadCanvasPng(canvas: HTMLCanvasElement, filename: string): void { + canvas.toBlob((b) => { + if (!b) return; + const url = URL.createObjectURL(b); + const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); + URL.revokeObjectURL(url); + }, "image/png"); +} diff --git a/examples/rest/massive-heatmap/frontend/src/color.ts b/examples/rest/massive-heatmap/frontend/src/color.ts new file mode 100644 index 0000000..7c7a4d8 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/color.ts @@ -0,0 +1,29 @@ +export const CLAMP = 0.06; // color scale saturates at +/- this fraction (default) +const NEUTRAL = [42, 49, 58] as const; // #2A313A dark slate +const GREEN = [22, 163, 74] as const; // #16A34A +const RED = [220, 38, 38] as const; // #DC2626 + +export const CLAMP_BY_LOOKBACK: Record = { + 1: 0.06, 7: 0.10, 30: 0.20, 90: 0.35, 180: 0.50, 365: 0.80, 1825: 3.0, +}; +export function clampForLookback(days: number): number { + return CLAMP_BY_LOOKBACK[days] ?? 0.06; +} + +// Legible sign color for text (e.g. the tooltip Change row). The diverging pctColor +// scale fades to near-neutral slate for small moves, which is right for tiles but +// unreadable as text on a dark surface, so color purely by sign at full brightness. +const TEXT_UP = "#22C55E", TEXT_DOWN = "#F87171", TEXT_FLAT = "#9AA4B2"; +export function pctTextColor(pct: number): string { + return pct > 0 ? TEXT_UP : pct < 0 ? TEXT_DOWN : TEXT_FLAT; +} + +function lerp(a: number, b: number, t: number) { return Math.round(a + (b - a) * t); } + +export function pctColor(pct: number, clamp = 0.06): string { + const c = Math.max(-clamp, Math.min(clamp, pct)); + const t = Math.abs(c) / clamp; + const target = c >= 0 ? GREEN : RED; + const [r, g, b] = [0, 1, 2].map((i) => lerp(NEUTRAL[i], target[i], t)); + return `rgb(${r}, ${g}, ${b})`; +} diff --git a/examples/rest/massive-heatmap/frontend/src/main.tsx b/examples/rest/massive-heatmap/frontend/src/main.tsx new file mode 100644 index 0000000..1fb62df --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/main.tsx @@ -0,0 +1,5 @@ +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; +import { SettingsProvider } from "./settings.js"; +import "./styles.css"; +createRoot(document.getElementById("root")!).render(); diff --git a/examples/rest/massive-heatmap/frontend/src/render.ts b/examples/rest/massive-heatmap/frontend/src/render.ts new file mode 100644 index 0000000..e82d941 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/render.ts @@ -0,0 +1,92 @@ +import type { Tile } from "../../shared/protocol.js"; +import { layout, type Layout } from "./treemap.js"; +import { pctColor } from "./color.js"; + +export const FONT = `ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif`; +const INK = "rgba(255,255,255,0.96)"; +const INK_DIM = "rgba(255,255,255,0.72)"; +const MUTED = "#7D8794"; +const GREEN = "#16A34A", RED = "#DC2626"; + +export function fmtPct(p: number): string { return `${p >= 0 ? "+" : ""}${(p * 100).toFixed(2)}%`; } + +function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) { + r = Math.max(0, Math.min(r, w / 2, h / 2)); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); +} + +// Cap-weighted sector aggregate percent changes. +export function sectorAggregates(tiles: Tile[]): Map { + const acc = new Map(); + for (const t of tiles) { + const a = acc.get(t.group) ?? { p: 0, c: 0 }; + a.p += t.pct * t.marketCap; a.c += t.marketCap; acc.set(t.group, a); + } + const out = new Map(); + for (const [g, a] of acc) out.set(g, a.c > 0 ? a.p / a.c : 0); + return out; +} + +// Draw the heatmap (sector headers + rounded tiles) using a precomputed layout. +// agg + byTicker are precomputed so per-frame redraws stay cheap. +export function drawHeatmap( + ctx: CanvasRenderingContext2D, lay: Layout, byTicker: Map, agg: Map, + clamp = 0.06, +) { + for (const r of lay.tiles) { + const t = byTicker.get(r.ticker); if (!t) continue; + const w = r.x1 - r.x0, h = r.y1 - r.y0; + ctx.fillStyle = pctColor(t.pct, clamp); + roundRect(ctx, r.x0, r.y0, w, h, Math.min(3, w / 4, h / 4)); ctx.fill(); + // Label every tile that can fit any text at all; font scales to the tile. + if (w >= 11 && h >= 7) { + const fs = Math.max(5, Math.min(13, Math.floor(w / 3.8), Math.floor(h / 2.2))); + ctx.textBaseline = "top"; ctx.textAlign = "left"; + ctx.fillStyle = "rgba(255,255,255,0.97)"; + ctx.font = `700 ${fs}px ${FONT}`; + ctx.fillText(t.ticker, r.x0 + 2, r.y0 + 1); + if (h >= fs * 2 + 4) { + ctx.fillStyle = "rgba(255,255,255,0.82)"; + ctx.font = `500 ${Math.max(5, fs - 1)}px ${FONT}`; + ctx.fillText(fmtPct(t.pct), r.x0 + 2, r.y0 + 1 + fs + 1); + } + } + } + ctx.textBaseline = "alphabetic"; ctx.textAlign = "left"; + for (const g of lay.groups) { + const label = g.name.toUpperCase(); + ctx.fillStyle = "#7D8794"; ctx.font = `600 10px ${FONT}`; + try { (ctx as any).letterSpacing = "0.4px"; } catch { /* ignore */ } + ctx.fillText(label, g.x0 + 1, g.y0 + 12); + const lw = ctx.measureText(label).width; + try { (ctx as any).letterSpacing = "0px"; } catch { /* ignore */ } + const a = agg.get(g.name) ?? 0; + ctx.fillStyle = a >= 0 ? "#16A34A" : "#DC2626"; + ctx.font = `700 10px ${FONT}`; + ctx.fillText(fmtPct(a), g.x0 + 1 + lw + 7, g.y0 + 12); + } +} + +// Convenience for one-shot renders (export): computes layout+agg then draws into a region. +// The layout is computed at (layoutW, layoutH) and then scaled to fit (w, h). Passing the +// live viewport dimensions as (layoutW, layoutH) makes the export's tile arrangement and +// label decisions identical to what is shown on screen. Defaults to the region size. +export function renderRegion( + ctx: CanvasRenderingContext2D, tiles: Tile[], x: number, y: number, w: number, h: number, + clamp = 0.06, layoutW = w, layoutH = h, +) { + const lay = layout(tiles, layoutW, layoutH); + const byTicker = new Map(tiles.map((t) => [t.ticker, t] as const)); + ctx.save(); + ctx.translate(x, y); + ctx.scale(w / layoutW, h / layoutH); + drawHeatmap(ctx, lay, byTicker, sectorAggregates(tiles), clamp); + ctx.restore(); + return lay; +} diff --git a/examples/rest/massive-heatmap/frontend/src/settings.tsx b/examples/rest/massive-heatmap/frontend/src/settings.tsx new file mode 100644 index 0000000..18c4cb3 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/settings.tsx @@ -0,0 +1,41 @@ +import { createContext, useContext, useState, useCallback, type ReactNode } from "react"; +import { + DEFAULT_SETTINGS, STORAGE_KEY, loadSettings, serializeSettings, + type Settings, +} from "./settingsStore.js"; + +interface Ctx { + settings: Settings; + setHiddenSegments: (v: Settings["hiddenSegments"]) => void; + setHiddenUniverses: (v: string[]) => void; + setClamps: (v: Record) => void; + setRefreshMs: (v: number) => void; +} + +const SettingsContext = createContext(null); + +function read(): Settings { + try { return loadSettings(localStorage.getItem(STORAGE_KEY)); } catch { return DEFAULT_SETTINGS; } +} + +export function SettingsProvider({ children }: { children: ReactNode }) { + const [settings, setSettings] = useState(read); + const commit = useCallback((next: Settings) => { + setSettings(next); + try { localStorage.setItem(STORAGE_KEY, serializeSettings(next)); } catch { /* ignore quota */ } + }, []); + const value: Ctx = { + settings, + setHiddenSegments: (v) => commit({ ...settings, hiddenSegments: v }), + setHiddenUniverses: (v) => commit({ ...settings, hiddenUniverses: v }), + setClamps: (v) => commit({ ...settings, clamps: v }), + setRefreshMs: (v) => commit({ ...settings, refreshMs: v }), + }; + return {children}; +} + +export function useSettings(): Ctx { + const c = useContext(SettingsContext); + if (!c) throw new Error("useSettings must be used within SettingsProvider"); + return c; +} diff --git a/examples/rest/massive-heatmap/frontend/src/settingsStore.ts b/examples/rest/massive-heatmap/frontend/src/settingsStore.ts new file mode 100644 index 0000000..e6ba07b --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/settingsStore.ts @@ -0,0 +1,45 @@ +import type { Segment } from "../../shared/universe.js"; + +// Refresh intervals (ms) the UI offers. This is a client-side poll cadence; the +// backend does not see or validate it. +export const REFRESH_OPTIONS = [2000, 5000, 10000, 30000, 60000]; +export const DEFAULT_REFRESH_MS = 5000; + +export interface Settings { + version: 1; + hiddenSegments: Segment[]; + hiddenUniverses: string[]; + // Per-lookback color-clamp overrides, keyed by lookback days; value is a fraction + // (e.g. 0.2 = ±20%). Missing keys fall back to the built-in default for that lookback. + clamps: Record; + // How often the backend re-polls the snapshot, in milliseconds. + refreshMs: number; +} + +export const STORAGE_KEY = "massive-heatmap-settings"; + +export const DEFAULT_SETTINGS: Settings = { + version: 1, + hiddenSegments: [], + hiddenUniverses: [], + clamps: {}, + refreshMs: DEFAULT_REFRESH_MS, +}; + +export function loadSettings(raw: string | null): Settings { + if (!raw) return DEFAULT_SETTINGS; + let parsed: any; + try { parsed = JSON.parse(raw); } catch { return DEFAULT_SETTINGS; } + if (!parsed || typeof parsed !== "object") return DEFAULT_SETTINGS; + return { + version: 1, + hiddenSegments: Array.isArray(parsed.hiddenSegments) ? parsed.hiddenSegments : [], + hiddenUniverses: Array.isArray(parsed.hiddenUniverses) ? parsed.hiddenUniverses : [], + clamps: parsed.clamps && typeof parsed.clamps === "object" ? parsed.clamps : {}, + refreshMs: REFRESH_OPTIONS.includes(parsed.refreshMs) ? parsed.refreshMs : DEFAULT_REFRESH_MS, + }; +} + +export function serializeSettings(s: Settings): string { + return JSON.stringify(s); +} diff --git a/examples/rest/massive-heatmap/frontend/src/store.ts b/examples/rest/massive-heatmap/frontend/src/store.ts new file mode 100644 index 0000000..41b0034 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/store.ts @@ -0,0 +1,22 @@ +import type { Tile, SnapshotMsg, DiffMsg, SessionPhase } from "../../shared/protocol.js"; + +export class TileStore { + private byTicker = new Map(); + private order: string[] = []; + private _session: SessionPhase = "closed"; + + applySnapshot(msg: SnapshotMsg): void { + this.byTicker.clear(); this.order = []; + for (const t of msg.tiles) { this.byTicker.set(t.ticker, { ...t }); this.order.push(t.ticker); } + this._session = msg.session; + } + applyDiff(msg: DiffMsg): void { + this._session = msg.session; + for (const u of msg.updates) { + const t = this.byTicker.get(u.ticker); + if (t) { t.price = u.price; t.pct = u.pct; } + } + } + tiles(): Tile[] { return this.order.map((k) => this.byTicker.get(k)!); } + session(): SessionPhase { return this._session; } +} diff --git a/examples/rest/massive-heatmap/frontend/src/styles.css b/examples/rest/massive-heatmap/frontend/src/styles.css new file mode 100644 index 0000000..fdfea15 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/styles.css @@ -0,0 +1,123 @@ +:root { color-scheme: dark; } +* { box-sizing: border-box; } +html, body, #root { margin: 0; height: 100%; background: #0A0D11; color: #fff; + font-family: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; } +.app { display: flex; flex-direction: column; height: 100vh; } + +.topbar { display: grid; grid-template-columns: 1fr auto auto auto; align-items: center; gap: 20px; + padding: 14px 22px; border-bottom: 1px solid #1B2230; background: #0A0D11; } +.title-block { display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: #7D8794; } +.headline { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; color: #fff; line-height: 1; } +.headline .accent { color: #fff; } + +.controls { display: flex; gap: 8px; } +.controls select { appearance: none; background: #0E141B; color: #cdd3dc; border: 1px solid #1B2230; + border-radius: 7px; padding: 7px 26px 7px 11px; font-size: 12px; font-weight: 600; cursor: pointer; + background-image: linear-gradient(45deg, transparent 50%, #7D8794 50%), linear-gradient(135deg, #7D8794 50%, transparent 50%); + background-position: calc(100% - 14px) 14px, calc(100% - 9px) 14px; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; } +.controls select:hover { border-color: #2a3650; } +.lookback-select { appearance: none; background: #0E141B; color: #cdd3dc; border: 1px solid #1B2230; + border-radius: 7px; padding: 7px 26px 7px 11px; font-size: 12px; font-weight: 600; cursor: pointer; + background-image: linear-gradient(45deg, transparent 50%, #7D8794 50%), linear-gradient(135deg, #7D8794 50%, transparent 50%); + background-position: calc(100% - 14px) 14px, calc(100% - 9px) 14px; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; } +.lookback-select:hover { border-color: #2a3650; } + +.right-cluster { display: flex; align-items: center; gap: 16px; } +.badge { display: inline-flex; align-items: center; gap: 6px; border: 1px solid; border-radius: 999px; + padding: 4px 11px; font-size: 11px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; } +.badge-dot { width: 7px; height: 7px; border-radius: 50%; } +.wordmark-img { height: 22px; width: auto; display: block; } + +/* Stage wraps the scroll container so the loading overlay can sit over the visible + viewport without scrolling/zooming along with the canvas inside .canvas-wrap. */ +.canvas-stage { flex: 1; position: relative; display: flex; min-height: 0; } +.canvas-wrap { flex: 1; position: relative; overflow: auto; background: #0A0D11; } +.canvas-wrap > canvas { transition: filter .18s ease, opacity .18s ease; } +.canvas-wrap.is-loading > canvas { filter: blur(3px); opacity: .45; pointer-events: none; } + +/* Covers the heatmap while a snapshot is in flight. */ +.loading-overlay { position: absolute; inset: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; + z-index: 10; pointer-events: none; } +.spinner { width: 34px; height: 34px; border-radius: 50%; + border: 3px solid rgba(255,255,255,0.14); border-top-color: #2563eb; + animation: spin .7s linear infinite; } +.loading-label { font-size: 13px; font-weight: 600; letter-spacing: 0.01em; color: #cdd3dc; } +@keyframes spin { to { transform: rotate(360deg); } } +@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 1.6s; } } + +.footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; + padding: 9px 22px; border-top: 1px solid #1B2230; font-size: 11px; color: #7D8794; } +.footer .legend { display: flex; align-items: center; gap: 9px; } +.legend-bar { width: 150px; height: 9px; border-radius: 3px; + background: linear-gradient(90deg, #DC2626, #2A313A 50%, #16A34A); } +.footer .right { display: flex; align-items: center; gap: 18px; } + +.tooltip { position: fixed; pointer-events: none; background: #0E141B; border: 1px solid #1B2230; + border-radius: 9px; padding: 9px 11px; font-size: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.6); z-index: 10; } +.tt-ticker { font-weight: 800; font-size: 13px; } +.tt-name { color: #7D8794; margin: 1px 0 5px; max-width: 240px; } +.tt-row { display: flex; justify-content: space-between; gap: 18px; line-height: 1.5; } +.tt-row .tt-label { color: #7D8794; } +.clamp-input { width: 64px; background: #0A0D11; color: inherit; border: 1px solid rgba(255,255,255,0.12); border-radius: 5px; padding: 3px 6px; font-size: 12px; text-align: right; } +.btn-primary { background: #2563eb; color: #fff; border: none; border-radius: 7px; padding: 8px 15px; font-size: 12px; font-weight: 700; cursor: pointer; } +.btn-primary:hover { background: #1d4ed8; } +.btn-ghost { background: transparent; color: #cdd3dc; border: 1px solid #1B2230; border-radius: 7px; padding: 7px 13px; font-size: 12px; font-weight: 600; cursor: pointer; } +.btn-ghost:hover { border-color: #2a3650; } +.overlay { position: fixed; inset: 0; background: rgba(0,0,0,.72); display: flex; align-items: center; justify-content: center; padding: 32px; z-index: 50; } +.export-card { width: min(1120px, 94vw); background: #0A0D11; border: 1px solid #1B2230; border-radius: 12px; padding: 14px; box-shadow: 0 30px 80px rgba(0,0,0,.6); } +.export-canvas { width: 100%; height: auto; display: block; border-radius: 8px; } +.export-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; } + +.zoom-controls { position: fixed; right: 20px; bottom: 64px; display: inline-flex; align-items: center; gap: 4px; + background: #0E141Bcc; border: 1px solid #1B2230; border-radius: 8px; padding: 4px 6px; font-size: 12px; color: #cdd3dc; z-index: 5; } +.zoom-controls button { background: transparent; color: #cdd3dc; border: none; cursor: pointer; font-size: 14px; font-weight: 700; padding: 2px 8px; border-radius: 5px; } +.zoom-controls button:hover { background: #1B2230; } +.zoom-controls span { min-width: 42px; text-align: center; font-variant-numeric: tabular-nums; } + +.canvas-wrap { cursor: grab; } +.canvas-wrap:active { cursor: grabbing; } + +.settings-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 50; } +.settings-panel { background: #11161C; color: rgba(255,255,255,0.92); width: min(560px, 92vw); max-height: 86vh; overflow-y: auto; border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 20px 22px; } +.settings-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } +.settings-head h2 { font-size: 16px; font-weight: 700; margin: 0; } +.settings-section-title { font-size: 11px; letter-spacing: 0.4px; text-transform: uppercase; color: #7D8794; margin-bottom: 4px; } +.settings-hint { font-size: 12px; color: #7D8794; margin: 0 0 10px; } +.settings-group { margin-bottom: 6px; } +.settings-row { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 3px 0; cursor: pointer; } +.settings-universes { padding-left: 22px; } +.settings-sub { color: rgba(255,255,255,0.72); } +.settings-textarea { width: 100%; min-height: 84px; background: #0A0D11; color: inherit; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; padding: 8px; font-family: ui-monospace, monospace; font-size: 12px; } +.settings-resolved { font-size: 12px; margin: 8px 0; } +.settings-dropped { color: #DC2626; } +.settings-actions { display: flex; gap: 8px; margin-top: 10px; } + +/* Watchlist search dropdown */ +.wl-search-results { position: absolute; top: 100%; left: 0; right: 0; z-index: 100; + background: #0E141B; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; + max-height: 220px; overflow-y: auto; list-style: none; margin: 2px 0 0; padding: 4px 0; } +.wl-search-row { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; + background: transparent; border: none; color: rgba(255,255,255,0.9); font-size: 12px; + padding: 7px 10px; cursor: pointer; } +.wl-search-row:hover { background: rgba(255,255,255,0.06); } +.wl-search-display { font-weight: 700; min-width: 54px; } +.wl-search-name { flex: 1; color: #7D8794; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.wl-class-tag { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; + color: #7D8794; background: rgba(255,255,255,0.06); border-radius: 4px; padding: 2px 5px; flex-shrink: 0; } + +/* Chips */ +.wl-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } +.wl-chip { display: inline-flex; align-items: center; gap: 4px; background: rgba(37,99,235,0.18); + border: 1px solid rgba(37,99,235,0.4); border-radius: 999px; padding: 3px 8px 3px 10px; + font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.9); } +.wl-chip-remove { background: transparent; border: none; color: #7D8794; font-size: 11px; + cursor: pointer; padding: 0 1px; line-height: 1; } +.wl-chip-remove:hover { color: #DC2626; } + +/* Advanced paste block */ +.wl-advanced { margin-top: 10px; } +.wl-advanced summary { font-size: 12px; color: #7D8794; cursor: pointer; user-select: none; padding: 4px 0; } +.wl-advanced summary:hover { color: rgba(255,255,255,0.7); } +.wl-advanced[open] summary { color: rgba(255,255,255,0.7); } diff --git a/examples/rest/massive-heatmap/frontend/src/treemap.ts b/examples/rest/massive-heatmap/frontend/src/treemap.ts new file mode 100644 index 0000000..387bf58 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/src/treemap.ts @@ -0,0 +1,31 @@ +import { hierarchy, treemap as d3treemap } from "d3-hierarchy"; +import type { Tile } from "../../shared/protocol.js"; + +export interface Rect { ticker: string; x0: number; y0: number; x1: number; y1: number; } +export interface GroupRect { name: string; x0: number; y0: number; x1: number; y1: number; } +export interface Layout { tiles: Rect[]; groups: GroupRect[]; } + +export function layout(tiles: Tile[], width: number, height: number): Layout { + const groups = new Map(); + for (const t of tiles) { + const arr = groups.get(t.group) ?? []; + if (!groups.has(t.group)) groups.set(t.group, arr); + arr.push(t); + } + const root = { + name: "root", + children: [...groups.entries()].map(([name, items]) => ({ + // Compress the size range (sqrt) so the mega-caps don't dwarf everything; + // this gives the smallest tiles enough area to show their labels. + name, children: items.map((t) => ({ + name: t.ticker, + value: Math.sqrt(Math.max(t.marketCap, 0)) + 0.01, + })), + })), + }; + const h = hierarchy(root).sum((d: any) => d.value ?? 0).sort((a, b) => (b.value ?? 0) - (a.value ?? 0)); + d3treemap().size([width, height]).paddingInner(2).paddingTop(18).round(true)(h as any); + const tileRects: Rect[] = (h.leaves() as any[]).map((l) => ({ ticker: l.data.name, x0: l.x0, y0: l.y0, x1: l.x1, y1: l.y1 })); + const groupRects: GroupRect[] = ((h.children ?? []) as any[]).map((c) => ({ name: c.data.name, x0: c.x0, y0: c.y0, x1: c.x1, y1: c.y1 })); + return { tiles: tileRects, groups: groupRects }; +} diff --git a/examples/rest/massive-heatmap/frontend/test/api.test.ts b/examples/rest/massive-heatmap/frontend/test/api.test.ts new file mode 100644 index 0000000..d865989 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/api.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { connect } from "../src/api.js"; + +const snapshotBody = { universeId: "sp500", label: "S&P 500", segment: "stocks", session: "regular", tiles: [] }; +const pricesBody = { session: "regular", updates: [{ ticker: "AAPL", price: 1, pct: 0 }] }; + +function mockFetch() { + return vi.fn(async (input: string) => ({ + ok: true, + status: 200, + json: async () => (input.includes("/api/snapshot") ? snapshotBody : pricesBody), + })) as unknown as typeof fetch; +} + +describe("api client", () => { + beforeEach(() => { vi.useFakeTimers(); vi.stubGlobal("fetch", mockFetch()); }); + afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); + + it("select fetches a snapshot and emits it tagged", async () => { + const msgs: any[] = []; + const c = connect((m) => msgs.push(m)); + c.select("sp500", 1); + await vi.waitFor(() => expect(msgs.find((m) => m.type === "snapshot")).toBeTruthy()); + expect(msgs[0]).toMatchObject({ type: "snapshot", universeId: "sp500", label: "S&P 500" }); + }); + + it("polls /api/prices on the interval and emits a tagged diff", async () => { + const msgs: any[] = []; + const c = connect((m) => msgs.push(m)); + c.select("sp500", 1); + c.setRefresh(2000); + await vi.advanceTimersByTimeAsync(2000); + await vi.waitFor(() => expect(msgs.find((m) => m.type === "diff")).toBeTruthy()); + expect(msgs.find((m) => m.type === "diff")).toMatchObject({ type: "diff", updates: pricesBody.updates }); + }); + + it("emits an error when the snapshot request fails", async () => { + vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 500, json: async () => ({}) })) as unknown as typeof fetch); + const msgs: any[] = []; + const c = connect((m) => msgs.push(m)); + c.select("sp500", 1); + await vi.waitFor(() => expect(msgs.find((m) => m.type === "error")).toBeTruthy()); + expect(msgs.find((m) => m.type === "error")).toMatchObject({ type: "error", message: "snapshot 500" }); + }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/capture.test.ts b/examples/rest/massive-heatmap/frontend/test/capture.test.ts new file mode 100644 index 0000000..d9f257f --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/capture.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi } from "vitest"; + +// Spy on renderRegion so we can assert what dimensions the export lays out at. +vi.mock("../src/render.js", () => ({ + renderRegion: vi.fn(() => ({ tiles: [], groups: [] })), + FONT: "sans-serif", +})); + +import { renderExport, fitRect, type ExportMeta } from "../src/capture.js"; +import { renderRegion } from "../src/render.js"; + +// Canvas/ctx stub: every method is a no-op; the few that must return objects do. +function fakeCanvas(): HTMLCanvasElement { + const ctx: any = new Proxy({}, { + get(_t, p) { + if (p === "measureText") return () => ({ width: 0 }); + if (p === "createLinearGradient") return () => ({ addColorStop() {} }); + return () => {}; + }, + set() { return true; }, + }); + return { getContext: () => ctx, width: 0, height: 0 } as unknown as HTMLCanvasElement; +} + +const META: ExportMeta = { + title: "Stocks S&P 500", sessionLabel: "Market open", dateStr: "June 30, 2026", lookbackLabel: "1 Day", +}; + +describe("fitRect", () => { + it("returns the whole box when the aspect already matches", () => { + expect(fitRect(300, 150, 2)).toEqual({ x: 0, y: 0, w: 300, h: 150 }); + }); + it("pillarboxes (centers horizontally) when content is taller than the box", () => { + expect(fitRect(300, 150, 1)).toEqual({ x: 75, y: 0, w: 150, h: 150 }); + }); + it("letterboxes (centers vertically) when content is wider than the box", () => { + expect(fitRect(300, 150, 3)).toEqual({ x: 0, y: 25, w: 300, h: 100 }); + }); + it("is defensive against zero/negative inputs", () => { + expect(fitRect(300, 150, 0)).toEqual({ x: 0, y: 0, w: 300, h: 150 }); + }); +}); + +describe("renderExport", () => { + it("lays the heatmap out at the live viewport dims so the arrangement matches the screen", () => { + (renderRegion as any).mockClear(); + renderExport(fakeCanvas(), [], META, undefined, 0.06, 1900, 950); + expect(renderRegion).toHaveBeenCalledTimes(1); + // renderRegion(ctx, tiles, x, y, w, h, clamp, layoutW, layoutH) + const [, , , , w, h, , layoutW, layoutH] = (renderRegion as any).mock.calls[0]; + expect(layoutW).toBe(1900); + expect(layoutH).toBe(950); + // placed region keeps the viewport aspect -> uniform scale, no skew + expect(w / h).toBeCloseTo(1900 / 950, 5); + }); + + it("falls back to filling the region when no viewport dims are given", () => { + (renderRegion as any).mockClear(); + renderExport(fakeCanvas(), [], META, undefined, 0.06); + const call = (renderRegion as any).mock.calls[0]; + // region is 1512 x 716 (1600/900 frame less padding), no layout dims passed + expect(call[4]).toBe(1512); + expect(call[5]).toBe(716); + expect(call[7]).toBeUndefined(); + }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/color.test.ts b/examples/rest/massive-heatmap/frontend/test/color.test.ts new file mode 100644 index 0000000..6f25971 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/color.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { pctColor, pctTextColor, clampForLookback } from "../src/color.js"; + +describe("pctColor", () => { + it("zero is neutral grey", () => { expect(pctColor(0)).toBe("rgb(42, 49, 58)"); }); + it("strong positive clamps to full green", () => { expect(pctColor(0.10)).toBe(pctColor(0.06)); }); + it("strong negative clamps to full red", () => { expect(pctColor(-0.10)).toBe(pctColor(-0.06)); }); + it("positive is greener than neutral", () => { expect(pctColor(0.02)).not.toBe(pctColor(0)); }); + it("clamp parameter: same pct saturates at small clamp but is mid-scale at large clamp", () => { + // 0.08 is beyond the default 0.06 clamp, so it saturates to full green. + // With clamp=0.20 (30D), 0.08 is only 40% of the way to full green. + const saturated = pctColor(0.08, 0.06); // clamps to max green + const midScale = pctColor(0.08, 0.20); // partial green + expect(saturated).toBe(pctColor(0.06, 0.06)); // same as full-green boundary + expect(saturated).not.toBe(midScale); // different colors + expect(midScale).not.toBe("rgb(42, 49, 58)"); // not neutral either + }); +}); + +describe("pctTextColor", () => { + it("stays full-brightness for tiny moves (unlike the tile scale)", () => { + // -0.41% is near-neutral on the tile scale but must read clearly as red text. + expect(pctTextColor(-0.0041)).toBe("#F87171"); + expect(pctTextColor(0.0041)).toBe("#22C55E"); + }); + it("colors by sign regardless of magnitude", () => { + expect(pctTextColor(0.5)).toBe(pctTextColor(0.0001)); + expect(pctTextColor(-0.5)).toBe(pctTextColor(-0.0001)); + }); + it("zero is muted, not green or red", () => { + expect(pctTextColor(0)).toBe("#9AA4B2"); + }); +}); + +describe("clampForLookback", () => { + it("returns 0.06 for 1-day lookback", () => { expect(clampForLookback(1)).toBe(0.06); }); + it("returns 0.20 for 30-day lookback", () => { expect(clampForLookback(30)).toBe(0.20); }); + it("returns 3.0 for 5-year lookback", () => { expect(clampForLookback(1825)).toBe(3.0); }); + it("falls back to 0.06 for unknown lookback", () => { expect(clampForLookback(999)).toBe(0.06); }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/render.test.ts b/examples/rest/massive-heatmap/frontend/test/render.test.ts new file mode 100644 index 0000000..2b01449 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/render.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Spy on the treemap layout to capture the dimensions it is called with. +vi.mock("../src/treemap.js", () => ({ + layout: vi.fn(() => ({ tiles: [], groups: [] })), +})); + +import { renderRegion } from "../src/render.js"; +import { layout } from "../src/treemap.js"; + +// Minimal canvas ctx stub: records scale/translate, ignores everything else. +function fakeCtx() { + const calls: { scale?: [number, number]; translate?: [number, number] } = {}; + const ctx: any = new Proxy({}, { + get(_t, prop) { + if (prop === "scale") return (sx: number, sy: number) => { calls.scale = [sx, sy]; }; + if (prop === "translate") return (x: number, y: number) => { calls.translate = [x, y]; }; + if (prop === "measureText") return () => ({ width: 0 }); + return () => {}; + }, + set() { return true; }, + }); + return { ctx, calls }; +} + +describe("renderRegion", () => { + beforeEach(() => { (layout as any).mockClear(); }); + + it("lays out at the region size by default", () => { + const { ctx, calls } = fakeCtx(); + renderRegion(ctx, [], 0, 0, 300, 150); + expect(layout).toHaveBeenCalledWith([], 300, 150); + expect(calls.scale).toEqual([1, 1]); + }); + + it("lays out at the supplied layout dims and scales to the region", () => { + const { ctx, calls } = fakeCtx(); + // region 300x150, but lay out at the live viewport dims 600x300 + renderRegion(ctx, [], 10, 20, 300, 150, 0.06, 600, 300); + expect(layout).toHaveBeenCalledWith([], 600, 300); // arrangement matches live, not the region + expect(calls.translate).toEqual([10, 20]); + expect(calls.scale).toEqual([300 / 600, 150 / 300]); // scaled to fit + }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/settingsStore.test.ts b/examples/rest/massive-heatmap/frontend/test/settingsStore.test.ts new file mode 100644 index 0000000..527e8dc --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/settingsStore.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest"; +import { DEFAULT_SETTINGS, loadSettings, serializeSettings } from "../src/settingsStore.js"; + +describe("settings store", () => { + it("returns defaults for empty/invalid storage", () => { + expect(loadSettings(null)).toEqual(DEFAULT_SETTINGS); + expect(loadSettings("not json")).toEqual(DEFAULT_SETTINGS); + }); + it("round-trips through serialize/load", () => { + const s = { ...DEFAULT_SETTINGS, hiddenSegments: ["futures"], hiddenUniverses: ["dow30"] }; + expect(loadSettings(serializeSettings(s as any))).toEqual(s); + }); + it("migrates missing version to current and keeps known fields", () => { + const raw = JSON.stringify({ hiddenSegments: ["crypto"] }); + const loaded = loadSettings(raw); + expect(loaded.version).toBe(1); + expect(loaded.hiddenSegments).toEqual(["crypto"]); + expect(loaded.hiddenUniverses).toEqual([]); + expect(loaded.clamps).toEqual({}); + }); + it("round-trips clamp overrides", () => { + const s = { ...DEFAULT_SETTINGS, clamps: { 1: 0.06, 365: 1.2 } }; + expect(loadSettings(serializeSettings(s as any)).clamps).toEqual({ 1: 0.06, 365: 1.2 }); + }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/store.test.ts b/examples/rest/massive-heatmap/frontend/test/store.test.ts new file mode 100644 index 0000000..763c026 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/store.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { TileStore } from "../src/store.js"; +import type { SnapshotMsg, DiffMsg } from "../../shared/protocol.js"; + +const snap: SnapshotMsg = { + type: "snapshot", universeId: "crypto", label: "Crypto", segment: "crypto", session: "open24", + tiles: [{ ticker: "BTC", name: "Bitcoin", group: "L1", marketCap: 100, priorClose: 100, price: 100, pct: 0 }], +}; + +describe("TileStore", () => { + it("applySnapshot replaces tiles", () => { + const s = new TileStore(); s.applySnapshot(snap); + expect(s.tiles()[0].ticker).toBe("BTC"); + expect(s.session()).toBe("open24"); + }); + it("applyDiff updates price/pct in place", () => { + const s = new TileStore(); s.applySnapshot(snap); + const diff: DiffMsg = { type: "diff", session: "open24", updates: [{ ticker: "BTC", price: 110, pct: 0.1 }] }; + s.applyDiff(diff); + expect(s.tiles()[0].price).toBe(110); + expect(s.tiles()[0].pct).toBeCloseTo(0.1); + }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/tooltip.test.ts b/examples/rest/massive-heatmap/frontend/test/tooltip.test.ts new file mode 100644 index 0000000..c031565 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/tooltip.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { placeTooltip } from "../src/Tooltip.js"; + +// viewport 1000x800, tooltip 200x100 unless noted +describe("placeTooltip", () => { + it("sits below-right of the cursor when there is room", () => { + expect(placeTooltip(100, 100, 200, 100, 1000, 800)).toEqual({ left: 114, top: 114 }); + }); + + it("flips to the left of the cursor near the right edge", () => { + // 950+14+200 = 1164 > 1000 -> flip: 950-14-200 = 736 + expect(placeTooltip(950, 100, 200, 100, 1000, 800).left).toBe(736); + }); + + it("flips above the cursor near the bottom edge", () => { + // 760+14+100 = 874 > 800 -> flip: 760-14-100 = 646 + expect(placeTooltip(100, 760, 200, 100, 1000, 800).top).toBe(646); + }); + + it("never lets the tooltip exceed the right/bottom margin", () => { + const { left, top } = placeTooltip(995, 795, 200, 100, 1000, 800); + expect(left).toBeLessThanOrEqual(1000 - 200 - 8); + expect(top).toBeLessThanOrEqual(800 - 100 - 8); + }); + + it("clamps to the top-left margin when the tooltip is larger than the space both ways", () => { + // tiny viewport, big tooltip -> pinned to the margin, never negative + expect(placeTooltip(10, 10, 400, 300, 200, 150)).toEqual({ left: 8, top: 8 }); + }); +}); diff --git a/examples/rest/massive-heatmap/frontend/test/treemap.test.ts b/examples/rest/massive-heatmap/frontend/test/treemap.test.ts new file mode 100644 index 0000000..0cc20a4 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/test/treemap.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { layout } from "../src/treemap.js"; +import type { Tile } from "../../shared/protocol.js"; + +const tiles: Tile[] = [ + { ticker: "A", name: "A", group: "Tech", marketCap: 300, priorClose: 1, price: 1, pct: 0 }, + { ticker: "B", name: "B", group: "Tech", marketCap: 100, priorClose: 1, price: 1, pct: 0 }, + { ticker: "C", name: "C", group: "Energy", marketCap: 200, priorClose: 1, price: 1, pct: 0 }, +]; + +describe("layout", () => { + it("returns one rect per tile within bounds", () => { + const { tiles: rects, groups } = layout(tiles, 800, 600); + expect(rects).toHaveLength(3); + for (const r of rects) { + expect(r.x0).toBeGreaterThanOrEqual(0); + expect(r.x1).toBeLessThanOrEqual(800); + expect(r.y1).toBeLessThanOrEqual(600); + expect(r.ticker).toBeTypeOf("string"); + } + expect(groups.map(g => g.name).sort()).toEqual(["Energy", "Tech"]); + }); + it("bigger market cap gets bigger area", () => { + const { tiles: rects } = layout(tiles, 800, 600); + const area = (t: string) => { const r = rects.find(x => x.ticker === t)!; return (r.x1-r.x0)*(r.y1-r.y0); }; + expect(area("A")).toBeGreaterThan(area("B")); + }); + +}); diff --git a/examples/rest/massive-heatmap/frontend/tsconfig.json b/examples/rest/massive-heatmap/frontend/tsconfig.json new file mode 100644 index 0000000..8972fda --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "noEmit": true + }, + "include": ["src", "test", "../shared"], + "exclude": ["test", "dist"] +} diff --git a/examples/rest/massive-heatmap/frontend/vite.config.ts b/examples/rest/massive-heatmap/frontend/vite.config.ts new file mode 100644 index 0000000..67b5785 --- /dev/null +++ b/examples/rest/massive-heatmap/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +export default defineConfig({ + plugins: [react()], + server: { proxy: { "/api": "http://localhost:8787" } }, + test: { environment: "jsdom" }, +}); diff --git a/examples/rest/massive-heatmap/package.json b/examples/rest/massive-heatmap/package.json new file mode 100644 index 0000000..887d5d7 --- /dev/null +++ b/examples/rest/massive-heatmap/package.json @@ -0,0 +1,16 @@ +{ + "name": "massive-heatmap", + "private": true, + "type": "module", + "workspaces": ["backend", "frontend"], + "scripts": { + "dev": "concurrently -n backend,frontend -c blue,green \"npm:dev -w backend\" \"npm:dev -w frontend\"", + "build": "npm run build -w backend && npm run build -w frontend", + "test": "npm run test -w backend && npm run test -w frontend" + }, + "devDependencies": { + "concurrently": "^9.0.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + } +} diff --git a/examples/rest/massive-heatmap/screenshots/crypto-top-coins.png b/examples/rest/massive-heatmap/screenshots/crypto-top-coins.png new file mode 100644 index 0000000..6738096 Binary files /dev/null and b/examples/rest/massive-heatmap/screenshots/crypto-top-coins.png differ diff --git a/examples/rest/massive-heatmap/screenshots/export-preview.png b/examples/rest/massive-heatmap/screenshots/export-preview.png new file mode 100644 index 0000000..c93804e Binary files /dev/null and b/examples/rest/massive-heatmap/screenshots/export-preview.png differ diff --git a/examples/rest/massive-heatmap/screenshots/hero-sp500.png b/examples/rest/massive-heatmap/screenshots/hero-sp500.png new file mode 100644 index 0000000..716b2e3 Binary files /dev/null and b/examples/rest/massive-heatmap/screenshots/hero-sp500.png differ diff --git a/examples/rest/massive-heatmap/screenshots/loading-overlay.png b/examples/rest/massive-heatmap/screenshots/loading-overlay.png new file mode 100644 index 0000000..4280cc2 Binary files /dev/null and b/examples/rest/massive-heatmap/screenshots/loading-overlay.png differ diff --git a/examples/rest/massive-heatmap/screenshots/settings.png b/examples/rest/massive-heatmap/screenshots/settings.png new file mode 100644 index 0000000..0036104 Binary files /dev/null and b/examples/rest/massive-heatmap/screenshots/settings.png differ diff --git a/examples/rest/massive-heatmap/shared/protocol.ts b/examples/rest/massive-heatmap/shared/protocol.ts new file mode 100644 index 0000000..d0ef425 --- /dev/null +++ b/examples/rest/massive-heatmap/shared/protocol.ts @@ -0,0 +1,42 @@ +import type { Segment } from "./universe.js"; + +// Re-export Segment so backend/frontend modules can import it from protocol. +export type { Segment } from "./universe.js"; + +export type SessionPhase = "premarket" | "regular" | "afterhours" | "closed" | "open24"; + +export interface Tile { + ticker: string; + name: string; + group: string; + marketCap: number; + priorClose: number; + price: number; + pct: number; // (price - priorClose) / priorClose, 0 when priorClose<=0 +} + +export interface TileUpdate { + ticker: string; + price: number; + pct: number; +} + +// server -> client +export interface SnapshotMsg { + type: "snapshot"; + universeId: string; + label: string; + segment: Segment; + session: SessionPhase; + tiles: Tile[]; +} +export interface DiffMsg { + type: "diff"; + session: SessionPhase; + updates: TileUpdate[]; +} +export interface ErrorMsg { + type: "error"; + message: string; +} +export type ServerMsg = SnapshotMsg | DiffMsg | ErrorMsg; diff --git a/examples/rest/massive-heatmap/shared/universe.ts b/examples/rest/massive-heatmap/shared/universe.ts new file mode 100644 index 0000000..399f73b --- /dev/null +++ b/examples/rest/massive-heatmap/shared/universe.ts @@ -0,0 +1,22 @@ +export type Segment = "stocks" | "etfs" | "crypto" | "forex" | "futures" | "indices"; + +export interface Constituent { + ticker: string; // display symbol, e.g. "AAPL" + wsSymbol: string; // subscription symbol, e.g. "AAPL", "BTC-USD", "EUR/USD" + name: string; + group: string; // GICS sector (stocks/etfs) or category (others) + marketCap: number; // sizing weight; arbitrary positive units for non-equities + segment?: Segment; // per-constituent asset class; defaults to Universe.segment +} + +export interface Universe { + id: string; + segment: Segment; // default segment for constituents that omit their own + label: string; + constituents: Constituent[]; +} + +// Effective asset class for a constituent: its own segment, else the universe default. +export function constituentSegment(universe: Universe, c: Constituent): Segment { + return c.segment ?? universe.segment; +} diff --git a/examples/rest/massive-heatmap/tsconfig.base.json b/examples/rest/massive-heatmap/tsconfig.base.json new file mode 100644 index 0000000..634d593 --- /dev/null +++ b/examples/rest/massive-heatmap/tsconfig.base.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": false + } +} diff --git a/examples/rest/massive-heatmap/universes/crypto-defi.json b/examples/rest/massive-heatmap/universes/crypto-defi.json new file mode 100644 index 0000000..b126c01 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/crypto-defi.json @@ -0,0 +1,84 @@ +{ + "id": "crypto-defi", + "segment": "crypto", + "label": "DeFi", + "constituents": [ + { + "ticker": "UNI", + "wsSymbol": "UNI-USD", + "name": "Uniswap", + "group": "DeFi", + "marketCap": 1786765124 + }, + { + "ticker": "AAVE", + "wsSymbol": "AAVE-USD", + "name": "Aave", + "group": "DeFi", + "marketCap": 1159450185 + }, + { + "ticker": "MKR", + "wsSymbol": "MKR-USD", + "name": "Maker", + "group": "DeFi", + "marketCap": 1800000000 + }, + { + "ticker": "LDO", + "wsSymbol": "LDO-USD", + "name": "Lido DAO", + "group": "DeFi", + "marketCap": 216297567 + }, + { + "ticker": "CRV", + "wsSymbol": "CRV-USD", + "name": "Curve DAO", + "group": "DeFi", + "marketCap": 302034009 + }, + { + "ticker": "SNX", + "wsSymbol": "SNX-USD", + "name": "Synthetix", + "group": "DeFi", + "marketCap": 150000000 + }, + { + "ticker": "COMP", + "wsSymbol": "COMP-USD", + "name": "Compound", + "group": "DeFi", + "marketCap": 160436323 + }, + { + "ticker": "LINK", + "wsSymbol": "LINK-USD", + "name": "Chainlink", + "group": "DeFi", + "marketCap": 5602174899 + }, + { + "ticker": "INJ", + "wsSymbol": "INJ-USD", + "name": "Injective", + "group": "DeFi", + "marketCap": 433479379 + }, + { + "ticker": "RUNE", + "wsSymbol": "RUNE-USD", + "name": "THORChain", + "group": "DeFi", + "marketCap": 500000000 + }, + { + "ticker": "GRT", + "wsSymbol": "GRT-USD", + "name": "The Graph", + "group": "DeFi", + "marketCap": 198937035 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/crypto-l1.json b/examples/rest/massive-heatmap/universes/crypto-l1.json new file mode 100644 index 0000000..e688585 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/crypto-l1.json @@ -0,0 +1,105 @@ +{ + "id": "crypto-l1", + "segment": "crypto", + "label": "Layer 1s", + "constituents": [ + { + "ticker": "BTC", + "wsSymbol": "BTC-USD", + "name": "Bitcoin", + "group": "Layer 1", + "marketCap": 1222824846528 + }, + { + "ticker": "ETH", + "wsSymbol": "ETH-USD", + "name": "Ethereum", + "group": "Layer 1", + "marketCap": 198415095297 + }, + { + "ticker": "SOL", + "wsSymbol": "SOL-USD", + "name": "Solana", + "group": "Layer 1", + "marketCap": 39883113178 + }, + { + "ticker": "ADA", + "wsSymbol": "ADA-USD", + "name": "Cardano", + "group": "Layer 1", + "marketCap": 5401402055 + }, + { + "ticker": "AVAX", + "wsSymbol": "AVAX-USD", + "name": "Avalanche", + "group": "Layer 1", + "marketCap": 2724658592 + }, + { + "ticker": "DOT", + "wsSymbol": "DOT-USD", + "name": "Polkadot", + "group": "Layer 1", + "marketCap": 1503774003 + }, + { + "ticker": "NEAR", + "wsSymbol": "NEAR-USD", + "name": "NEAR Protocol", + "group": "Layer 1", + "marketCap": 2507333593 + }, + { + "ticker": "ATOM", + "wsSymbol": "ATOM-USD", + "name": "Cosmos", + "group": "Layer 1", + "marketCap": 856918556 + }, + { + "ticker": "APT", + "wsSymbol": "APT-USD", + "name": "Aptos", + "group": "Layer 1", + "marketCap": 523283883 + }, + { + "ticker": "SUI", + "wsSymbol": "SUI-USD", + "name": "Sui", + "group": "Layer 1", + "marketCap": 2757284692 + }, + { + "ticker": "TRX", + "wsSymbol": "TRX-USD", + "name": "TRON", + "group": "Layer 1", + "marketCap": 31173784913 + }, + { + "ticker": "ALGO", + "wsSymbol": "ALGO-USD", + "name": "Algorand", + "group": "Layer 1", + "marketCap": 794214149 + }, + { + "ticker": "HBAR", + "wsSymbol": "HBAR-USD", + "name": "Hedera", + "group": "Layer 1", + "marketCap": 3266821871 + }, + { + "ticker": "TON", + "wsSymbol": "TON-USD", + "name": "Toncoin", + "group": "Layer 1", + "marketCap": 4200000000 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/crypto.json b/examples/rest/massive-heatmap/universes/crypto.json new file mode 100644 index 0000000..20f1c4a --- /dev/null +++ b/examples/rest/massive-heatmap/universes/crypto.json @@ -0,0 +1,88 @@ +{ + "id": "crypto", + "segment": "crypto", + "label": "Top Coins", + "constituents": [ + { "ticker": "BTC", "wsSymbol": "BTC-USD", "name": "Bitcoin", "group": "L1", "marketCap": 1188946657824 }, + { "ticker": "ETH", "wsSymbol": "ETH-USD", "name": "Ethereum", "group": "L1", "marketCap": 188102912391 }, + { "ticker": "BNB", "wsSymbol": "BNB-USD", "name": "BNB", "group": "Exchange", "marketCap": 73936121787 }, + { "ticker": "XRP", "wsSymbol": "XRP-USD", "name": "XRP", "group": "Payments", "marketCap": 65142282822 }, + { "ticker": "SOL", "wsSymbol": "SOL-USD", "name": "Solana", "group": "L1", "marketCap": 37681286604 }, + { "ticker": "TRX", "wsSymbol": "TRX-USD", "name": "TRON", "group": "L1", "marketCap": 30827501656 }, + { "ticker": "HYPE", "wsSymbol": "HYPE-USD", "name": "Hyperliquid", "group": "Exchange", "marketCap": 13179057411 }, + { "ticker": "DOGE", "wsSymbol": "DOGE-USD", "name": "Dogecoin", "group": "Meme", "marketCap": 11300603307 }, + { "ticker": "ZEC", "wsSymbol": "ZEC-USD", "name": "Zcash", "group": "Other", "marketCap": 6582392170 }, + { "ticker": "XLM", "wsSymbol": "XLM-USD", "name": "Stellar", "group": "L1", "marketCap": 6164717577 }, + { "ticker": "XMR", "wsSymbol": "XMR-USD", "name": "Monero", "group": "Payments", "marketCap": 5827069946 }, + { "ticker": "WBT", "wsSymbol": "WBT-USD", "name": "WhiteBIT Coin", "group": "Exchange", "marketCap": 5702364169 }, + { "ticker": "LINK", "wsSymbol": "LINK-USD", "name": "Chainlink", "group": "DeFi", "marketCap": 5339597341 }, + { "ticker": "ADA", "wsSymbol": "ADA-USD", "name": "Cardano", "group": "L1", "marketCap": 5213497464 }, + { "ticker": "TON", "wsSymbol": "TON-USD", "name": "Toncoin", "group": "L1", "marketCap": 4119842392 }, + { "ticker": "BCH", "wsSymbol": "BCH-USD", "name": "Bitcoin Cash", "group": "Payments", "marketCap": 3636427193 }, + { "ticker": "HBAR", "wsSymbol": "HBAR-USD", "name": "Hedera", "group": "L1", "marketCap": 3204293550 }, + { "ticker": "LTC", "wsSymbol": "LTC-USD", "name": "Litecoin", "group": "Payments", "marketCap": 3050059105 }, + { "ticker": "SUI", "wsSymbol": "SUI-USD", "name": "Sui", "group": "L1", "marketCap": 2646577183 }, + { "ticker": "AVAX", "wsSymbol": "AVAX-USD", "name": "Avalanche", "group": "L1", "marketCap": 2597368056 }, + { "ticker": "SHIB", "wsSymbol": "SHIB-USD", "name": "Shiba Inu", "group": "Meme", "marketCap": 2487086560 }, + { "ticker": "NEAR", "wsSymbol": "NEAR-USD", "name": "NEAR Protocol", "group": "L1", "marketCap": 2424555502 }, + { "ticker": "TAO", "wsSymbol": "TAO-USD", "name": "Bittensor", "group": "AI", "marketCap": 1990014998 }, + { "ticker": "UNI", "wsSymbol": "UNI-USD", "name": "Uniswap", "group": "DeFi", "marketCap": 1700611260 }, + { "ticker": "MNT", "wsSymbol": "MNT-USD", "name": "Mantle", "group": "Other", "marketCap": 1626024815 }, + { "ticker": "ONDO", "wsSymbol": "ONDO-USD", "name": "Ondo", "group": "Other", "marketCap": 1432504814 }, + { "ticker": "DOT", "wsSymbol": "DOT-USD", "name": "Polkadot", "group": "L1", "marketCap": 1429158240 }, + { "ticker": "SKY", "wsSymbol": "SKY-USD", "name": "Sky", "group": "Other", "marketCap": 1211164754 }, + { "ticker": "ICP", "wsSymbol": "ICP-USD", "name": "Internet Computer", "group": "Other", "marketCap": 1137640433 }, + { "ticker": "AAVE", "wsSymbol": "AAVE-USD", "name": "Aave", "group": "DeFi", "marketCap": 1100674670 }, + { "ticker": "MORPHO", "wsSymbol": "MORPHO-USD", "name": "Morpho", "group": "DeFi", "marketCap": 1084289566 }, + { "ticker": "ETC", "wsSymbol": "ETC-USD", "name": "Ethereum Classic", "group": "L1", "marketCap": 1048800137 }, + { "ticker": "PEPE", "wsSymbol": "PEPE-USD", "name": "Pepe", "group": "Meme", "marketCap": 1022143621 }, + { "ticker": "QNT", "wsSymbol": "QNT-USD", "name": "Quant", "group": "Other", "marketCap": 967396252 }, + { "ticker": "ATOM", "wsSymbol": "ATOM-USD", "name": "Cosmos Hub", "group": "L1", "marketCap": 828638990 }, + { "ticker": "RENDER", "wsSymbol": "RENDER-USD", "name": "Render", "group": "AI", "marketCap": 775494191 }, + { "ticker": "ALGO", "wsSymbol": "ALGO-USD", "name": "Algorand", "group": "L1", "marketCap": 771087013 }, + { "ticker": "POL", "wsSymbol": "POL-USD", "name": "POL (ex-MATIC)", "group": "L2", "marketCap": 768965338 }, + { "ticker": "KAS", "wsSymbol": "KAS-USD", "name": "Kaspa", "group": "L1", "marketCap": 746103541 }, + { "ticker": "ENA", "wsSymbol": "ENA-USD", "name": "Ethena", "group": "Other", "marketCap": 739477500 }, + { "ticker": "NEXO", "wsSymbol": "NEXO-USD", "name": "NEXO", "group": "Other", "marketCap": 728506748 }, + { "ticker": "JST", "wsSymbol": "JST-USD", "name": "JUST", "group": "Exchange", "marketCap": 688707735 }, + { "ticker": "JUP", "wsSymbol": "JUP-USD", "name": "Jupiter", "group": "Exchange", "marketCap": 652863337 }, + { "ticker": "FLR", "wsSymbol": "FLR-USD", "name": "Flare", "group": "Other", "marketCap": 581971487 }, + { "ticker": "XDC", "wsSymbol": "XDC-USD", "name": "XDC Network", "group": "Payments", "marketCap": 566128345 }, + { "ticker": "FIL", "wsSymbol": "FIL-USD", "name": "Filecoin", "group": "Other", "marketCap": 554698780 }, + { "ticker": "APT", "wsSymbol": "APT-USD", "name": "Aptos", "group": "L1", "marketCap": 492772086 }, + { "ticker": "ARB", "wsSymbol": "ARB-USD", "name": "Arbitrum", "group": "L2", "marketCap": 464892174 }, + { "ticker": "AERO", "wsSymbol": "AERO-USD", "name": "Aerodrome Finance", "group": "DeFi", "marketCap": 454080058 }, + { "ticker": "INJ", "wsSymbol": "INJ-USD", "name": "Injective", "group": "DeFi", "marketCap": 417413166 }, + { "ticker": "DASH", "wsSymbol": "DASH-USD", "name": "Dash", "group": "Payments", "marketCap": 415785078 }, + { "ticker": "PRIME", "wsSymbol": "PRIME-USD", "name": "PRIME", "group": "Other", "marketCap": 401285501 }, + { "ticker": "TRUMP", "wsSymbol": "TRUMP-USD", "name": "Official Trump", "group": "Meme", "marketCap": 386375736 }, + { "ticker": "VET", "wsSymbol": "VET-USD", "name": "VeChain", "group": "Other", "marketCap": 379843785 }, + { "ticker": "FET", "wsSymbol": "FET-USD", "name": "Artificial Superintelligence Alliance", "group": "AI", "marketCap": 370392184 }, + { "ticker": "BONK", "wsSymbol": "BONK-USD", "name": "Bonk", "group": "Meme", "marketCap": 355363256 }, + { "ticker": "SEI", "wsSymbol": "SEI-USD", "name": "Sei", "group": "L1", "marketCap": 345102348 }, + { "ticker": "TIA", "wsSymbol": "TIA-USD", "name": "Celestia", "group": "L1", "marketCap": 321357801 }, + { "ticker": "JTO", "wsSymbol": "JTO-USD", "name": "Jito", "group": "Other", "marketCap": 304614450 }, + { "ticker": "STX", "wsSymbol": "STX-USD", "name": "Stacks", "group": "Other", "marketCap": 302975113 }, + { "ticker": "ETHFI", "wsSymbol": "ETHFI-USD", "name": "Ether.fi", "group": "Other", "marketCap": 296231585 }, + { "ticker": "CRV", "wsSymbol": "CRV-USD", "name": "Curve DAO", "group": "DeFi", "marketCap": 287632362 }, + { "ticker": "GNO", "wsSymbol": "GNO-USD", "name": "Gnosis", "group": "Other", "marketCap": 272771587 }, + { "ticker": "BTT", "wsSymbol": "BTT-USD", "name": "BitTorrent", "group": "Other", "marketCap": 261378679 }, + { "ticker": "PYTH", "wsSymbol": "PYTH-USD", "name": "Pyth Network", "group": "Other", "marketCap": 256051588 }, + { "ticker": "XTZ", "wsSymbol": "XTZ-USD", "name": "Tezos", "group": "L1", "marketCap": 226697133 }, + { "ticker": "FLOKI", "wsSymbol": "FLOKI-USD", "name": "FLOKI", "group": "Meme", "marketCap": 213987561 }, + { "ticker": "OP", "wsSymbol": "OP-USD", "name": "Optimism", "group": "L2", "marketCap": 205470169 }, + { "ticker": "LDO", "wsSymbol": "LDO-USD", "name": "Lido DAO", "group": "DeFi", "marketCap": 205355862 }, + { "ticker": "PENDLE", "wsSymbol": "PENDLE-USD", "name": "Pendle", "group": "DeFi", "marketCap": 203492813 }, + { "ticker": "STRK", "wsSymbol": "STRK-USD", "name": "Starknet", "group": "L2", "marketCap": 194946933 }, + { "ticker": "GRT", "wsSymbol": "GRT-USD", "name": "The Graph", "group": "DeFi", "marketCap": 191221959 }, + { "ticker": "COMP", "wsSymbol": "COMP-USD", "name": "Compound", "group": "DeFi", "marketCap": 158140583 }, + { "ticker": "WIF", "wsSymbol": "WIF-USD", "name": "dogwifhat", "group": "Meme", "marketCap": 141372944 }, + { "ticker": "RUNE", "wsSymbol": "RUNE-USD", "name": "THORChain", "group": "DeFi", "marketCap": 134517400 }, + { "ticker": "DYDX", "wsSymbol": "DYDX-USD", "name": "dYdX", "group": "DeFi", "marketCap": 118181046 }, + { "ticker": "IMX", "wsSymbol": "IMX-USD", "name": "Immutable", "group": "L2", "marketCap": 107697837 }, + { "ticker": "EGLD", "wsSymbol": "EGLD-USD", "name": "MultiversX", "group": "L1", "marketCap": 75951593 }, + { "ticker": "SNX", "wsSymbol": "SNX-USD", "name": "Synthetix", "group": "DeFi", "marketCap": 73557957 }, + { "ticker": "FLOW", "wsSymbol": "FLOW-USD", "name": "Flow", "group": "L1", "marketCap": 45889955 }, + { "ticker": "SUSHI", "wsSymbol": "SUSHI-USD", "name": "SushiSwap", "group": "DeFi", "marketCap": 39716073 } + ] +} diff --git a/examples/rest/massive-heatmap/universes/dow30.json b/examples/rest/massive-heatmap/universes/dow30.json new file mode 100644 index 0000000..3e140a4 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/dow30.json @@ -0,0 +1,217 @@ +{ + "id": "dow30", + "segment": "stocks", + "label": "Dow 30", + "constituents": [ + { + "ticker": "AAPL", + "wsSymbol": "AAPL", + "name": "Apple", + "group": "Information Technology", + "marketCap": 4322488870800 + }, + { + "ticker": "AMGN", + "wsSymbol": "AMGN", + "name": "Amgen", + "group": "Health Care", + "marketCap": 187284168160 + }, + { + "ticker": "AXP", + "wsSymbol": "AXP", + "name": "American Express", + "group": "Financials", + "marketCap": 230476225916 + }, + { + "ticker": "BA", + "wsSymbol": "BA", + "name": "Boeing", + "group": "Industrials", + "marketCap": 170832998151 + }, + { + "ticker": "CAT", + "wsSymbol": "CAT", + "name": "Caterpillar", + "group": "Industrials", + "marketCap": 453333053347 + }, + { + "ticker": "CRM", + "wsSymbol": "CRM", + "name": "Salesforce", + "group": "Information Technology", + "marketCap": 258000000000 + }, + { + "ticker": "CSCO", + "wsSymbol": "CSCO", + "name": "Cisco", + "group": "Information Technology", + "marketCap": 477504809664 + }, + { + "ticker": "CVX", + "wsSymbol": "CVX", + "name": "Chevron", + "group": "Energy", + "marketCap": 350481368877 + }, + { + "ticker": "DIS", + "wsSymbol": "DIS", + "name": "Walt Disney", + "group": "Communication Services", + "marketCap": 179781027726 + }, + { + "ticker": "GS", + "wsSymbol": "GS", + "name": "Goldman Sachs", + "group": "Financials", + "marketCap": 322867921839 + }, + { + "ticker": "HD", + "wsSymbol": "HD", + "name": "Home Depot", + "group": "Consumer Discretionary", + "marketCap": 323514507474 + }, + { + "ticker": "HON", + "wsSymbol": "HON", + "name": "Honeywell", + "group": "Industrials", + "marketCap": 140905444072 + }, + { + "ticker": "IBM", + "wsSymbol": "IBM", + "name": "IBM", + "group": "Information Technology", + "marketCap": 249013206083 + }, + { + "ticker": "JNJ", + "wsSymbol": "JNJ", + "name": "Johnson & Johnson", + "group": "Health Care", + "marketCap": 575517433426 + }, + { + "ticker": "JPM", + "wsSymbol": "JPM", + "name": "JPMorgan Chase", + "group": "Financials", + "marketCap": 895331945210 + }, + { + "ticker": "KO", + "wsSymbol": "KO", + "name": "Coca-Cola", + "group": "Consumer Staples", + "marketCap": 345532362989 + }, + { + "ticker": "MCD", + "wsSymbol": "MCD", + "name": "McDonald's", + "group": "Consumer Discretionary", + "marketCap": 193016021655 + }, + { + "ticker": "MMM", + "wsSymbol": "MMM", + "name": "3M", + "group": "Industrials", + "marketCap": 84410445520 + }, + { + "ticker": "MRK", + "wsSymbol": "MRK", + "name": "Merck", + "group": "Health Care", + "marketCap": 295391000034 + }, + { + "ticker": "MSFT", + "wsSymbol": "MSFT", + "name": "Microsoft", + "group": "Information Technology", + "marketCap": 2777788873213 + }, + { + "ticker": "NKE", + "wsSymbol": "NKE", + "name": "Nike", + "group": "Consumer Discretionary", + "marketCap": 62759992458 + }, + { + "ticker": "NVDA", + "wsSymbol": "NVDA", + "name": "NVIDIA", + "group": "Information Technology", + "marketCap": 4845168840000 + }, + { + "ticker": "PG", + "wsSymbol": "PG", + "name": "Procter & Gamble", + "group": "Consumer Staples", + "marketCap": 351292441821 + }, + { + "ticker": "SHW", + "wsSymbol": "SHW", + "name": "Sherwin-Williams", + "group": "Materials", + "marketCap": 79638642020 + }, + { + "ticker": "TRV", + "wsSymbol": "TRV", + "name": "Travelers", + "group": "Financials", + "marketCap": 67399861893 + }, + { + "ticker": "UNH", + "wsSymbol": "UNH", + "name": "UnitedHealth Group", + "group": "Health Care", + "marketCap": 371658097337 + }, + { + "ticker": "V", + "wsSymbol": "V", + "name": "Visa", + "group": "Financials", + "marketCap": 624684803424 + }, + { + "ticker": "VZ", + "wsSymbol": "VZ", + "name": "Verizon", + "group": "Communication Services", + "marketCap": 195123867864 + }, + { + "ticker": "WMT", + "wsSymbol": "WMT", + "name": "Walmart", + "group": "Consumer Staples", + "marketCap": 950353812690 + }, + { + "ticker": "DOW", + "wsSymbol": "DOW", + "name": "Dow Inc.", + "group": "Materials", + "marketCap": 21860087814 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/etf-broad.json b/examples/rest/massive-heatmap/universes/etf-broad.json new file mode 100644 index 0000000..9072735 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/etf-broad.json @@ -0,0 +1,77 @@ +{ + "id": "etf-broad", + "segment": "etfs", + "label": "Broad Market", + "constituents": [ + { + "ticker": "SPY", + "wsSymbol": "SPY", + "name": "SPDR S&P 500 ETF Trust", + "group": "Broad", + "marketCap": 783070 + }, + { + "ticker": "QQQ", + "wsSymbol": "QQQ", + "name": "Invesco QQQ Trust", + "group": "Broad", + "marketCap": 480530 + }, + { + "ticker": "IWM", + "wsSymbol": "IWM", + "name": "iShares Russell 2000 ETF", + "group": "Broad", + "marketCap": 88000 + }, + { + "ticker": "DIA", + "wsSymbol": "DIA", + "name": "SPDR Dow Jones Industrial Average ETF", + "group": "Broad", + "marketCap": 34000 + }, + { + "ticker": "VTI", + "wsSymbol": "VTI", + "name": "Vanguard Total Stock Market ETF", + "group": "Broad", + "marketCap": 490000 + }, + { + "ticker": "VOO", + "wsSymbol": "VOO", + "name": "Vanguard S&P 500 ETF", + "group": "Broad", + "marketCap": 590000 + }, + { + "ticker": "VEA", + "wsSymbol": "VEA", + "name": "Vanguard FTSE Developed Markets ETF", + "group": "Broad", + "marketCap": 120000 + }, + { + "ticker": "VWO", + "wsSymbol": "VWO", + "name": "Vanguard FTSE Emerging Markets ETF", + "group": "Broad", + "marketCap": 75000 + }, + { + "ticker": "EFA", + "wsSymbol": "EFA", + "name": "iShares MSCI EAFE ETF", + "group": "Broad", + "marketCap": 55000 + }, + { + "ticker": "EEM", + "wsSymbol": "EEM", + "name": "iShares MSCI Emerging Markets ETF", + "group": "Broad", + "marketCap": 28000 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/etf-sectors.json b/examples/rest/massive-heatmap/universes/etf-sectors.json new file mode 100644 index 0000000..4f50ae5 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/etf-sectors.json @@ -0,0 +1,84 @@ +{ + "id": "etf-sectors", + "segment": "etfs", + "label": "Sector SPDRs", + "constituents": [ + { + "ticker": "XLK", + "wsSymbol": "XLK", + "name": "Technology Select Sector SPDR Fund", + "group": "Technology", + "marketCap": 117780 + }, + { + "ticker": "XLF", + "wsSymbol": "XLF", + "name": "Financial Select Sector SPDR Fund", + "group": "Financials", + "marketCap": 49530 + }, + { + "ticker": "XLE", + "wsSymbol": "XLE", + "name": "Energy Select Sector SPDR Fund", + "group": "Energy", + "marketCap": 39120 + }, + { + "ticker": "XLV", + "wsSymbol": "XLV", + "name": "Health Care Select Sector SPDR Fund", + "group": "Health Care", + "marketCap": 39240 + }, + { + "ticker": "XLY", + "wsSymbol": "XLY", + "name": "Consumer Discretionary Select Sector SPDR Fund", + "group": "Consumer Discretionary", + "marketCap": 22440 + }, + { + "ticker": "XLP", + "wsSymbol": "XLP", + "name": "Consumer Staples Select Sector SPDR Fund", + "group": "Consumer Staples", + "marketCap": 14670 + }, + { + "ticker": "XLI", + "wsSymbol": "XLI", + "name": "Industrial Select Sector SPDR Fund", + "group": "Industrials", + "marketCap": 31070 + }, + { + "ticker": "XLB", + "wsSymbol": "XLB", + "name": "Materials Select Sector SPDR Fund", + "group": "Materials", + "marketCap": 7830 + }, + { + "ticker": "XLRE", + "wsSymbol": "XLRE", + "name": "Real Estate Select Sector SPDR Fund", + "group": "Real Estate", + "marketCap": 8170 + }, + { + "ticker": "XLU", + "wsSymbol": "XLU", + "name": "Utilities Select Sector SPDR Fund", + "group": "Utilities", + "marketCap": 22550 + }, + { + "ticker": "XLC", + "wsSymbol": "XLC", + "name": "Communication Services Select Sector SPDR Fund", + "group": "Communication Services", + "marketCap": 23800 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/etf-thematic.json b/examples/rest/massive-heatmap/universes/etf-thematic.json new file mode 100644 index 0000000..4f799c5 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/etf-thematic.json @@ -0,0 +1,91 @@ +{ + "id": "etf-thematic", + "segment": "etfs", + "label": "Thematic", + "constituents": [ + { + "ticker": "SMH", + "wsSymbol": "SMH", + "name": "VanEck Semiconductor ETF", + "group": "Semis", + "marketCap": 24000 + }, + { + "ticker": "XBI", + "wsSymbol": "XBI", + "name": "SPDR S&P Biotech ETF", + "group": "Biotech", + "marketCap": 9000 + }, + { + "ticker": "ARKK", + "wsSymbol": "ARKK", + "name": "ARK Innovation ETF", + "group": "Innovation", + "marketCap": 6000 + }, + { + "ticker": "IBB", + "wsSymbol": "IBB", + "name": "iShares Biotechnology ETF", + "group": "Biotech", + "marketCap": 8000 + }, + { + "ticker": "TAN", + "wsSymbol": "TAN", + "name": "Invesco Solar ETF", + "group": "Clean Energy", + "marketCap": 1200 + }, + { + "ticker": "LIT", + "wsSymbol": "LIT", + "name": "Global X Lithium & Battery Tech ETF", + "group": "Clean Energy", + "marketCap": 900 + }, + { + "ticker": "JETS", + "wsSymbol": "JETS", + "name": "U.S. Global Jets ETF", + "group": "Airlines", + "marketCap": 500 + }, + { + "ticker": "HACK", + "wsSymbol": "HACK", + "name": "Amplify Cybersecurity ETF", + "group": "Cybersecurity", + "marketCap": 1800 + }, + { + "ticker": "SKYY", + "wsSymbol": "SKYY", + "name": "First Trust Cloud Computing ETF", + "group": "Cloud", + "marketCap": 5000 + }, + { + "ticker": "BOTZ", + "wsSymbol": "BOTZ", + "name": "Global X Robotics & AI ETF", + "group": "Robotics", + "marketCap": 3500 + }, + { + "ticker": "ICLN", + "wsSymbol": "ICLN", + "name": "iShares Global Clean Energy ETF", + "group": "Clean Energy", + "marketCap": 800 + }, + { + "ticker": "XOP", + "wsSymbol": "XOP", + "name": "SPDR S&P Oil & Gas Exploration & Production ETF", + "group": "Energy", + "marketCap": 4000 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/forex-crosses.json b/examples/rest/massive-heatmap/universes/forex-crosses.json new file mode 100644 index 0000000..8c7969d --- /dev/null +++ b/examples/rest/massive-heatmap/universes/forex-crosses.json @@ -0,0 +1,77 @@ +{ + "id": "forex-crosses", + "segment": "forex", + "label": "Crosses", + "constituents": [ + { + "ticker": "EURGBP", + "wsSymbol": "EUR/GBP", + "name": "Euro / British Pound", + "group": "Cross", + "marketCap": 600 + }, + { + "ticker": "EURJPY", + "wsSymbol": "EUR/JPY", + "name": "Euro / Japanese Yen", + "group": "Cross", + "marketCap": 500 + }, + { + "ticker": "GBPJPY", + "wsSymbol": "GBP/JPY", + "name": "British Pound / Japanese Yen", + "group": "Cross", + "marketCap": 350 + }, + { + "ticker": "EURCHF", + "wsSymbol": "EUR/CHF", + "name": "Euro / Swiss Franc", + "group": "Cross", + "marketCap": 300 + }, + { + "ticker": "AUDJPY", + "wsSymbol": "AUD/JPY", + "name": "Australian Dollar / Japanese Yen", + "group": "Cross", + "marketCap": 250 + }, + { + "ticker": "EURAUD", + "wsSymbol": "EUR/AUD", + "name": "Euro / Australian Dollar", + "group": "Cross", + "marketCap": 200 + }, + { + "ticker": "GBPCHF", + "wsSymbol": "GBP/CHF", + "name": "British Pound / Swiss Franc", + "group": "Cross", + "marketCap": 200 + }, + { + "ticker": "CADJPY", + "wsSymbol": "CAD/JPY", + "name": "Canadian Dollar / Japanese Yen", + "group": "Cross", + "marketCap": 150 + }, + { + "ticker": "NZDJPY", + "wsSymbol": "NZD/JPY", + "name": "New Zealand Dollar / Japanese Yen", + "group": "Cross", + "marketCap": 150 + }, + { + "ticker": "EURCAD", + "wsSymbol": "EUR/CAD", + "name": "Euro / Canadian Dollar", + "group": "Cross", + "marketCap": 200 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/forex-exotic.json b/examples/rest/massive-heatmap/universes/forex-exotic.json new file mode 100644 index 0000000..c83a1ea --- /dev/null +++ b/examples/rest/massive-heatmap/universes/forex-exotic.json @@ -0,0 +1,63 @@ +{ + "id": "forex-exotic", + "segment": "forex", + "label": "USD Exotics", + "constituents": [ + { + "ticker": "USDMXN", + "wsSymbol": "USD/MXN", + "name": "US Dollar / Mexican Peso", + "group": "Exotic", + "marketCap": 400 + }, + { + "ticker": "USDZAR", + "wsSymbol": "USD/ZAR", + "name": "US Dollar / South African Rand", + "group": "Exotic", + "marketCap": 150 + }, + { + "ticker": "USDSGD", + "wsSymbol": "USD/SGD", + "name": "US Dollar / Singapore Dollar", + "group": "Exotic", + "marketCap": 200 + }, + { + "ticker": "USDHKD", + "wsSymbol": "USD/HKD", + "name": "US Dollar / Hong Kong Dollar", + "group": "Exotic", + "marketCap": 300 + }, + { + "ticker": "USDNOK", + "wsSymbol": "USD/NOK", + "name": "US Dollar / Norwegian Krone", + "group": "Exotic", + "marketCap": 150 + }, + { + "ticker": "USDSEK", + "wsSymbol": "USD/SEK", + "name": "US Dollar / Swedish Krona", + "group": "Exotic", + "marketCap": 150 + }, + { + "ticker": "USDPLN", + "wsSymbol": "USD/PLN", + "name": "US Dollar / Polish Zloty", + "group": "Exotic", + "marketCap": 100 + }, + { + "ticker": "USDTRY", + "wsSymbol": "USD/TRY", + "name": "US Dollar / Turkish Lira", + "group": "Exotic", + "marketCap": 200 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/forex.json b/examples/rest/massive-heatmap/universes/forex.json new file mode 100644 index 0000000..1dda0c2 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/forex.json @@ -0,0 +1,14 @@ +{ + "id": "forex", + "segment": "forex", + "label": "FX Majors", + "constituents": [ + { "ticker": "EURUSD", "wsSymbol": "EUR/USD", "name": "Euro / US Dollar", "group": "Major", "marketCap": 1000 }, + { "ticker": "USDJPY", "wsSymbol": "USD/JPY", "name": "US Dollar / Japanese Yen", "group": "Major", "marketCap": 900 }, + { "ticker": "GBPUSD", "wsSymbol": "GBP/USD", "name": "British Pound / US Dollar", "group": "Major", "marketCap": 700 }, + { "ticker": "USDCHF", "wsSymbol": "USD/CHF", "name": "US Dollar / Swiss Franc", "group": "Major", "marketCap": 500 }, + { "ticker": "AUDUSD", "wsSymbol": "AUD/USD", "name": "Australian Dollar / US Dollar", "group": "Major", "marketCap": 400 }, + { "ticker": "USDCAD", "wsSymbol": "USD/CAD", "name": "US Dollar / Canadian Dollar", "group": "Major", "marketCap": 400 }, + { "ticker": "NZDUSD", "wsSymbol": "NZD/USD", "name": "New Zealand Dollar / US Dollar", "group": "Major", "marketCap": 200 } + ] +} diff --git a/examples/rest/massive-heatmap/universes/futures-energy.json b/examples/rest/massive-heatmap/universes/futures-energy.json new file mode 100644 index 0000000..13b0ea5 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/futures-energy.json @@ -0,0 +1,35 @@ +{ + "id": "futures-energy", + "segment": "futures", + "label": "Energy Futures", + "constituents": [ + { + "ticker": "CL", + "wsSymbol": "CLJ9", + "name": "Crude Oil (WTI)", + "group": "Energy", + "marketCap": 6518 + }, + { + "ticker": "RB", + "wsSymbol": "RBQ6", + "name": "RBOB Gasoline", + "group": "Energy", + "marketCap": 13770 + }, + { + "ticker": "HO", + "wsSymbol": "HOQ6", + "name": "Heating Oil", + "group": "Energy", + "marketCap": 10445 + }, + { + "ticker": "BZ", + "wsSymbol": "BZF0", + "name": "Brent Crude", + "group": "Energy", + "marketCap": 1506 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/futures-equity.json b/examples/rest/massive-heatmap/universes/futures-equity.json new file mode 100644 index 0000000..93ddc11 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/futures-equity.json @@ -0,0 +1,35 @@ +{ + "id": "futures-equity", + "segment": "futures", + "label": "Equity Index Futures", + "constituents": [ + { + "ticker": "ES", + "wsSymbol": "ESU6", + "name": "E-mini S&P 500", + "group": "Equity Index", + "marketCap": 680564 + }, + { + "ticker": "NQ", + "wsSymbol": "NQU6", + "name": "E-mini Nasdaq 100", + "group": "Equity Index", + "marketCap": 296986 + }, + { + "ticker": "YM", + "wsSymbol": "YMU6", + "name": "E-mini Dow", + "group": "Equity Index", + "marketCap": 44622 + }, + { + "ticker": "RTY", + "wsSymbol": "RTYU6", + "name": "E-mini Russell 2000", + "group": "Equity Index", + "marketCap": 72071 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/futures-metals.json b/examples/rest/massive-heatmap/universes/futures-metals.json new file mode 100644 index 0000000..4dec802 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/futures-metals.json @@ -0,0 +1,42 @@ +{ + "id": "futures-metals", + "segment": "futures", + "label": "Metals Futures", + "constituents": [ + { + "ticker": "GC", + "wsSymbol": "GCQ6", + "name": "Gold", + "group": "Metals", + "marketCap": 130165 + }, + { + "ticker": "SI", + "wsSymbol": "SIN6", + "name": "Silver", + "group": "Metals", + "marketCap": 33990 + }, + { + "ticker": "HG", + "wsSymbol": "HGN6", + "name": "Copper", + "group": "Metals", + "marketCap": 30258 + }, + { + "ticker": "PL", + "wsSymbol": "PLN6", + "name": "Platinum", + "group": "Metals", + "marketCap": 10297 + }, + { + "ticker": "PA", + "wsSymbol": "PAU6", + "name": "Palladium", + "group": "Metals", + "marketCap": 4120 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/indices-global.json b/examples/rest/massive-heatmap/universes/indices-global.json new file mode 100644 index 0000000..e824a8a --- /dev/null +++ b/examples/rest/massive-heatmap/universes/indices-global.json @@ -0,0 +1,20 @@ +{ + "id": "indices-global", + "segment": "indices", + "label": "Global", + "constituents": [ + { "ticker": "A1DOW", "wsSymbol": "I:A1DOW", "name": "Dow Jones Americas Index", "group": "Global", "marketCap": 100 }, + { "ticker": "GBDOW", "wsSymbol": "I:GBDOW", "name": "Dow Jones U.K. Index", "group": "Global", "marketCap": 100 }, + { "ticker": "DEDOW", "wsSymbol": "I:DEDOW", "name": "Dow Jones Germany Index", "group": "Global", "marketCap": 100 }, + { "ticker": "FRDOW", "wsSymbol": "I:FRDOW", "name": "Dow Jones France Index", "group": "Global", "marketCap": 100 }, + { "ticker": "JPDOW", "wsSymbol": "I:JPDOW", "name": "Dow Jones Japan Index", "group": "Global", "marketCap": 100 }, + { "ticker": "DJCHINA", "wsSymbol": "I:DJCHINA", "name": "Dow Jones China Broad Market Index", "group": "Global", "marketCap": 100 }, + { "ticker": "HKDOW", "wsSymbol": "I:HKDOW", "name": "Dow Jones Hong Kong Index", "group": "Global", "marketCap": 100 }, + { "ticker": "CADOW", "wsSymbol": "I:CADOW", "name": "Dow Jones Canada Index", "group": "Global", "marketCap": 100 }, + { "ticker": "DJAU", "wsSymbol": "I:DJAU", "name": "Dow Jones Australia Index", "group": "Global", "marketCap": 100 }, + { "ticker": "CHDOW", "wsSymbol": "I:CHDOW", "name": "Dow Jones Switzerland Index", "group": "Global", "marketCap": 100 }, + { "ticker": "KRDOW", "wsSymbol": "I:KRDOW", "name": "Dow Jones South Korea Index", "group": "Global", "marketCap": 100 }, + { "ticker": "TWDOW", "wsSymbol": "I:TWDOW", "name": "Dow Jones Taiwan Index", "group": "Global", "marketCap": 100 }, + { "ticker": "SGDOW", "wsSymbol": "I:SGDOW", "name": "Dow Jones Singapore Index", "group": "Global", "marketCap": 100 } + ] +} diff --git a/examples/rest/massive-heatmap/universes/indices-sectors.json b/examples/rest/massive-heatmap/universes/indices-sectors.json new file mode 100644 index 0000000..ca76efa --- /dev/null +++ b/examples/rest/massive-heatmap/universes/indices-sectors.json @@ -0,0 +1,17 @@ +{ + "id": "indices-sectors", + "segment": "indices", + "label": "Sectors", + "constituents": [ + { "ticker": "A1TEC", "wsSymbol": "I:A1TEC", "name": "Dow Jones Americas Technology Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1FIN", "wsSymbol": "I:A1FIN", "name": "Dow Jones Americas Financials Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1ENE", "wsSymbol": "I:A1ENE", "name": "Dow Jones Americas Oil & Gas Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1HCR", "wsSymbol": "I:A1HCR", "name": "Dow Jones Americas Health Care Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1IDU", "wsSymbol": "I:A1IDU", "name": "Dow Jones Americas Industrials Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1BSC", "wsSymbol": "I:A1BSC", "name": "Dow Jones Americas Basic Materials Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1CYC", "wsSymbol": "I:A1CYC", "name": "Dow Jones Americas Consumer Services Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1NCY", "wsSymbol": "I:A1NCY", "name": "Dow Jones Americas Consumer Goods Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1TLS", "wsSymbol": "I:A1TLS", "name": "Dow Jones Americas Telecommunications Index", "group": "Sector", "marketCap": 100 }, + { "ticker": "A1UTI", "wsSymbol": "I:A1UTI", "name": "Dow Jones Americas Utilities Index", "group": "Sector", "marketCap": 100 } + ] +} diff --git a/examples/rest/massive-heatmap/universes/indices.json b/examples/rest/massive-heatmap/universes/indices.json new file mode 100644 index 0000000..82cbf9b --- /dev/null +++ b/examples/rest/massive-heatmap/universes/indices.json @@ -0,0 +1,12 @@ +{ + "id": "indices", + "segment": "indices", + "label": "Major Indices", + "constituents": [ + { "ticker": "SPX", "wsSymbol": "I:SPX", "name": "S&P 500", "group": "Index", "marketCap": 100 }, + { "ticker": "NDX", "wsSymbol": "I:NDX", "name": "Nasdaq 100", "group": "Index", "marketCap": 100 }, + { "ticker": "DJI", "wsSymbol": "I:DJI", "name": "Dow Jones Industrial Average", "group": "Index", "marketCap": 100 }, + { "ticker": "RUT", "wsSymbol": "I:RUT", "name": "Russell 2000", "group": "Index", "marketCap": 100 }, + { "ticker": "VIX", "wsSymbol": "I:VIX", "name": "CBOE Volatility Index", "group": "Index", "marketCap": 100 } + ] +} diff --git a/examples/rest/massive-heatmap/universes/nasdaq100.json b/examples/rest/massive-heatmap/universes/nasdaq100.json new file mode 100644 index 0000000..e7b5784 --- /dev/null +++ b/examples/rest/massive-heatmap/universes/nasdaq100.json @@ -0,0 +1,714 @@ +{ + "id": "nasdaq100", + "segment": "stocks", + "label": "Nasdaq 100", + "constituents": [ + { + "ticker": "ADBE", + "wsSymbol": "ADBE", + "name": "Adobe Inc.", + "group": "Information Technology", + "marketCap": 77576100000 + }, + { + "ticker": "AMD", + "wsSymbol": "AMD", + "name": "Advanced Micro Devices", + "group": "Information Technology", + "marketCap": 876235865379 + }, + { + "ticker": "ABNB", + "wsSymbol": "ABNB", + "name": "Airbnb", + "group": "Consumer Discretionary", + "marketCap": 84521036084 + }, + { + "ticker": "ALNY", + "wsSymbol": "ALNY", + "name": "Alnylam Pharmaceuticals", + "group": "Health Care", + "marketCap": 37128511426 + }, + { + "ticker": "GOOGL", + "wsSymbol": "GOOGL", + "name": "Alphabet Inc. (Class A)", + "group": "Information Technology", + "marketCap": 4490912988666 + }, + { + "ticker": "GOOG", + "wsSymbol": "GOOG", + "name": "Alphabet Inc. (Class C)", + "group": "Information Technology", + "marketCap": 4483957521982 + }, + { + "ticker": "AMZN", + "wsSymbol": "AMZN", + "name": "Amazon", + "group": "Consumer Discretionary", + "marketCap": 2628929975064 + }, + { + "ticker": "AEP", + "wsSymbol": "AEP", + "name": "American Electric Power", + "group": "Utilities", + "marketCap": 69476771536 + }, + { + "ticker": "AMGN", + "wsSymbol": "AMGN", + "name": "Amgen", + "group": "Health Care", + "marketCap": 182205513302 + }, + { + "ticker": "ADI", + "wsSymbol": "ADI", + "name": "Analog Devices", + "group": "Information Technology", + "marketCap": 211619835398 + }, + { + "ticker": "AAPL", + "wsSymbol": "AAPL", + "name": "Apple Inc.", + "group": "Information Technology", + "marketCap": 4376978961560 + }, + { + "ticker": "AMAT", + "wsSymbol": "AMAT", + "name": "Applied Materials", + "group": "Information Technology", + "marketCap": 489960303847 + }, + { + "ticker": "APP", + "wsSymbol": "APP", + "name": "AppLovin", + "group": "Information Technology", + "marketCap": 157794152409 + }, + { + "ticker": "ARM", + "wsSymbol": "ARM", + "name": "Arm Holdings", + "group": "Information Technology", + "marketCap": 469377891870 + }, + { + "ticker": "ASML", + "wsSymbol": "ASML", + "name": "ASML Holding", + "group": "Information Technology", + "marketCap": 743732759797 + }, + { + "ticker": "ADSK", + "wsSymbol": "ADSK", + "name": "Autodesk", + "group": "Information Technology", + "marketCap": 40924172549 + }, + { + "ticker": "ADP", + "wsSymbol": "ADP", + "name": "Automatic Data Processing", + "group": "Industrials", + "marketCap": 87305964532 + }, + { + "ticker": "AXON", + "wsSymbol": "AXON", + "name": "Axon Enterprise", + "group": "Industrials", + "marketCap": 34126919402 + }, + { + "ticker": "BKR", + "wsSymbol": "BKR", + "name": "Baker Hughes", + "group": "Energy", + "marketCap": 57946732592 + }, + { + "ticker": "BKNG", + "wsSymbol": "BKNG", + "name": "Booking Holdings", + "group": "Consumer Discretionary", + "marketCap": 133108617736 + }, + { + "ticker": "AVGO", + "wsSymbol": "AVGO", + "name": "Broadcom", + "group": "Information Technology", + "marketCap": 1957030614447 + }, + { + "ticker": "CDNS", + "wsSymbol": "CDNS", + "name": "Cadence Design Systems", + "group": "Information Technology", + "marketCap": 106848360240 + }, + { + "ticker": "CHTR", + "wsSymbol": "CHTR", + "name": "Charter Communications", + "group": "Communication Services", + "marketCap": 15524338106 + }, + { + "ticker": "CTAS", + "wsSymbol": "CTAS", + "name": "Cintas", + "group": "Industrials", + "marketCap": 68363386119 + }, + { + "ticker": "CSCO", + "wsSymbol": "CSCO", + "name": "Cisco", + "group": "Communication Services", + "marketCap": 471159099854 + }, + { + "ticker": "CCEP", + "wsSymbol": "CCEP", + "name": "Coca-Cola Europacific Partners", + "group": "Consumer Staples", + "marketCap": 42918379360 + }, + { + "ticker": "CTSH", + "wsSymbol": "CTSH", + "name": "Cognizant", + "group": "Information Technology", + "marketCap": 20670100000 + }, + { + "ticker": "CMCSA", + "wsSymbol": "CMCSA", + "name": "Comcast", + "group": "Communication Services", + "marketCap": 80125086534 + }, + { + "ticker": "CEG", + "wsSymbol": "CEG", + "name": "Constellation Energy", + "group": "Utilities", + "marketCap": 97867378779 + }, + { + "ticker": "CPRT", + "wsSymbol": "CPRT", + "name": "Copart", + "group": "Consumer Discretionary", + "marketCap": 27987281101 + }, + { + "ticker": "COST", + "wsSymbol": "COST", + "name": "Costco", + "group": "Consumer Discretionary", + "marketCap": 421947908066 + }, + { + "ticker": "CRWD", + "wsSymbol": "CRWD", + "name": "CrowdStrike", + "group": "Information Technology", + "marketCap": 174341262625 + }, + { + "ticker": "CSX", + "wsSymbol": "CSX", + "name": "CSX Corporation", + "group": "Industrials", + "marketCap": 84786875999 + }, + { + "ticker": "DDOG", + "wsSymbol": "DDOG", + "name": "Datadog", + "group": "Information Technology", + "marketCap": 79379122147 + }, + { + "ticker": "DXCM", + "wsSymbol": "DXCM", + "name": "DexCom", + "group": "Health Care", + "marketCap": 27964214643 + }, + { + "ticker": "FANG", + "wsSymbol": "FANG", + "name": "Diamondback Energy", + "group": "Energy", + "marketCap": 51620990000 + }, + { + "ticker": "DASH", + "wsSymbol": "DASH", + "name": "DoorDash", + "group": "Information Technology", + "marketCap": 75579769518 + }, + { + "ticker": "EA", + "wsSymbol": "EA", + "name": "Electronic Arts", + "group": "Consumer Discretionary", + "marketCap": 50689412491 + }, + { + "ticker": "EXC", + "wsSymbol": "EXC", + "name": "Exelon", + "group": "Utilities", + "marketCap": 46873164985 + }, + { + "ticker": "FAST", + "wsSymbol": "FAST", + "name": "Fastenal", + "group": "Industrials", + "marketCap": 52683328949 + }, + { + "ticker": "FER", + "wsSymbol": "FER", + "name": "Ferrovial", + "group": "Industrials", + "marketCap": 49546726786 + }, + { + "ticker": "FTNT", + "wsSymbol": "FTNT", + "name": "Fortinet", + "group": "Information Technology", + "marketCap": 106036201629 + }, + { + "ticker": "GEHC", + "wsSymbol": "GEHC", + "name": "GE HealthCare", + "group": "Health Care", + "marketCap": 28016785100 + }, + { + "ticker": "GILD", + "wsSymbol": "GILD", + "name": "Gilead Sciences", + "group": "Health Care", + "marketCap": 153656687606 + }, + { + "ticker": "HON", + "wsSymbol": "HON", + "name": "Honeywell", + "group": "Industrials", + "marketCap": 145112900782 + }, + { + "ticker": "IDXX", + "wsSymbol": "IDXX", + "name": "Idexx Laboratories", + "group": "Health Care", + "marketCap": 44339343784 + }, + { + "ticker": "INSM", + "wsSymbol": "INSM", + "name": "Insmed Incorporated", + "group": "Health Care", + "marketCap": 20764884806 + }, + { + "ticker": "INTC", + "wsSymbol": "INTC", + "name": "Intel", + "group": "Information Technology", + "marketCap": 673433740000 + }, + { + "ticker": "INTU", + "wsSymbol": "INTU", + "name": "Intuit", + "group": "Information Technology", + "marketCap": 73034379000 + }, + { + "ticker": "ISRG", + "wsSymbol": "ISRG", + "name": "Intuitive Surgical", + "group": "Health Care", + "marketCap": 144066360869 + }, + { + "ticker": "KDP", + "wsSymbol": "KDP", + "name": "Keurig Dr Pepper", + "group": "Consumer Staples", + "marketCap": 41850809328 + }, + { + "ticker": "KLAC", + "wsSymbol": "KLAC", + "name": "KLA Corporation", + "group": "Information Technology", + "marketCap": 339056793508 + }, + { + "ticker": "KHC", + "wsSymbol": "KHC", + "name": "Kraft Heinz", + "group": "Consumer Staples", + "marketCap": 27059445699 + }, + { + "ticker": "LRCX", + "wsSymbol": "LRCX", + "name": "Lam Research", + "group": "Information Technology", + "marketCap": 486522141840 + }, + { + "ticker": "LIN", + "wsSymbol": "LIN", + "name": "Linde plc", + "group": "Materials", + "marketCap": 236791174816 + }, + { + "ticker": "LITE", + "wsSymbol": "LITE", + "name": "Lumentum", + "group": "Information Technology", + "marketCap": 66130000000 + }, + { + "ticker": "MAR", + "wsSymbol": "MAR", + "name": "Marriott International", + "group": "Consumer Discretionary", + "marketCap": 104473432433 + }, + { + "ticker": "MRVL", + "wsSymbol": "MRVL", + "name": "Marvell Technology", + "group": "Information Technology", + "marketCap": 271695384000 + }, + { + "ticker": "MELI", + "wsSymbol": "MELI", + "name": "Mercado Libre", + "group": "Consumer Discretionary", + "marketCap": 82897497147 + }, + { + "ticker": "META", + "wsSymbol": "META", + "name": "Meta Platforms", + "group": "Information Technology", + "marketCap": 1465228699535 + }, + { + "ticker": "MCHP", + "wsSymbol": "MCHP", + "name": "Microchip Technology", + "group": "Information Technology", + "marketCap": 54083249865 + }, + { + "ticker": "MU", + "wsSymbol": "MU", + "name": "Micron Technology", + "group": "Information Technology", + "marketCap": 1278839136493 + }, + { + "ticker": "MSFT", + "wsSymbol": "MSFT", + "name": "Microsoft", + "group": "Information Technology", + "marketCap": 2818348126698 + }, + { + "ticker": "MSTR", + "wsSymbol": "MSTR", + "name": "MicroStrategy", + "group": "Information Technology", + "marketCap": 39954191398 + }, + { + "ticker": "MDLZ", + "wsSymbol": "MDLZ", + "name": "Mondelez International", + "group": "Consumer Staples", + "marketCap": 77173023932 + }, + { + "ticker": "MPWR", + "wsSymbol": "MPWR", + "name": "Monolithic Power Systems", + "group": "Information Technology", + "marketCap": 76824581000 + }, + { + "ticker": "MNST", + "wsSymbol": "MNST", + "name": "Monster Beverage", + "group": "Consumer Staples", + "marketCap": 89331260767 + }, + { + "ticker": "NFLX", + "wsSymbol": "NFLX", + "name": "Netflix, Inc.", + "group": "Consumer Discretionary", + "marketCap": 325831590097 + }, + { + "ticker": "NVDA", + "wsSymbol": "NVDA", + "name": "Nvidia", + "group": "Information Technology", + "marketCap": 5103122490000 + }, + { + "ticker": "NXPI", + "wsSymbol": "NXPI", + "name": "NXP Semiconductors", + "group": "Information Technology", + "marketCap": 79091614918 + }, + { + "ticker": "ORLY", + "wsSymbol": "ORLY", + "name": "O'Reilly Automotive", + "group": "Consumer Discretionary", + "marketCap": 71965617460 + }, + { + "ticker": "ODFL", + "wsSymbol": "ODFL", + "name": "Old Dominion Freight Line", + "group": "Industrials", + "marketCap": 45969309274 + }, + { + "ticker": "PCAR", + "wsSymbol": "PCAR", + "name": "Paccar", + "group": "Consumer Discretionary", + "marketCap": 62601746940 + }, + { + "ticker": "PLTR", + "wsSymbol": "PLTR", + "name": "Palantir Technologies", + "group": "Information Technology", + "marketCap": 307982669685 + }, + { + "ticker": "PANW", + "wsSymbol": "PANW", + "name": "Palo Alto Networks", + "group": "Information Technology", + "marketCap": 234540700000 + }, + { + "ticker": "PAYX", + "wsSymbol": "PAYX", + "name": "Paychex", + "group": "Industrials", + "marketCap": 35198496149 + }, + { + "ticker": "PYPL", + "wsSymbol": "PYPL", + "name": "PayPal", + "group": "Industrials", + "marketCap": 37498304507 + }, + { + "ticker": "PDD", + "wsSymbol": "PDD", + "name": "PDD Holdings", + "group": "Information Technology", + "marketCap": 113245422517 + }, + { + "ticker": "PEP", + "wsSymbol": "PEP", + "name": "PepsiCo", + "group": "Consumer Staples", + "marketCap": 194108436096 + }, + { + "ticker": "QCOM", + "wsSymbol": "QCOM", + "name": "Qualcomm", + "group": "Information Technology", + "marketCap": 238319940000 + }, + { + "ticker": "REGN", + "wsSymbol": "REGN", + "name": "Regeneron Pharmaceuticals", + "group": "Health Care", + "marketCap": 63945519178 + }, + { + "ticker": "ROP", + "wsSymbol": "ROP", + "name": "Roper Technologies", + "group": "Information Technology", + "marketCap": 33328462397 + }, + { + "ticker": "ROST", + "wsSymbol": "ROST", + "name": "Ross Stores", + "group": "Consumer Discretionary", + "marketCap": 74677834493 + }, + { + "ticker": "SNDK", + "wsSymbol": "SNDK", + "name": "Sandisk", + "group": "Information Technology", + "marketCap": 323539098790 + }, + { + "ticker": "STX", + "wsSymbol": "STX", + "name": "Seagate Technology", + "group": "Information Technology", + "marketCap": 242141802107 + }, + { + "ticker": "SHOP", + "wsSymbol": "SHOP", + "name": "Shopify", + "group": "Information Technology", + "marketCap": 141249704298 + }, + { + "ticker": "SBUX", + "wsSymbol": "SBUX", + "name": "Starbucks", + "group": "Consumer Discretionary", + "marketCap": 114710805000 + }, + { + "ticker": "SNPS", + "wsSymbol": "SNPS", + "name": "Synopsys", + "group": "Information Technology", + "marketCap": 87220747331 + }, + { + "ticker": "TMUS", + "wsSymbol": "TMUS", + "name": "T-Mobile US", + "group": "Communication Services", + "marketCap": 196604130937 + }, + { + "ticker": "TTWO", + "wsSymbol": "TTWO", + "name": "Take-Two Interactive", + "group": "Consumer Discretionary", + "marketCap": 44426319123 + }, + { + "ticker": "TSLA", + "wsSymbol": "TSLA", + "name": "Tesla, Inc.", + "group": "Consumer Discretionary", + "marketCap": 1504129853097 + }, + { + "ticker": "TXN", + "wsSymbol": "TXN", + "name": "Texas Instruments", + "group": "Information Technology", + "marketCap": 293832558502 + }, + { + "ticker": "TRI", + "wsSymbol": "TRI", + "name": "Thomson Reuters", + "group": "Information Technology", + "marketCap": 34303221811 + }, + { + "ticker": "VRSK", + "wsSymbol": "VRSK", + "name": "Verisk Analytics", + "group": "Industrials", + "marketCap": 22771595792 + }, + { + "ticker": "VRTX", + "wsSymbol": "VRTX", + "name": "Vertex Pharmaceuticals", + "group": "Health Care", + "marketCap": 114626140480 + }, + { + "ticker": "WMT", + "wsSymbol": "WMT", + "name": "Walmart", + "group": "Consumer Discretionary", + "marketCap": 932527715383 + }, + { + "ticker": "WBD", + "wsSymbol": "WBD", + "name": "Warner Bros. Discovery", + "group": "Consumer Discretionary", + "marketCap": 65686981592 + }, + { + "ticker": "WDC", + "wsSymbol": "WDC", + "name": "Western Digital", + "group": "Information Technology", + "marketCap": 257212146616 + }, + { + "ticker": "WDAY", + "wsSymbol": "WDAY", + "name": "Workday, Inc.", + "group": "Information Technology", + "marketCap": 28878608899 + }, + { + "ticker": "XEL", + "wsSymbol": "XEL", + "name": "Xcel Energy", + "group": "Utilities", + "marketCap": 48324706640 + }, + { + "ticker": "ZS", + "wsSymbol": "ZS", + "name": "Zscaler", + "group": "Information Technology", + "marketCap": 20189434196 + } + ] +} diff --git a/examples/rest/massive-heatmap/universes/sp500.json b/examples/rest/massive-heatmap/universes/sp500.json new file mode 100644 index 0000000..f821d7e --- /dev/null +++ b/examples/rest/massive-heatmap/universes/sp500.json @@ -0,0 +1,3514 @@ +{ + "id": "sp500", + "segment": "stocks", + "label": "S&P 500", + "constituents": [ + { + "ticker": "MMM", + "wsSymbol": "MMM", + "name": "3M", + "group": "Industrials", + "marketCap": 83763702117 + }, + { + "ticker": "AOS", + "wsSymbol": "AOS", + "name": "A. O. Smith", + "group": "Industrials", + "marketCap": 8024382489 + }, + { + "ticker": "ABT", + "wsSymbol": "ABT", + "name": "Abbott Laboratories", + "group": "Health Care", + "marketCap": 153993700945 + }, + { + "ticker": "ABBV", + "wsSymbol": "ABBV", + "name": "AbbVie", + "group": "Health Care", + "marketCap": 382492977818 + }, + { + "ticker": "ACN", + "wsSymbol": "ACN", + "name": "Accenture", + "group": "Information Technology", + "marketCap": 78316351110 + }, + { + "ticker": "ADBE", + "wsSymbol": "ADBE", + "name": "Adobe Inc.", + "group": "Information Technology", + "marketCap": 77576100000 + }, + { + "ticker": "AMD", + "wsSymbol": "AMD", + "name": "Advanced Micro Devices", + "group": "Information Technology", + "marketCap": 876235865379 + }, + { + "ticker": "AES", + "wsSymbol": "AES", + "name": "AES Corporation", + "group": "Utilities", + "marketCap": 10426365764 + }, + { + "ticker": "AFL", + "wsSymbol": "AFL", + "name": "Aflac", + "group": "Financials", + "marketCap": 58772486865 + }, + { + "ticker": "A", + "wsSymbol": "A", + "name": "Agilent Technologies", + "group": "Health Care", + "marketCap": 35885802805 + }, + { + "ticker": "APD", + "wsSymbol": "APD", + "name": "Air Products", + "group": "Materials", + "marketCap": 62397355024 + }, + { + "ticker": "ABNB", + "wsSymbol": "ABNB", + "name": "Airbnb", + "group": "Consumer Discretionary", + "marketCap": 84521036084 + }, + { + "ticker": "AKAM", + "wsSymbol": "AKAM", + "name": "Akamai Technologies", + "group": "Information Technology", + "marketCap": 18160096684 + }, + { + "ticker": "ALB", + "wsSymbol": "ALB", + "name": "Albemarle Corporation", + "group": "Materials", + "marketCap": 18910821288 + }, + { + "ticker": "ARE", + "wsSymbol": "ARE", + "name": "Alexandria Real Estate Equities", + "group": "Real Estate", + "marketCap": 8892971564 + }, + { + "ticker": "ALGN", + "wsSymbol": "ALGN", + "name": "Align Technology", + "group": "Health Care", + "marketCap": 13040228564 + }, + { + "ticker": "ALLE", + "wsSymbol": "ALLE", + "name": "Allegion", + "group": "Industrials", + "marketCap": 11478452954 + }, + { + "ticker": "LNT", + "wsSymbol": "LNT", + "name": "Alliant Energy", + "group": "Utilities", + "marketCap": 18854223701 + }, + { + "ticker": "ALL", + "wsSymbol": "ALL", + "name": "Allstate", + "group": "Financials", + "marketCap": 56933787088 + }, + { + "ticker": "GOOGL", + "wsSymbol": "GOOGL", + "name": "Alphabet Inc. (Class A)", + "group": "Communication Services", + "marketCap": 4490912988666 + }, + { + "ticker": "GOOG", + "wsSymbol": "GOOG", + "name": "Alphabet Inc. (Class C)", + "group": "Communication Services", + "marketCap": 4483957521982 + }, + { + "ticker": "MO", + "wsSymbol": "MO", + "name": "Altria", + "group": "Consumer Staples", + "marketCap": 115422882163 + }, + { + "ticker": "AMZN", + "wsSymbol": "AMZN", + "name": "Amazon", + "group": "Consumer Discretionary", + "marketCap": 2628929975064 + }, + { + "ticker": "AMCR", + "wsSymbol": "AMCR", + "name": "Amcor", + "group": "Materials", + "marketCap": 18993160945 + }, + { + "ticker": "AEE", + "wsSymbol": "AEE", + "name": "Ameren", + "group": "Utilities", + "marketCap": 30074598111 + }, + { + "ticker": "AEP", + "wsSymbol": "AEP", + "name": "American Electric Power", + "group": "Utilities", + "marketCap": 69476771536 + }, + { + "ticker": "AXP", + "wsSymbol": "AXP", + "name": "American Express", + "group": "Financials", + "marketCap": 230626337734 + }, + { + "ticker": "AIG", + "wsSymbol": "AIG", + "name": "American International Group", + "group": "Financials", + "marketCap": 39245864478 + }, + { + "ticker": "AMT", + "wsSymbol": "AMT", + "name": "American Tower", + "group": "Real Estate", + "marketCap": 82020474797 + }, + { + "ticker": "AWK", + "wsSymbol": "AWK", + "name": "American Water Works", + "group": "Utilities", + "marketCap": 24423734761 + }, + { + "ticker": "AMP", + "wsSymbol": "AMP", + "name": "Ameriprise Financial", + "group": "Financials", + "marketCap": 42020593974 + }, + { + "ticker": "AME", + "wsSymbol": "AME", + "name": "Ametek", + "group": "Industrials", + "marketCap": 54417376735 + }, + { + "ticker": "AMGN", + "wsSymbol": "AMGN", + "name": "Amgen", + "group": "Health Care", + "marketCap": 182205513302 + }, + { + "ticker": "APH", + "wsSymbol": "APH", + "name": "Amphenol", + "group": "Information Technology", + "marketCap": 201709239602 + }, + { + "ticker": "ADI", + "wsSymbol": "ADI", + "name": "Analog Devices", + "group": "Information Technology", + "marketCap": 211619835398 + }, + { + "ticker": "AON", + "wsSymbol": "AON", + "name": "Aon plc", + "group": "Financials", + "marketCap": 67861939775 + }, + { + "ticker": "APA", + "wsSymbol": "APA", + "name": "APA Corporation", + "group": "Energy", + "marketCap": 11675121598 + }, + { + "ticker": "APO", + "wsSymbol": "APO", + "name": "Apollo Global Management", + "group": "Financials", + "marketCap": 79271158038 + }, + { + "ticker": "AAPL", + "wsSymbol": "AAPL", + "name": "Apple Inc.", + "group": "Information Technology", + "marketCap": 4376978961560 + }, + { + "ticker": "AMAT", + "wsSymbol": "AMAT", + "name": "Applied Materials", + "group": "Information Technology", + "marketCap": 489960303847 + }, + { + "ticker": "APP", + "wsSymbol": "APP", + "name": "AppLovin", + "group": "Information Technology", + "marketCap": 157794152409 + }, + { + "ticker": "APTV", + "wsSymbol": "APTV", + "name": "Aptiv", + "group": "Consumer Discretionary", + "marketCap": 13475995159 + }, + { + "ticker": "ACGL", + "wsSymbol": "ACGL", + "name": "Arch Capital Group", + "group": "Financials", + "marketCap": 31857342634 + }, + { + "ticker": "ADM", + "wsSymbol": "ADM", + "name": "Archer Daniels Midland", + "group": "Consumer Staples", + "marketCap": 36194935178 + }, + { + "ticker": "ARES", + "wsSymbol": "ARES", + "name": "Ares Management", + "group": "Financials", + "marketCap": 29168541061 + }, + { + "ticker": "ANET", + "wsSymbol": "ANET", + "name": "Arista Networks", + "group": "Information Technology", + "marketCap": 213643278545 + }, + { + "ticker": "AJG", + "wsSymbol": "AJG", + "name": "Arthur J. Gallagher & Co.", + "group": "Financials", + "marketCap": 55001004520 + }, + { + "ticker": "AIZ", + "wsSymbol": "AIZ", + "name": "Assurant", + "group": "Financials", + "marketCap": 12875448951 + }, + { + "ticker": "T", + "wsSymbol": "T", + "name": "AT&T", + "group": "Communication Services", + "marketCap": 152932937758 + }, + { + "ticker": "ATO", + "wsSymbol": "ATO", + "name": "Atmos Energy", + "group": "Utilities", + "marketCap": 28394730920 + }, + { + "ticker": "ADSK", + "wsSymbol": "ADSK", + "name": "Autodesk", + "group": "Information Technology", + "marketCap": 40924172549 + }, + { + "ticker": "ADP", + "wsSymbol": "ADP", + "name": "Automatic Data Processing", + "group": "Industrials", + "marketCap": 87305964532 + }, + { + "ticker": "AZO", + "wsSymbol": "AZO", + "name": "AutoZone", + "group": "Consumer Discretionary", + "marketCap": 50028723890 + }, + { + "ticker": "AVB", + "wsSymbol": "AVB", + "name": "AvalonBay Communities", + "group": "Real Estate", + "marketCap": 25156755275 + }, + { + "ticker": "AVY", + "wsSymbol": "AVY", + "name": "Avery Dennison", + "group": "Materials", + "marketCap": 12137494292 + }, + { + "ticker": "AXON", + "wsSymbol": "AXON", + "name": "Axon Enterprise", + "group": "Industrials", + "marketCap": 34126919402 + }, + { + "ticker": "BKR", + "wsSymbol": "BKR", + "name": "Baker Hughes", + "group": "Energy", + "marketCap": 57946732592 + }, + { + "ticker": "BALL", + "wsSymbol": "BALL", + "name": "Ball Corporation", + "group": "Materials", + "marketCap": 15367748615 + }, + { + "ticker": "BAC", + "wsSymbol": "BAC", + "name": "Bank of America", + "group": "Financials", + "marketCap": 398828394586 + }, + { + "ticker": "BAX", + "wsSymbol": "BAX", + "name": "Baxter International", + "group": "Health Care", + "marketCap": 10272568569 + }, + { + "ticker": "BDX", + "wsSymbol": "BDX", + "name": "Becton Dickinson", + "group": "Health Care", + "marketCap": 39672310679 + }, + { + "ticker": "BRK.B", + "wsSymbol": "BRK.B", + "name": "Berkshire Hathaway", + "group": "Financials", + "marketCap": 1055693659480 + }, + { + "ticker": "BBY", + "wsSymbol": "BBY", + "name": "Best Buy", + "group": "Consumer Discretionary", + "marketCap": 15750627326 + }, + { + "ticker": "TECH", + "wsSymbol": "TECH", + "name": "Bio-Techne", + "group": "Health Care", + "marketCap": 9071593433 + }, + { + "ticker": "BIIB", + "wsSymbol": "BIIB", + "name": "Biogen", + "group": "Health Care", + "marketCap": 29022504460 + }, + { + "ticker": "BLK", + "wsSymbol": "BLK", + "name": "BlackRock", + "group": "Financials", + "marketCap": 163009659509 + }, + { + "ticker": "BX", + "wsSymbol": "BX", + "name": "Blackstone Inc.", + "group": "Financials", + "marketCap": 151211236752 + }, + { + "ticker": "XYZ", + "wsSymbol": "XYZ", + "name": "Block, Inc.", + "group": "Financials", + "marketCap": 44507635180 + }, + { + "ticker": "BNY", + "wsSymbol": "BNY", + "name": "BNY Mellon", + "group": "Financials", + "marketCap": 98584622233 + }, + { + "ticker": "BA", + "wsSymbol": "BA", + "name": "Boeing", + "group": "Industrials", + "marketCap": 175570695160 + }, + { + "ticker": "BKNG", + "wsSymbol": "BKNG", + "name": "Booking Holdings", + "group": "Consumer Discretionary", + "marketCap": 133108617736 + }, + { + "ticker": "BSX", + "wsSymbol": "BSX", + "name": "Boston Scientific", + "group": "Health Care", + "marketCap": 67317026057 + }, + { + "ticker": "BMY", + "wsSymbol": "BMY", + "name": "Bristol Myers Squibb", + "group": "Health Care", + "marketCap": 110271847338 + }, + { + "ticker": "AVGO", + "wsSymbol": "AVGO", + "name": "Broadcom", + "group": "Information Technology", + "marketCap": 1957030614447 + }, + { + "ticker": "BR", + "wsSymbol": "BR", + "name": "Broadridge Financial Solutions", + "group": "Industrials", + "marketCap": 15914581667 + }, + { + "ticker": "BRO", + "wsSymbol": "BRO", + "name": "Brown & Brown", + "group": "Financials", + "marketCap": 20031817403 + }, + { + "ticker": "BF.B", + "wsSymbol": "BF.B", + "name": "Brown-Forman", + "group": "Consumer Staples", + "marketCap": 12224144020 + }, + { + "ticker": "BLDR", + "wsSymbol": "BLDR", + "name": "Builders FirstSource", + "group": "Industrials", + "marketCap": 8668250407 + }, + { + "ticker": "BG", + "wsSymbol": "BG", + "name": "Bunge Global", + "group": "Consumer Staples", + "marketCap": 21842559387 + }, + { + "ticker": "BXP", + "wsSymbol": "BXP", + "name": "BXP, Inc.", + "group": "Real Estate", + "marketCap": 10318187198 + }, + { + "ticker": "CHRW", + "wsSymbol": "CHRW", + "name": "C.H. Robinson", + "group": "Industrials", + "marketCap": 21811227692 + }, + { + "ticker": "CDNS", + "wsSymbol": "CDNS", + "name": "Cadence Design Systems", + "group": "Information Technology", + "marketCap": 106848360240 + }, + { + "ticker": "CPT", + "wsSymbol": "CPT", + "name": "Camden Property Trust", + "group": "Real Estate", + "marketCap": 10799884058 + }, + { + "ticker": "COF", + "wsSymbol": "COF", + "name": "Capital One", + "group": "Financials", + "marketCap": 125410664558 + }, + { + "ticker": "CAH", + "wsSymbol": "CAH", + "name": "Cardinal Health", + "group": "Health Care", + "marketCap": 51939832463 + }, + { + "ticker": "CCL", + "wsSymbol": "CCL", + "name": "Carnival Corporation", + "group": "Consumer Discretionary", + "marketCap": 42759258742 + }, + { + "ticker": "CARR", + "wsSymbol": "CARR", + "name": "Carrier Global", + "group": "Industrials", + "marketCap": 59643980176 + }, + { + "ticker": "CVNA", + "wsSymbol": "CVNA", + "name": "Carvana", + "group": "Consumer Discretionary", + "marketCap": 47676154906 + }, + { + "ticker": "CASY", + "wsSymbol": "CASY", + "name": "Casey's", + "group": "Consumer Staples", + "marketCap": 31128743018 + }, + { + "ticker": "CAT", + "wsSymbol": "CAT", + "name": "Caterpillar Inc.", + "group": "Industrials", + "marketCap": 454060788681 + }, + { + "ticker": "CBOE", + "wsSymbol": "CBOE", + "name": "Cboe Global Markets", + "group": "Financials", + "marketCap": 26069257594 + }, + { + "ticker": "CBRE", + "wsSymbol": "CBRE", + "name": "CBRE Group", + "group": "Real Estate", + "marketCap": 38520020967 + }, + { + "ticker": "CDW", + "wsSymbol": "CDW", + "name": "CDW Corporation", + "group": "Information Technology", + "marketCap": 16399710505 + }, + { + "ticker": "COR", + "wsSymbol": "COR", + "name": "Cencora", + "group": "Health Care", + "marketCap": 52887532940 + }, + { + "ticker": "CNC", + "wsSymbol": "CNC", + "name": "Centene Corporation", + "group": "Health Care", + "marketCap": 30131492940 + }, + { + "ticker": "CNP", + "wsSymbol": "CNP", + "name": "CenterPoint Energy", + "group": "Utilities", + "marketCap": 28011537134 + }, + { + "ticker": "CF", + "wsSymbol": "CF", + "name": "CF Industries", + "group": "Materials", + "marketCap": 15813061173 + }, + { + "ticker": "CRL", + "wsSymbol": "CRL", + "name": "Charles River Laboratories", + "group": "Health Care", + "marketCap": 8911030050 + }, + { + "ticker": "SCHW", + "wsSymbol": "SCHW", + "name": "Charles Schwab Corporation", + "group": "Financials", + "marketCap": 159478725992 + }, + { + "ticker": "CHTR", + "wsSymbol": "CHTR", + "name": "Charter Communications", + "group": "Communication Services", + "marketCap": 15524338106 + }, + { + "ticker": "CVX", + "wsSymbol": "CVX", + "name": "Chevron Corporation", + "group": "Energy", + "marketCap": 345801114207 + }, + { + "ticker": "CMG", + "wsSymbol": "CMG", + "name": "Chipotle Mexican Grill", + "group": "Consumer Discretionary", + "marketCap": 41676027660 + }, + { + "ticker": "CB", + "wsSymbol": "CB", + "name": "Chubb Limited", + "group": "Financials", + "marketCap": 125433958280 + }, + { + "ticker": "CHD", + "wsSymbol": "CHD", + "name": "Church & Dwight", + "group": "Consumer Staples", + "marketCap": 22658916659 + }, + { + "ticker": "CIEN", + "wsSymbol": "CIEN", + "name": "Ciena", + "group": "Information Technology", + "marketCap": 60615792259 + }, + { + "ticker": "CI", + "wsSymbol": "CI", + "name": "Cigna", + "group": "Health Care", + "marketCap": 73875964465 + }, + { + "ticker": "CINF", + "wsSymbol": "CINF", + "name": "Cincinnati Financial", + "group": "Financials", + "marketCap": 26327683488 + }, + { + "ticker": "CTAS", + "wsSymbol": "CTAS", + "name": "Cintas", + "group": "Industrials", + "marketCap": 68363386119 + }, + { + "ticker": "CSCO", + "wsSymbol": "CSCO", + "name": "Cisco", + "group": "Information Technology", + "marketCap": 471159099854 + }, + { + "ticker": "C", + "wsSymbol": "C", + "name": "Citigroup", + "group": "Financials", + "marketCap": 243999839182 + }, + { + "ticker": "CFG", + "wsSymbol": "CFG", + "name": "Citizens Financial Group", + "group": "Financials", + "marketCap": 28366920669 + }, + { + "ticker": "CLX", + "wsSymbol": "CLX", + "name": "Clorox", + "group": "Consumer Staples", + "marketCap": 11584265426 + }, + { + "ticker": "CME", + "wsSymbol": "CME", + "name": "CME Group", + "group": "Financials", + "marketCap": 89022699391 + }, + { + "ticker": "CMS", + "wsSymbol": "CMS", + "name": "CMS Energy", + "group": "Utilities", + "marketCap": 22668520395 + }, + { + "ticker": "KO", + "wsSymbol": "KO", + "name": "Coca-Cola Company (The)", + "group": "Consumer Staples", + "marketCap": 341574079165 + }, + { + "ticker": "CTSH", + "wsSymbol": "CTSH", + "name": "Cognizant", + "group": "Information Technology", + "marketCap": 20670100000 + }, + { + "ticker": "COHR", + "wsSymbol": "COHR", + "name": "Coherent Corp.", + "group": "Information Technology", + "marketCap": 76215210282 + }, + { + "ticker": "COIN", + "wsSymbol": "COIN", + "name": "Coinbase", + "group": "Financials", + "marketCap": 43012604657 + }, + { + "ticker": "CL", + "wsSymbol": "CL", + "name": "Colgate-Palmolive", + "group": "Consumer Staples", + "marketCap": 71600939459 + }, + { + "ticker": "CMCSA", + "wsSymbol": "CMCSA", + "name": "Comcast", + "group": "Communication Services", + "marketCap": 80125086534 + }, + { + "ticker": "FIX", + "wsSymbol": "FIX", + "name": "Comfort Systems USA", + "group": "Industrials", + "marketCap": 69221288515 + }, + { + "ticker": "CAG", + "wsSymbol": "CAG", + "name": "Conagra Brands", + "group": "Consumer Staples", + "marketCap": 6315370459 + }, + { + "ticker": "COP", + "wsSymbol": "COP", + "name": "ConocoPhillips", + "group": "Energy", + "marketCap": 131258996314 + }, + { + "ticker": "ED", + "wsSymbol": "ED", + "name": "Consolidated Edison", + "group": "Utilities", + "marketCap": 39196819424 + }, + { + "ticker": "STZ", + "wsSymbol": "STZ", + "name": "Constellation Brands", + "group": "Consumer Staples", + "marketCap": 24211356186 + }, + { + "ticker": "CEG", + "wsSymbol": "CEG", + "name": "Constellation Energy", + "group": "Utilities", + "marketCap": 97867378779 + }, + { + "ticker": "COO", + "wsSymbol": "COO", + "name": "Cooper Companies (The)", + "group": "Health Care", + "marketCap": 12854468823 + }, + { + "ticker": "CPRT", + "wsSymbol": "CPRT", + "name": "Copart", + "group": "Industrials", + "marketCap": 27987281101 + }, + { + "ticker": "GLW", + "wsSymbol": "GLW", + "name": "Corning Inc.", + "group": "Information Technology", + "marketCap": 167755524849 + }, + { + "ticker": "CPAY", + "wsSymbol": "CPAY", + "name": "Corpay", + "group": "Financials", + "marketCap": 22568163392 + }, + { + "ticker": "CTVA", + "wsSymbol": "CTVA", + "name": "Corteva", + "group": "Materials", + "marketCap": 52562563800 + }, + { + "ticker": "CSGP", + "wsSymbol": "CSGP", + "name": "CoStar Group", + "group": "Real Estate", + "marketCap": 12299674136 + }, + { + "ticker": "COST", + "wsSymbol": "COST", + "name": "Costco", + "group": "Consumer Staples", + "marketCap": 421947908066 + }, + { + "ticker": "CRH", + "wsSymbol": "CRH", + "name": "CRH plc", + "group": "Materials", + "marketCap": 74330592028 + }, + { + "ticker": "CRWD", + "wsSymbol": "CRWD", + "name": "CrowdStrike", + "group": "Information Technology", + "marketCap": 174341262625 + }, + { + "ticker": "CCI", + "wsSymbol": "CCI", + "name": "Crown Castle", + "group": "Real Estate", + "marketCap": 35810885205 + }, + { + "ticker": "CSX", + "wsSymbol": "CSX", + "name": "CSX Corporation", + "group": "Industrials", + "marketCap": 84786875999 + }, + { + "ticker": "CMI", + "wsSymbol": "CMI", + "name": "Cummins", + "group": "Industrials", + "marketCap": 98918842570 + }, + { + "ticker": "CVS", + "wsSymbol": "CVS", + "name": "CVS Health", + "group": "Health Care", + "marketCap": 125449172824 + }, + { + "ticker": "DHR", + "wsSymbol": "DHR", + "name": "Danaher Corporation", + "group": "Health Care", + "marketCap": 125395721986 + }, + { + "ticker": "DRI", + "wsSymbol": "DRI", + "name": "Darden Restaurants", + "group": "Consumer Discretionary", + "marketCap": 24447496390 + }, + { + "ticker": "DDOG", + "wsSymbol": "DDOG", + "name": "Datadog", + "group": "Information Technology", + "marketCap": 79379122147 + }, + { + "ticker": "DVA", + "wsSymbol": "DVA", + "name": "DaVita", + "group": "Health Care", + "marketCap": 13344703350 + }, + { + "ticker": "DECK", + "wsSymbol": "DECK", + "name": "Deckers Brands", + "group": "Consumer Discretionary", + "marketCap": 15153301218 + }, + { + "ticker": "DE", + "wsSymbol": "DE", + "name": "Deere & Company", + "group": "Industrials", + "marketCap": 159057928307 + }, + { + "ticker": "DELL", + "wsSymbol": "DELL", + "name": "Dell Technologies", + "group": "Information Technology", + "marketCap": 264595324266 + }, + { + "ticker": "DAL", + "wsSymbol": "DAL", + "name": "Delta Air Lines", + "group": "Industrials", + "marketCap": 55305783541 + }, + { + "ticker": "DVN", + "wsSymbol": "DVN", + "name": "Devon Energy", + "group": "Energy", + "marketCap": 48581338867 + }, + { + "ticker": "DXCM", + "wsSymbol": "DXCM", + "name": "Dexcom", + "group": "Health Care", + "marketCap": 27964214643 + }, + { + "ticker": "FANG", + "wsSymbol": "FANG", + "name": "Diamondback Energy", + "group": "Energy", + "marketCap": 51620990000 + }, + { + "ticker": "DLR", + "wsSymbol": "DLR", + "name": "Digital Realty", + "group": "Real Estate", + "marketCap": 66117052070 + }, + { + "ticker": "DG", + "wsSymbol": "DG", + "name": "Dollar General", + "group": "Consumer Staples", + "marketCap": 25025555102 + }, + { + "ticker": "DLTR", + "wsSymbol": "DLTR", + "name": "Dollar Tree", + "group": "Consumer Staples", + "marketCap": 21456292750 + }, + { + "ticker": "D", + "wsSymbol": "D", + "name": "Dominion Energy", + "group": "Utilities", + "marketCap": 60167449030 + }, + { + "ticker": "DPZ", + "wsSymbol": "DPZ", + "name": "Domino's", + "group": "Consumer Discretionary", + "marketCap": 10393336519 + }, + { + "ticker": "DASH", + "wsSymbol": "DASH", + "name": "DoorDash", + "group": "Consumer Discretionary", + "marketCap": 75579769518 + }, + { + "ticker": "DOV", + "wsSymbol": "DOV", + "name": "Dover Corporation", + "group": "Industrials", + "marketCap": 30106252328 + }, + { + "ticker": "DOW", + "wsSymbol": "DOW", + "name": "Dow Inc.", + "group": "Materials", + "marketCap": 22869125828 + }, + { + "ticker": "DHI", + "wsSymbol": "DHI", + "name": "D. R. Horton", + "group": "Consumer Discretionary", + "marketCap": 44751698728 + }, + { + "ticker": "DTE", + "wsSymbol": "DTE", + "name": "DTE Energy", + "group": "Utilities", + "marketCap": 30696657866 + }, + { + "ticker": "DUK", + "wsSymbol": "DUK", + "name": "Duke Energy", + "group": "Utilities", + "marketCap": 96560787438 + }, + { + "ticker": "DD", + "wsSymbol": "DD", + "name": "DuPont", + "group": "Materials", + "marketCap": 19325326817 + }, + { + "ticker": "ETN", + "wsSymbol": "ETN", + "name": "Eaton Corporation", + "group": "Industrials", + "marketCap": 163773291000 + }, + { + "ticker": "EBAY", + "wsSymbol": "EBAY", + "name": "eBay Inc.", + "group": "Consumer Discretionary", + "marketCap": 48058560000 + }, + { + "ticker": "SATS", + "wsSymbol": "SATS", + "name": "EchoStar", + "group": "Communication Services", + "marketCap": 31638938485 + }, + { + "ticker": "ECL", + "wsSymbol": "ECL", + "name": "Ecolab", + "group": "Materials", + "marketCap": 75740460538 + }, + { + "ticker": "EIX", + "wsSymbol": "EIX", + "name": "Edison International", + "group": "Utilities", + "marketCap": 27662836418 + }, + { + "ticker": "EW", + "wsSymbol": "EW", + "name": "Edwards Lifesciences", + "group": "Health Care", + "marketCap": 50301888000 + }, + { + "ticker": "EA", + "wsSymbol": "EA", + "name": "Electronic Arts", + "group": "Communication Services", + "marketCap": 50689412491 + }, + { + "ticker": "ELV", + "wsSymbol": "ELV", + "name": "Elevance Health", + "group": "Health Care", + "marketCap": 84367584630 + }, + { + "ticker": "EME", + "wsSymbol": "EME", + "name": "Emcor", + "group": "Industrials", + "marketCap": 37178292172 + }, + { + "ticker": "EMR", + "wsSymbol": "EMR", + "name": "Emerson Electric", + "group": "Industrials", + "marketCap": 84384666000 + }, + { + "ticker": "ETR", + "wsSymbol": "ETR", + "name": "Entergy", + "group": "Utilities", + "marketCap": 50875807570 + }, + { + "ticker": "EOG", + "wsSymbol": "EOG", + "name": "EOG Resources", + "group": "Energy", + "marketCap": 69231056329 + }, + { + "ticker": "EQT", + "wsSymbol": "EQT", + "name": "EQT Corporation", + "group": "Energy", + "marketCap": 31724244160 + }, + { + "ticker": "EFX", + "wsSymbol": "EFX", + "name": "Equifax", + "group": "Industrials", + "marketCap": 18328815917 + }, + { + "ticker": "EQIX", + "wsSymbol": "EQIX", + "name": "Equinix", + "group": "Real Estate", + "marketCap": 107716417423 + }, + { + "ticker": "EQR", + "wsSymbol": "EQR", + "name": "Equity Residential", + "group": "Real Estate", + "marketCap": 24015301309 + }, + { + "ticker": "ERIE", + "wsSymbol": "ERIE", + "name": "Erie Indemnity", + "group": "Financials", + "marketCap": 11563381410 + }, + { + "ticker": "ESS", + "wsSymbol": "ESS", + "name": "Essex Property Trust", + "group": "Real Estate", + "marketCap": 17589080582 + }, + { + "ticker": "EL", + "wsSymbol": "EL", + "name": "Estee Lauder Companies (The)", + "group": "Consumer Staples", + "marketCap": 30683826741 + }, + { + "ticker": "EG", + "wsSymbol": "EG", + "name": "Everest Group", + "group": "Financials", + "marketCap": 13281416779 + }, + { + "ticker": "EVRG", + "wsSymbol": "EVRG", + "name": "Evergy", + "group": "Utilities", + "marketCap": 19017324562 + }, + { + "ticker": "ES", + "wsSymbol": "ES", + "name": "Eversource Energy", + "group": "Utilities", + "marketCap": 26171408940 + }, + { + "ticker": "EXC", + "wsSymbol": "EXC", + "name": "Exelon", + "group": "Utilities", + "marketCap": 46873164985 + }, + { + "ticker": "EXE", + "wsSymbol": "EXE", + "name": "Expand Energy", + "group": "Energy", + "marketCap": 20808125286 + }, + { + "ticker": "EXPE", + "wsSymbol": "EXPE", + "name": "Expedia Group", + "group": "Consumer Discretionary", + "marketCap": 28913318349 + }, + { + "ticker": "EXPD", + "wsSymbol": "EXPD", + "name": "Expeditors International", + "group": "Industrials", + "marketCap": 21099225576 + }, + { + "ticker": "EXR", + "wsSymbol": "EXR", + "name": "Extra Space Storage", + "group": "Real Estate", + "marketCap": 30702551829 + }, + { + "ticker": "XOM", + "wsSymbol": "XOM", + "name": "ExxonMobil", + "group": "Energy", + "marketCap": 571215168395 + }, + { + "ticker": "FFIV", + "wsSymbol": "FFIV", + "name": "F5, Inc.", + "group": "Information Technology", + "marketCap": 21749055526 + }, + { + "ticker": "FDS", + "wsSymbol": "FDS", + "name": "FactSet", + "group": "Financials", + "marketCap": 8061844094 + }, + { + "ticker": "FICO", + "wsSymbol": "FICO", + "name": "Fair Isaac", + "group": "Information Technology", + "marketCap": 25428364611 + }, + { + "ticker": "FAST", + "wsSymbol": "FAST", + "name": "Fastenal", + "group": "Industrials", + "marketCap": 52683328949 + }, + { + "ticker": "FRT", + "wsSymbol": "FRT", + "name": "Federal Realty Investment Trust", + "group": "Real Estate", + "marketCap": 10400395427 + }, + { + "ticker": "FDX", + "wsSymbol": "FDX", + "name": "FedEx", + "group": "Industrials", + "marketCap": 77833523807 + }, + { + "ticker": "FIS", + "wsSymbol": "FIS", + "name": "Fidelity National Information Services", + "group": "Financials", + "marketCap": 19749952360 + }, + { + "ticker": "FITB", + "wsSymbol": "FITB", + "name": "Fifth Third Bancorp", + "group": "Financials", + "marketCap": 47798871042 + }, + { + "ticker": "FSLR", + "wsSymbol": "FSLR", + "name": "First Solar", + "group": "Information Technology", + "marketCap": 27690731645 + }, + { + "ticker": "FE", + "wsSymbol": "FE", + "name": "FirstEnergy", + "group": "Utilities", + "marketCap": 26868128079 + }, + { + "ticker": "FI", + "wsSymbol": "FI", + "name": "Fiserv", + "group": "Financials", + "marketCap": 43012604657 + }, + { + "ticker": "FLEX", + "wsSymbol": "FLEX", + "name": "Flex Ltd.", + "group": "Information Technology", + "marketCap": 54081045214 + }, + { + "ticker": "F", + "wsSymbol": "F", + "name": "Ford Motor Company", + "group": "Consumer Discretionary", + "marketCap": 56024778378 + }, + { + "ticker": "FTNT", + "wsSymbol": "FTNT", + "name": "Fortinet", + "group": "Information Technology", + "marketCap": 106036201629 + }, + { + "ticker": "FTV", + "wsSymbol": "FTV", + "name": "Fortive", + "group": "Industrials", + "marketCap": 18605668112 + }, + { + "ticker": "FOXA", + "wsSymbol": "FOXA", + "name": "Fox Corporation (Class A)", + "group": "Communication Services", + "marketCap": 21931411054 + }, + { + "ticker": "FOX", + "wsSymbol": "FOX", + "name": "Fox Corporation (Class B)", + "group": "Communication Services", + "marketCap": 19714335611 + }, + { + "ticker": "BEN", + "wsSymbol": "BEN", + "name": "Franklin Resources", + "group": "Financials", + "marketCap": 17174008204 + }, + { + "ticker": "FCX", + "wsSymbol": "FCX", + "name": "Freeport-McMoRan", + "group": "Materials", + "marketCap": 98731611528 + }, + { + "ticker": "GRMN", + "wsSymbol": "GRMN", + "name": "Garmin", + "group": "Consumer Discretionary", + "marketCap": 45166923445 + }, + { + "ticker": "IT", + "wsSymbol": "IT", + "name": "Gartner", + "group": "Information Technology", + "marketCap": 8535696329 + }, + { + "ticker": "GE", + "wsSymbol": "GE", + "name": "GE Aerospace", + "group": "Industrials", + "marketCap": 373139128725 + }, + { + "ticker": "GEHC", + "wsSymbol": "GEHC", + "name": "GE HealthCare", + "group": "Health Care", + "marketCap": 28016785100 + }, + { + "ticker": "GEV", + "wsSymbol": "GEV", + "name": "GE Vernova", + "group": "Industrials", + "marketCap": 298206640051 + }, + { + "ticker": "GEN", + "wsSymbol": "GEN", + "name": "Gen Digital", + "group": "Information Technology", + "marketCap": 14566850590 + }, + { + "ticker": "GNRC", + "wsSymbol": "GNRC", + "name": "Generac", + "group": "Industrials", + "marketCap": 16433192301 + }, + { + "ticker": "GD", + "wsSymbol": "GD", + "name": "General Dynamics", + "group": "Industrials", + "marketCap": 94653269752 + }, + { + "ticker": "GIS", + "wsSymbol": "GIS", + "name": "General Mills", + "group": "Consumer Staples", + "marketCap": 17835626306 + }, + { + "ticker": "GM", + "wsSymbol": "GM", + "name": "General Motors", + "group": "Consumer Discretionary", + "marketCap": 71493033312 + }, + { + "ticker": "GPC", + "wsSymbol": "GPC", + "name": "Genuine Parts Company", + "group": "Consumer Discretionary", + "marketCap": 14959788042 + }, + { + "ticker": "GILD", + "wsSymbol": "GILD", + "name": "Gilead Sciences", + "group": "Health Care", + "marketCap": 153656687606 + }, + { + "ticker": "GPN", + "wsSymbol": "GPN", + "name": "Global Payments", + "group": "Financials", + "marketCap": 18294589748 + }, + { + "ticker": "GL", + "wsSymbol": "GL", + "name": "Globe Life", + "group": "Financials", + "marketCap": 13258619388 + }, + { + "ticker": "GDDY", + "wsSymbol": "GDDY", + "name": "GoDaddy", + "group": "Information Technology", + "marketCap": 10200854613 + }, + { + "ticker": "GS", + "wsSymbol": "GS", + "name": "Goldman Sachs", + "group": "Financials", + "marketCap": 323493337572 + }, + { + "ticker": "HAL", + "wsSymbol": "HAL", + "name": "Halliburton", + "group": "Energy", + "marketCap": 29180442884 + }, + { + "ticker": "HIG", + "wsSymbol": "HIG", + "name": "Hartford (The)", + "group": "Financials", + "marketCap": 35157277665 + }, + { + "ticker": "HAS", + "wsSymbol": "HAS", + "name": "Hasbro", + "group": "Consumer Discretionary", + "marketCap": 11989921240 + }, + { + "ticker": "HCA", + "wsSymbol": "HCA", + "name": "HCA Healthcare", + "group": "Health Care", + "marketCap": 83227637766 + }, + { + "ticker": "DOC", + "wsSymbol": "DOC", + "name": "Healthpeak Properties", + "group": "Real Estate", + "marketCap": 13485049371 + }, + { + "ticker": "HSIC", + "wsSymbol": "HSIC", + "name": "Henry Schein", + "group": "Health Care", + "marketCap": 9104227219 + }, + { + "ticker": "HSY", + "wsSymbol": "HSY", + "name": "Hershey Company (The)", + "group": "Consumer Staples", + "marketCap": 35016851308 + }, + { + "ticker": "HPE", + "wsSymbol": "HPE", + "name": "Hewlett Packard Enterprise", + "group": "Information Technology", + "marketCap": 62780488931 + }, + { + "ticker": "HLT", + "wsSymbol": "HLT", + "name": "Hilton Worldwide", + "group": "Consumer Discretionary", + "marketCap": 79412858088 + }, + { + "ticker": "HD", + "wsSymbol": "HD", + "name": "Home Depot (The)", + "group": "Consumer Discretionary", + "marketCap": 333316164459 + }, + { + "ticker": "HON", + "wsSymbol": "HON", + "name": "Honeywell", + "group": "Industrials", + "marketCap": 145112900782 + }, + { + "ticker": "HRL", + "wsSymbol": "HRL", + "name": "Hormel Foods", + "group": "Consumer Staples", + "marketCap": 13295609409 + }, + { + "ticker": "HST", + "wsSymbol": "HST", + "name": "Host Hotels & Resorts", + "group": "Real Estate", + "marketCap": 17128830818 + }, + { + "ticker": "HWM", + "wsSymbol": "HWM", + "name": "Howmet Aerospace", + "group": "Industrials", + "marketCap": 111093843730 + }, + { + "ticker": "HPQ", + "wsSymbol": "HPQ", + "name": "HP Inc.", + "group": "Information Technology", + "marketCap": 21491283215 + }, + { + "ticker": "HUBB", + "wsSymbol": "HUBB", + "name": "Hubbell Incorporated", + "group": "Industrials", + "marketCap": 27672710197 + }, + { + "ticker": "HUM", + "wsSymbol": "HUM", + "name": "Humana", + "group": "Health Care", + "marketCap": 43300179975 + }, + { + "ticker": "HBAN", + "wsSymbol": "HBAN", + "name": "Huntington Bancshares", + "group": "Financials", + "marketCap": 34177421697 + }, + { + "ticker": "HII", + "wsSymbol": "HII", + "name": "Huntington Ingalls Industries", + "group": "Industrials", + "marketCap": 11247091997 + }, + { + "ticker": "IBM", + "wsSymbol": "IBM", + "name": "IBM", + "group": "Information Technology", + "marketCap": 234125423248 + }, + { + "ticker": "IEX", + "wsSymbol": "IEX", + "name": "IDEX Corporation", + "group": "Industrials", + "marketCap": 16648205646 + }, + { + "ticker": "IDXX", + "wsSymbol": "IDXX", + "name": "Idexx Laboratories", + "group": "Health Care", + "marketCap": 44339343784 + }, + { + "ticker": "ITW", + "wsSymbol": "ITW", + "name": "Illinois Tool Works", + "group": "Industrials", + "marketCap": 75978693000 + }, + { + "ticker": "INCY", + "wsSymbol": "INCY", + "name": "Incyte", + "group": "Health Care", + "marketCap": 19622603264 + }, + { + "ticker": "IR", + "wsSymbol": "IR", + "name": "Ingersoll Rand", + "group": "Industrials", + "marketCap": 30489052737 + }, + { + "ticker": "PODD", + "wsSymbol": "PODD", + "name": "Insulet Corporation", + "group": "Health Care", + "marketCap": 10096018882 + }, + { + "ticker": "INTC", + "wsSymbol": "INTC", + "name": "Intel", + "group": "Information Technology", + "marketCap": 673433740000 + }, + { + "ticker": "IBKR", + "wsSymbol": "IBKR", + "name": "Interactive Brokers", + "group": "Financials", + "marketCap": 42766537344 + }, + { + "ticker": "ICE", + "wsSymbol": "ICE", + "name": "Intercontinental Exchange", + "group": "Financials", + "marketCap": 75710812831 + }, + { + "ticker": "IFF", + "wsSymbol": "IFF", + "name": "International Flavors & Fragrances", + "group": "Materials", + "marketCap": 19586882449 + }, + { + "ticker": "IP", + "wsSymbol": "IP", + "name": "International Paper", + "group": "Materials", + "marketCap": 19496814983 + }, + { + "ticker": "INTU", + "wsSymbol": "INTU", + "name": "Intuit", + "group": "Information Technology", + "marketCap": 73034379000 + }, + { + "ticker": "ISRG", + "wsSymbol": "ISRG", + "name": "Intuitive Surgical", + "group": "Health Care", + "marketCap": 144066360869 + }, + { + "ticker": "IVZ", + "wsSymbol": "IVZ", + "name": "Invesco", + "group": "Financials", + "marketCap": 12474367140 + }, + { + "ticker": "INVH", + "wsSymbol": "INVH", + "name": "Invitation Homes", + "group": "Real Estate", + "marketCap": 16876731970 + }, + { + "ticker": "IQV", + "wsSymbol": "IQV", + "name": "IQVIA", + "group": "Health Care", + "marketCap": 28000813000 + }, + { + "ticker": "IRM", + "wsSymbol": "IRM", + "name": "Iron Mountain", + "group": "Real Estate", + "marketCap": 38032579972 + }, + { + "ticker": "JBHT", + "wsSymbol": "JBHT", + "name": "J.B. Hunt", + "group": "Industrials", + "marketCap": 25575829838 + }, + { + "ticker": "JBL", + "wsSymbol": "JBL", + "name": "Jabil", + "group": "Information Technology", + "marketCap": 39234315069 + }, + { + "ticker": "JKHY", + "wsSymbol": "JKHY", + "name": "Jack Henry & Associates", + "group": "Financials", + "marketCap": 8968739959 + }, + { + "ticker": "J", + "wsSymbol": "J", + "name": "Jacobs Solutions", + "group": "Industrials", + "marketCap": 14284243933 + }, + { + "ticker": "JNJ", + "wsSymbol": "JNJ", + "name": "Johnson & Johnson", + "group": "Health Care", + "marketCap": 549784284007 + }, + { + "ticker": "JCI", + "wsSymbol": "JCI", + "name": "Johnson Controls", + "group": "Industrials", + "marketCap": 88356941626 + }, + { + "ticker": "JPM", + "wsSymbol": "JPM", + "name": "JPMorgan Chase", + "group": "Financials", + "marketCap": 871430703362 + }, + { + "ticker": "KVUE", + "wsSymbol": "KVUE", + "name": "Kenvue", + "group": "Consumer Staples", + "marketCap": 34790557064 + }, + { + "ticker": "KDP", + "wsSymbol": "KDP", + "name": "Keurig Dr Pepper", + "group": "Consumer Staples", + "marketCap": 41850809328 + }, + { + "ticker": "KEY", + "wsSymbol": "KEY", + "name": "KeyCorp", + "group": "Financials", + "marketCap": 24382937668 + }, + { + "ticker": "KEYS", + "wsSymbol": "KEYS", + "name": "Keysight Technologies", + "group": "Information Technology", + "marketCap": 62149518481 + }, + { + "ticker": "KMB", + "wsSymbol": "KMB", + "name": "Kimberly-Clark", + "group": "Consumer Staples", + "marketCap": 34043803014 + }, + { + "ticker": "KIM", + "wsSymbol": "KIM", + "name": "Kimco Realty", + "group": "Real Estate", + "marketCap": 16441623129 + }, + { + "ticker": "KMI", + "wsSymbol": "KMI", + "name": "Kinder Morgan", + "group": "Energy", + "marketCap": 70282245664 + }, + { + "ticker": "KKR", + "wsSymbol": "KKR", + "name": "KKR & Co.", + "group": "Financials", + "marketCap": 87102654006 + }, + { + "ticker": "KLAC", + "wsSymbol": "KLAC", + "name": "KLA Corporation", + "group": "Information Technology", + "marketCap": 339056793508 + }, + { + "ticker": "KHC", + "wsSymbol": "KHC", + "name": "Kraft Heinz", + "group": "Consumer Staples", + "marketCap": 27059445699 + }, + { + "ticker": "KR", + "wsSymbol": "KR", + "name": "Kroger", + "group": "Consumer Staples", + "marketCap": 34701930000 + }, + { + "ticker": "LHX", + "wsSymbol": "LHX", + "name": "L3Harris", + "group": "Industrials", + "marketCap": 54923477454 + }, + { + "ticker": "LH", + "wsSymbol": "LH", + "name": "Labcorp", + "group": "Health Care", + "marketCap": 20977240000 + }, + { + "ticker": "LRCX", + "wsSymbol": "LRCX", + "name": "Lam Research", + "group": "Information Technology", + "marketCap": 486522141840 + }, + { + "ticker": "LVS", + "wsSymbol": "LVS", + "name": "Las Vegas Sands", + "group": "Consumer Discretionary", + "marketCap": 32283690474 + }, + { + "ticker": "LDOS", + "wsSymbol": "LDOS", + "name": "Leidos", + "group": "Industrials", + "marketCap": 13474139118 + }, + { + "ticker": "LEN", + "wsSymbol": "LEN", + "name": "Lennar", + "group": "Consumer Discretionary", + "marketCap": 22100346100 + }, + { + "ticker": "LII", + "wsSymbol": "LII", + "name": "Lennox International", + "group": "Industrials", + "marketCap": 18528546962 + }, + { + "ticker": "LLY", + "wsSymbol": "LLY", + "name": "Lilly (Eli)", + "group": "Health Care", + "marketCap": 979640356389 + }, + { + "ticker": "LIN", + "wsSymbol": "LIN", + "name": "Linde plc", + "group": "Materials", + "marketCap": 236791174816 + }, + { + "ticker": "LYV", + "wsSymbol": "LYV", + "name": "Live Nation Entertainment", + "group": "Communication Services", + "marketCap": 39849739701 + }, + { + "ticker": "LMT", + "wsSymbol": "LMT", + "name": "Lockheed Martin", + "group": "Industrials", + "marketCap": 117806475508 + }, + { + "ticker": "L", + "wsSymbol": "L", + "name": "Loews Corporation", + "group": "Financials", + "marketCap": 22074884695 + }, + { + "ticker": "LOW", + "wsSymbol": "LOW", + "name": "Lowe's", + "group": "Consumer Discretionary", + "marketCap": 124589104510 + }, + { + "ticker": "LULU", + "wsSymbol": "LULU", + "name": "Lululemon Athletica", + "group": "Consumer Discretionary", + "marketCap": 12691921415 + }, + { + "ticker": "LITE", + "wsSymbol": "LITE", + "name": "Lumentum", + "group": "Information Technology", + "marketCap": 66130000000 + }, + { + "ticker": "LYB", + "wsSymbol": "LYB", + "name": "LyondellBasell", + "group": "Materials", + "marketCap": 19389681314 + }, + { + "ticker": "MTB", + "wsSymbol": "MTB", + "name": "M&T Bank", + "group": "Financials", + "marketCap": 32967711907 + }, + { + "ticker": "MPC", + "wsSymbol": "MPC", + "name": "Marathon Petroleum", + "group": "Energy", + "marketCap": 70914328008 + }, + { + "ticker": "MAR", + "wsSymbol": "MAR", + "name": "Marriott International", + "group": "Consumer Discretionary", + "marketCap": 104473432433 + }, + { + "ticker": "MMC", + "wsSymbol": "MMC", + "name": "Marsh McLennan", + "group": "Financials", + "marketCap": 43012604657 + }, + { + "ticker": "MLM", + "wsSymbol": "MLM", + "name": "Martin Marietta Materials", + "group": "Materials", + "marketCap": 36575158608 + }, + { + "ticker": "MRVL", + "wsSymbol": "MRVL", + "name": "Marvell Technology", + "group": "Information Technology", + "marketCap": 271695384000 + }, + { + "ticker": "MAS", + "wsSymbol": "MAS", + "name": "Masco", + "group": "Industrials", + "marketCap": 15005007424 + }, + { + "ticker": "MA", + "wsSymbol": "MA", + "name": "Mastercard", + "group": "Financials", + "marketCap": 432770536340 + }, + { + "ticker": "MKC", + "wsSymbol": "MKC", + "name": "McCormick & Company", + "group": "Consumer Staples", + "marketCap": 12538846615 + }, + { + "ticker": "MCD", + "wsSymbol": "MCD", + "name": "McDonald's", + "group": "Consumer Discretionary", + "marketCap": 197954037376 + }, + { + "ticker": "MCK", + "wsSymbol": "MCK", + "name": "McKesson Corporation", + "group": "Health Care", + "marketCap": 87882067729 + }, + { + "ticker": "MDT", + "wsSymbol": "MDT", + "name": "Medtronic", + "group": "Health Care", + "marketCap": 101558785375 + }, + { + "ticker": "MRK", + "wsSymbol": "MRK", + "name": "Merck & Co.", + "group": "Health Care", + "marketCap": 281238906136 + }, + { + "ticker": "META", + "wsSymbol": "META", + "name": "Meta Platforms", + "group": "Communication Services", + "marketCap": 1465228699535 + }, + { + "ticker": "MET", + "wsSymbol": "MET", + "name": "MetLife", + "group": "Financials", + "marketCap": 55065319547 + }, + { + "ticker": "MTD", + "wsSymbol": "MTD", + "name": "Mettler Toledo", + "group": "Health Care", + "marketCap": 23134469929 + }, + { + "ticker": "MGM", + "wsSymbol": "MGM", + "name": "MGM Resorts", + "group": "Consumer Discretionary", + "marketCap": 11984071847 + }, + { + "ticker": "MCHP", + "wsSymbol": "MCHP", + "name": "Microchip Technology", + "group": "Information Technology", + "marketCap": 54083249865 + }, + { + "ticker": "MU", + "wsSymbol": "MU", + "name": "Micron Technology", + "group": "Information Technology", + "marketCap": 1278839136493 + }, + { + "ticker": "MSFT", + "wsSymbol": "MSFT", + "name": "Microsoft", + "group": "Information Technology", + "marketCap": 2818348126698 + }, + { + "ticker": "MAA", + "wsSymbol": "MAA", + "name": "Mid-America Apartment Communities", + "group": "Real Estate", + "marketCap": 15420986265 + }, + { + "ticker": "MRNA", + "wsSymbol": "MRNA", + "name": "Moderna", + "group": "Health Care", + "marketCap": 25378449126 + }, + { + "ticker": "TAP", + "wsSymbol": "TAP", + "name": "Molson Coors Beverage Company", + "group": "Consumer Staples", + "marketCap": 7389523584 + }, + { + "ticker": "MDLZ", + "wsSymbol": "MDLZ", + "name": "Mondelez International", + "group": "Consumer Staples", + "marketCap": 77173023932 + }, + { + "ticker": "MPWR", + "wsSymbol": "MPWR", + "name": "Monolithic Power Systems", + "group": "Information Technology", + "marketCap": 76824581000 + }, + { + "ticker": "MNST", + "wsSymbol": "MNST", + "name": "Monster Beverage", + "group": "Consumer Staples", + "marketCap": 89331260767 + }, + { + "ticker": "MCO", + "wsSymbol": "MCO", + "name": "Moody's Corporation", + "group": "Financials", + "marketCap": 78721430313 + }, + { + "ticker": "MS", + "wsSymbol": "MS", + "name": "Morgan Stanley", + "group": "Financials", + "marketCap": 352002652610 + }, + { + "ticker": "MOS", + "wsSymbol": "MOS", + "name": "Mosaic Company (The)", + "group": "Materials", + "marketCap": 7278688148 + }, + { + "ticker": "MSI", + "wsSymbol": "MSI", + "name": "Motorola Solutions", + "group": "Information Technology", + "marketCap": 65596474139 + }, + { + "ticker": "MSCI", + "wsSymbol": "MSCI", + "name": "MSCI Inc.", + "group": "Financials", + "marketCap": 42310632000 + }, + { + "ticker": "NDAQ", + "wsSymbol": "NDAQ", + "name": "Nasdaq, Inc.", + "group": "Financials", + "marketCap": 46510075228 + }, + { + "ticker": "NTAP", + "wsSymbol": "NTAP", + "name": "NetApp", + "group": "Information Technology", + "marketCap": 31290371541 + }, + { + "ticker": "NFLX", + "wsSymbol": "NFLX", + "name": "Netflix", + "group": "Communication Services", + "marketCap": 325831590097 + }, + { + "ticker": "NEM", + "wsSymbol": "NEM", + "name": "Newmont", + "group": "Materials", + "marketCap": 110801301376 + }, + { + "ticker": "NWSA", + "wsSymbol": "NWSA", + "name": "News Corp (Class A)", + "group": "Communication Services", + "marketCap": 13755429238 + }, + { + "ticker": "NWS", + "wsSymbol": "NWS", + "name": "News Corp (Class B)", + "group": "Communication Services", + "marketCap": 15643536020 + }, + { + "ticker": "NEE", + "wsSymbol": "NEE", + "name": "NextEra Energy", + "group": "Utilities", + "marketCap": 180926264806 + }, + { + "ticker": "NKE", + "wsSymbol": "NKE", + "name": "Nike, Inc.", + "group": "Consumer Discretionary", + "marketCap": 66936093892 + }, + { + "ticker": "NI", + "wsSymbol": "NI", + "name": "NiSource", + "group": "Utilities", + "marketCap": 22658298719 + }, + { + "ticker": "NDSN", + "wsSymbol": "NDSN", + "name": "Nordson Corporation", + "group": "Industrials", + "marketCap": 16488055172 + }, + { + "ticker": "NSC", + "wsSymbol": "NSC", + "name": "Norfolk Southern", + "group": "Industrials", + "marketCap": 67396167820 + }, + { + "ticker": "NTRS", + "wsSymbol": "NTRS", + "name": "Northern Trust", + "group": "Financials", + "marketCap": 31848439170 + }, + { + "ticker": "NOC", + "wsSymbol": "NOC", + "name": "Northrop Grumman", + "group": "Industrials", + "marketCap": 74070457734 + }, + { + "ticker": "NCLH", + "wsSymbol": "NCLH", + "name": "Norwegian Cruise Line Holdings", + "group": "Consumer Discretionary", + "marketCap": 9384211262 + }, + { + "ticker": "NRG", + "wsSymbol": "NRG", + "name": "NRG Energy", + "group": "Utilities", + "marketCap": 28495832638 + }, + { + "ticker": "NUE", + "wsSymbol": "NUE", + "name": "Nucor", + "group": "Materials", + "marketCap": 55530075838 + }, + { + "ticker": "NVDA", + "wsSymbol": "NVDA", + "name": "Nvidia", + "group": "Information Technology", + "marketCap": 5103122490000 + }, + { + "ticker": "NVR", + "wsSymbol": "NVR", + "name": "NVR, Inc.", + "group": "Consumer Discretionary", + "marketCap": 17520915422 + }, + { + "ticker": "NXPI", + "wsSymbol": "NXPI", + "name": "NXP Semiconductors", + "group": "Information Technology", + "marketCap": 79091614918 + }, + { + "ticker": "ORLY", + "wsSymbol": "ORLY", + "name": "O'Reilly Automotive", + "group": "Consumer Discretionary", + "marketCap": 71965617460 + }, + { + "ticker": "OXY", + "wsSymbol": "OXY", + "name": "Occidental Petroleum", + "group": "Energy", + "marketCap": 51541970206 + }, + { + "ticker": "ODFL", + "wsSymbol": "ODFL", + "name": "Old Dominion", + "group": "Industrials", + "marketCap": 45969309274 + }, + { + "ticker": "OMC", + "wsSymbol": "OMC", + "name": "Omnicom Group", + "group": "Communication Services", + "marketCap": 20335192085 + }, + { + "ticker": "ON", + "wsSymbol": "ON", + "name": "ON Semiconductor", + "group": "Information Technology", + "marketCap": 47286244089 + }, + { + "ticker": "OKE", + "wsSymbol": "OKE", + "name": "Oneok", + "group": "Energy", + "marketCap": 53571693406 + }, + { + "ticker": "ORCL", + "wsSymbol": "ORCL", + "name": "Oracle Corporation", + "group": "Information Technology", + "marketCap": 530026517340 + }, + { + "ticker": "OTIS", + "wsSymbol": "OTIS", + "name": "Otis Worldwide", + "group": "Industrials", + "marketCap": 28114876889 + }, + { + "ticker": "PCAR", + "wsSymbol": "PCAR", + "name": "Paccar", + "group": "Industrials", + "marketCap": 62601746940 + }, + { + "ticker": "PKG", + "wsSymbol": "PKG", + "name": "Packaging Corporation of America", + "group": "Materials", + "marketCap": 20405372136 + }, + { + "ticker": "PLTR", + "wsSymbol": "PLTR", + "name": "Palantir Technologies", + "group": "Information Technology", + "marketCap": 307982669685 + }, + { + "ticker": "PANW", + "wsSymbol": "PANW", + "name": "Palo Alto Networks", + "group": "Information Technology", + "marketCap": 234540700000 + }, + { + "ticker": "PSKY", + "wsSymbol": "PSKY", + "name": "Paramount Skydance Corporation", + "group": "Communication Services", + "marketCap": 11124580197 + }, + { + "ticker": "PH", + "wsSymbol": "PH", + "name": "Parker Hannifin", + "group": "Industrials", + "marketCap": 120194372042 + }, + { + "ticker": "PAYX", + "wsSymbol": "PAYX", + "name": "Paychex", + "group": "Industrials", + "marketCap": 35198496149 + }, + { + "ticker": "PYPL", + "wsSymbol": "PYPL", + "name": "PayPal", + "group": "Financials", + "marketCap": 37498304507 + }, + { + "ticker": "PNR", + "wsSymbol": "PNR", + "name": "Pentair", + "group": "Industrials", + "marketCap": 12010523807 + }, + { + "ticker": "PEP", + "wsSymbol": "PEP", + "name": "PepsiCo", + "group": "Consumer Staples", + "marketCap": 194108436096 + }, + { + "ticker": "PFE", + "wsSymbol": "PFE", + "name": "Pfizer", + "group": "Health Care", + "marketCap": 143682987500 + }, + { + "ticker": "PCG", + "wsSymbol": "PCG", + "name": "PG&E Corporation", + "group": "Utilities", + "marketCap": 36292663517 + }, + { + "ticker": "PM", + "wsSymbol": "PM", + "name": "Philip Morris International", + "group": "Consumer Staples", + "marketCap": 278046898126 + }, + { + "ticker": "PSX", + "wsSymbol": "PSX", + "name": "Phillips 66", + "group": "Energy", + "marketCap": 66611344223 + }, + { + "ticker": "PNW", + "wsSymbol": "PNW", + "name": "Pinnacle West Capital", + "group": "Utilities", + "marketCap": 12399870825 + }, + { + "ticker": "PNC", + "wsSymbol": "PNC", + "name": "PNC Financial Services", + "group": "Financials", + "marketCap": 93179057209 + }, + { + "ticker": "PPG", + "wsSymbol": "PPG", + "name": "PPG Industries", + "group": "Materials", + "marketCap": 26357925000 + }, + { + "ticker": "PPL", + "wsSymbol": "PPL", + "name": "PPL Corporation", + "group": "Utilities", + "marketCap": 26618155808 + }, + { + "ticker": "PFG", + "wsSymbol": "PFG", + "name": "Principal Financial Group", + "group": "Financials", + "marketCap": 23668476915 + }, + { + "ticker": "PG", + "wsSymbol": "PG", + "name": "Procter & Gamble", + "group": "Consumer Staples", + "marketCap": 350174714312 + }, + { + "ticker": "PGR", + "wsSymbol": "PGR", + "name": "Progressive Corporation", + "group": "Financials", + "marketCap": 119275314000 + }, + { + "ticker": "PLD", + "wsSymbol": "PLD", + "name": "Prologis", + "group": "Real Estate", + "marketCap": 131030782520 + }, + { + "ticker": "PRU", + "wsSymbol": "PRU", + "name": "Prudential Financial", + "group": "Financials", + "marketCap": 37000543649 + }, + { + "ticker": "PEG", + "wsSymbol": "PEG", + "name": "Public Service Enterprise Group", + "group": "Utilities", + "marketCap": 39810896806 + }, + { + "ticker": "PTC", + "wsSymbol": "PTC", + "name": "PTC Inc.", + "group": "Information Technology", + "marketCap": 13254289517 + }, + { + "ticker": "PSA", + "wsSymbol": "PSA", + "name": "Public Storage", + "group": "Real Estate", + "marketCap": 55855400167 + }, + { + "ticker": "PHM", + "wsSymbol": "PHM", + "name": "PulteGroup", + "group": "Consumer Discretionary", + "marketCap": 24184147377 + }, + { + "ticker": "PWR", + "wsSymbol": "PWR", + "name": "Quanta Services", + "group": "Industrials", + "marketCap": 105379739635 + }, + { + "ticker": "QCOM", + "wsSymbol": "QCOM", + "name": "Qualcomm", + "group": "Information Technology", + "marketCap": 238319940000 + }, + { + "ticker": "DGX", + "wsSymbol": "DGX", + "name": "Quest Diagnostics", + "group": "Health Care", + "marketCap": 21585803655 + }, + { + "ticker": "RL", + "wsSymbol": "RL", + "name": "Ralph Lauren Corporation", + "group": "Consumer Discretionary", + "marketCap": 24580846061 + }, + { + "ticker": "RJF", + "wsSymbol": "RJF", + "name": "Raymond James Financial", + "group": "Financials", + "marketCap": 30374469679 + }, + { + "ticker": "RTX", + "wsSymbol": "RTX", + "name": "RTX Corporation", + "group": "Industrials", + "marketCap": 249944444237 + }, + { + "ticker": "O", + "wsSymbol": "O", + "name": "Realty Income", + "group": "Real Estate", + "marketCap": 56173350007 + }, + { + "ticker": "REG", + "wsSymbol": "REG", + "name": "Regency Centers", + "group": "Real Estate", + "marketCap": 14076481215 + }, + { + "ticker": "REGN", + "wsSymbol": "REGN", + "name": "Regeneron Pharmaceuticals", + "group": "Health Care", + "marketCap": 63945519178 + }, + { + "ticker": "RF", + "wsSymbol": "RF", + "name": "Regions Financial Corporation", + "group": "Financials", + "marketCap": 24423722378 + }, + { + "ticker": "RSG", + "wsSymbol": "RSG", + "name": "Republic Services", + "group": "Industrials", + "marketCap": 63052677990 + }, + { + "ticker": "RMD", + "wsSymbol": "RMD", + "name": "ResMed", + "group": "Health Care", + "marketCap": 27361985714 + }, + { + "ticker": "RVTY", + "wsSymbol": "RVTY", + "name": "Revvity", + "group": "Health Care", + "marketCap": 11155977300 + }, + { + "ticker": "HOOD", + "wsSymbol": "HOOD", + "name": "Robinhood Markets", + "group": "Financials", + "marketCap": 97389621915 + }, + { + "ticker": "ROK", + "wsSymbol": "ROK", + "name": "Rockwell Automation", + "group": "Industrials", + "marketCap": 52720486192 + }, + { + "ticker": "ROL", + "wsSymbol": "ROL", + "name": "Rollins, Inc.", + "group": "Industrials", + "marketCap": 21646606288 + }, + { + "ticker": "ROP", + "wsSymbol": "ROP", + "name": "Roper Technologies", + "group": "Information Technology", + "marketCap": 33328462397 + }, + { + "ticker": "ROST", + "wsSymbol": "ROST", + "name": "Ross Stores", + "group": "Consumer Discretionary", + "marketCap": 74677834493 + }, + { + "ticker": "RCL", + "wsSymbol": "RCL", + "name": "Royal Caribbean Group", + "group": "Consumer Discretionary", + "marketCap": 83813612575 + }, + { + "ticker": "SPGI", + "wsSymbol": "SPGI", + "name": "S&P Global", + "group": "Financials", + "marketCap": 121632320000 + }, + { + "ticker": "CRM", + "wsSymbol": "CRM", + "name": "Salesforce", + "group": "Information Technology", + "marketCap": 124307820000 + }, + { + "ticker": "SNDK", + "wsSymbol": "SNDK", + "name": "Sandisk", + "group": "Information Technology", + "marketCap": 323539098790 + }, + { + "ticker": "SBAC", + "wsSymbol": "SBAC", + "name": "SBA Communications", + "group": "Real Estate", + "marketCap": 19819995987 + }, + { + "ticker": "SLB", + "wsSymbol": "SLB", + "name": "Schlumberger", + "group": "Energy", + "marketCap": 71897322917 + }, + { + "ticker": "STX", + "wsSymbol": "STX", + "name": "Seagate Technology", + "group": "Information Technology", + "marketCap": 242141802107 + }, + { + "ticker": "SRE", + "wsSymbol": "SRE", + "name": "Sempra", + "group": "Utilities", + "marketCap": 59284724197 + }, + { + "ticker": "NOW", + "wsSymbol": "NOW", + "name": "ServiceNow", + "group": "Information Technology", + "marketCap": 98015512320 + }, + { + "ticker": "SHW", + "wsSymbol": "SHW", + "name": "Sherwin-Williams", + "group": "Materials", + "marketCap": 79118240861 + }, + { + "ticker": "SPG", + "wsSymbol": "SPG", + "name": "Simon Property Group", + "group": "Real Estate", + "marketCap": 68532187103 + }, + { + "ticker": "SWKS", + "wsSymbol": "SWKS", + "name": "Skyworks Solutions", + "group": "Information Technology", + "marketCap": 10896912019 + }, + { + "ticker": "SJM", + "wsSymbol": "SJM", + "name": "J.M. Smucker Company (The)", + "group": "Consumer Staples", + "marketCap": 11824564064 + }, + { + "ticker": "SW", + "wsSymbol": "SW", + "name": "Smurfit Westrock", + "group": "Materials", + "marketCap": 23181312248 + }, + { + "ticker": "SNA", + "wsSymbol": "SNA", + "name": "Snap-on", + "group": "Industrials", + "marketCap": 20060291971 + }, + { + "ticker": "SOLV", + "wsSymbol": "SOLV", + "name": "Solventum", + "group": "Health Care", + "marketCap": 13062568526 + }, + { + "ticker": "SO", + "wsSymbol": "SO", + "name": "Southern Company", + "group": "Utilities", + "marketCap": 104940459213 + }, + { + "ticker": "LUV", + "wsSymbol": "LUV", + "name": "Southwest Airlines", + "group": "Industrials", + "marketCap": 23446475013 + }, + { + "ticker": "SWK", + "wsSymbol": "SWK", + "name": "Stanley Black & Decker", + "group": "Industrials", + "marketCap": 13485770177 + }, + { + "ticker": "SBUX", + "wsSymbol": "SBUX", + "name": "Starbucks", + "group": "Consumer Discretionary", + "marketCap": 114710805000 + }, + { + "ticker": "STT", + "wsSymbol": "STT", + "name": "State Street Corporation", + "group": "Financials", + "marketCap": 46582659324 + }, + { + "ticker": "STLD", + "wsSymbol": "STLD", + "name": "Steel Dynamics", + "group": "Materials", + "marketCap": 36040216100 + }, + { + "ticker": "STE", + "wsSymbol": "STE", + "name": "Steris", + "group": "Health Care", + "marketCap": 19745900545 + }, + { + "ticker": "SYK", + "wsSymbol": "SYK", + "name": "Stryker Corporation", + "group": "Health Care", + "marketCap": 117998442544 + }, + { + "ticker": "SMCI", + "wsSymbol": "SMCI", + "name": "Supermicro", + "group": "Information Technology", + "marketCap": 19833127008 + }, + { + "ticker": "SYF", + "wsSymbol": "SYF", + "name": "Synchrony Financial", + "group": "Financials", + "marketCap": 25315224413 + }, + { + "ticker": "SNPS", + "wsSymbol": "SNPS", + "name": "Synopsys", + "group": "Information Technology", + "marketCap": 87220747331 + }, + { + "ticker": "SYY", + "wsSymbol": "SYY", + "name": "Sysco", + "group": "Consumer Staples", + "marketCap": 37632971250 + }, + { + "ticker": "TMUS", + "wsSymbol": "TMUS", + "name": "T-Mobile US", + "group": "Communication Services", + "marketCap": 196604130937 + }, + { + "ticker": "TROW", + "wsSymbol": "TROW", + "name": "T. Rowe Price", + "group": "Financials", + "marketCap": 23065800136 + }, + { + "ticker": "TTWO", + "wsSymbol": "TTWO", + "name": "Take-Two Interactive", + "group": "Communication Services", + "marketCap": 44426319123 + }, + { + "ticker": "TPR", + "wsSymbol": "TPR", + "name": "Tapestry, Inc.", + "group": "Consumer Discretionary", + "marketCap": 28993416459 + }, + { + "ticker": "TRGP", + "wsSymbol": "TRGP", + "name": "Targa Resources", + "group": "Energy", + "marketCap": 55502620438 + }, + { + "ticker": "TGT", + "wsSymbol": "TGT", + "name": "Target Corporation", + "group": "Consumer Staples", + "marketCap": 59380945983 + }, + { + "ticker": "TEL", + "wsSymbol": "TEL", + "name": "TE Connectivity", + "group": "Information Technology", + "marketCap": 63528201694 + }, + { + "ticker": "TDY", + "wsSymbol": "TDY", + "name": "Teledyne Technologies", + "group": "Information Technology", + "marketCap": 28704607322 + }, + { + "ticker": "TER", + "wsSymbol": "TER", + "name": "Teradyne", + "group": "Information Technology", + "marketCap": 68552943583 + }, + { + "ticker": "TSLA", + "wsSymbol": "TSLA", + "name": "Tesla, Inc.", + "group": "Consumer Discretionary", + "marketCap": 1504129853097 + }, + { + "ticker": "TXN", + "wsSymbol": "TXN", + "name": "Texas Instruments", + "group": "Information Technology", + "marketCap": 293832558502 + }, + { + "ticker": "TPL", + "wsSymbol": "TPL", + "name": "Texas Pacific Land Corporation", + "group": "Energy", + "marketCap": 24493509482 + }, + { + "ticker": "TXT", + "wsSymbol": "TXT", + "name": "Textron", + "group": "Industrials", + "marketCap": 15557847130 + }, + { + "ticker": "TMO", + "wsSymbol": "TMO", + "name": "Thermo Fisher Scientific", + "group": "Health Care", + "marketCap": 172659048854 + }, + { + "ticker": "TJX", + "wsSymbol": "TJX", + "name": "TJX Companies", + "group": "Consumer Discretionary", + "marketCap": 180961635299 + }, + { + "ticker": "TKO", + "wsSymbol": "TKO", + "name": "TKO Group Holdings", + "group": "Communication Services", + "marketCap": 14902074039 + }, + { + "ticker": "TTD", + "wsSymbol": "TTD", + "name": "Trade Desk (The)", + "group": "Communication Services", + "marketCap": 8701705762 + }, + { + "ticker": "TSCO", + "wsSymbol": "TSCO", + "name": "Tractor Supply", + "group": "Consumer Discretionary", + "marketCap": 15859356388 + }, + { + "ticker": "TT", + "wsSymbol": "TT", + "name": "Trane Technologies", + "group": "Industrials", + "marketCap": 106858217582 + }, + { + "ticker": "TDG", + "wsSymbol": "TDG", + "name": "TransDigm Group", + "group": "Industrials", + "marketCap": 74297137635 + }, + { + "ticker": "TRV", + "wsSymbol": "TRV", + "name": "Travelers Companies (The)", + "group": "Financials", + "marketCap": 65454162952 + }, + { + "ticker": "TRMB", + "wsSymbol": "TRMB", + "name": "Trimble Inc.", + "group": "Information Technology", + "marketCap": 11459762274 + }, + { + "ticker": "TFC", + "wsSymbol": "TFC", + "name": "Truist Financial", + "group": "Financials", + "marketCap": 60213345361 + }, + { + "ticker": "TYL", + "wsSymbol": "TYL", + "name": "Tyler Technologies", + "group": "Information Technology", + "marketCap": 11474512195 + }, + { + "ticker": "TSN", + "wsSymbol": "TSN", + "name": "Tyson Foods", + "group": "Consumer Staples", + "marketCap": 19529096635 + }, + { + "ticker": "USB", + "wsSymbol": "USB", + "name": "U.S. Bancorp", + "group": "Financials", + "marketCap": 90566522666 + }, + { + "ticker": "UBER", + "wsSymbol": "UBER", + "name": "Uber", + "group": "Industrials", + "marketCap": 145830313291 + }, + { + "ticker": "UDR", + "wsSymbol": "UDR", + "name": "UDR, Inc.", + "group": "Real Estate", + "marketCap": 12203831927 + }, + { + "ticker": "ULTA", + "wsSymbol": "ULTA", + "name": "Ulta Beauty", + "group": "Consumer Discretionary", + "marketCap": 19608714426 + }, + { + "ticker": "UNP", + "wsSymbol": "UNP", + "name": "Union Pacific Corporation", + "group": "Industrials", + "marketCap": 152513050155 + }, + { + "ticker": "UAL", + "wsSymbol": "UAL", + "name": "United Airlines Holdings", + "group": "Industrials", + "marketCap": 38403090217 + }, + { + "ticker": "UPS", + "wsSymbol": "UPS", + "name": "United Parcel Service", + "group": "Industrials", + "marketCap": 89131658416 + }, + { + "ticker": "URI", + "wsSymbol": "URI", + "name": "United Rentals", + "group": "Industrials", + "marketCap": 67458439043 + }, + { + "ticker": "UNH", + "wsSymbol": "UNH", + "name": "UnitedHealth Group", + "group": "Health Care", + "marketCap": 364129580228 + }, + { + "ticker": "UHS", + "wsSymbol": "UHS", + "name": "Universal Health Services", + "group": "Health Care", + "marketCap": 8545916671 + }, + { + "ticker": "VLO", + "wsSymbol": "VLO", + "name": "Valero Energy", + "group": "Energy", + "marketCap": 70165216387 + }, + { + "ticker": "VEEV", + "wsSymbol": "VEEV", + "name": "Veeva Systems", + "group": "Health Care", + "marketCap": 24902556510 + }, + { + "ticker": "VTR", + "wsSymbol": "VTR", + "name": "Ventas", + "group": "Real Estate", + "marketCap": 39671449397 + }, + { + "ticker": "VLTO", + "wsSymbol": "VLTO", + "name": "Veralto", + "group": "Industrials", + "marketCap": 20635250329 + }, + { + "ticker": "VRSN", + "wsSymbol": "VRSN", + "name": "Verisign", + "group": "Information Technology", + "marketCap": 24082240000 + }, + { + "ticker": "VRSK", + "wsSymbol": "VRSK", + "name": "Verisk Analytics", + "group": "Industrials", + "marketCap": 22771595792 + }, + { + "ticker": "VZ", + "wsSymbol": "VZ", + "name": "Verizon", + "group": "Communication Services", + "marketCap": 189445107747 + }, + { + "ticker": "VRTX", + "wsSymbol": "VRTX", + "name": "Vertex Pharmaceuticals", + "group": "Health Care", + "marketCap": 114626140480 + }, + { + "ticker": "VRT", + "wsSymbol": "VRT", + "name": "Vertiv", + "group": "Industrials", + "marketCap": 127927441169 + }, + { + "ticker": "VTRS", + "wsSymbol": "VTRS", + "name": "Viatris", + "group": "Health Care", + "marketCap": 17899174292 + }, + { + "ticker": "VICI", + "wsSymbol": "VICI", + "name": "Vici Properties", + "group": "Real Estate", + "marketCap": 28297783314 + }, + { + "ticker": "V", + "wsSymbol": "V", + "name": "Visa Inc.", + "group": "Financials", + "marketCap": 622326641112 + }, + { + "ticker": "VST", + "wsSymbol": "VST", + "name": "Vistra Corp.", + "group": "Utilities", + "marketCap": 55213629135 + }, + { + "ticker": "VMC", + "wsSymbol": "VMC", + "name": "Vulcan Materials Company", + "group": "Materials", + "marketCap": 39294969373 + }, + { + "ticker": "WRB", + "wsSymbol": "WRB", + "name": "W. R. Berkley Corporation", + "group": "Financials", + "marketCap": 25009550856 + }, + { + "ticker": "GWW", + "wsSymbol": "GWW", + "name": "W. W. Grainger", + "group": "Industrials", + "marketCap": 64465264814 + }, + { + "ticker": "WAB", + "wsSymbol": "WAB", + "name": "Wabtec", + "group": "Industrials", + "marketCap": 46462747655 + }, + { + "ticker": "WMT", + "wsSymbol": "WMT", + "name": "Walmart", + "group": "Consumer Staples", + "marketCap": 932527715383 + }, + { + "ticker": "DIS", + "wsSymbol": "DIS", + "name": "Walt Disney Company (The)", + "group": "Communication Services", + "marketCap": 180406171839 + }, + { + "ticker": "WBD", + "wsSymbol": "WBD", + "name": "Warner Bros. Discovery", + "group": "Communication Services", + "marketCap": 65686981592 + }, + { + "ticker": "WM", + "wsSymbol": "WM", + "name": "Waste Management", + "group": "Industrials", + "marketCap": 86178156379 + }, + { + "ticker": "WAT", + "wsSymbol": "WAT", + "name": "Waters Corporation", + "group": "Health Care", + "marketCap": 34899180301 + }, + { + "ticker": "WEC", + "wsSymbol": "WEC", + "name": "WEC Energy Group", + "group": "Utilities", + "marketCap": 36536649301 + }, + { + "ticker": "WFC", + "wsSymbol": "WFC", + "name": "Wells Fargo", + "group": "Financials", + "marketCap": 251547576407 + }, + { + "ticker": "WELL", + "wsSymbol": "WELL", + "name": "Welltower", + "group": "Real Estate", + "marketCap": 145877221092 + }, + { + "ticker": "WST", + "wsSymbol": "WST", + "name": "West Pharmaceutical Services", + "group": "Health Care", + "marketCap": 23168878780 + }, + { + "ticker": "WDC", + "wsSymbol": "WDC", + "name": "Western Digital", + "group": "Information Technology", + "marketCap": 257212146616 + }, + { + "ticker": "WY", + "wsSymbol": "WY", + "name": "Weyerhaeuser", + "group": "Real Estate", + "marketCap": 17535756251 + }, + { + "ticker": "WSM", + "wsSymbol": "WSM", + "name": "Williams-Sonoma, Inc.", + "group": "Consumer Discretionary", + "marketCap": 26719060968 + }, + { + "ticker": "WMB", + "wsSymbol": "WMB", + "name": "Williams Companies", + "group": "Energy", + "marketCap": 89425633137 + }, + { + "ticker": "WTW", + "wsSymbol": "WTW", + "name": "Willis Towers Watson", + "group": "Financials", + "marketCap": 24103123475 + }, + { + "ticker": "WDAY", + "wsSymbol": "WDAY", + "name": "Workday, Inc.", + "group": "Information Technology", + "marketCap": 28878608899 + }, + { + "ticker": "WYNN", + "wsSymbol": "WYNN", + "name": "Wynn Resorts", + "group": "Consumer Discretionary", + "marketCap": 10952613511 + }, + { + "ticker": "XEL", + "wsSymbol": "XEL", + "name": "Xcel Energy", + "group": "Utilities", + "marketCap": 48324706640 + }, + { + "ticker": "XYL", + "wsSymbol": "XYL", + "name": "Xylem Inc.", + "group": "Industrials", + "marketCap": 26483808990 + }, + { + "ticker": "YUM", + "wsSymbol": "YUM", + "name": "Yum! Brands", + "group": "Consumer Discretionary", + "marketCap": 41891666492 + }, + { + "ticker": "ZBRA", + "wsSymbol": "ZBRA", + "name": "Zebra Technologies", + "group": "Information Technology", + "marketCap": 11240527844 + }, + { + "ticker": "ZBH", + "wsSymbol": "ZBH", + "name": "Zimmer Biomet", + "group": "Health Care", + "marketCap": 17018894542 + }, + { + "ticker": "ZTS", + "wsSymbol": "ZTS", + "name": "Zoetis", + "group": "Health Care", + "marketCap": 32997445168 + } + ] +}