217 lines
9.0 KiB
HTML
217 lines
9.0 KiB
HTML
<!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 style="display: flex; gap: 16px; justify-content: center; margin-bottom: 16px;">
|
|
<a href="cumulative.html" style="color: #5b8def; text-decoration: none; font-size: 0.82rem;">Drawdowns from ATH →</a>
|
|
<a href="alltime-lows.html" style="color: #7ed321; text-decoration: none; font-size: 0.82rem;">Gains from ATL →</a>
|
|
</div>
|
|
|
|
<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 = {};
|
|
const CLOSES = {};
|
|
for (const idx of Object.keys(RAW)) {
|
|
const sorted = [...RAW[idx]].sort((a, b) => a[0] * 100 + a[1] - (b[0] * 100 + b[1]));
|
|
CLOSES[idx] = sorted.map(([y, m, price]) => ({
|
|
x: new Date(y, m - 1, 1),
|
|
price,
|
|
year: y
|
|
}));
|
|
DATA[idx] = sorted.map(([y, m, price], i) => {
|
|
const prev = i > 0 ? sorted[i - 1][2] : price;
|
|
const monthlyReturn = ((price - prev) / prev) * 100;
|
|
return { x: new Date(y, m - 1, 1), y: monthlyReturn, 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>
|