297 lines
11 KiB
HTML
297 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>
|
|
|
|
<a href="index.html" class="back-link">← Back to Monthly Returns</a>
|
|
<h1>Cumulative Returns & Drawdown Cycles</h1>
|
|
<p class="subtitle">Growth of $1 invested, with 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>Cumulative Return (Growth of $1)</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 cum = 1;
|
|
let ath = 1;
|
|
let athDate = null;
|
|
const cumulative = [];
|
|
const drawdownSeries = [];
|
|
const cycles = [];
|
|
let currentCycle = null;
|
|
|
|
for (const [y, m, ret] of sorted) {
|
|
cum *= (1 + ret / 100);
|
|
const date = new Date(y, m - 1, 1);
|
|
|
|
cumulative.push({ x: date, y: cum, year: y });
|
|
|
|
if (cum > ath) {
|
|
if (athDate && currentCycle) {
|
|
currentCycle.recoveryDate = date;
|
|
currentCycle.recoveryDuration = (y - currentCycle.troughYear) * 12 + (m - currentCycle.troughMonth);
|
|
cycles.push(currentCycle);
|
|
currentCycle = null;
|
|
}
|
|
ath = cum;
|
|
athDate = date;
|
|
}
|
|
|
|
const dd = ((cum - 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 && dd < ((currentCycle.troughValue || Infinity) - currentCycle.peakValue) / currentCycle.peakValue * 100) {
|
|
currentCycle.troughDate = date;
|
|
currentCycle.troughValue = cum;
|
|
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 + ' (Growth of $1)',
|
|
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>
|