Files
carl-marketreturns/cumulative.html
T
2026-05-15 19:09:35 -04:00

298 lines
11 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 - Cumulative & Drawdown Cycles</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: 4px; 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); }
.chart-wrapper { position: relative; height: 440px; }
.chart-wrapper.small { height: 340px; }
h2 { font-size: 0.95rem; color: #aaa; margin-bottom: 16px; text-align: center; font-weight: 500; letter-spacing: 0.5px; text-transform: uppercase; }
.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 select { padding: 6px 10px; background: #1a1d27; border: 1px solid #333; border-radius: 6px; color: #e0e0e0; font-size: 0.85rem; }
.control-group select:focus { outline: none; border-color: #5b8def; }
.toggles { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 20px; }
.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; }
.cycle-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.cycle-table th { padding: 8px 10px; text-align: left; color: #888; font-weight: 500; border-bottom: 1px solid #2a2d37; white-space: nowrap; }
.cycle-table td { padding: 6px 10px; border-bottom: 1px solid #1f222c; white-space: nowrap; }
.cycle-table tr:hover td { background: rgba(255,255,255,0.02); }
.pos { color: #7ed321; }
.neg { color: #e74c3c; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
.back-link { display: inline-block; color: #5b8def; text-decoration: none; font-size: 0.82rem; margin-bottom: 16px; }
.back-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="nav-links">
<a href="index.html">&larr; Monthly Returns</a>
<a href="alltime-lows.html">Gains from ATL &rarr;</a>
</div>
<h1>Cumulative Returns & Drawdown Cycles</h1>
<p class="subtitle">Actual index values and drawdown from all-time highs</p>
<div class="controls">
<div class="control-group">
<label>Index:</label>
<select id="indexSelect">
<option value="S&P_500">S&P 500</option>
<option value="NASDAQ">NASDAQ</option>
<option value="DOW_JONES">Dow Jones</option>
<option value="RUSSELL_2000">Russell 2000</option>
</select>
</div>
</div>
<div class="chart-container">
<h2>Index Value Over Time</h2>
<div class="chart-wrapper">
<canvas id="cumulativeChart"></canvas>
</div>
</div>
<div class="chart-container">
<h2>Drawdown from All-Time High (%)</h2>
<div class="chart-wrapper small">
<canvas id="drawdownChart"></canvas>
</div>
</div>
<div class="chart-container">
<h2>Cycles: Peak-to-Trough Drawdowns</h2>
<div style="overflow-x:auto">
<table class="cycle-table" id="cycleTable">
<thead><tr>
<th>Peak Date</th><th>Peak Value</th><th>Trough Date</th><th>Trough Value</th>
<th>Drawdown</th><th>Duration</th><th>Recovery Date</th><th>Recovery Duration</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<script src="data/marketdata.js"></script>
<script>
const COLORS = {
'S&P_500': { border: '#5b8def', bg: 'rgba(91,141,239,0.12)', label: 'S&P 500' },
'NASDAQ': { border: '#f5a623', bg: 'rgba(245,166,35,0.12)', label: 'NASDAQ' },
'DOW_JONES': { border: '#7ed321', bg: 'rgba(126,211,33,0.12)', label: 'Dow Jones' },
'RUSSELL_2000': { border: '#e74c8b', bg: 'rgba(231,76,139,0.12)', label: 'Russell 2000' }
};
function buildSeries(indexName) {
const raw = RAW[indexName];
if (!raw) return { cumulative: [], drawdown: [], cycles: [] };
const sorted = [...raw].sort((a, b) => a[0] * 100 + a[1] - (b[0] * 100 + b[1]));
let ath = sorted[0][2];
let athDate = null;
const cumulative = [];
const drawdownSeries = [];
const cycles = [];
let currentCycle = null;
for (const [y, m, price] of sorted) {
const date = new Date(y, m - 1, 1);
cumulative.push({ x: date, y: price, year: y });
if (price > ath) {
if (athDate && currentCycle) {
currentCycle.recoveryDate = date;
currentCycle.recoveryDuration = (y - currentCycle.troughYear) * 12 + (m - currentCycle.troughMonth);
cycles.push(currentCycle);
currentCycle = null;
}
ath = price;
athDate = date;
}
const dd = ((price - ath) / ath) * 100;
drawdownSeries.push({ x: date, y: dd, year: y });
if (dd < 0 && !currentCycle && athDate) {
currentCycle = {
peakDate: athDate,
peakValue: ath,
peakYear: athDate.getFullYear(),
peakMonth: athDate.getMonth() + 1
};
}
if (currentCycle && price < (currentCycle.troughValue || Infinity)) {
currentCycle.troughDate = date;
currentCycle.troughValue = price;
currentCycle.troughYear = y;
currentCycle.troughMonth = m;
}
}
if (currentCycle) {
currentCycle.recoveryDate = null;
cycles.push(currentCycle);
}
const enrichedCycles = cycles.map(c => ({
...c,
drawdownPct: ((c.troughValue - c.peakValue) / c.peakValue) * 100,
peakDuration: (c.troughYear - c.peakYear) * 12 + (c.troughMonth - c.peakMonth)
})).filter(c => c.drawdownPct < -3);
return { cumulative, drawdown: drawdownSeries, cycles: enrichedCycles };
}
let cumChart = null;
let ddChart = null;
function updateCharts() {
const idx = document.getElementById('indexSelect').value;
const c = COLORS[idx];
const { cumulative, drawdown, cycles } = buildSeries(idx);
if (cumChart) cumChart.destroy();
const ctx1 = document.getElementById('cumulativeChart').getContext('2d');
cumChart = new Chart(ctx1, {
type: 'line',
data: {
datasets: [{
label: c.label,
data: cumulative.map(p => ({ x: p.x, y: p.y })),
borderColor: c.border,
backgroundColor: c.bg,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 4,
tension: 0.3,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#ccc', usePointStyle: true, pointStyle: 'line' } },
tooltip: {
backgroundColor: '#1a1d27',
titleColor: '#fff',
bodyColor: '#ccc',
borderColor: '#333',
borderWidth: 1,
callbacks: {
label: ctx => `$${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.toFixed(0) }
}
}
}
});
if (ddChart) ddChart.destroy();
const ctx2 = document.getElementById('drawdownChart').getContext('2d');
ddChart = new Chart(ctx2, {
type: 'line',
data: {
datasets: [{
label: c.label + ' Drawdown from ATH',
data: drawdown.map(p => ({ x: p.x, y: p.y })),
borderColor: '#e74c3c',
backgroundColor: 'rgba(231,76,60,0.12)',
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 4,
tension: 0.3,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#ccc', usePointStyle: true, pointStyle: 'line' } },
tooltip: {
backgroundColor: '#1a1d27',
titleColor: '#fff',
bodyColor: '#ccc',
borderColor: '#333',
borderWidth: 1,
callbacks: {
label: ctx => `${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.toFixed(0) + '%' },
max: 0
}
}
}
});
const tbody = document.querySelector('#cycleTable tbody');
tbody.innerHTML = '';
for (const cyc of cycles) {
const peakStr = cyc.peakDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
const troughStr = cyc.troughDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
const recovStr = cyc.recoveryDate ? cyc.recoveryDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short' }) : '<span class="neg">Not yet recovered</span>';
const recovDur = cyc.recoveryDuration ? cyc.recoveryDuration + ' months' : '—';
const peakDurStr = cyc.peakDuration + ' months';
tbody.innerHTML += `<tr>
<td>${peakStr}</td>
<td>$${cyc.peakValue.toFixed(2)}</td>
<td>${troughStr}</td>
<td>$${cyc.troughValue.toFixed(2)}</td>
<td class="neg">${cyc.drawdownPct.toFixed(1)}%</td>
<td>${peakDurStr}</td>
<td>${recovStr}</td>
<td>${recovDur}</td>
</tr>`;
}
}
document.getElementById('indexSelect').addEventListener('change', updateCharts);
updateCharts();
</script>
</body>
</html>