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
|
// Find trades for this symbol
|
||||||
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
|
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
|
||||||
|
console.log('Symbol trades for', symbol, ':', symbolTrades);
|
||||||
|
|
||||||
const tradeMarkers = symbolTrades
|
const tradeMarkers = symbolTrades
|
||||||
.filter(trade => trade.entryPrice != null && trade.entryDate != null)
|
.filter(trade => {
|
||||||
.map(trade => ({
|
// Check multiple possible field names
|
||||||
time: new Date(trade.entryDate).getTime() / 1000,
|
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,
|
position: 'belowBar' as const,
|
||||||
color: trade.side === 'buy' ? '#10b981' : '#ef4444',
|
color: side === 'buy' ? '#10b981' : '#ef4444',
|
||||||
shape: trade.side === 'buy' ? 'arrowUp' : 'arrowDown' as const,
|
shape: (side === 'buy' ? 'arrowUp' : 'arrowDown') as const,
|
||||||
text: `${trade.side === 'buy' ? 'Buy' : 'Sell'} @ $${trade.entryPrice.toFixed(2)}`
|
text: `${side === 'buy' ? 'Buy' : 'Sell'} @ $${price.toFixed(2)}`,
|
||||||
}));
|
price: price
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Trade markers:', tradeMarkers);
|
||||||
|
|
||||||
const processedOhlcData = ohlcData
|
const processedOhlcData = ohlcData
|
||||||
.filter(d => d && d.timestamp != null && d.open != null && d.high != null && d.low != null && d.close != null)
|
.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';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
interface BacktestTradesProps {
|
interface BacktestTradesProps {
|
||||||
result: BacktestResult | null;
|
result: any | null;
|
||||||
isLoading: boolean;
|
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',
|
accessorKey: 'symbol',
|
||||||
header: 'Symbol',
|
header: 'Symbol',
|
||||||
|
|
@ -38,11 +41,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'side',
|
id: 'side',
|
||||||
header: 'Side',
|
header: 'Side',
|
||||||
size: 80,
|
size: 80,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ row }) => {
|
||||||
const side = getValue() as string || 'unknown';
|
const trade = row.original;
|
||||||
|
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell');
|
||||||
return (
|
return (
|
||||||
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-md ${
|
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-md ${
|
||||||
side === 'buy'
|
side === 'buy'
|
||||||
|
|
@ -55,11 +59,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'entryDate',
|
id: 'date',
|
||||||
header: 'Entry Date',
|
header: 'Date',
|
||||||
size: 180,
|
size: 180,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ row }) => {
|
||||||
const date = getValue() as string;
|
const trade = row.original;
|
||||||
|
const date = trade.timestamp || trade.entryDate || trade.date;
|
||||||
return (
|
return (
|
||||||
<span className="text-sm text-text-secondary">
|
<span className="text-sm text-text-secondary">
|
||||||
{date ? new Date(date).toLocaleString() : '-'}
|
{date ? new Date(date).toLocaleString() : '-'}
|
||||||
|
|
@ -68,37 +73,12 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'entryPrice',
|
id: 'price',
|
||||||
header: 'Entry Price',
|
header: 'Price',
|
||||||
size: 100,
|
size: 100,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ row }) => {
|
||||||
const price = getValue() as number;
|
const trade = row.original;
|
||||||
return (
|
const price = trade.price || trade.entryPrice;
|
||||||
<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;
|
|
||||||
return (
|
return (
|
||||||
<span className="text-sm text-text-primary">
|
<span className="text-sm text-text-primary">
|
||||||
${price != null ? price.toFixed(2) : '0.00'}
|
${price != null ? price.toFixed(2) : '0.00'}
|
||||||
|
|
@ -110,50 +90,84 @@ export function BacktestTrades({ result, isLoading }: BacktestTradesProps) {
|
||||||
accessorKey: 'quantity',
|
accessorKey: 'quantity',
|
||||||
header: 'Quantity',
|
header: 'Quantity',
|
||||||
size: 80,
|
size: 80,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => {
|
||||||
<span className="text-sm text-text-primary">{getValue() as number || 0}</span>
|
const qty = Math.abs(getValue() as number || 0);
|
||||||
),
|
return <span className="text-sm text-text-primary">{qty}</span>;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'pnl',
|
accessorKey: 'commission',
|
||||||
header: 'P&L',
|
header: 'Commission',
|
||||||
size: 100,
|
size: 100,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const pnl = getValue() as number || 0;
|
const commission = getValue() as number || 0;
|
||||||
return (
|
return (
|
||||||
<span className={`text-sm font-medium ${
|
<span className="text-sm text-text-secondary">
|
||||||
pnl >= 0 ? 'text-success' : 'text-error'
|
${commission.toFixed(2)}
|
||||||
}`}>
|
|
||||||
${pnl != null ? pnl.toFixed(2) : '0.00'}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'pnlPercent',
|
id: 'pnl',
|
||||||
header: 'P&L %',
|
header: 'P&L',
|
||||||
size: 100,
|
size: 100,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ row }) => {
|
||||||
const pnlPercent = getValue() as number || 0;
|
const trade = row.original;
|
||||||
|
const pnl = trade.pnl || trade.realizedPnl || 0;
|
||||||
return (
|
return (
|
||||||
<span className={`text-sm font-medium ${
|
<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>
|
</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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4 grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
<h3 className="text-base font-medium text-text-primary">Trade History</h3>
|
<div className="bg-surface-secondary rounded-lg border border-border p-3">
|
||||||
<p className="text-sm text-text-secondary mt-1">
|
<p className="text-xs text-text-secondary">Total Trades</p>
|
||||||
Total: {result.trades.length} trades
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={result.trades}
|
data={result.trades}
|
||||||
|
|
|
||||||
|
|
@ -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 { BacktestConfiguration } from './BacktestConfiguration';
|
||||||
export { BacktestControls } from './BacktestControls';
|
|
||||||
export { BacktestResults } from './BacktestResults';
|
|
||||||
export { MetricsCard } from './MetricsCard';
|
export { MetricsCard } from './MetricsCard';
|
||||||
export { PositionsTable } from './PositionsTable';
|
|
||||||
export { TradeLog } from './TradeLog';
|
|
||||||
export { RunsList } from './RunsList';
|
|
||||||
export { RunsListWithMetrics } from './RunsListWithMetrics';
|
export { RunsListWithMetrics } from './RunsListWithMetrics';
|
||||||
export { CompactPerformanceMetrics } from './CompactPerformanceMetrics';
|
export { CompactPerformanceMetrics } from './CompactPerformanceMetrics';
|
||||||
export { CompactPositionsTable } from './CompactPositionsTable';
|
|
||||||
export { PositionsSummary } from './PositionsSummary';
|
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 { BacktestListPageV2 } from './BacktestListPageV2';
|
||||||
export { BacktestDetailPageV2 } from './BacktestDetailPageV2';
|
export { BacktestDetailPageV2 } from './BacktestDetailPageV2';
|
||||||
export * from './types';
|
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 { SymbolChart } from './SymbolChart';
|
||||||
export { ChartToolbar } from './ChartToolbar';
|
export { ChartToolbar } from './ChartToolbar';
|
||||||
export { MarketOverview } from './MarketOverview';
|
|
||||||
export { ChartErrorBoundary } from './ChartErrorBoundary';
|
|
||||||
export { IndicatorSelector } from './IndicatorSelector';
|
export { IndicatorSelector } from './IndicatorSelector';
|
||||||
export { IndicatorList } from './IndicatorList';
|
export { IndicatorList } from './IndicatorList';
|
||||||
export { IndicatorSettings } from './IndicatorSettings';
|
export { IndicatorSettings } from './IndicatorSettings';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue