cleaned up web-app
This commit is contained in:
parent
805ce0ebf1
commit
3843dc95a3
21 changed files with 105 additions and 2420 deletions
|
|
@ -1,232 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Dialog } from '@headlessui/react';
|
||||
import {
|
||||
Bars3Icon,
|
||||
XMarkIcon,
|
||||
HomeIcon,
|
||||
ChartBarIcon,
|
||||
CogIcon,
|
||||
DocumentTextIcon,
|
||||
CurrencyDollarIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '#', icon: HomeIcon, current: true },
|
||||
{ name: 'Portfolio', href: '#', icon: CurrencyDollarIcon, current: false },
|
||||
{ name: 'Analytics', href: '#', icon: ChartBarIcon, current: false },
|
||||
{ name: 'Reports', href: '#', icon: DocumentTextIcon, current: false },
|
||||
{ name: 'Settings', href: '#', icon: CogIcon, current: false },
|
||||
];
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50 lg:hidden"
|
||||
open={sidebarOpen}
|
||||
onClose={setSidebarOpen}
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/80" />
|
||||
|
||||
<div className="fixed inset-0 flex">
|
||||
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
|
||||
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 p-2.5"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon className="h-6 w-6 text-text-primary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex grow flex-col gap-y-3 overflow-y-auto bg-background px-3 pb-2 scrollbar-thin">
|
||||
<div className="flex h-12 shrink-0 items-center border-b border-border">
|
||||
<h1 className="text-lg font-bold text-text-primary">Stock Bot</h1>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-4">
|
||||
<li>
|
||||
<ul role="list" className="-mx-1 space-y-0.5">
|
||||
{navigation.map(item => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
|
||||
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
|
||||
'group flex gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'text-primary-400'
|
||||
: 'text-text-muted group-hover:text-primary-400',
|
||||
'h-4 w-4 shrink-0 transition-colors'
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
|
||||
<div className="flex grow flex-col gap-y-3 overflow-y-auto border-r border-border bg-background px-3 scrollbar-thin">
|
||||
<div className="flex h-12 shrink-0 items-center border-b border-border">
|
||||
<h1 className="text-lg font-bold text-text-primary">Stock Bot</h1>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-4">
|
||||
<li>
|
||||
<ul role="list" className="-mx-1 space-y-0.5">
|
||||
{navigation.map(item => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
|
||||
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
|
||||
'group flex gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'text-primary-400'
|
||||
: 'text-text-muted group-hover:text-primary-400',
|
||||
'h-4 w-4 shrink-0 transition-colors'
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sticky top-0 z-40 flex items-center gap-x-4 bg-background px-3 py-2 shadow-sm sm:px-4 lg:hidden border-b border-border">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-1.5 p-1.5 text-text-secondary lg:hidden hover:text-text-primary transition-colors"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="flex-1 text-sm font-medium leading-tight text-text-primary">
|
||||
Dashboard
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="py-4 lg:pl-60 w-full">
|
||||
<div className="px-4">
|
||||
<h1 className="text-lg font-bold text-text-primary mb-2">
|
||||
Welcome to Stock Bot Dashboard
|
||||
</h1>
|
||||
<p className="text-text-secondary mb-6 text-sm">
|
||||
Monitor your trading performance, manage portfolios, and analyze market data.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mb-6">
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border hover:border-primary-500/50 transition-colors">
|
||||
<div className="flex items-center">
|
||||
<div className="p-1.5 bg-primary-500/10 rounded">
|
||||
<CurrencyDollarIcon className="h-5 w-5 text-primary-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-text-primary">Portfolio Value</h3>
|
||||
<p className="text-lg font-bold text-primary-400">$0.00</p>
|
||||
<p className="text-xs text-text-muted">Total assets</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border hover:border-success/50 transition-colors">
|
||||
<div className="flex items-center">
|
||||
<div className="p-1.5 bg-success/10 rounded">
|
||||
<ChartBarIcon className="h-5 w-5 text-success" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-text-primary">Total Return</h3>
|
||||
<p className="text-lg font-bold text-success">+0.00%</p>
|
||||
<p className="text-xs text-text-muted">Since inception</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border hover:border-warning/50 transition-colors">
|
||||
<div className="flex items-center">
|
||||
<div className="p-1.5 bg-warning/10 rounded">
|
||||
<DocumentTextIcon className="h-5 w-5 text-warning" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-text-primary">
|
||||
Active Strategies
|
||||
</h3>
|
||||
<p className="text-lg font-bold text-warning">0</p>
|
||||
<p className="text-xs text-text-muted">Running algorithms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-3">
|
||||
Recent Activity
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between p-2 bg-surface-tertiary rounded border border-border">
|
||||
<span className="text-text-secondary text-sm">No recent activity</span>
|
||||
<span className="text-text-muted text-xs">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-3">
|
||||
Market Overview
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between p-2 bg-surface-tertiary rounded border border-border">
|
||||
<span className="text-text-secondary text-sm">
|
||||
Market data loading...
|
||||
</span>
|
||||
<span className="text-text-muted text-xs">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useBacktestList } from './hooks/useBacktest';
|
||||
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':
|
||||
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>
|
||||
<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"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
<span>New Backtest</span>
|
||||
</button>
|
||||
</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>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
<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)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { BacktestConfiguration } from './components/BacktestConfiguration';
|
||||
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 {
|
||||
backtest,
|
||||
results,
|
||||
isLoading,
|
||||
isPolling,
|
||||
error,
|
||||
createBacktest,
|
||||
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);
|
||||
|
||||
// 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 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: {
|
||||
name: newConfig.name,
|
||||
commission: newConfig.commission,
|
||||
slippage: newConfig.slippage,
|
||||
speedMultiplier: newConfig.speedMultiplier,
|
||||
useTypeScriptImplementation: true, // Enable TypeScript strategy execution
|
||||
},
|
||||
});
|
||||
}, [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">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-lg font-bold text-text-primary mb-2">Backtest Strategy</h1>
|
||||
<p className="text-text-secondary mb-6 text-sm">
|
||||
Test your trading strategies against historical data to evaluate performance and risk.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-1 min-h-0">
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<BacktestConfiguration
|
||||
onSubmit={handleConfigSubmit}
|
||||
disabled={status === 'running' || isLoading || isPolling}
|
||||
/>
|
||||
|
||||
{config && (
|
||||
<BacktestControls
|
||||
status={status}
|
||||
onStart={handleStart}
|
||||
onPause={handlePause}
|
||||
onResume={handleResume}
|
||||
onStop={handleStop}
|
||||
onStep={handleStep}
|
||||
currentTime={currentTime}
|
||||
startTime={config.startDate.getTime()}
|
||||
endTime={config.endDate.getTime()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<BacktestResults
|
||||
status={status}
|
||||
results={adaptedResults}
|
||||
currentTime={currentTime}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -46,15 +46,32 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
|
|||
|
||||
// Find trades for this symbol
|
||||
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
|
||||
console.log('Symbol trades for', symbol, ':', symbolTrades);
|
||||
|
||||
const tradeMarkers = symbolTrades
|
||||
.filter(trade => trade.entryPrice != null && trade.entryDate != null)
|
||||
.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)}`
|
||||
}));
|
||||
.filter(trade => {
|
||||
// Check multiple possible field names
|
||||
const hasPrice = trade.price != null || trade.entryPrice != null;
|
||||
const hasTime = trade.timestamp != null || trade.entryDate != null || trade.date != null;
|
||||
return hasPrice && hasTime;
|
||||
})
|
||||
.map(trade => {
|
||||
// Use whatever field names are present
|
||||
const price = trade.price || trade.entryPrice;
|
||||
const timestamp = trade.timestamp || trade.entryDate || trade.date;
|
||||
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell');
|
||||
|
||||
return {
|
||||
time: new Date(timestamp).getTime() / 1000,
|
||||
position: 'belowBar' as const,
|
||||
color: side === 'buy' ? '#10b981' : '#ef4444',
|
||||
shape: (side === 'buy' ? 'arrowUp' : 'arrowDown') as const,
|
||||
text: `${side === 'buy' ? 'Buy' : 'Sell'} @ $${price.toFixed(2)}`,
|
||||
price: price
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Trade markers:', tradeMarkers);
|
||||
|
||||
const processedOhlcData = ohlcData
|
||||
.filter(d => d && d.timestamp != null && d.open != null && d.high != null && d.low != null && d.close != null)
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
import {
|
||||
ArrowPathIcon,
|
||||
ForwardIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
StopIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { BacktestStatus } from '../types';
|
||||
|
||||
interface BacktestControlsProps {
|
||||
status: BacktestStatus;
|
||||
onStart: () => void;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
onStop: () => void;
|
||||
onStep: () => void;
|
||||
currentTime: number | null;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export function BacktestControls({
|
||||
status,
|
||||
onStart,
|
||||
onPause,
|
||||
onResume,
|
||||
onStop,
|
||||
onStep,
|
||||
currentTime,
|
||||
startTime,
|
||||
endTime,
|
||||
}: BacktestControlsProps) {
|
||||
const progress = currentTime
|
||||
? ((currentTime - startTime) / (endTime - startTime)) * 100
|
||||
: 0;
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h2 className="text-base font-medium text-text-primary mb-4">Controls</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
{status === 'configured' || status === 'stopped' ? (
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-success text-white rounded-md text-sm font-medium hover:bg-success/90 transition-colors"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
Start
|
||||
</button>
|
||||
) : status === 'running' ? (
|
||||
<button
|
||||
onClick={onPause}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-warning text-white rounded-md text-sm font-medium hover:bg-warning/90 transition-colors"
|
||||
>
|
||||
<PauseIcon className="w-4 h-4" />
|
||||
Pause
|
||||
</button>
|
||||
) : status === 'paused' ? (
|
||||
<button
|
||||
onClick={onResume}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-success text-white rounded-md text-sm font-medium hover:bg-success/90 transition-colors"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
Resume
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{(status === 'running' || status === 'paused') && (
|
||||
<button
|
||||
onClick={onStop}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-error text-white rounded-md text-sm font-medium hover:bg-error/90 transition-colors"
|
||||
>
|
||||
<StopIcon className="w-4 h-4" />
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
|
||||
{status === 'paused' && (
|
||||
<button
|
||||
onClick={onStep}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
<ForwardIcon className="w-4 h-4" />
|
||||
Step
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status !== 'idle' && status !== 'configured' && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm text-text-secondary mb-1">
|
||||
<span>Progress</span>
|
||||
<span>{progress.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-background rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary-500 h-2 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Status</span>
|
||||
<span className={`text-text-primary font-medium ${
|
||||
status === 'running' ? 'text-success' :
|
||||
status === 'paused' ? 'text-warning' :
|
||||
status === 'completed' ? 'text-primary-400' :
|
||||
status === 'error' ? 'text-error' :
|
||||
'text-text-muted'
|
||||
}`}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{currentTime && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Current Time</span>
|
||||
<span className="text-text-primary text-xs">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'completed' && (
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
<ArrowPathIcon className="w-4 h-4" />
|
||||
Run Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
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;
|
||||
results: BacktestResult | null;
|
||||
currentTime: number | null;
|
||||
}
|
||||
|
||||
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
|
||||
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
|
||||
|
||||
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) >= 0 ? '+' : ''}${(results.metrics.totalReturn || 0).toFixed(2)}%`}
|
||||
trend={(results.metrics.totalReturn || 0) >= 0 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Sharpe Ratio"
|
||||
value={results.metrics.sharpeRatio?.toFixed(2) || '0.00'}
|
||||
trend={(results.metrics.sharpeRatio || 0) >= 1 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Max Drawdown"
|
||||
value={`${((results.metrics.maxDrawdown || 0) * 100).toFixed(2)}%`}
|
||||
trend="down"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Win Rate"
|
||||
value={`${(results.metrics.winRate || 0).toFixed(1)}%`}
|
||||
trend={(results.metrics.winRate || 0) >= 50 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Trades"
|
||||
value={(results.metrics.totalTrades || 0).toString()}
|
||||
/>
|
||||
{results.metrics.profitFactor !== null && results.metrics.profitFactor !== undefined && (
|
||||
<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">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-base font-medium text-text-primary">
|
||||
Portfolio Performance
|
||||
</h3>
|
||||
{results.ohlcData && Object.keys(results.ohlcData).length > 1 && (
|
||||
<select
|
||||
value={selectedSymbol || Object.keys(results.ohlcData)[0]}
|
||||
onChange={(e) => setSelectedSymbol(e.target.value)}
|
||||
className="px-3 py-1 text-sm bg-background border border-border rounded-md text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
>
|
||||
{Object.keys(results.ohlcData).map(symbol => (
|
||||
<option key={symbol} value={symbol}>{symbol}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
{(() => {
|
||||
const hasOhlcData = results.ohlcData && Object.keys(results.ohlcData).length > 0;
|
||||
const hasEquityData = results.equity && results.equity.length > 0;
|
||||
|
||||
if (hasOhlcData) {
|
||||
const activeSymbol = selectedSymbol || Object.keys(results.ohlcData)[0];
|
||||
const ohlcData = results.ohlcData[activeSymbol];
|
||||
|
||||
// Create trade markers for the selected symbol (individual fills)
|
||||
const tradeMarkers = results.trades
|
||||
.filter(trade => trade.symbol === activeSymbol)
|
||||
.map(trade => {
|
||||
// Buy = green up arrow, Sell = red down arrow
|
||||
const isBuy = trade.side === 'buy';
|
||||
const pnlText = trade.pnl !== undefined ? ` (${trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)})` : '';
|
||||
const positionText = ` → ${trade.positionAfter > 0 ? '+' : ''}${trade.positionAfter}`;
|
||||
|
||||
return {
|
||||
time: Math.floor(new Date(trade.timestamp).getTime() / 1000),
|
||||
position: isBuy ? 'belowBar' as const : 'aboveBar' as const,
|
||||
color: isBuy ? '#10b981' : '#ef4444',
|
||||
shape: isBuy ? 'arrowUp' as const : 'arrowDown' as const,
|
||||
text: `${trade.side.toUpperCase()} ${trade.quantity}@${trade.price.toFixed(2)}${positionText}${pnlText}`,
|
||||
id: trade.id,
|
||||
price: trade.price
|
||||
};
|
||||
});
|
||||
|
||||
// Convert OHLC data timestamps
|
||||
const chartData = ohlcData.map((bar: any) => ({
|
||||
...bar,
|
||||
time: bar.timestamp || bar.time
|
||||
}));
|
||||
|
||||
return (
|
||||
<Chart
|
||||
data={chartData}
|
||||
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
|
||||
}
|
||||
] : []}
|
||||
tradeMarkers={tradeMarkers}
|
||||
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 Log */}
|
||||
{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 Log ({results.trades.length} fills)
|
||||
</h3>
|
||||
<TradeLog trades={results.trades} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { DataTable } from '@/components/ui/DataTable';
|
|||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
interface BacktestTradesProps {
|
||||
result: BacktestResult | null;
|
||||
result: any | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,10 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const columns: ColumnDef<typeof result.trades[0]>[] = [
|
||||
// Add debug logging
|
||||
console.log('Trades data:', result.trades);
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: 'symbol',
|
||||
header: 'Symbol',
|
||||
|
|
@ -38,11 +41,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
|||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'side',
|
||||
id: 'side',
|
||||
header: 'Side',
|
||||
size: 80,
|
||||
cell: ({ getValue }) => {
|
||||
const side = getValue() as string || 'unknown';
|
||||
cell: ({ row }) => {
|
||||
const trade = row.original;
|
||||
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell');
|
||||
return (
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-md ${
|
||||
side === 'buy'
|
||||
|
|
@ -55,11 +59,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
|||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'entryDate',
|
||||
header: 'Entry Date',
|
||||
id: 'date',
|
||||
header: 'Date',
|
||||
size: 180,
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue() as string;
|
||||
cell: ({ row }) => {
|
||||
const trade = row.original;
|
||||
const date = trade.timestamp || trade.entryDate || trade.date;
|
||||
return (
|
||||
<span className="text-sm text-text-secondary">
|
||||
{date ? new Date(date).toLocaleString() : '-'}
|
||||
|
|
@ -68,37 +73,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
|||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'entryPrice',
|
||||
header: 'Entry Price',
|
||||
id: 'price',
|
||||
header: 'Price',
|
||||
size: 100,
|
||||
cell: ({ getValue }) => {
|
||||
const price = getValue() as number;
|
||||
return (
|
||||
<span className="text-sm text-text-primary">
|
||||
${price != null ? price.toFixed(2) : '0.00'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'exitDate',
|
||||
header: 'Exit Date',
|
||||
size: 180,
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue() as string | null;
|
||||
return (
|
||||
<span className="text-sm text-text-secondary">
|
||||
{date ? new Date(date).toLocaleString() : '-'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'exitPrice',
|
||||
header: 'Exit Price',
|
||||
size: 100,
|
||||
cell: ({ getValue }) => {
|
||||
const price = getValue() as number;
|
||||
cell: ({ row }) => {
|
||||
const trade = row.original;
|
||||
const price = trade.price || trade.entryPrice;
|
||||
return (
|
||||
<span className="text-sm text-text-primary">
|
||||
${price != null ? price.toFixed(2) : '0.00'}
|
||||
|
|
@ -110,49 +90,83 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
|||
accessorKey: 'quantity',
|
||||
header: 'Quantity',
|
||||
size: 80,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-sm text-text-primary">{getValue() as number || 0}</span>
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const qty = Math.abs(getValue() as number || 0);
|
||||
return <span className="text-sm text-text-primary">{qty}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'pnl',
|
||||
header: 'P&L',
|
||||
accessorKey: 'commission',
|
||||
header: 'Commission',
|
||||
size: 100,
|
||||
cell: ({ getValue }) => {
|
||||
const pnl = getValue() as number || 0;
|
||||
const commission = getValue() as number || 0;
|
||||
return (
|
||||
<span className={`text-sm font-medium ${
|
||||
pnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
${pnl != null ? pnl.toFixed(2) : '0.00'}
|
||||
<span className="text-sm text-text-secondary">
|
||||
${commission.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'pnlPercent',
|
||||
header: 'P&L %',
|
||||
id: 'pnl',
|
||||
header: 'P&L',
|
||||
size: 100,
|
||||
cell: ({ getValue }) => {
|
||||
const pnlPercent = getValue() as number || 0;
|
||||
cell: ({ row }) => {
|
||||
const trade = row.original;
|
||||
const pnl = trade.pnl || trade.realizedPnl || 0;
|
||||
return (
|
||||
<span className={`text-sm font-medium ${
|
||||
pnlPercent >= 0 ? 'text-success' : 'text-error'
|
||||
pnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{pnlPercent != null ? pnlPercent.toFixed(2) : '0.00'}%
|
||||
${pnl.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Calculate trade statistics
|
||||
const tradeStats = result.trades.reduce((stats: any, trade: any) => {
|
||||
const pnl = trade.pnl || trade.realizedPnl || 0;
|
||||
if (pnl > 0) {
|
||||
stats.wins++;
|
||||
stats.totalWin += pnl;
|
||||
} else if (pnl < 0) {
|
||||
stats.losses++;
|
||||
stats.totalLoss += Math.abs(pnl);
|
||||
}
|
||||
stats.totalPnL += pnl;
|
||||
return stats;
|
||||
}, { wins: 0, losses: 0, totalWin: 0, totalLoss: 0, totalPnL: 0 });
|
||||
|
||||
const winRate = result.trades.length > 0 ? (tradeStats.wins / result.trades.length) * 100 : 0;
|
||||
|
||||
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 className="mb-4 grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-3">
|
||||
<p className="text-xs text-text-secondary">Total Trades</p>
|
||||
<p className="text-lg font-medium text-text-primary">{result.trades.length}</p>
|
||||
</div>
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-3">
|
||||
<p className="text-xs text-text-secondary">Win Rate</p>
|
||||
<p className="text-lg font-medium text-text-primary">{winRate.toFixed(1)}%</p>
|
||||
</div>
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-3">
|
||||
<p className="text-xs text-text-secondary">Wins</p>
|
||||
<p className="text-lg font-medium text-success">{tradeStats.wins}</p>
|
||||
</div>
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-3">
|
||||
<p className="text-xs text-text-secondary">Losses</p>
|
||||
<p className="text-lg font-medium text-error">{tradeStats.losses}</p>
|
||||
</div>
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-3">
|
||||
<p className="text-xs text-text-secondary">Total P&L</p>
|
||||
<p className={`text-lg font-medium ${tradeStats.totalPnL >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
${tradeStats.totalPnL.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice?: number;
|
||||
avgPrice?: number;
|
||||
currentPrice?: number;
|
||||
lastPrice?: number;
|
||||
}
|
||||
|
||||
interface CompactPositionsTableProps {
|
||||
positions: Position[];
|
||||
onExpand?: () => void;
|
||||
}
|
||||
|
||||
export function CompactPositionsTable({ positions, onExpand }: CompactPositionsTableProps) {
|
||||
if (positions.length === 0) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-4">
|
||||
<h3 className="text-sm font-medium text-text-primary mb-2">Open Positions</h3>
|
||||
<p className="text-sm text-text-secondary">No open positions</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalPnL = positions.reduce((sum, p) => {
|
||||
const quantity = p.quantity || 0;
|
||||
const avgPrice = p.averagePrice || p.avgPrice || 0;
|
||||
const currentPrice = p.currentPrice || p.lastPrice || avgPrice;
|
||||
return sum + ((currentPrice - avgPrice) * quantity);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-sm font-medium text-text-primary">
|
||||
Open Positions ({positions.length})
|
||||
</h3>
|
||||
<span className={`text-sm font-medium ${totalPnL >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
P&L: ${totalPnL.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{positions.slice(0, 8).map((position, index) => {
|
||||
const quantity = position.quantity || 0;
|
||||
const avgPrice = position.averagePrice || position.avgPrice || 0;
|
||||
const currentPrice = position.currentPrice || position.lastPrice || avgPrice;
|
||||
const side = quantity > 0 ? 'long' : 'short';
|
||||
const absQuantity = Math.abs(quantity);
|
||||
const pnl = (currentPrice - avgPrice) * quantity;
|
||||
const pnlPercent = avgPrice > 0 ? (pnl / (avgPrice * absQuantity)) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center justify-between py-1.5 px-2 rounded hover:bg-surface-tertiary transition-colors">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-text-primary">{position.symbol}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
side === 'long' ? 'bg-success/20 text-success' : 'bg-error/20 text-error'
|
||||
}`}>
|
||||
{side.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-xs text-text-secondary">
|
||||
{absQuantity} @ ${avgPrice.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-text-secondary">
|
||||
${currentPrice.toFixed(2)}
|
||||
</span>
|
||||
<span className={`text-xs font-medium ${pnl >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{positions.length > 8 && (
|
||||
<button
|
||||
onClick={onExpand}
|
||||
className="w-full text-xs text-primary-500 hover:text-primary-600 text-center pt-2 font-medium"
|
||||
>
|
||||
View all {positions.length} positions
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import type { Position } from '../types';
|
||||
|
||||
interface PositionsTableProps {
|
||||
positions: Position[];
|
||||
}
|
||||
|
||||
export function PositionsTable({ positions }: PositionsTableProps) {
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatPnl = (value: number) => {
|
||||
const formatted = formatCurrency(Math.abs(value));
|
||||
return value >= 0 ? `+${formatted}` : `-${formatted}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<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-2 font-medium text-text-secondary">Symbol</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Quantity</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Avg Price</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Current</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">P&L</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Unrealized</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{positions.map((position) => {
|
||||
const totalPnl = position.realizedPnl + position.unrealizedPnl;
|
||||
return (
|
||||
<tr key={position.symbol} className="border-b border-border hover:bg-surface-tertiary">
|
||||
<td className="py-2 px-2 font-medium text-text-primary">{position.symbol}</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{position.quantity.toLocaleString()}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(position.averagePrice)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(position.currentPrice)}
|
||||
</td>
|
||||
<td className={`text-right py-2 px-2 font-medium ${
|
||||
totalPnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{formatPnl(totalPnl)}
|
||||
</td>
|
||||
<td className={`text-right py-2 px-2 ${
|
||||
position.unrealizedPnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{formatPnl(position.unrealizedPnl)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
import { DataTable } from '@/components/ui/DataTable';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import type { Run } from '../services/backtestApiV2';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
ClockIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface RunsListProps {
|
||||
runs: Run[];
|
||||
currentRunId?: string;
|
||||
onSelectRun: (runId: string) => void;
|
||||
}
|
||||
|
||||
export function RunsList({ runs, currentRunId, onSelectRun }: RunsListProps) {
|
||||
const navigate = useNavigate();
|
||||
const { id: backtestId } = useParams<{ id: string }>();
|
||||
|
||||
const getStatusIcon = (status: Run['status']) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircleIcon className="w-4 h-4 text-success" />;
|
||||
case 'failed':
|
||||
return <XCircleIcon className="w-4 h-4 text-error" />;
|
||||
case 'cancelled':
|
||||
return <XCircleIcon className="w-4 h-4 text-text-secondary" />;
|
||||
case 'running':
|
||||
return <PlayIcon className="w-4 h-4 text-primary-500" />;
|
||||
case 'paused':
|
||||
return <PauseIcon className="w-4 h-4 text-warning" />;
|
||||
case 'pending':
|
||||
return <ClockIcon className="w-4 h-4 text-text-secondary" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: Run['status']) => {
|
||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||
};
|
||||
|
||||
const formatDuration = (startedAt?: string, completedAt?: string) => {
|
||||
if (!startedAt) return '-';
|
||||
const start = new Date(startedAt).getTime();
|
||||
const end = completedAt ? new Date(completedAt).getTime() : Date.now();
|
||||
const duration = end - start;
|
||||
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<Run>[] = [
|
||||
{
|
||||
accessorKey: 'runNumber',
|
||||
header: 'Run #',
|
||||
size: 80,
|
||||
cell: ({ getValue, row }) => (
|
||||
<span className={`text-sm font-medium ${row.original.id === currentRunId ? 'text-primary-500' : 'text-text-primary'}`}>
|
||||
#{getValue() as number}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
size: 120,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue() as Run['status'];
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusIcon(status)}
|
||||
<span className="text-sm text-text-primary">{getStatusLabel(status)}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'progress',
|
||||
header: 'Progress',
|
||||
size: 150,
|
||||
cell: ({ row }) => {
|
||||
const progress = row.original.progress;
|
||||
const status = row.original.status;
|
||||
|
||||
if (status === 'pending') return <span className="text-sm text-text-secondary">-</span>;
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1 bg-surface-tertiary rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary-500 h-2 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-text-secondary">{progress.toFixed(0)}%</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'speedMultiplier',
|
||||
header: 'Speed',
|
||||
size: 80,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-sm text-text-primary">{getValue() as number}x</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'startedAt',
|
||||
header: 'Started',
|
||||
size: 180,
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue() as string | undefined;
|
||||
return (
|
||||
<span className="text-sm text-text-secondary">
|
||||
{date ? new Date(date).toLocaleString() : '-'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
header: 'Duration',
|
||||
size: 100,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-text-secondary">
|
||||
{formatDuration(row.original.startedAt, row.original.completedAt)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'error',
|
||||
header: 'Error',
|
||||
size: 200,
|
||||
cell: ({ getValue }) => {
|
||||
const error = getValue() as string | undefined;
|
||||
return error ? (
|
||||
<span className="text-sm text-error truncate" title={error}>{error}</span>
|
||||
) : (
|
||||
<span className="text-sm text-text-secondary">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (runs.length === 0) {
|
||||
return (
|
||||
<div className="bg-surface-secondary rounded-lg border border-border p-8 text-center">
|
||||
<p className="text-text-secondary">No runs yet. Create a new run to get started.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={runs}
|
||||
columns={columns}
|
||||
onRowClick={(run) => {
|
||||
navigate(`/backtests/${backtestId}/run/${run.id}`);
|
||||
onSelectRun(run.id);
|
||||
}}
|
||||
className="bg-surface-secondary rounded-lg border border-border"
|
||||
height={400}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import type { Trade } from '../types';
|
||||
|
||||
interface TradeLogProps {
|
||||
trades: Trade[];
|
||||
}
|
||||
|
||||
export function TradeLog({ trades }: TradeLogProps) {
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// Show latest trades first
|
||||
const sortedTrades = [...trades].reverse();
|
||||
|
||||
// Check if any trades have P&L
|
||||
const showPnLColumn = trades.some(t => t.pnl !== undefined);
|
||||
|
||||
// Determine the action type based on side and position change
|
||||
const getActionType = (trade: Trade): string => {
|
||||
const positionBefore = trade.positionAfter + (trade.side === 'buy' ? -trade.quantity : trade.quantity);
|
||||
|
||||
if (trade.side === 'buy') {
|
||||
// If we had a negative position (short) and buying reduces it, it's a COVER
|
||||
if (positionBefore < 0 && trade.positionAfter > positionBefore) {
|
||||
return 'COVER';
|
||||
}
|
||||
// Otherwise it's a BUY (opening or adding to long)
|
||||
return 'BUY';
|
||||
} else {
|
||||
// If we had a positive position (long) and selling reduces it, it's a SELL
|
||||
if (positionBefore > 0 && trade.positionAfter < positionBefore) {
|
||||
return 'SELL';
|
||||
}
|
||||
// Otherwise it's a SHORT (opening or adding to short)
|
||||
return 'SHORT';
|
||||
}
|
||||
};
|
||||
|
||||
// Get color for action type
|
||||
const getActionColor = (action: string): string => {
|
||||
switch (action) {
|
||||
case 'BUY':
|
||||
return 'bg-success/10 text-success';
|
||||
case 'SELL':
|
||||
return 'bg-error/10 text-error';
|
||||
case 'SHORT':
|
||||
return 'bg-warning/10 text-warning';
|
||||
case 'COVER':
|
||||
return 'bg-primary/10 text-primary';
|
||||
default:
|
||||
return 'bg-surface-tertiary text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">Time</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">Action</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">Price</th>
|
||||
<th className="text-right py-2 px-3 font-medium text-text-secondary">Value</th>
|
||||
<th className="text-right py-2 px-3 font-medium text-text-secondary">Position</th>
|
||||
<th className="text-right py-2 px-3 font-medium text-text-secondary">Comm.</th>
|
||||
{showPnLColumn && (
|
||||
<th className="text-right py-2 px-3 font-medium text-text-secondary">P&L</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTrades.map((trade) => {
|
||||
const tradeValue = trade.quantity * trade.price;
|
||||
const actionType = getActionType(trade);
|
||||
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">
|
||||
{formatTime(trade.timestamp)}
|
||||
</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 ${getActionColor(actionType)}`}>
|
||||
{actionType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right py-2 px-3 text-text-primary">
|
||||
{trade.quantity.toLocaleString()}
|
||||
</td>
|
||||
<td className="text-right py-2 px-3 text-text-primary">
|
||||
{formatCurrency(trade.price)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-3 text-text-primary">
|
||||
{formatCurrency(tradeValue)}
|
||||
</td>
|
||||
<td className={`text-right py-2 px-3 font-medium ${
|
||||
trade.positionAfter > 0 ? 'text-success' :
|
||||
trade.positionAfter < 0 ? 'text-error' :
|
||||
'text-text-muted'
|
||||
}`}>
|
||||
{trade.positionAfter > 0 ? '+' : ''}{trade.positionAfter.toLocaleString()}
|
||||
</td>
|
||||
<td className="text-right py-2 px-3 text-text-muted">
|
||||
{formatCurrency(trade.commission)}
|
||||
</td>
|
||||
{showPnLColumn && (
|
||||
<td className={`text-right py-2 px-3 font-medium ${
|
||||
trade.pnl !== undefined ? (trade.pnl >= 0 ? 'text-success' : 'text-error') : 'text-text-muted'
|
||||
}`}>
|
||||
{trade.pnl !== undefined ? (
|
||||
<>{trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)}</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
export { BacktestConfiguration } from './BacktestConfiguration';
|
||||
export { BacktestControls } from './BacktestControls';
|
||||
export { BacktestResults } from './BacktestResults';
|
||||
export { MetricsCard } from './MetricsCard';
|
||||
export { PositionsTable } from './PositionsTable';
|
||||
export { TradeLog } from './TradeLog';
|
||||
export { RunsList } from './RunsList';
|
||||
export { RunsListWithMetrics } from './RunsListWithMetrics';
|
||||
export { CompactPerformanceMetrics } from './CompactPerformanceMetrics';
|
||||
export { CompactPositionsTable } from './CompactPositionsTable';
|
||||
export { PositionsSummary } from './PositionsSummary';
|
||||
export { ChartContainer } from './ChartContainer';
|
||||
export { ChartContainer } from './ChartContainer';
|
||||
export { BacktestChart } from './BacktestChart';
|
||||
export { BacktestMetrics } from './BacktestMetrics';
|
||||
export { BacktestPlayback } from './BacktestPlayback';
|
||||
export { BacktestTrades } from './BacktestTrades';
|
||||
export { RunControlsCompact } from './RunControlsCompact';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { useBacktest } from './useBacktest';
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { BacktestJob, BacktestRequest, BacktestResult } from '../services/backtestApi';
|
||||
import { backtestApi, } from '../services/backtestApi';
|
||||
|
||||
interface UseBacktestReturn {
|
||||
// State
|
||||
backtest: BacktestJob | null;
|
||||
results: BacktestResult | null;
|
||||
isLoading: boolean;
|
||||
isPolling: boolean;
|
||||
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;
|
||||
}
|
||||
|
||||
export function useBacktest(): UseBacktestReturn {
|
||||
const [backtest, setBacktest] = useState<BacktestJob | null>(null);
|
||||
const [results, setResults] = useState<BacktestResult | 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);
|
||||
|
||||
// Poll for status updates
|
||||
const pollStatus = useCallback(async (backtestId: string) => {
|
||||
try {
|
||||
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
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 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);
|
||||
setError(null);
|
||||
setResults(null);
|
||||
|
||||
try {
|
||||
const newBacktest = await backtestApi.createBacktest(request);
|
||||
setBacktest(newBacktest);
|
||||
|
||||
// Start polling for updates
|
||||
setIsPolling(true);
|
||||
pollingIntervalRef.current = setInterval(() => {
|
||||
pollStatus(newBacktest.id);
|
||||
}, 2000); // Poll every 2 seconds
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [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;
|
||||
|
||||
try {
|
||||
await backtestApi.cancelBacktest(backtest.id);
|
||||
setBacktest({ ...backtest, status: 'cancelled' });
|
||||
setIsPolling(false);
|
||||
|
||||
// Clear polling interval
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to cancel backtest');
|
||||
}
|
||||
}, [backtest]);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
backtest,
|
||||
results,
|
||||
isLoading,
|
||||
isPolling,
|
||||
error,
|
||||
loadBacktest,
|
||||
createBacktest,
|
||||
updateBacktest,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
export { BacktestPage } from './BacktestPage';
|
||||
export { BacktestListPage } from './BacktestListPage';
|
||||
export { BacktestDetailPage } from './BacktestDetailPage';
|
||||
export { BacktestListPageV2 } from './BacktestListPageV2';
|
||||
export { BacktestDetailPageV2 } from './BacktestDetailPageV2';
|
||||
export * from './types';
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
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 {
|
||||
// Identification
|
||||
backtestId: string;
|
||||
status: 'completed' | 'failed' | 'cancelled';
|
||||
completedAt: string;
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
name: string;
|
||||
strategy: string;
|
||||
symbols: string[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
initialCapital: number;
|
||||
commission: number;
|
||||
slippage: number;
|
||||
dataFrequency: string;
|
||||
};
|
||||
|
||||
// Performance metrics
|
||||
metrics: {
|
||||
totalReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
winRate: number;
|
||||
totalTrades: number;
|
||||
profitFactor: number;
|
||||
profitableTrades: number;
|
||||
avgWin: number;
|
||||
avgLoss: number;
|
||||
expectancy: number;
|
||||
calmarRatio: number;
|
||||
sortinoRatio: number;
|
||||
};
|
||||
|
||||
// Chart data
|
||||
equity: Array<{ date: string; value: number }>;
|
||||
ohlcData: Record<string, Array<{
|
||||
time: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume?: number;
|
||||
}>>;
|
||||
|
||||
// Trade history
|
||||
trades: Array<{
|
||||
id: string;
|
||||
symbol: string;
|
||||
entryDate: string;
|
||||
exitDate: string | null;
|
||||
entryPrice: number;
|
||||
exitPrice: number;
|
||||
quantity: number;
|
||||
side: string;
|
||||
pnl: number;
|
||||
pnlPercent: number;
|
||||
commission: number;
|
||||
duration: number;
|
||||
}>;
|
||||
|
||||
// Positions
|
||||
positions: Array<{
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
unrealizedPnl: number;
|
||||
realizedPnl: number;
|
||||
}>;
|
||||
|
||||
// Analytics
|
||||
analytics: {
|
||||
drawdownSeries: Array<{ timestamp: number; value: number }>;
|
||||
dailyReturns: number[];
|
||||
monthlyReturns: Record<string, number>;
|
||||
exposureTime: number;
|
||||
riskMetrics: Record<string, 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}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types';
|
||||
|
||||
const API_BASE = '/api/backtest';
|
||||
|
||||
export class BacktestService {
|
||||
static async createBacktest(config: BacktestConfig): Promise<{ id: string }> {
|
||||
const response = await fetch(`${API_BASE}/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...config,
|
||||
startDate: config.startDate.toISOString(),
|
||||
endDate: config.endDate.toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create backtest');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async startBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/start`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to start backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async pauseBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/pause`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to pause backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/resume`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to resume backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async stopBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/stop`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to stop backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async stepBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/step`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to step backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async getBacktestStatus(id: string): Promise<{
|
||||
status: BacktestStatus;
|
||||
currentTime?: number;
|
||||
progress?: number;
|
||||
}> {
|
||||
const response = await fetch(`${API_BASE}/${id}/status`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get backtest status');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async getBacktestResults(id: string): Promise<BacktestResult> {
|
||||
const response = await fetch(`${API_BASE}/${id}/results`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get backtest results');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
return {
|
||||
...data,
|
||||
config: {
|
||||
...data.config,
|
||||
startDate: new Date(data.config.startDate),
|
||||
endDate: new Date(data.config.endDate),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static async listBacktests(): Promise<BacktestResult[]> {
|
||||
const response = await fetch(`${API_BASE}/list`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to list backtests');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
return data.map((backtest: any) => ({
|
||||
...backtest,
|
||||
config: {
|
||||
...backtest.config,
|
||||
startDate: new Date(backtest.config.startDate),
|
||||
endDate: new Date(backtest.config.endDate),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
static async deleteBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete backtest');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to poll for updates
|
||||
static async pollBacktestUpdates(
|
||||
id: string,
|
||||
onUpdate: (status: BacktestStatus, progress?: number, currentTime?: number) => void,
|
||||
interval: number = 200
|
||||
): Promise<() => void> {
|
||||
let isPolling = true;
|
||||
|
||||
const poll = async () => {
|
||||
while (isPolling) {
|
||||
try {
|
||||
const { status, progress, currentTime } = await this.getBacktestStatus(id);
|
||||
onUpdate(status, progress, currentTime);
|
||||
|
||||
if (status === 'completed' || status === 'error' || status === 'stopped') {
|
||||
isPolling = false;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
isPolling = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { Component, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ChartErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Chart error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full bg-surface-secondary rounded-lg border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="text-error text-6xl mb-4">⚠️</div>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-2">
|
||||
Chart Loading Error
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-4">
|
||||
{this.state.error?.message || 'Unable to load the chart'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null })}
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/24/solid';
|
||||
import type { MarketData } from '../types';
|
||||
|
||||
interface MarketOverviewProps {
|
||||
data: MarketData | null;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function MarketOverview({ data, loading }: MarketOverviewProps) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<div className="p-4 bg-surface-secondary border-b border-border">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-6 w-32 bg-surface-tertiary rounded mb-2"></div>
|
||||
<div className="h-8 w-24 bg-surface-tertiary rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isPositive = data.change >= 0;
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-surface-secondary border-b border-border">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h2 className="text-xl font-bold text-text-primary">{data.symbol}</h2>
|
||||
<span className="text-sm text-text-secondary">{data.name}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-3xl font-bold text-text-primary">
|
||||
${data.price.toFixed(2)}
|
||||
</span>
|
||||
<div className={`flex items-center gap-1 ${isPositive ? 'text-success' : 'text-error'}`}>
|
||||
{isPositive ? (
|
||||
<ArrowUpIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<ArrowDownIcon className="w-4 h-4" />
|
||||
)}
|
||||
<span className="text-lg font-medium">
|
||||
${Math.abs(data.change).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-lg font-medium">
|
||||
({isPositive ? '+' : ''}{data.changePercent.toFixed(2)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-text-muted">Open</span>
|
||||
<p className="text-text-primary font-medium">${data.open.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">High</span>
|
||||
<p className="text-text-primary font-medium">${data.high.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Low</span>
|
||||
<p className="text-text-primary font-medium">${data.low.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Prev Close</span>
|
||||
<p className="text-text-primary font-medium">${data.previousClose.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Volume</span>
|
||||
<p className="text-text-primary font-medium">
|
||||
{(data.volume / 1000000).toFixed(2)}M
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Time</span>
|
||||
<p className="text-text-primary font-medium">
|
||||
{new Date(data.timestamp).toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
export { SymbolChart } from './SymbolChart';
|
||||
export { ChartToolbar } from './ChartToolbar';
|
||||
export { MarketOverview } from './MarketOverview';
|
||||
export { ChartErrorBoundary } from './ChartErrorBoundary';
|
||||
export { IndicatorSelector } from './IndicatorSelector';
|
||||
export { IndicatorList } from './IndicatorList';
|
||||
export { IndicatorSettings } from './IndicatorSettings';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue