297 lines
No EOL
12 KiB
TypeScript
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>
|
|
);
|
|
} |