stock-bot/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx
2025-07-03 09:55:13 -04:00

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>
);
}