import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { BacktestResult } from '../../../services/strategy.service'; import { Chart, ChartOptions } from 'chart.js/auto'; @Component({ selector: 'app-drawdown-chart', standalone: true, imports: [CommonModule], template: `
`, styles: ` .drawdown-chart-container { width: 100%; height: 300px; margin-bottom: 20px; } ` }) export class DrawdownChartComponent implements OnChanges { @Input() backtestResult?: BacktestResult; private chart?: Chart; private chartElement?: HTMLCanvasElement; ngOnChanges(changes: SimpleChanges): void { if (changes['backtestResult'] && this.backtestResult) { this.renderChart(); } } ngAfterViewInit(): void { this.chartElement = document.querySelector('canvas') as HTMLCanvasElement; if (this.backtestResult) { this.renderChart(); } } private renderChart(): void { if (!this.chartElement || !this.backtestResult) return; // Clean up previous chart if it exists if (this.chart) { this.chart.destroy(); } // Calculate drawdown series from daily returns const drawdownData = this.calculateDrawdownSeries(this.backtestResult); // Create chart this.chart = new Chart(this.chartElement, { type: 'line', data: { labels: drawdownData.dates.map(date => this.formatDate(date)), datasets: [ { label: 'Drawdown', data: drawdownData.drawdowns, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: true, tension: 0.3, borderWidth: 2 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { ticks: { maxTicksLimit: 12, maxRotation: 0, minRotation: 0 }, grid: { display: false } }, y: { ticks: { callback: function(value) { return (value * 100).toFixed(1) + '%'; } }, grid: { color: 'rgba(200, 200, 200, 0.2)' }, min: -0.05, // Show at least 5% drawdown for context suggestedMax: 0.01 } }, plugins: { tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += (context.parsed.y * 100).toFixed(2) + '%'; } return label; } } }, legend: { position: 'top', } } } as ChartOptions }); } private calculateDrawdownSeries(result: BacktestResult): { dates: Date[]; drawdowns: number[]; } { const dates: Date[] = []; const drawdowns: number[] = []; // Sort daily returns by date const sortedReturns = [...result.dailyReturns].sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); // Calculate equity curve let equity = 1; const equityCurve: number[] = []; for (const daily of sortedReturns) { equity *= (1 + daily.return); equityCurve.push(equity); dates.push(new Date(daily.date)); } // Calculate running maximum (high water mark) let hwm = equityCurve[0]; for (let i = 0; i < equityCurve.length; i++) { // Update high water mark hwm = Math.max(hwm, equityCurve[i]); // Calculate drawdown as percentage from high water mark const drawdown = (equityCurve[i] / hwm) - 1; drawdowns.push(drawdown); } return { dates, drawdowns }; } private formatDate(date: Date): string { return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } }