Skip to content

Commit eaf6e1a

Browse files
Smartoneinokclaude
andcommitted
feat(examples/rest): add massive-heatmap, a live cross-asset REST heatmap
A Node + TypeScript backend with a React + Vite frontend that renders a market-cap-weighted treemap colored by percent change across stocks, ETFs, crypto, forex, indices, and futures. The backend holds the Massive API key and brokers all data: it seeds prior-close baselines and serves current prices over two REST endpoints (/api/snapshot, /api/prices), and the browser polls on a configurable interval (2 to 60 seconds). Includes 18 prebuilt universes, selectable lookbacks, session awareness, zoom and pan, hover details, and branded PNG export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9c6f0e2 commit eaf6e1a

81 files changed

Lines changed: 7552 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,5 @@ reports/
7979
Claude.md
8080
.claude/
8181
.playwright-mcp/
82-
docs/
82+
docs/
83+
.codex-audits/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# A Massive API key with REST snapshot access for at least one asset class.
2+
# A real-time tier gives live colors; a delayed tier works too, just on a delay.
3+
MASSIVE_API_KEY=your_key_here
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
.env
4+
.vite/
5+
package-lock.json
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Massive Heatmap
2+
3+
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.
4+
5+
![S&P 500 heatmap](screenshots/hero-sp500.png)
6+
7+
## What it does
8+
9+
- **Cross-asset coverage.** Stocks (S&P 500, Nasdaq 100, Dow 30), ETFs, crypto, forex, indices, and futures, across 18 prebuilt universes.
10+
- **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.
11+
- **Market cap sizing.** Tile area is proportional to market cap, so the names that move the index get the most screen.
12+
- **Sector and category grouping.** GICS sectors for equities, thematic categories (Layer 1s, DeFi, Exchange, Energy, Metals, and so on) for everything else.
13+
- **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.
14+
- **Session awareness.** A badge shows pre-market, regular session, after-hours, or closed for equities, and 24/7 for crypto and forex.
15+
- **Zoom and pan.** Scroll to zoom toward the cursor, drag to pan, double-click to reset. Useful on the 500-tile S&P view.
16+
- **Hover details.** Hovering a tile shows the ticker, name, current price, the period reference price, and the percent change.
17+
- **PNG export.** One click renders a branded 16:9 snapshot of the current view and downloads it.
18+
19+
## How it works
20+
21+
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.
22+
23+
```
24+
browser ──GET /api/snapshot──> Node backend ──REST seed───────> api.massive.com
25+
browser ──GET /api/prices ───> Node backend ──REST snapshot───> api.massive.com
26+
(every refresh interval)
27+
```
28+
29+
**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.
30+
31+
- For **1D**, the reference is the prior official close, read straight from the snapshot.
32+
- 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.
33+
34+
**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.
35+
36+
**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.)
37+
38+
![Loading a new universe](screenshots/loading-overlay.png)
39+
40+
### Project layout
41+
42+
- `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`).
43+
- `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.
44+
- `universes/`: JSON files defining ticker membership, sector or category groupings, and market cap weights for each universe.
45+
- `shared/`: the wire protocol and universe types shared by both ends.
46+
47+
## Massive subscriptions you need
48+
49+
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.
50+
51+
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).
52+
53+
| App asset class | Massive product | Real-time | Lowest tier with snapshot (delayed) |
54+
|---|---|---|---|
55+
| Stocks and ETFs | Stocks | Stocks Advanced, $199/mo | Stocks Starter, $29/mo (15-min delayed) |
56+
| Crypto and Forex | Currencies | Currencies Starter, $49/mo (real-time included) | same plan |
57+
| Indices | Indices | Indices Advanced, $99/mo | Indices Starter, $49/mo (15-min delayed) |
58+
| Futures | Futures | Futures Advanced, $199/mo | Futures Starter, $29/mo (10-min delayed) |
59+
60+
A few things worth knowing:
61+
62+
- **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.
63+
- **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.)
64+
- **ETFs ride on the Stocks subscription**, since they are equities. There is no separate ETF plan.
65+
- **Real-time individual plans are non-professional use only.** Commercial use needs a Business plan.
66+
67+
**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.
68+
69+
### Business use
70+
71+
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.
72+
73+
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.
74+
75+
![Settings: hide asset classes your key cannot reach](screenshots/settings.png)
76+
77+
## Getting started
78+
79+
**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.
80+
81+
```bash
82+
cp .env.example .env
83+
# open .env and set MASSIVE_API_KEY
84+
```
85+
86+
```bash
87+
npm install
88+
npm run dev
89+
```
90+
91+
`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.
92+
93+
## Configuration
94+
95+
Open **Settings** in the top bar to:
96+
97+
- **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.
98+
- **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.
99+
- **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.
100+
101+
Settings persist in the browser's local storage. There is nothing to configure on the backend beyond the API key.
102+
103+
## Universes
104+
105+
Eighteen universes ship across six asset classes, grouped in the two dropdowns by class and then by universe.
106+
107+
| Asset class | Universes |
108+
|---|---|
109+
| Stocks | S&P 500, Nasdaq 100, Dow 30 |
110+
| ETFs | Sector SPDRs, Broad Market, Thematic |
111+
| Crypto | Top Coins, Layer 1s, DeFi |
112+
| Forex | FX Majors, Crosses, USD Exotics |
113+
| Futures | Equity Index, Energy, Metals |
114+
| Indices | Major Indices, Sectors, Global |
115+
116+
![Crypto top coins, grouped by category](screenshots/crypto-top-coins.png)
117+
118+
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`.
119+
120+
## Export
121+
122+
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.
123+
124+
![Branded PNG export preview](screenshots/export-preview.png)
125+
126+
## Running locally
127+
128+
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.
129+
130+
## Notes
131+
132+
- **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.
133+
- **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.
134+
- **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.
135+
136+
## Disclaimer
137+
138+
**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.
139+
140+
## License
141+
142+
This project is licensed under the [MIT License](../../../LICENSE).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "backend",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "tsx watch src/index.ts",
7+
"build": "tsc -p tsconfig.json",
8+
"test": "vitest run"
9+
},
10+
"dependencies": {
11+
"@massive.com/client-js": "^10.7.0",
12+
"dotenv": "^16.4.0"
13+
},
14+
"devDependencies": {
15+
"@types/node": "^22.0.0",
16+
"tsx": "^4.16.0"
17+
}
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { config } from "dotenv";
2+
import { fileURLToPath } from "node:url";
3+
import { dirname, join } from "node:path";
4+
5+
// Load the demo-root .env regardless of the process cwd. `npm run dev` runs the
6+
// backend with its cwd in backend/, but the .env (and .env.example) live at the
7+
// demo root, one level up from backend/.
8+
const HERE = dirname(fileURLToPath(import.meta.url)); // backend/src
9+
config({ path: join(HERE, "..", "..", ".env") });
10+
11+
export function apiKey(): string {
12+
const k = process.env.MASSIVE_API_KEY;
13+
if (!k) {
14+
throw new Error(
15+
"MASSIVE_API_KEY not set. Copy .env.example to .env (in the massive-heatmap root) and add your key.",
16+
);
17+
}
18+
return k;
19+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { startServer } from "./server.js";
2+
const PORT = Number(process.env.PORT ?? 8787);
3+
await startServer({ port: PORT });
4+
console.log(`[massive-heatmap] backend listening on http://localhost:${PORT}`);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const LOOKBACKS = [1, 7, 30, 90, 180, 365, 1825] as const;
2+
export type Lookback = (typeof LOOKBACKS)[number];
3+
4+
export function isLookback(n: unknown): n is Lookback {
5+
return typeof n === "number" && (LOOKBACKS as readonly number[]).includes(n);
6+
}
7+
8+
// UTC YYYY-MM-DD for an epoch.
9+
function ymd(ms: number): string {
10+
return new Date(ms).toISOString().slice(0, 10);
11+
}
12+
13+
// Candidate window-start dates to try, newest first: (now - days), then walk back
14+
// up to `back` more calendar days to skip weekends/holidays (grouped returns empty).
15+
export function windowStartCandidates(nowMs: number, days: number, back = 6): string[] {
16+
const start = nowMs - days * 86_400_000;
17+
const out: string[] = [];
18+
for (let i = 0; i <= back; i++) out.push(ymd(start - i * 86_400_000));
19+
return out;
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { restClient } from "@massive.com/client-js";
2+
import type { Universe } from "../../shared/universe.js";
3+
import { groupBySegment } from "./seed.js";
4+
import { fetchSnapshot } from "./snapshot.js";
5+
import { apiKey } from "./env.js";
6+
7+
// Current price per display ticker across every asset class in the universe, via REST
8+
// snapshots. No historical work: the prior-close baseline was set once at seed time.
9+
// One REST snapshot call per asset class (per product code for futures) returns
10+
// every current price.
11+
export async function fetchCurrentPrices(
12+
universe: Universe,
13+
rest = restClient(apiKey(), "https://api.massive.com"),
14+
): Promise<Map<string, number>> {
15+
const out = new Map<string, number>();
16+
for (const [segment, cons] of groupBySegment(universe)) {
17+
const snap = await fetchSnapshot(rest, segment, cons);
18+
for (const [ticker, row] of snap) out.set(ticker, row.price);
19+
}
20+
return out;
21+
}

0 commit comments

Comments
 (0)