stock-bot/apps/dashboard/src/app/pages/strategies/components/performance-metrics.component.ts
2025-06-13 13:38:02 -04:00

322 lines
9.7 KiB
TypeScript

import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [CommonModule, MatCardModule, MatGridListModule, MatDividerModule, MatTooltipModule],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">
Total Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.totalReturn || 0)"
>
{{ formatPercent(backtestResult?.totalReturn || 0) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Annualized return (adjusted for the backtest duration)"
>
Annualized Return
</div>
<div
class="metric-value"
[ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)"
>
{{ formatPercent(backtestResult?.annualizedReturn || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{ formatPercent(backtestResult?.cagr || 0) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">
Max Drawdown
</div>
<div class="metric-value negative">
{{ formatPercent(backtestResult?.maxDrawdown || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">
Max DD Duration
</div>
<div class="metric-value">
{{ formatDays(backtestResult?.maxDrawdownDuration || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">
Volatility
</div>
<div class="metric-value">
{{ formatPercent(backtestResult?.volatility || 0) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Square root of the sum of the squares of drawdowns"
>
Ulcer Index
</div>
<div class="metric-value">
{{ (backtestResult?.ulcerIndex || 0).toFixed(4) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">
Sharpe Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)"
>
{{ (backtestResult?.sharpeRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">
Sortino Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)"
>
{{ (backtestResult?.sortinoRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">
Calmar Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)"
>
{{ (backtestResult?.calmarRatio || 0).toFixed(2) }}
</div>
</div>
<div class="metric">
<div
class="metric-name"
matTooltip="Probability-weighted ratio of gains vs. losses"
>
Omega Ratio
</div>
<div
class="metric-value"
[ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)"
>
{{ (backtestResult?.omegaRatio || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{ backtestResult?.totalTrades || 0 }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{ formatPercent(backtestResult?.winRate || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{ formatPercent(backtestResult?.averageWinningTrade || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{ formatPercent(backtestResult?.averageLosingTrade || 0) }}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">
Profit Factor
</div>
<div
class="metric-value"
[ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)"
>
{{ (backtestResult?.profitFactor || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4caf50;
}
.negative {
color: #f44336;
}
.neutral {
color: #ffa000;
}
mat-divider {
margin: 8px 0;
}
`,
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) {
return 'positive';
}
if (value < 0) {
return 'negative';
}
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) {
return 'positive';
}
if (value >= 1) {
return 'neutral';
}
if (value < 0) {
return 'negative';
}
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) {
return 'positive';
}
if (value >= 0.45) {
return 'neutral';
}
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) {
return 'positive';
}
if (value >= 1) {
return 'neutral';
}
return 'negative';
}
}