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-equity-chart', standalone: true, imports: [CommonModule], template: `
`, styles: ` .equity-chart-container { width: 100%; height: 400px; margin-bottom: 20px; } ` }) export class EquityChartComponent 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(); } // Prepare data const equityCurve = this.calculateEquityCurve(this.backtestResult); // Create chart this.chart = new Chart(this.chartElement, { type: 'line', data: { labels: equityCurve.dates.map(date => this.formatDate(date)), datasets: [ { label: 'Portfolio Value', data: equityCurve.values, borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.2)', tension: 0.3, borderWidth: 2, fill: true }, { label: 'Benchmark', data: equityCurve.benchmark, borderColor: 'rgba(153, 102, 255, 0.5)', backgroundColor: 'rgba(153, 102, 255, 0.1)', borderDash: [5, 5], tension: 0.3, borderWidth: 1, fill: false } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { ticks: { maxTicksLimit: 12, maxRotation: 0, minRotation: 0 }, grid: { display: false } }, y: { ticks: { callback: function(value) { return '$' + value.toLocaleString(); } }, grid: { color: 'rgba(200, 200, 200, 0.2)' } } }, plugins: { tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) .format(context.parsed.y); } return label; } } }, legend: { position: 'top', } } } as ChartOptions }); } private calculateEquityCurve(result: BacktestResult): { dates: Date[]; values: number[]; benchmark: number[]; } { const initialValue = result.initialCapital; const dates: Date[] = []; const values: number[] = []; const benchmark: number[] = []; // Sort daily returns by date const sortedReturns = [...result.dailyReturns].sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); // Calculate cumulative portfolio values let portfolioValue = initialValue; let benchmarkValue = initialValue; for (const daily of sortedReturns) { const date = new Date(daily.date); portfolioValue = portfolioValue * (1 + daily.return); // Simple benchmark (e.g., assuming 8% annualized return for a market index) benchmarkValue = benchmarkValue * (1 + 0.08 / 365); dates.push(date); values.push(portfolioValue); benchmark.push(benchmarkValue); } return { dates, values, benchmark }; } private formatDate(date: Date): string { return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } }