React + Vite frontend with a local-JSON-backed API (Vite middleware) for tracking portfolio holdings, contributions/withdrawals, recurring contribution plans, and a forecast chart comparing the portfolio's real value against a same-contributions S&P 500 benchmark. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
34 lines
1.3 KiB
JavaScript
34 lines
1.3 KiB
JavaScript
// Refreshes data/sp500.json with daily S&P 500 close prices from Yahoo Finance's
|
|
// public chart endpoint (no API key required). Run with: node scripts/fetch-sp500.mjs
|
|
import { writeFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const OUT_PATH = join(__dirname, "..", "data", "sp500.json");
|
|
const URL =
|
|
"https://query1.finance.yahoo.com/v8/finance/chart/%5EGSPC?range=10y&interval=1d";
|
|
|
|
const res = await fetch(URL, { headers: { "User-Agent": "Mozilla/5.0" } });
|
|
if (!res.ok) {
|
|
throw new Error(`Yahoo Finance request failed: ${res.status} ${res.statusText}`);
|
|
}
|
|
const data = await res.json();
|
|
const result = data?.chart?.result?.[0];
|
|
if (!result) {
|
|
throw new Error("Unexpected response shape from Yahoo Finance");
|
|
}
|
|
|
|
const { timestamp } = result;
|
|
const closes = result.indicators.quote[0].close;
|
|
|
|
const series = [];
|
|
for (let i = 0; i < timestamp.length; i++) {
|
|
if (closes[i] == null) continue;
|
|
const date = new Date(timestamp[i] * 1000).toISOString().slice(0, 10);
|
|
series.push({ date, close: Number(closes[i].toFixed(2)) });
|
|
}
|
|
|
|
writeFileSync(OUT_PATH, JSON.stringify({ source: "yahoo-finance:^GSPC", updatedAt: new Date().toISOString(), series }, null, 2));
|
|
console.log(`Wrote ${series.length} points to ${OUT_PATH}`);
|