278 lines
No EOL
11 KiB
TypeScript
278 lines
No EOL
11 KiB
TypeScript
import type { BacktestStatus } from '../types';
|
|
import type { BacktestResult } from '../services/backtestApi';
|
|
import { MetricsCard } from './MetricsCard';
|
|
import { PositionsTable } from './PositionsTable';
|
|
import { Chart } from '../../../components/charts';
|
|
import { useState, useMemo } from 'react';
|
|
|
|
interface BacktestResultsProps {
|
|
status: BacktestStatus;
|
|
results: BacktestResult | null;
|
|
currentTime: number | null;
|
|
}
|
|
|
|
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
|
|
if (status === 'idle') {
|
|
return (
|
|
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-medium text-text-primary mb-2">
|
|
Configure Your Backtest
|
|
</h3>
|
|
<p className="text-sm text-text-secondary">
|
|
Set up your strategy parameters and click "Configure Backtest" to begin.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'configured') {
|
|
return (
|
|
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-medium text-text-primary mb-2">
|
|
Ready to Start
|
|
</h3>
|
|
<p className="text-sm text-text-secondary">
|
|
Click the "Start" button to begin backtesting your strategy.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'running' && !results) {
|
|
return (
|
|
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-primary-500 mx-auto mb-4"></div>
|
|
<h3 className="text-lg font-medium text-text-primary mb-2">
|
|
Running Backtest...
|
|
</h3>
|
|
<p className="text-sm text-text-secondary">
|
|
Processing historical data and executing trades.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!results) {
|
|
return (
|
|
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-medium text-text-primary mb-2">
|
|
No Results Yet
|
|
</h3>
|
|
<p className="text-sm text-text-secondary">
|
|
Results will appear here once the backtest is complete.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 h-full overflow-y-auto">
|
|
{/* Metrics Overview */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<MetricsCard
|
|
title="Total Return"
|
|
value={`${results.metrics.totalReturn >= 0 ? '+' : ''}${results.metrics.totalReturn.toFixed(2)}%`}
|
|
trend={results.metrics.totalReturn >= 0 ? 'up' : 'down'}
|
|
/>
|
|
<MetricsCard
|
|
title="Sharpe Ratio"
|
|
value={results.metrics.sharpeRatio.toFixed(2)}
|
|
trend={results.metrics.sharpeRatio >= 1 ? 'up' : 'down'}
|
|
/>
|
|
<MetricsCard
|
|
title="Max Drawdown"
|
|
value={`${results.metrics.maxDrawdown.toFixed(2)}%`}
|
|
trend="down"
|
|
/>
|
|
<MetricsCard
|
|
title="Win Rate"
|
|
value={`${results.metrics.winRate.toFixed(1)}%`}
|
|
trend={results.metrics.winRate >= 50 ? 'up' : 'down'}
|
|
/>
|
|
<MetricsCard
|
|
title="Total Trades"
|
|
value={results.metrics.totalTrades.toString()}
|
|
/>
|
|
{results.metrics.profitFactor && (
|
|
<MetricsCard
|
|
title="Profit Factor"
|
|
value={results.metrics.profitFactor.toFixed(2)}
|
|
trend={results.metrics.profitFactor >= 1 ? 'up' : 'down'}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Performance Chart */}
|
|
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
|
<h3 className="text-base font-medium text-text-primary mb-4">
|
|
Portfolio Performance
|
|
</h3>
|
|
{(() => {
|
|
const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0;
|
|
const hasEquityData = results.equity && results.equity.length > 0;
|
|
|
|
if (hasOhlcData) {
|
|
const firstSymbol = Object.keys(results.ohlcData)[0];
|
|
const ohlcData = results.ohlcData[firstSymbol];
|
|
|
|
return (
|
|
<Chart
|
|
data={ohlcData}
|
|
height={400}
|
|
type="candlestick"
|
|
showVolume={true}
|
|
theme="dark"
|
|
overlayData={hasEquityData ? [
|
|
{
|
|
name: 'Portfolio Value',
|
|
data: results.equity.map(point => ({
|
|
time: Math.floor(new Date(point.date).getTime() / 1000),
|
|
value: point.value
|
|
})),
|
|
color: '#10b981',
|
|
lineWidth: 3
|
|
}
|
|
] : []}
|
|
className="rounded"
|
|
/>
|
|
);
|
|
} else if (hasEquityData) {
|
|
return (
|
|
<Chart
|
|
data={results.equity.map(point => ({
|
|
time: Math.floor(new Date(point.date).getTime() / 1000),
|
|
value: point.value
|
|
}))}
|
|
height={400}
|
|
type="area"
|
|
showVolume={false}
|
|
theme="dark"
|
|
className="rounded"
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<div className="h-96 bg-background rounded border border-border flex items-center justify-center">
|
|
<p className="text-sm text-text-muted">
|
|
No data available
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
})()}
|
|
</div>
|
|
|
|
{/* Trade History Table */}
|
|
{results.trades && results.trades.length > 0 && (
|
|
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
|
<h3 className="text-base font-medium text-text-primary mb-4">
|
|
Trade History ({results.trades.length} trades)
|
|
</h3>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-border">
|
|
<th className="text-left py-2 px-3 font-medium text-text-secondary">Date</th>
|
|
<th className="text-left py-2 px-3 font-medium text-text-secondary">Symbol</th>
|
|
<th className="text-center py-2 px-3 font-medium text-text-secondary">Side</th>
|
|
<th className="text-right py-2 px-3 font-medium text-text-secondary">Qty</th>
|
|
<th className="text-right py-2 px-3 font-medium text-text-secondary">Entry</th>
|
|
<th className="text-right py-2 px-3 font-medium text-text-secondary">Exit</th>
|
|
<th className="text-right py-2 px-3 font-medium text-text-secondary">P&L</th>
|
|
<th className="text-right py-2 px-3 font-medium text-text-secondary">Return</th>
|
|
<th className="text-right py-2 px-3 font-medium text-text-secondary">Duration</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{results.trades.slice().reverse().map((trade) => {
|
|
const formatDate = (dateStr: string) => {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: '2-digit'
|
|
});
|
|
};
|
|
|
|
const formatDuration = (ms: number) => {
|
|
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
|
|
if (days > 0) return `${days}d`;
|
|
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
if (hours > 0) return `${hours}h`;
|
|
return '<1h';
|
|
};
|
|
|
|
return (
|
|
<tr key={trade.id} className="border-b border-border hover:bg-surface-tertiary">
|
|
<td className="py-2 px-3 text-text-muted whitespace-nowrap">
|
|
{formatDate(trade.entryDate)}
|
|
</td>
|
|
<td className="py-2 px-3 font-medium text-text-primary">
|
|
{trade.symbol}
|
|
</td>
|
|
<td className="text-center py-2 px-3">
|
|
<span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
|
trade.side === 'buy'
|
|
? 'bg-success/10 text-success'
|
|
: 'bg-error/10 text-error'
|
|
}`}>
|
|
{trade.side.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td className="text-right py-2 px-3 text-text-primary">
|
|
{trade.quantity}
|
|
</td>
|
|
<td className="text-right py-2 px-3 text-text-primary">
|
|
${trade.entryPrice.toFixed(2)}
|
|
</td>
|
|
<td className="text-right py-2 px-3 text-text-primary">
|
|
${trade.exitPrice.toFixed(2)}
|
|
</td>
|
|
<td className={`text-right py-2 px-3 font-medium ${
|
|
trade.pnl >= 0 ? 'text-success' : 'text-error'
|
|
}`}>
|
|
{trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)}
|
|
</td>
|
|
<td className={`text-right py-2 px-3 font-medium ${
|
|
trade.pnlPercent >= 0 ? 'text-success' : 'text-error'
|
|
}`}>
|
|
{trade.pnlPercent >= 0 ? '+' : ''}{trade.pnlPercent.toFixed(2)}%
|
|
</td>
|
|
<td className="text-right py-2 px-3 text-text-muted">
|
|
{formatDuration(trade.duration)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
<tfoot className="border-t-2 border-border">
|
|
<tr className="font-medium">
|
|
<td colSpan={6} className="py-2 px-3 text-text-primary">
|
|
Total
|
|
</td>
|
|
<td className={`text-right py-2 px-3 ${
|
|
results.trades.reduce((sum, t) => sum + t.pnl, 0) >= 0 ? 'text-success' : 'text-error'
|
|
}`}>
|
|
${results.trades.reduce((sum, t) => sum + t.pnl, 0).toFixed(2)}
|
|
</td>
|
|
<td className="text-right py-2 px-3 text-text-secondary">
|
|
Avg: {(results.trades.reduce((sum, t) => sum + t.pnlPercent, 0) / results.trades.length).toFixed(2)}%
|
|
</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |