import { CommonModule } from '@angular/common'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Chart, ChartOptions } from 'chart.js/auto'; import { BacktestResult } from '../../../services/strategy.service'; @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', }); } }