initial version. downloads historical market data. creates a graph from peak to trough.
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Market Returns by Index</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f1117; color: #e0e0e0; padding: 24px; }
|
||||
h1 { text-align: center; margin-bottom: 8px; font-size: 1.6rem; color: #fff; }
|
||||
.subtitle { text-align: center; color: #888; margin-bottom: 24px; font-size: 0.85rem; }
|
||||
.chart-container { background: #1a1d27; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 4px 24px rgba(0,0,0,0.4); position: relative; }
|
||||
.chart-wrapper { position: relative; height: 520px; }
|
||||
.controls { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; justify-content: center; margin-bottom: 20px; }
|
||||
.control-group { display: flex; align-items: center; gap: 8px; }
|
||||
.control-group label { font-size: 0.8rem; color: #aaa; }
|
||||
.control-group input[type="number"] { width: 80px; padding: 6px 10px; background: #1a1d27; border: 1px solid #333; border-radius: 6px; color: #e0e0e0; font-size: 0.85rem; }
|
||||
.control-group input[type="number"]:focus { outline: none; border-color: #5b8def; }
|
||||
.toggles { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
||||
.toggle-btn { padding: 8px 18px; border: 2px solid; border-radius: 8px; background: transparent; cursor: pointer; font-size: 0.82rem; font-weight: 600; transition: all 0.2s; }
|
||||
.toggle-btn.active { color: #fff; }
|
||||
.toggle-btn.sp500 { border-color: #5b8def; color: #5b8def; }
|
||||
.toggle-btn.sp500.active { background: #5b8def; }
|
||||
.toggle-btn.nasdaq { border-color: #f5a623; color: #f5a623; }
|
||||
.toggle-btn.nasdaq.active { background: #f5a623; }
|
||||
.toggle-btn.dj { border-color: #7ed321; color: #7ed321; }
|
||||
.toggle-btn.dj.active { background: #7ed321; }
|
||||
.toggle-btn.r2k { border-color: #e74c8b; color: #e74c8b; }
|
||||
.toggle-btn.r2k.active { background: #e74c8b; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
||||
.stat-card { background: #1a1d27; border-radius: 12px; padding: 20px; box-shadow: 0 4px 24px rgba(0,0,0,0.4); }
|
||||
.stat-card h3 { font-size: 0.85rem; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
||||
.stat-card h3 .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
||||
.stat-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 0.8rem; }
|
||||
.stat-row .label { color: #888; }
|
||||
.stat-row .value { font-weight: 600; }
|
||||
.stat-row .value.positive { color: #7ed321; }
|
||||
.stat-row .value.negative { color: #e74c3c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Monthly Market Returns by Index</h1>
|
||||
<p class="subtitle">S&P 500, NASDAQ, Dow Jones, Russell 2000</p>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>From:</label>
|
||||
<input type="number" id="yearFrom" value="1986" min="1986" max="2026">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>To:</label>
|
||||
<input type="number" id="yearTo" value="2026" min="1986" max="2026">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggles">
|
||||
<button class="toggle-btn sp500 active" data-index="S&P_500">S&P 500</button>
|
||||
<button class="toggle-btn nasdaq active" data-index="NASDAQ">NASDAQ</button>
|
||||
<button class="toggle-btn dj active" data-index="DOW_JONES">Dow Jones</button>
|
||||
<button class="toggle-btn r2k active" data-index="RUSSELL_2000">Russell 2000</button>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="returnsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" id="statsGrid"></div>
|
||||
|
||||
<script src="data/marketdata.js"></script>
|
||||
<script>
|
||||
const DATA = {};
|
||||
for (const idx of Object.keys(RAW)) {
|
||||
DATA[idx] = RAW[idx].map(([y, m, r]) => ({
|
||||
x: new Date(y, m - 1, 1),
|
||||
y: r,
|
||||
year: y
|
||||
}));
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
'S&P_500': { border: '#5b8def', bg: 'rgba(91,141,239,0.15)', label: 'S&P 500' },
|
||||
'NASDAQ': { border: '#f5a623', bg: 'rgba(245,166,35,0.15)', label: 'NASDAQ' },
|
||||
'DOW_JONES': { border: '#7ed321', bg: 'rgba(126,211,33,0.15)', label: 'Dow Jones' },
|
||||
'RUSSELL_2000': { border: '#e74c8b', bg: 'rgba(231,76,139,0.15)', label: 'Russell 2000' }
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
|
||||
function buildChart() {
|
||||
const from = parseInt(document.getElementById('yearFrom').value);
|
||||
const to = parseInt(document.getElementById('yearTo').value);
|
||||
const activeIndices = [...document.querySelectorAll('.toggle-btn.active')].map(b => b.dataset.index);
|
||||
|
||||
const datasets = [];
|
||||
for (const idx of activeIndices) {
|
||||
const c = COLORS[idx];
|
||||
const points = (DATA[idx] || []).filter(p => p.year >= from && p.year <= to);
|
||||
datasets.push({
|
||||
label: c.label,
|
||||
data: points.map(p => ({ x: p.x, y: p.y })),
|
||||
borderColor: c.border,
|
||||
backgroundColor: c.bg,
|
||||
borderWidth: 1.5,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 5,
|
||||
tension: 0.1,
|
||||
fill: false
|
||||
});
|
||||
}
|
||||
|
||||
if (chart) chart.destroy();
|
||||
|
||||
const ctx = document.getElementById('returnsChart').getContext('2d');
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#ccc', usePointStyle: true, pointStyle: 'line', padding: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: '#1a1d27',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#ccc',
|
||||
borderColor: '#333',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y >= 0 ? '+' : ''}${ctx.parsed.y.toFixed(2)}%`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: { unit: 'year', displayFormats: { year: 'yyyy' } },
|
||||
grid: { color: 'rgba(255,255,255,0.06)' },
|
||||
ticks: { color: '#888', maxTicksLimit: 20 }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(255,255,255,0.06)' },
|
||||
ticks: {
|
||||
color: '#888',
|
||||
callback: v => v + '%'
|
||||
},
|
||||
title: { display: true, text: 'Monthly Return (%)', color: '#888' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateStats(activeIndices, from, to);
|
||||
}
|
||||
|
||||
function updateStats(indices, from, to) {
|
||||
const grid = document.getElementById('statsGrid');
|
||||
grid.innerHTML = '';
|
||||
for (const idx of indices) {
|
||||
const c = COLORS[idx];
|
||||
const points = (DATA[idx] || []).filter(p => p.year >= from && p.year <= to);
|
||||
if (points.length === 0) continue;
|
||||
const returns = points.map(p => p.y);
|
||||
const avg = returns.reduce((a, b) => a + b, 0) / returns.length;
|
||||
const best = Math.max(...returns);
|
||||
const worst = Math.min(...returns);
|
||||
const positive = returns.filter(r => r > 0).length;
|
||||
const negative = returns.filter(r => r < 0).length;
|
||||
|
||||
grid.innerHTML += `
|
||||
<div class="stat-card">
|
||||
<h3><span class="dot" style="background:${c.border}"></span>${c.label}</h3>
|
||||
<div class="stat-row"><span class="label">Avg Monthly</span><span class="value ${avg >= 0 ? 'positive' : 'negative'}">${avg >= 0 ? '+' : ''}${avg.toFixed(2)}%</span></div>
|
||||
<div class="stat-row"><span class="label">Best Month</span><span class="value positive">+${best.toFixed(2)}%</span></div>
|
||||
<div class="stat-row"><span class="label">Worst Month</span><span class="value negative">${worst.toFixed(2)}%</span></div>
|
||||
<div class="stat-row"><span class="label">Positive</span><span class="value">${positive} (${(positive / returns.length * 100).toFixed(0)}%)</span></div>
|
||||
<div class="stat-row"><span class="label">Negative</span><span class="value">${negative} (${(negative / returns.length * 100).toFixed(0)}%)</span></div>
|
||||
<div class="stat-row"><span class="label">Data Points</span><span class="value">${returns.length}</span></div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.classList.toggle('active');
|
||||
buildChart();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('yearFrom').addEventListener('change', buildChart);
|
||||
document.getElementById('yearTo').addEventListener('change', buildChart);
|
||||
|
||||
buildChart();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user