messy work. backtests / mock-data

This commit is contained in:
Boki 2025-07-03 08:37:23 -04:00
parent 4e4a048988
commit fa70ada2bb
51 changed files with 2576 additions and 887 deletions

View file

@ -0,0 +1,215 @@
import * as LightweightCharts from 'lightweight-charts';
import { useEffect, useRef, useState } from 'react';
export interface ChartData {
time: number;
open?: number;
high?: number;
low?: number;
close?: number;
value?: number;
volume?: number;
}
export interface ChartProps {
data: ChartData[];
height?: number;
type?: 'candlestick' | 'line' | 'area';
showVolume?: boolean;
theme?: 'light' | 'dark';
overlayData?: Array<{
name: string;
data: Array<{ time: number; value: number }>;
color?: string;
lineWidth?: number;
}>;
className?: string;
}
export function Chart({
data,
height = 400,
type = 'candlestick',
showVolume = true,
theme = 'dark',
overlayData = [],
className = '',
}: ChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<LightweightCharts.IChartApi | null>(null);
const mainSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
// Debug logging
console.log('Chart - data received:', data);
console.log('Chart - data length:', data?.length);
console.log('Chart - data type:', Array.isArray(data) ? 'array' : typeof data);
console.log('Chart - first data point:', data?.[0]);
useEffect(() => {
if (!chartContainerRef.current || !data || !data.length) {
console.log('Chart - early return:', {
hasContainer: !!chartContainerRef.current,
hasData: !!data,
dataLength: data?.length
});
return;
}
// Create chart
const chart = LightweightCharts.createChart(chartContainerRef.current, {
width: chartContainerRef.current.clientWidth,
height: height,
layout: {
background: {
type: LightweightCharts.ColorType.Solid,
color: theme === 'dark' ? '#0f0f0f' : '#ffffff'
},
textColor: theme === 'dark' ? '#d1d5db' : '#374151',
},
grid: {
vertLines: {
color: theme === 'dark' ? '#1f2937' : '#e5e7eb',
visible: true,
},
horzLines: {
color: theme === 'dark' ? '#1f2937' : '#e5e7eb',
visible: true,
},
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb',
autoScale: true,
},
timeScale: {
borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb',
timeVisible: true,
secondsVisible: false,
},
});
chartRef.current = chart;
// Create main series
if (type === 'candlestick' && data[0].open !== undefined) {
mainSeriesRef.current = chart.addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#10b981',
wickDownColor: '#ef4444',
});
mainSeriesRef.current.setData(data as LightweightCharts.CandlestickData[]);
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) {
mainSeriesRef.current = chart.addLineSeries({
color: '#3b82f6',
lineWidth: 2,
});
const lineData = data.map(d => ({
time: d.time,
value: d.value ?? d.close ?? 0
}));
mainSeriesRef.current.setData(lineData);
} else if (type === 'area') {
mainSeriesRef.current = chart.addAreaSeries({
lineColor: '#3b82f6',
topColor: '#3b82f6',
bottomColor: 'rgba(59, 130, 246, 0.1)',
lineWidth: 2,
});
const areaData = data.map(d => ({
time: d.time,
value: d.value ?? d.close ?? 0
}));
mainSeriesRef.current.setData(areaData);
}
// Add volume if available
if (showVolume && data.some(d => d.volume)) {
volumeSeriesRef.current = chart.addHistogramSeries({
color: '#3b82f680',
priceFormat: {
type: 'volume',
},
priceScaleId: 'volume',
});
volumeSeriesRef.current.priceScale().applyOptions({
scaleMargins: {
top: 0.8,
bottom: 0,
},
});
const volumeData = data
.filter(d => d.volume !== undefined)
.map(d => ({
time: d.time,
value: d.volume!,
color: d.close && d.open ?
(d.close >= d.open ? '#10b98140' : '#ef444440') :
'#3b82f640',
}));
volumeSeriesRef.current.setData(volumeData);
}
// Add overlay series
overlaySeriesRef.current.clear();
overlayData.forEach((overlay, index) => {
const series = chart.addLineSeries({
color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4],
lineWidth: overlay.lineWidth || 2,
title: overlay.name,
priceScaleId: index === 0 ? '' : `overlay-${index}`, // First overlay uses main scale
});
if (index > 0) {
series.priceScale().applyOptions({
scaleMargins: {
top: 0.1,
bottom: 0.1,
},
});
}
series.setData(overlay.data);
overlaySeriesRef.current.set(overlay.name, series);
});
// Fit content
chart.timeScale().fitContent();
// Handle resize
const handleResize = () => {
if (chartContainerRef.current && chart) {
chart.applyOptions({
width: chartContainerRef.current.clientWidth,
});
}
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.remove();
}
};
}, [data, height, type, showVolume, theme, overlayData]);
return (
<div className={`relative ${className}`}>
<div
ref={chartContainerRef}
style={{ width: '100%', height: `${height}px` }}
/>
</div>
);
}

View file

@ -0,0 +1,2 @@
export { Chart } from './Chart';
export type { ChartProps, ChartData } from './Chart';

View file

@ -0,0 +1,119 @@
import { useEffect } from 'react';
import { useBacktestList } from './hooks/useBacktest';
import { Link } from 'react-router-dom';
export function BacktestListPage() {
const { backtests, isLoading, error, loadBacktests } = useBacktestList();
useEffect(() => {
loadBacktests();
}, [loadBacktests]);
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'text-success';
case 'running':
return 'text-primary-400';
case 'failed':
return 'text-error';
case 'cancelled':
return 'text-text-muted';
default:
return 'text-text-secondary';
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
return (
<div className="flex flex-col h-full space-y-6">
<div className="flex-shrink-0 flex justify-between items-center">
<div>
<h1 className="text-lg font-bold text-text-primary mb-2">Backtest History</h1>
<p className="text-text-secondary text-sm">
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"
>
New Backtest
</Link>
</div>
{error && (
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="text-text-secondary">Loading backtests...</div>
</div>
) : 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"
>
Create Your First Backtest
</Link>
</div>
) : (
<div className="bg-surface-secondary rounded-lg border border-border overflow-hidden">
<table className="w-full">
<thead className="bg-background border-b border-border">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">ID</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Strategy</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Symbols</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Period</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Status</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Created</th>
<th className="px-4 py-3 text-left text-sm font-medium text-text-secondary">Actions</th>
</tr>
</thead>
<tbody>
{backtests.map((backtest) => (
<tr key={backtest.id} className="border-b border-border hover:bg-background/50 transition-colors">
<td className="px-4 py-3 text-sm text-text-primary font-mono">
{backtest.id.slice(0, 8)}...
</td>
<td className="px-4 py-3 text-sm text-text-primary capitalize">
{backtest.strategy}
</td>
<td className="px-4 py-3 text-sm text-text-primary">
{backtest.symbols.join(', ')}
</td>
<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>
<td className="px-4 py-3 text-sm text-text-secondary">
{formatDate(backtest.createdAt)}
</td>
<td className="px-4 py-3 text-sm">
<Link
to={`/backtests/${backtest.id}`}
className="text-primary-400 hover:text-primary-300 font-medium"
>
View
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}

View file

@ -1,23 +1,114 @@
import { useCallback, useEffect, useState } from 'react';
import { BacktestConfiguration } from './components/BacktestConfiguration';
import { BacktestResults } from './components/BacktestResults';
import { BacktestControls } from './components/BacktestControls';
import { BacktestResults } from './components/BacktestResults';
import { useBacktest } from './hooks/useBacktest';
import type { BacktestConfig, BacktestResult as LocalBacktestResult } from './types/backtest.types';
export function BacktestPage() {
const {
config,
status,
backtest,
results,
currentTime,
error,
isLoading,
handleConfigSubmit,
handleStart,
handlePause,
handleResume,
handleStop,
handleStep,
isPolling,
error,
createBacktest,
cancelBacktest,
reset,
} = 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);
// 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;
// Adapt the results when they come in
useEffect(() => {
if (results && config) {
setAdaptedResults({
id: backtest?.id || '',
config,
metrics: {
totalReturn: results.metrics.totalReturn,
sharpeRatio: results.metrics.sharpeRatio,
maxDrawdown: results.metrics.maxDrawdown,
winRate: results.metrics.winRate,
totalTrades: results.metrics.totalTrades,
profitableTrades: Math.round(results.metrics.totalTrades * results.metrics.winRate / 100),
},
positions: [], // Not provided by current API
trades: results.trades?.map(t => ({
id: `${t.symbol}-${t.entryDate}`,
timestamp: t.exitDate,
symbol: t.symbol,
side: t.pnl > 0 ? 'buy' : 'sell',
quantity: t.quantity,
price: t.exitPrice,
commission: 0,
pnl: t.pnl,
})) || [],
performanceData: results.equity.map(e => ({
timestamp: e.date,
portfolioValue: e.value,
pnl: 0, // Would need to calculate from equity curve
drawdown: 0, // Would need to calculate
})),
});
}
}, [results, config, backtest]);
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
setConfig(newConfig);
setAdaptedResults(null);
// Convert local config to API format
await createBacktest({
strategy: newConfig.strategy,
symbols: newConfig.symbols,
startDate: newConfig.startDate.toISOString().split('T')[0],
endDate: newConfig.endDate.toISOString().split('T')[0],
initialCapital: newConfig.initialCapital,
config: {
commission: newConfig.commission,
slippage: newConfig.slippage,
speedMultiplier: newConfig.speedMultiplier,
},
});
}, [createBacktest]);
const handleStart = useCallback(() => {
// Backtest starts automatically after creation in the new API
// Nothing to do here
}, []);
const handlePause = useCallback(() => {
// Pause not supported in current API
console.warn('Pause not supported in current API');
}, []);
const handleResume = useCallback(() => {
// Resume not supported in current API
console.warn('Resume not supported in current API');
}, []);
const handleStop = useCallback(async () => {
await cancelBacktest();
}, [cancelBacktest]);
const handleStep = useCallback(() => {
// Step not supported in current API
console.warn('Step not supported in current API');
}, []);
return (
<div className="flex flex-col h-full space-y-6">
@ -38,7 +129,7 @@ export function BacktestPage() {
<div className="lg:col-span-1 space-y-4">
<BacktestConfiguration
onSubmit={handleConfigSubmit}
disabled={status === 'running' || isLoading}
disabled={status === 'running' || isLoading || isPolling}
/>
{config && (
@ -59,7 +150,7 @@ export function BacktestPage() {
<div className="lg:col-span-2">
<BacktestResults
status={status}
results={results}
results={adaptedResults}
currentTime={currentTime}
/>
</div>

View file

@ -1,7 +1,10 @@
import type { BacktestResult, BacktestStatus } from '../types';
import type { BacktestStatus } from '../types';
import type { BacktestResult } from '../services/backtestApi';
import { MetricsCard } from './MetricsCard';
import { PositionsTable } from './PositionsTable';
import { TradeLog } from './TradeLog';
import { Chart } from '../../../components/charts';
import { useState, useMemo } from 'react';
interface BacktestResultsProps {
status: BacktestStatus;
@ -10,6 +13,11 @@ interface BacktestResultsProps {
}
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
// Debug logging
console.log('BacktestResults - results:', results);
console.log('BacktestResults - ohlcData keys:', results?.ohlcData ? Object.keys(results.ohlcData) : 'No ohlcData');
console.log('BacktestResults - first symbol data:', results?.ohlcData && Object.keys(results.ohlcData).length > 0 ? results.ohlcData[Object.keys(results.ohlcData)[0]] : 'No data');
console.log('BacktestResults - equity data:', results?.equity);
if (status === 'idle') {
return (
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
@ -99,41 +107,98 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult
title="Total Trades"
value={results.metrics.totalTrades.toString()}
/>
<MetricsCard
title="Profitable Trades"
value={results.metrics.profitableTrades.toString()}
/>
{results.metrics.profitFactor && (
<MetricsCard
title="Profit Factor"
value={results.metrics.profitFactor.toFixed(2)}
trend={results.metrics.profitFactor >= 1 ? 'up' : 'down'}
/>
)}
</div>
{/* Performance Chart Placeholder */}
{/* 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>
<div className="h-64 bg-background rounded border border-border flex items-center justify-center">
<p className="text-sm text-text-muted">
Performance chart will be displayed here (requires recharts)
</p>
</div>
{(() => {
const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0;
const hasEquityData = results.equity && results.equity.length > 0;
console.log('Chart section - hasOhlcData:', hasOhlcData);
console.log('Chart section - hasEquityData:', hasEquityData);
if (hasOhlcData) {
const firstSymbol = Object.keys(results.ohlcData)[0];
const ohlcData = results.ohlcData[firstSymbol];
console.log('Chart section - using OHLC data for symbol:', firstSymbol);
console.log('Chart section - OHLC data points:', ohlcData?.length);
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) {
console.log('Chart section - using equity data only');
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 {
console.log('Chart section - showing no data message');
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>
{/* Positions Table */}
{results.positions.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">
Current Positions
</h3>
<PositionsTable positions={results.positions} />
</div>
)}
{/* Trade Log */}
{results.trades.length > 0 && (
{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
</h3>
<TradeLog trades={results.trades} />
<TradeLog trades={results.trades.map(trade => ({
id: crypto.randomUUID(),
timestamp: trade.entryDate,
symbol: trade.symbol,
side: 'buy' as const,
quantity: trade.quantity,
price: trade.entryPrice,
commission: 0,
pnl: trade.pnl
}))} />
</div>
)}
</div>

View file

@ -1,169 +1,175 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { BacktestService } from '../services/backtestService';
import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types';
import type { BacktestJob, BacktestRequest, BacktestResult } from '../services/backtestApi';
import { backtestApi, } from '../services/backtestApi';
export function useBacktest() {
const [backtestId, setBacktestId] = useState<string | null>(null);
const [config, setConfig] = useState<BacktestConfig | null>(null);
const [status, setStatus] = useState<BacktestStatus>('idle');
interface UseBacktestReturn {
// State
backtest: BacktestJob | null;
results: BacktestResult | null;
isLoading: boolean;
isPolling: boolean;
error: string | null;
// Actions
createBacktest: (request: BacktestRequest) => Promise<void>;
cancelBacktest: () => Promise<void>;
reset: () => void;
}
export function useBacktest(): UseBacktestReturn {
const [backtest, setBacktest] = useState<BacktestJob | null>(null);
const [results, setResults] = useState<BacktestResult | null>(null);
const [currentTime, setCurrentTime] = useState<number | null>(null);
const [progress, setProgress] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isPolling, setIsPolling] = useState(false);
const [error, setError] = useState<string | null>(null);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const cleanupRef = useRef<(() => void) | null>(null);
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
// Poll for status updates
const pollStatus = useCallback(async (backtestId: string) => {
try {
setIsLoading(true);
setError(null);
const updatedBacktest = await backtestApi.getBacktest(backtestId);
setBacktest(updatedBacktest);
if (updatedBacktest.status === 'completed') {
// Fetch results
const backtestResults = await backtestApi.getBacktestResults(backtestId);
setResults(backtestResults);
setIsPolling(false);
// Clear polling interval
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
} else if (updatedBacktest.status === 'failed' || updatedBacktest.status === 'cancelled') {
setIsPolling(false);
// Clear polling interval
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
if (updatedBacktest.status === 'failed' && updatedBacktest.error) {
setError(updatedBacktest.error);
}
}
} catch (err) {
console.error('Error polling backtest status:', err);
// Don't stop polling on transient errors
}
}, []);
// Create a new backtest
const createBacktest = useCallback(async (request: BacktestRequest) => {
setIsLoading(true);
setError(null);
setResults(null);
try {
const newBacktest = await backtestApi.createBacktest(request);
setBacktest(newBacktest);
// Create backtest
const { id } = await BacktestService.createBacktest(newConfig);
// Start polling for updates
setIsPolling(true);
pollingIntervalRef.current = setInterval(() => {
pollStatus(newBacktest.id);
}, 2000); // Poll every 2 seconds
setBacktestId(id);
setConfig(newConfig);
setStatus('configured');
setResults(null);
setCurrentTime(null);
setProgress(0);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create backtest');
} finally {
setIsLoading(false);
}
}, []);
}, [pollStatus]);
const handleStart = useCallback(async () => {
if (!backtestId) return;
// Cancel running backtest
const cancelBacktest = useCallback(async () => {
if (!backtest || backtest.status !== 'running') return;
try {
setIsLoading(true);
setError(null);
await backtestApi.cancelBacktest(backtest.id);
setBacktest({ ...backtest, status: 'cancelled' });
setIsPolling(false);
await BacktestService.startBacktest(backtestId);
setStatus('running');
// Start polling for updates
cleanupRef.current = await BacktestService.pollBacktestUpdates(
backtestId,
(newStatus, newProgress, newTime) => {
setStatus(newStatus);
if (newProgress !== undefined) setProgress(newProgress);
if (newTime !== undefined) setCurrentTime(newTime);
// Fetch full results when completed
if (newStatus === 'completed') {
BacktestService.getBacktestResults(backtestId).then(setResults);
}
}
);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start backtest');
} finally {
setIsLoading(false);
}
}, [backtestId]);
const handlePause = useCallback(async () => {
if (!backtestId) return;
try {
setIsLoading(true);
setError(null);
await BacktestService.pauseBacktest(backtestId);
setStatus('paused');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to pause backtest');
} finally {
setIsLoading(false);
}
}, [backtestId]);
const handleResume = useCallback(async () => {
if (!backtestId) return;
try {
setIsLoading(true);
setError(null);
await BacktestService.resumeBacktest(backtestId);
setStatus('running');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to resume backtest');
} finally {
setIsLoading(false);
}
}, [backtestId]);
const handleStop = useCallback(async () => {
if (!backtestId) return;
try {
setIsLoading(true);
setError(null);
await BacktestService.stopBacktest(backtestId);
setStatus('stopped');
// Stop polling
if (cleanupRef.current) {
cleanupRef.current();
cleanupRef.current = null;
// Clear polling interval
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to stop backtest');
} finally {
setIsLoading(false);
setError(err instanceof Error ? err.message : 'Failed to cancel backtest');
}
}, [backtestId]);
}, [backtest]);
const handleStep = useCallback(async () => {
if (!backtestId) return;
try {
setIsLoading(true);
setError(null);
await BacktestService.stepBacktest(backtestId);
// Get updated status
const statusUpdate = await BacktestService.getBacktestStatus(backtestId);
setStatus(statusUpdate.status);
if (statusUpdate.progress !== undefined) setProgress(statusUpdate.progress);
if (statusUpdate.currentTime !== undefined) setCurrentTime(statusUpdate.currentTime);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to step backtest');
} finally {
setIsLoading(false);
// Reset state
const reset = useCallback(() => {
setBacktest(null);
setResults(null);
setError(null);
setIsLoading(false);
setIsPolling(false);
// Clear polling interval
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
}, [backtestId]);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (cleanupRef.current) {
cleanupRef.current();
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
return {
backtestId,
config,
status,
backtest,
results,
currentTime,
progress,
error,
isLoading,
handleConfigSubmit,
handleStart,
handlePause,
handleResume,
handleStop,
handleStep,
isPolling,
error,
createBacktest,
cancelBacktest,
reset,
};
}
// Separate hook for listing backtests
interface UseBacktestListReturn {
backtests: BacktestJob[];
isLoading: boolean;
error: string | null;
loadBacktests: (limit?: number, offset?: number) => Promise<void>;
}
export function useBacktestList(): UseBacktestListReturn {
const [backtests, setBacktests] = useState<BacktestJob[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadBacktests = useCallback(async (limit = 50, offset = 0) => {
setIsLoading(true);
setError(null);
try {
const list = await backtestApi.listBacktests(limit, offset);
setBacktests(list);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load backtests');
} finally {
setIsLoading(false);
}
}, []);
return {
backtests,
isLoading,
error,
loadBacktests,
};
}

View file

@ -0,0 +1,119 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:2003';
export interface BacktestRequest {
strategy: string;
symbols: string[];
startDate: string;
endDate: string;
initialCapital: number;
config?: Record<string, any>;
}
export interface BacktestJob {
id: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
strategy: string;
symbols: string[];
startDate: string;
endDate: string;
initialCapital: number;
config: Record<string, any>;
createdAt: string;
updatedAt: string;
error?: string;
}
export interface BacktestResult {
backtestId: string;
metrics: {
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
winRate: number;
totalTrades: number;
profitFactor?: number;
};
equity: Array<{ date: string; value: number }>;
trades?: Array<{
symbol: string;
entryDate: string;
exitDate: string;
entryPrice: number;
exitPrice: number;
quantity: number;
pnl: number;
}>;
ohlcData?: Record<string, Array<{
time: number;
open: number;
high: number;
low: number;
close: number;
volume?: number;
}>>;
}
export const backtestApi = {
// Create a new backtest
async createBacktest(request: BacktestRequest): Promise<BacktestJob> {
const response = await fetch(`${API_BASE_URL}/api/backtests`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`Failed to create backtest: ${response.statusText}`);
}
return response.json();
},
// Get backtest status
async getBacktest(id: string): Promise<BacktestJob> {
const response = await fetch(`${API_BASE_URL}/api/backtests/${id}`);
if (!response.ok) {
throw new Error(`Failed to get backtest: ${response.statusText}`);
}
return response.json();
},
// Get backtest results
async getBacktestResults(id: string): Promise<BacktestResult> {
const response = await fetch(`${API_BASE_URL}/api/backtests/${id}/results`);
if (!response.ok) {
throw new Error(`Failed to get results: ${response.statusText}`);
}
return response.json();
},
// List all backtests
async listBacktests(limit = 50, offset = 0): Promise<BacktestJob[]> {
const response = await fetch(
`${API_BASE_URL}/api/backtests?limit=${limit}&offset=${offset}`
);
if (!response.ok) {
throw new Error(`Failed to list backtests: ${response.statusText}`);
}
return response.json();
},
// Cancel a running backtest
async cancelBacktest(id: string): Promise<void> {
const response = await fetch(`${API_BASE_URL}/api/backtests/${id}/cancel`, {
method: 'POST',
});
if (!response.ok) {
throw new Error(`Failed to cancel backtest: ${response.statusText}`);
}
},
};