adding backtest table / pages

This commit is contained in:
Boki 2025-07-04 14:27:34 -04:00
parent 38a6e73ad5
commit a876f3c35b
19 changed files with 1058 additions and 69 deletions

View file

@ -3,7 +3,7 @@ import { DashboardPage } from '@/features/dashboard';
import { ExchangesPage } from '@/features/exchanges';
import { MonitoringPage } from '@/features/monitoring';
import { PipelinePage } from '@/features/pipeline';
import { BacktestPage } from '@/features/backtest';
import { BacktestListPage, BacktestDetailPage } from '@/features/backtest';
import { SymbolsPage, SymbolDetailPage } from '@/features/symbols';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
@ -31,7 +31,10 @@ export function App() {
path="analytics"
element={<div className="p-4">Analytics Page - Coming Soon</div>}
/>
<Route path="backtest" element={<BacktestPage />} />
<Route path="backtests">
<Route index element={<BacktestListPage />} />
<Route path=":id" element={<BacktestDetailPage />} />
</Route>
<Route path="settings" element={<div className="p-4">Settings Page - Coming Soon</div>} />
<Route path="system/monitoring" element={<MonitoringPage />} />
<Route path="system/pipeline" element={<PipelinePage />} />

View file

@ -0,0 +1,252 @@
import { useCallback, useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { BacktestConfiguration } from './components/BacktestConfiguration';
import { BacktestMetrics } from './components/BacktestMetrics';
import { BacktestChart } from './components/BacktestChart';
import { BacktestTrades } from './components/BacktestTrades';
import { useBacktest } from './hooks/useBacktest';
import type { BacktestConfig, BacktestResult as LocalBacktestResult } from './types/backtest.types';
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
const tabs = [
{ id: 'settings', name: 'Settings' },
{ id: 'metrics', name: 'Performance Metrics' },
{ id: 'chart', name: 'Chart' },
{ id: 'trades', name: 'Trades' },
];
export function BacktestDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('settings');
const {
backtest,
results,
isLoading,
isPolling,
error,
loadBacktest,
createBacktest,
updateBacktest,
cancelBacktest,
} = useBacktest();
// Local state to bridge between the API format and the existing UI components
const [config, setConfig] = useState<BacktestConfig | null>(null);
const [adaptedResults, setAdaptedResults] = useState<LocalBacktestResult | null>(null);
// Load the specific backtest on mount
useEffect(() => {
if (id) {
loadBacktest(id);
}
}, [id, loadBacktest]);
// Adapt the backtest data to config format when loaded
useEffect(() => {
if (backtest && !config) {
const backtestConfig: BacktestConfig = {
name: backtest.config?.name || '',
startDate: new Date(backtest.startDate),
endDate: new Date(backtest.endDate),
initialCapital: backtest.initialCapital,
symbols: backtest.symbols,
strategy: backtest.strategy,
speedMultiplier: backtest.config?.speedMultiplier || 1,
commission: backtest.config?.commission || 0.001,
slippage: backtest.config?.slippage || 0.0001,
};
setConfig(backtestConfig);
}
}, [backtest, config]);
// Adapt the backtest status from API format to local format
const status = backtest ?
(backtest.status === 'pending' ? 'configured' :
backtest.status === 'running' ? 'running' :
backtest.status === 'completed' ? 'completed' :
backtest.status === 'failed' ? 'error' :
backtest.status === 'cancelled' ? 'stopped' : 'idle') : 'idle';
// Current time is not available in the new API, so we'll estimate it based on progress
const currentTime = null;
// No adaptation needed - results are already in the correct format
useEffect(() => {
setAdaptedResults(results);
}, [results]);
const handleRunBacktest = useCallback(async () => {
if (!config) return;
const backtestRequest = {
strategy: config.strategy,
symbols: config.symbols,
startDate: config.startDate.toISOString().split('T')[0],
endDate: config.endDate.toISOString().split('T')[0],
initialCapital: config.initialCapital,
config: {
name: config.name,
commission: config.commission,
slippage: config.slippage,
speedMultiplier: config.speedMultiplier,
useTypeScriptImplementation: true,
},
};
await createBacktest(backtestRequest);
}, [config, createBacktest]);
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
setConfig(newConfig);
setAdaptedResults(null);
const backtestRequest = {
strategy: newConfig.strategy,
symbols: newConfig.symbols,
startDate: newConfig.startDate.toISOString().split('T')[0],
endDate: newConfig.endDate.toISOString().split('T')[0],
initialCapital: newConfig.initialCapital,
config: {
name: newConfig.name,
commission: newConfig.commission,
slippage: newConfig.slippage,
speedMultiplier: newConfig.speedMultiplier,
useTypeScriptImplementation: true, // Enable TypeScript strategy execution
},
};
// If we have an existing backtest ID, update it, otherwise create new
if (id) {
await updateBacktest(id, backtestRequest);
} else {
await createBacktest(backtestRequest);
}
}, [id, createBacktest, updateBacktest]);
const handleStop = useCallback(async () => {
await cancelBacktest();
}, [cancelBacktest]);
const renderTabContent = () => {
switch (activeTab) {
case 'settings':
return (
<div className="max-w-4xl mx-auto">
<BacktestConfiguration
onSubmit={handleConfigSubmit}
disabled={status === 'running'}
initialConfig={config}
/>
</div>
);
case 'metrics':
return (
<div className="h-full overflow-y-auto">
<BacktestMetrics
result={adaptedResults}
isLoading={isLoading || isPolling}
/>
</div>
);
case 'chart':
return (
<BacktestChart
result={adaptedResults}
isLoading={isLoading || isPolling}
/>
);
case 'trades':
return (
<div className="h-full overflow-y-auto">
<BacktestTrades
result={adaptedResults}
isLoading={isLoading || isPolling}
/>
</div>
);
default:
return null;
}
};
return (
<div className="h-full flex flex-col">
{/* Header with Tabs */}
<div className="flex-shrink-0 border-b border-border">
<div className="flex items-center justify-between">
<div className="flex items-center">
<button
onClick={() => navigate('/backtests')}
className="p-2 hover:bg-surface-tertiary transition-colors"
aria-label="Back to backtests"
>
<ArrowLeftIcon className="h-5 w-5 text-text-secondary" />
</button>
<div className="px-2">
<h1 className="text-sm font-bold text-text-primary flex items-center gap-1">
{backtest?.config?.name || config?.name || 'Backtest Detail'}
{id && (
<span className="text-xs font-normal text-text-secondary">
ID: {id}
</span>
)}
</h1>
<p className="text-xs text-text-secondary">
{backtest?.strategy || config?.strategy || 'No strategy selected'}
</p>
</div>
{/* Tabs */}
<nav className="flex ml-4" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
whitespace-nowrap py-2 px-3 border-b-2 font-medium text-sm
${activeTab === tab.id
? 'border-primary-500 text-primary-500'
: 'border-transparent text-text-secondary hover:text-text-primary hover:border-border'
}
`}
>
{tab.name}
</button>
))}
</nav>
</div>
{/* Run/Stop button */}
<div className="pr-4">
{status === 'running' ? (
<button
onClick={handleStop}
className="px-4 py-1.5 bg-error/10 text-error rounded-md text-sm font-medium hover:bg-error/20 transition-colors"
>
Stop
</button>
) : status !== 'running' && config && config.symbols.length > 0 ? (
<button
onClick={handleRunBacktest}
className="px-4 py-1.5 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
>
Run Backtest
</button>
) : null}
</div>
</div>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-auto p-4">
{error && (
<div className="mb-4 p-4 bg-error/10 border border-error/20 rounded-lg">
<p className="text-sm text-error">{error}</p>
</div>
)}
{renderTabContent()}
</div>
</div>
);
}

View file

@ -1,14 +1,56 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useBacktestList } from './hooks/useBacktest';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import {
PlusIcon,
PlayIcon,
CheckCircleIcon,
XCircleIcon,
ClockIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/solid';
export function BacktestListPage() {
const { backtests, isLoading, error, loadBacktests } = useBacktestList();
const navigate = useNavigate();
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
loadBacktests();
// Refresh every 5 seconds if there are running backtests
const interval = setInterval(() => {
if (backtests.some(b => b.status === 'running' || b.status === 'pending')) {
loadBacktests();
}
}, 5000);
setRefreshInterval(interval);
return () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
};
}, [loadBacktests]);
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircleIcon className="w-4 h-4 text-success" />;
case 'running':
return <PlayIcon className="w-4 h-4 text-primary-400 animate-pulse" />;
case 'pending':
return <ClockIcon className="w-4 h-4 text-text-secondary" />;
case 'failed':
return <XCircleIcon className="w-4 h-4 text-error" />;
case 'cancelled':
return <ExclamationTriangleIcon className="w-4 h-4 text-text-muted" />;
default:
return <ClockIcon className="w-4 h-4 text-text-secondary" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
@ -37,12 +79,13 @@ export function BacktestListPage() {
View and manage your backtest runs
</p>
</div>
<Link
to="/backtests/new"
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
<button
onClick={() => navigate('/backtests/new')}
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors flex items-center space-x-2"
>
New Backtest
</Link>
<PlusIcon className="w-4 h-4" />
<span>New Backtest</span>
</button>
</div>
{error && (
@ -58,12 +101,13 @@ export function BacktestListPage() {
) : backtests.length === 0 ? (
<div className="bg-surface-secondary p-8 rounded-lg border border-border text-center">
<p className="text-text-secondary mb-4">No backtests found</p>
<Link
to="/backtests/new"
className="inline-flex px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
<button
onClick={() => navigate('/backtests/new')}
className="inline-flex items-center space-x-2 px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
>
Create Your First Backtest
</Link>
<PlusIcon className="w-4 h-4" />
<span>Create Your First Backtest</span>
</button>
</div>
) : (
<div className="bg-surface-secondary rounded-lg border border-border overflow-hidden">
@ -94,8 +138,13 @@ export function BacktestListPage() {
<td className="px-4 py-3 text-sm text-text-primary">
{new Date(backtest.startDate).toLocaleDateString()} - {new Date(backtest.endDate).toLocaleDateString()}
</td>
<td className={`px-4 py-3 text-sm font-medium capitalize ${getStatusColor(backtest.status)}`}>
{backtest.status}
<td className="px-4 py-3">
<div className="flex items-center space-x-2">
{getStatusIcon(backtest.status)}
<span className={`text-sm font-medium capitalize ${getStatusColor(backtest.status)}`}>
{backtest.status}
</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-text-secondary">
{formatDate(backtest.createdAt)}

View file

@ -48,6 +48,7 @@ export function BacktestPage() {
endDate: newConfig.endDate.toISOString().split('T')[0],
initialCapital: newConfig.initialCapital,
config: {
name: newConfig.name,
commission: newConfig.commission,
slippage: newConfig.slippage,
speedMultiplier: newConfig.speedMultiplier,

View file

@ -0,0 +1,93 @@
import type { BacktestResult } from '../types/backtest.types';
import { useState, useMemo } from 'react';
import { Chart } from '../../../components/charts';
interface BacktestChartProps {
result: BacktestResult | null;
isLoading: boolean;
}
export function BacktestChart({ result, isLoading }: BacktestChartProps) {
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
const chartData = useMemo(() => {
if (!result?.equity || !result?.ohlcData) return null;
const symbols = Object.keys(result.ohlcData);
const symbol = selectedSymbol || symbols[0] || '';
const ohlcData = result.ohlcData[symbol] || [];
const equityData = result.equity.map(e => ({
time: new Date(e.date).getTime() / 1000,
value: e.value
}));
// Find trades for this symbol
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
const tradeMarkers = symbolTrades.map(trade => ({
time: new Date(trade.entryDate).getTime() / 1000,
position: 'belowBar' as const,
color: trade.side === 'buy' ? '#10b981' : '#ef4444',
shape: trade.side === 'buy' ? 'arrowUp' : 'arrowDown' as const,
text: `${trade.side === 'buy' ? 'Buy' : 'Sell'} @ $${trade.entryPrice.toFixed(2)}`
}));
return {
ohlcData: ohlcData.map(d => ({
time: d.time / 1000,
open: d.open,
high: d.high,
low: d.low,
close: d.close,
volume: d.volume
})),
equityData,
tradeMarkers,
symbols
};
}, [result, selectedSymbol]);
if (isLoading) {
return (
<div className="bg-surface-secondary rounded-lg border border-border p-4 h-full animate-pulse">
<div className="h-full bg-surface rounded"></div>
</div>
);
}
if (!result || !chartData) {
return (
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center h-full flex items-center justify-center">
<p className="text-text-secondary">Run a backtest to see the equity curve and price chart.</p>
</div>
);
}
return (
<div className="h-full flex flex-col">
{chartData.symbols.length > 1 && (
<div className="flex space-x-2 mb-4">
<label className="text-sm text-text-secondary">Symbol:</label>
<select
value={selectedSymbol || chartData.symbols[0]}
onChange={(e) => setSelectedSymbol(e.target.value)}
className="px-2 py-1 bg-background border border-border rounded text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
>
{chartData.symbols.map(symbol => (
<option key={symbol} value={symbol}>{symbol}</option>
))}
</select>
</div>
)}
<div className="flex-1">
<Chart
data={chartData.ohlcData}
equityData={chartData.equityData}
markers={chartData.tradeMarkers}
height={500}
/>
</div>
</div>
);
}

View file

@ -1,13 +1,14 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import type { BacktestConfig } from '../types';
interface BacktestConfigurationProps {
onSubmit: (config: BacktestConfig) => void;
disabled?: boolean;
initialConfig?: BacktestConfig;
}
export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurationProps) {
const [formData, setFormData] = useState<BacktestConfig>({
export function BacktestConfiguration({ onSubmit, disabled, initialConfig }: BacktestConfigurationProps) {
const [formData, setFormData] = useState<BacktestConfig>(initialConfig || {
name: '',
startDate: new Date(new Date().setMonth(new Date().getMonth() - 6)), // 6 months ago
endDate: new Date(),
@ -21,6 +22,12 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
const [symbolInput, setSymbolInput] = useState('');
useEffect(() => {
if (initialConfig) {
setFormData(initialConfig);
}
}, [initialConfig]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (formData.symbols.length === 0) {
@ -62,10 +69,7 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
};
return (
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h2 className="text-base font-medium text-text-primary mb-4">Configuration</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-text-secondary mb-1">
Backtest Name
@ -257,9 +261,8 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat
className="w-full px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={disabled}
>
Configure Backtest
Save
</button>
</form>
</div>
);
}

View file

@ -0,0 +1,114 @@
import type { BacktestResult } from '../types/backtest.types';
import { MetricsCard } from './MetricsCard';
interface BacktestMetricsProps {
result: BacktestResult | null;
isLoading: boolean;
}
export function BacktestMetrics({ result, isLoading }: BacktestMetricsProps) {
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-surface-secondary rounded-lg border border-border p-4 h-24 animate-pulse">
<div className="h-4 bg-surface rounded w-1/2 mb-2"></div>
<div className="h-6 bg-surface rounded w-3/4"></div>
</div>
))}
</div>
);
}
if (!result) {
return (
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center">
<p className="text-text-secondary">Run a backtest to see performance metrics.</p>
</div>
);
}
const { metrics } = result;
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<MetricsCard
title="Total Return"
value={`${(metrics.totalReturn * 100).toFixed(2)}%`}
color={metrics.totalReturn >= 0 ? 'success' : 'error'}
/>
<MetricsCard
title="Sharpe Ratio"
value={metrics.sharpeRatio.toFixed(2)}
color={metrics.sharpeRatio > 1 ? 'success' : metrics.sharpeRatio > 0 ? 'warning' : 'error'}
/>
<MetricsCard
title="Max Drawdown"
value={`${(metrics.maxDrawdown * 100).toFixed(2)}%`}
color={metrics.maxDrawdown > -0.2 ? 'warning' : 'error'}
/>
<MetricsCard
title="Win Rate"
value={`${(metrics.winRate * 100).toFixed(1)}%`}
color={metrics.winRate > 0.5 ? 'success' : 'warning'}
/>
<MetricsCard
title="Total Trades"
value={metrics.totalTrades.toString()}
/>
<MetricsCard
title="Profit Factor"
value={metrics.profitFactor.toFixed(2)}
color={metrics.profitFactor > 1.5 ? 'success' : metrics.profitFactor > 1 ? 'warning' : 'error'}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-surface-secondary rounded-lg border border-border p-4">
<h4 className="text-sm font-medium text-text-secondary mb-3">Trade Statistics</h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Profitable Trades</span>
<span className="text-text-primary text-sm font-medium">{metrics.profitableTrades}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Average Win</span>
<span className="text-success text-sm font-medium">${metrics.avgWin.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Average Loss</span>
<span className="text-error text-sm font-medium">${Math.abs(metrics.avgLoss).toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Expectancy</span>
<span className={`text-sm font-medium ${metrics.expectancy >= 0 ? 'text-success' : 'text-error'}`}>
${metrics.expectancy.toFixed(2)}
</span>
</div>
</div>
</div>
<div className="bg-surface-secondary rounded-lg border border-border p-4">
<h4 className="text-sm font-medium text-text-secondary mb-3">Risk Metrics</h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Calmar Ratio</span>
<span className="text-text-primary text-sm font-medium">{metrics.calmarRatio.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Sortino Ratio</span>
<span className="text-text-primary text-sm font-medium">{metrics.sortinoRatio.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary text-sm">Exposure Time</span>
<span className="text-text-primary text-sm font-medium">
{result.analytics?.exposureTime ? `${(result.analytics.exposureTime * 100).toFixed(1)}%` : 'N/A'}
</span>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,143 @@
import type { BacktestResult } from '../types/backtest.types';
import { TradeLog } from './TradeLog';
interface BacktestTradesProps {
result: BacktestResult | null;
isLoading: boolean;
}
export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
if (isLoading) {
return (
<div className="bg-surface-secondary rounded-lg border border-border p-4 animate-pulse">
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-surface rounded"></div>
))}
</div>
</div>
);
}
if (!result || !result.trades || result.trades.length === 0) {
return (
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center">
<p className="text-text-secondary">No trades executed in this backtest.</p>
</div>
);
}
return (
<div>
<div className="mb-4">
<h3 className="text-base font-medium text-text-primary">Trade History</h3>
<p className="text-sm text-text-secondary mt-1">
Total: {result.trades.length} trades
</p>
</div>
<div className="overflow-x-auto bg-surface-secondary rounded-lg border border-border">
<table className="w-full">
<thead className="bg-background border-b border-border">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Symbol
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Side
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Entry Date
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Entry Price
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Exit Date
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Exit Price
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
Quantity
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
P&L
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-text-secondary uppercase tracking-wider">
P&L %
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{result.trades.map((trade) => (
<tr key={trade.id} className="hover:bg-background/50 transition-colors">
<td className="px-4 py-3 text-sm text-text-primary font-medium">
{trade.symbol}
</td>
<td className="px-4 py-3 text-sm">
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-md ${
trade.side === 'buy'
? 'bg-success/10 text-success'
: 'bg-error/10 text-error'
}`}>
{trade.side.toUpperCase()}
</span>
</td>
<td className="px-4 py-3 text-sm text-text-secondary">
{new Date(trade.entryDate).toLocaleString()}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
${trade.entryPrice.toFixed(2)}
</td>
<td className="px-4 py-3 text-sm text-text-secondary">
{trade.exitDate ? new Date(trade.exitDate).toLocaleString() : '-'}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
${trade.exitPrice.toFixed(2)}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
{trade.quantity}
</td>
<td className={`px-4 py-3 text-sm font-medium ${
trade.pnl >= 0 ? 'text-success' : 'text-error'
}`}>
${trade.pnl.toFixed(2)}
</td>
<td className={`px-4 py-3 text-sm font-medium ${
trade.pnlPercent >= 0 ? 'text-success' : 'text-error'
}`}>
{trade.pnlPercent.toFixed(2)}%
</td>
</tr>
))}
</tbody>
</table>
</div>
{result.positions && Object.keys(result.positions).length > 0 && (
<div className="mt-4 bg-surface-secondary rounded-lg border border-border p-4">
<h4 className="text-sm font-medium text-text-secondary mb-3">Open Positions</h4>
<div className="space-y-2">
{Object.entries(result.positions).map(([symbol, position]) => (
<div key={symbol} className="flex justify-between items-center p-2 bg-background rounded">
<span className="text-sm font-medium text-text-primary">{symbol}</span>
<div className="flex space-x-4 text-sm">
<span className="text-text-secondary">
Qty: {position.quantity}
</span>
<span className="text-text-secondary">
Avg: ${position.averagePrice.toFixed(2)}
</span>
<span className={position.unrealizedPnl >= 0 ? 'text-success' : 'text-error'}>
P&L: ${position.unrealizedPnl.toFixed(2)}
</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}

View file

@ -11,7 +11,9 @@ interface UseBacktestReturn {
error: string | null;
// Actions
loadBacktest: (id: string) => Promise<void>;
createBacktest: (request: BacktestRequest) => Promise<void>;
updateBacktest: (id: string, request: BacktestRequest) => Promise<void>;
cancelBacktest: () => Promise<void>;
reset: () => void;
}
@ -61,6 +63,35 @@ export function useBacktest(): UseBacktestReturn {
}
}, []);
// Load a specific backtest by ID
const loadBacktest = useCallback(async (id: string) => {
setIsLoading(true);
setError(null);
try {
const loadedBacktest = await backtestApi.getBacktest(id);
setBacktest(loadedBacktest);
// If completed, also load results
if (loadedBacktest.status === 'completed') {
const backtestResults = await backtestApi.getBacktestResults(id);
setResults(backtestResults);
}
// If running, start polling
if (loadedBacktest.status === 'running') {
setIsPolling(true);
pollingIntervalRef.current = setInterval(() => {
pollStatus(id);
}, 2000);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load backtest');
} finally {
setIsLoading(false);
}
}, [pollStatus]);
// Create a new backtest
const createBacktest = useCallback(async (request: BacktestRequest) => {
setIsLoading(true);
@ -75,7 +106,7 @@ export function useBacktest(): UseBacktestReturn {
setIsPolling(true);
pollingIntervalRef.current = setInterval(() => {
pollStatus(newBacktest.id);
}, 200); // Poll every 2 seconds
}, 2000); // Poll every 2 seconds
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create backtest');
@ -84,6 +115,29 @@ export function useBacktest(): UseBacktestReturn {
}
}, [pollStatus]);
// Update an existing backtest
const updateBacktest = useCallback(async (id: string, request: BacktestRequest) => {
setIsLoading(true);
setError(null);
try {
// For now, we'll delete and recreate since update isn't implemented in the API
await backtestApi.deleteBacktest(id);
const newBacktest = await backtestApi.createBacktest(request);
setBacktest(newBacktest);
// Start polling for updates
setIsPolling(true);
pollingIntervalRef.current = setInterval(() => {
pollStatus(newBacktest.id);
}, 2000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update backtest');
} finally {
setIsLoading(false);
}
}, [pollStatus]);
// Cancel running backtest
const cancelBacktest = useCallback(async () => {
if (!backtest || backtest.status !== 'running') return;
@ -133,7 +187,9 @@ export function useBacktest(): UseBacktestReturn {
isLoading,
isPolling,
error,
loadBacktest,
createBacktest,
updateBacktest,
cancelBacktest,
reset,
};

View file

@ -1,2 +1,4 @@
export { BacktestPage } from './BacktestPage';
export { BacktestListPage } from './BacktestListPage';
export { BacktestDetailPage } from './BacktestDetailPage';
export * from './types';

View file

@ -32,7 +32,7 @@ export const navigation: NavigationItem[] = [
{ name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },
{ name: 'Strategies', href: '/strategies', icon: DocumentTextIcon },
{ name: 'Analytics', href: '/analytics', icon: PresentationChartLineIcon },
{ name: 'Backtest', href: '/backtest', icon: BeakerIcon },
{ name: 'Backtests', href: '/backtests', icon: BeakerIcon },
{
name: 'System',
icon: ServerStackIcon,