stock-bot/apps/stock/web-app/src/features/backtest/components/CompactPerformanceMetrics.tsx
2025-07-04 18:23:59 -04:00

297 lines
No EOL
12 KiB
TypeScript

import type { BacktestResult } from '../types/backtest.types';
interface ExtendedMetrics {
// Core metrics
totalReturn?: number;
sharpeRatio?: number;
maxDrawdown?: number;
winRate?: number;
totalProfit?: number;
profitFactor?: number;
totalTrades?: number;
avgWin?: number;
avgLoss?: number;
expectancy?: number;
sortinoRatio?: number;
calmarRatio?: number;
profitableTrades?: number;
// Extended metrics from orchestrator
annualizedReturn?: number;
volatility?: number;
avgHoldingPeriod?: number;
maxConsecutiveLosses?: number;
maxConsecutiveWins?: number;
payoffRatio?: number;
largestWin?: number;
largestLoss?: number;
avgWinLoss?: number;
skewness?: number;
kurtosis?: number;
tailRatio?: number;
kellyFraction?: number;
informationRatio?: number;
avgTradesPerDay?: number;
}
interface CompactPerformanceMetricsProps {
result: any | null;
isLoading: boolean;
}
export function CompactPerformanceMetrics({ result, isLoading }: CompactPerformanceMetricsProps) {
if (isLoading || !result) {
return null;
}
const metrics = result.metrics as ExtendedMetrics;
const analytics = result.analytics || {};
// Merge metrics from both sources
const allMetrics = {
...metrics,
...analytics,
// Override with metrics values if they exist
...metrics
};
// Calculate totalProfit if not provided
const totalProfit = allMetrics.totalProfit ??
(allMetrics.totalReturn && result.config?.initialCapital ?
allMetrics.totalReturn * result.config.initialCapital :
undefined);
const formatValue = (value: number | undefined, format: 'percent' | 'number' | 'currency', decimals = 2) => {
if (value === undefined || value === null) return '-';
switch (format) {
case 'percent':
return `${(value * 100).toFixed(decimals)}%`;
case 'currency':
const prefix = value < 0 ? '-$' : '$';
return `${prefix}${Math.abs(value).toFixed(decimals)}`;
case 'number':
return value.toFixed(decimals);
}
};
const getColorClass = (value: number | undefined, thresholds: { good: number; warning?: number }) => {
if (value === undefined || value === null) return 'text-text-secondary';
if (thresholds.warning !== undefined) {
if (value >= thresholds.good) return 'text-success';
if (value >= thresholds.warning) return 'text-warning';
return 'text-error';
}
return value >= thresholds.good ? 'text-success' : 'text-error';
};
return (
<div className="bg-surface-secondary rounded-lg border border-border p-4">
<h3 className="text-sm font-medium text-text-primary mb-3">Performance Analytics</h3>
<div className="grid grid-cols-3 gap-3">
{/* Column 1 - Return Metrics */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Total Return</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.totalReturn, { good: 0 })}`}>
{formatValue(allMetrics.totalReturn, 'percent')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Sharpe Ratio</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.sharpeRatio, { good: 1, warning: 0 })}`}>
{formatValue(allMetrics.sharpeRatio, 'number')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Max Drawdown</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.maxDrawdown, { good: -0.1, warning: -0.2 })}`}>
{formatValue(allMetrics.maxDrawdown, 'percent')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Win Rate</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.winRate, { good: 0.5 })}`}>
{formatValue(allMetrics.winRate, 'percent', 1)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Total Profit</span>
<span className={`text-sm font-medium ${getColorClass(totalProfit, { good: 0 })}`}>
{formatValue(totalProfit, 'currency')}
</span>
</div>
</div>
{/* Column 2 - Risk Metrics */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Profit Factor</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.profitFactor, { good: 1.5, warning: 1 })}`}>
{formatValue(allMetrics.profitFactor, 'number')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Total Trades</span>
<span className="text-sm font-medium text-text-primary">
{allMetrics.totalTrades ?? '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Avg Win/Loss</span>
<span className="text-sm font-medium text-text-primary">
<span className="text-success">{formatValue(allMetrics.avgWin, 'currency')}</span>
<span className="text-text-secondary mx-1">/</span>
<span className="text-error">{formatValue(allMetrics.avgLoss, 'currency')}</span>
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Expectancy</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.expectancy, { good: 0 })}`}>
{formatValue(allMetrics.expectancy, 'currency')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Sortino Ratio</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.sortinoRatio, { good: 1, warning: 0 })}`}>
{formatValue(allMetrics.sortinoRatio, 'number')}
</span>
</div>
</div>
{/* Column 3 - Additional Metrics */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Annual Return</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.annualizedReturn, { good: 0.1, warning: 0 })}`}>
{formatValue(allMetrics.annualizedReturn, 'percent')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Volatility</span>
<span className={`text-sm font-medium ${getColorClass(-(allMetrics.volatility ?? 0), { good: -15, warning: -25 })}`}>
{formatValue(allMetrics.volatility ? allMetrics.volatility / 100 : undefined, 'percent', 1)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Avg Holding</span>
<span className="text-sm font-medium text-text-primary">
{allMetrics.avgHoldingPeriod ? `${(allMetrics.avgHoldingPeriod / 60).toFixed(1)}h` : '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Max Consec Loss</span>
<span className={`text-sm font-medium ${allMetrics.maxConsecutiveLosses > 5 ? 'text-error' : 'text-text-primary'}`}>
{allMetrics.maxConsecutiveLosses ?? '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-text-secondary">Payoff Ratio</span>
<span className={`text-sm font-medium ${getColorClass(allMetrics.payoffRatio, { good: 1.5, warning: 1 })}`}>
{formatValue(allMetrics.payoffRatio, 'number')}
</span>
</div>
</div>
</div>
{/* Advanced Metrics - Show only if available */}
{(allMetrics.skewness !== undefined || allMetrics.kurtosis !== undefined ||
allMetrics.informationRatio !== undefined || allMetrics.kellyFraction !== undefined) && (
<div className="mt-2 pt-2 border-t border-border">
<div className="grid grid-cols-4 gap-2 text-xs">
{allMetrics.informationRatio !== undefined && (
<div className="text-center">
<span className="text-text-secondary">Info Ratio</span>
<div className={`font-medium ${getColorClass(allMetrics.informationRatio, { good: 0.5, warning: 0 })}`}>
{formatValue(allMetrics.informationRatio, 'number')}
</div>
</div>
)}
{allMetrics.skewness !== undefined && (
<div className="text-center">
<span className="text-text-secondary">Skewness</span>
<div className={`font-medium ${getColorClass(allMetrics.skewness, { good: 0 })}`}>
{formatValue(allMetrics.skewness, 'number')}
</div>
</div>
)}
{allMetrics.kurtosis !== undefined && (
<div className="text-center">
<span className="text-text-secondary">Kurtosis</span>
<div className="font-medium text-text-primary">
{formatValue(allMetrics.kurtosis, 'number')}
</div>
</div>
)}
{allMetrics.kellyFraction !== undefined && (
<div className="text-center">
<span className="text-text-secondary">Kelly %</span>
<div className="font-medium text-text-primary">
{formatValue(allMetrics.kellyFraction, 'percent')}
</div>
</div>
)}
</div>
</div>
)}
{/* Additional Metrics Row */}
<div className="mt-2 pt-2 border-t border-border">
<div className="grid grid-cols-5 gap-2 text-xs">
<div className="text-center">
<span className="text-text-secondary">Calmar</span>
<div className={`font-medium ${getColorClass(allMetrics.calmarRatio, { good: 1, warning: 0.5 })}`}>
{formatValue(allMetrics.calmarRatio, 'number')}
</div>
</div>
<div className="text-center">
<span className="text-text-secondary">Profitable</span>
<div className="font-medium text-text-primary">
{allMetrics.profitableTrades ?? '-'}/{allMetrics.totalTrades ?? '-'}
</div>
</div>
<div className="text-center">
<span className="text-text-secondary">Exposure</span>
<div className="font-medium text-text-primary">
{result.analytics?.exposureTime ? formatValue(result.analytics.exposureTime, 'percent', 1) : '-'}
</div>
</div>
<div className="text-center">
<span className="text-text-secondary">Largest Win</span>
<div className={`font-medium text-success`}>
{formatValue(allMetrics.largestWin, 'currency')}
</div>
</div>
<div className="text-center">
<span className="text-text-secondary">Largest Loss</span>
<div className={`font-medium text-error`}>
{formatValue(allMetrics.largestLoss, 'currency')}
</div>
</div>
</div>
</div>
</div>
);
}